From b85747ba4e0ac2efb17721a6e705fe31a898dcb8 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Tue, 1 Apr 2025 15:23:21 -0400 Subject: [PATCH 1/7] [Feat] Add bundler implementation to Firestore SSR Serialization feature branch (#8872) Merge the initial bundler implementation into the Firestore Result Serialization feature branch. Implementation includes the ability to invoke `toJSON()` on `QuerySnapshot` and `DocumentSnapshot` instances. --- common/api-review/firestore.api.md | 4 + docs-devsite/firestore_.documentsnapshot.md | 12 + docs-devsite/firestore_.querysnapshot.md | 12 + packages/firestore/src/api/snapshot.ts | 109 +++++++ .../src/lite-api/user_data_reader.ts | 2 +- packages/firestore/src/remote/serializer.ts | 5 +- .../firestore/src/util/bundle_builder_impl.ts | 285 ++++++++++++++++++ .../test/integration/api/query.test.ts | 41 +++ .../firestore/test/unit/api/database.test.ts | 87 ++++++ packages/firestore/test/util/api_helpers.ts | 10 +- 10 files changed, 562 insertions(+), 5 deletions(-) create mode 100644 packages/firestore/src/util/bundle_builder_impl.ts diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index 34b56b97f21..3f17e415e52 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -178,6 +178,8 @@ export class DocumentSnapshot; + // (undocumented) + toJSON(): object; } export { EmulatorMockTokenOptions } @@ -610,6 +612,8 @@ export class QuerySnapshot; get size(): number; + // (undocumented) + toJSON(): object; } // @public diff --git a/docs-devsite/firestore_.documentsnapshot.md b/docs-devsite/firestore_.documentsnapshot.md index 476588b78c0..8c4825593dc 100644 --- a/docs-devsite/firestore_.documentsnapshot.md +++ b/docs-devsite/firestore_.documentsnapshot.md @@ -41,6 +41,7 @@ export declare class DocumentSnapshotObject. Returns undefined if the document doesn't exist.By default, serverTimestamp() values that have not yet been set to their final value will be returned as null. You can override this by passing an options object. | | [exists()](./firestore_.documentsnapshot.md#documentsnapshotexists) | | Returns whether or not the data exists. True if the document exists. | | [get(fieldPath, options)](./firestore_.documentsnapshot.md#documentsnapshotget) | | Retrieves the field specified by fieldPath. Returns undefined if the document or field doesn't exist.By default, a serverTimestamp() that has not yet been set to its final value will be returned as null. You can override this by passing an options object. | +| [toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson) | | | ## DocumentSnapshot.(constructor) @@ -144,3 +145,14 @@ any The data at the specified field location or undefined if no such field exists in the document. +## DocumentSnapshot.toJSON() + +Signature: + +```typescript +toJSON(): object; +``` +Returns: + +object + diff --git a/docs-devsite/firestore_.querysnapshot.md b/docs-devsite/firestore_.querysnapshot.md index d9930c68d90..da0913d7b6e 100644 --- a/docs-devsite/firestore_.querysnapshot.md +++ b/docs-devsite/firestore_.querysnapshot.md @@ -34,6 +34,7 @@ export declare class QuerySnapshotQuerySnapshot. | +| [toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) | | | ## QuerySnapshot.docs @@ -126,3 +127,14 @@ forEach(callback: (result: QueryDocumentSnapshot) => void +## QuerySnapshot.toJSON() + +Signature: + +```typescript +toJSON(): object; +``` +Returns: + +object + diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index 29e1616b61c..9d2ddf41a7e 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -36,7 +36,13 @@ import { AbstractUserDataWriter } from '../lite-api/user_data_writer'; import { Document } from '../model/document'; import { DocumentKey } from '../model/document_key'; import { debugAssert, fail } from '../util/assert'; +import { + BundleBuilder, + DocumentSnapshotBundleData, + QuerySnapshotBundleData +} from '../util/bundle_builder_impl'; import { Code, FirestoreError } from '../util/error'; +import { AutoId } from '../util/misc'; import { Firestore } from './database'; import { SnapshotListenOptions } from './reference_impl'; @@ -496,6 +502,46 @@ export class DocumentSnapshot< } return undefined; } + + toJSON(): object { + const document = this._document; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result: any = {}; + result['bundle'] = ''; + result['source'] = 'DocumentSnapshot'; + + if ( + !document || + !document.isValidDocument() || + !document.isFoundDocument() + ) { + return result; + } + const builder: BundleBuilder = new BundleBuilder( + this._firestore, + AutoId.newId() + ); + const documentData = this._userDataWriter.convertObjectMap( + document.data.value.mapValue.fields, + 'previous' + ); + if (this.metadata.hasPendingWrites) { + throw new FirestoreError( + Code.FAILED_PRECONDITION, + 'DocumentSnapshot.toJSON() attempted to serialize a document with pending writes. ' + + 'Await waitForPendingWrites() before invoking toJSON().' + ); + } + builder.addBundleDocument( + documentToDocumentSnapshotBundleData( + this.ref.path, + documentData, + document + ) + ); + result['bundle'] = builder.build(); + return result; + } } /** @@ -651,6 +697,52 @@ export class QuerySnapshot< return this._cachedChanges; } + + toJSON(): object { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result: any = {}; + result['source'] = 'QuerySnapshot'; + const builder: BundleBuilder = new BundleBuilder( + this._firestore, + AutoId.newId() + ); + const databaseId = this._firestore._databaseId.database; + const projectId = this._firestore._databaseId.projectId; + const parent = `projects/${projectId}/databases/${databaseId}/documents`; + const docBundleDataArray: DocumentSnapshotBundleData[] = []; + const docArray = this.docs; + docArray.forEach(doc => { + if (doc._document === null) { + return; + } + const documentData = this._userDataWriter.convertObjectMap( + doc._document.data.value.mapValue.fields, + 'previous' + ); + if (this.metadata.hasPendingWrites) { + throw new FirestoreError( + Code.FAILED_PRECONDITION, + 'QuerySnapshot.toJSON() attempted to serialize a document with pending writes. ' + + 'Await waitForPendingWrites() before invoking toJSON().' + ); + } + docBundleDataArray.push( + documentToDocumentSnapshotBundleData( + doc.ref.path, + documentData, + doc._document + ) + ); + }); + const bundleData: QuerySnapshotBundleData = { + query: this.query._query, + parent, + docBundleDataArray + }; + builder.addBundleQuery(bundleData); + result['bundle'] = builder.build(); + return result; + } } /** Calculates the array of `DocumentChange`s for a given `ViewSnapshot`. */ @@ -790,3 +882,20 @@ export function snapshotEqual( return false; } + +// Formats Document data for bundling a DocumentSnapshot. +function documentToDocumentSnapshotBundleData( + path: string, + documentData: DocumentData, + document: Document +): DocumentSnapshotBundleData { + return { + documentData, + documentKey: document.mutableCopy().key, + documentPath: path, + documentExists: true, + createdTime: document.createTime.toTimestamp(), + readTime: document.readTime.toTimestamp(), + versionTime: document.version.toTimestamp() + }; +} diff --git a/packages/firestore/src/lite-api/user_data_reader.ts b/packages/firestore/src/lite-api/user_data_reader.ts index ebd4b49085f..1cd6741dc35 100644 --- a/packages/firestore/src/lite-api/user_data_reader.ts +++ b/packages/firestore/src/lite-api/user_data_reader.ts @@ -778,7 +778,7 @@ export function parseData( } } -function parseObject( +export function parseObject( obj: Dict, context: ParseContextImpl ): { mapValue: ProtoMapValue } { diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index 811c2ac4df6..78a7ceda6f9 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -226,7 +226,10 @@ export function toTimestamp( } } -function fromTimestamp(date: ProtoTimestamp): Timestamp { +/** + * Returns a Timestamp typed object given protobuf timestamp value. + */ +export function fromTimestamp(date: ProtoTimestamp): Timestamp { const timestamp = normalizeTimestamp(date); return new Timestamp(timestamp.seconds, timestamp.nanos); } diff --git a/packages/firestore/src/util/bundle_builder_impl.ts b/packages/firestore/src/util/bundle_builder_impl.ts new file mode 100644 index 00000000000..d516e512db0 --- /dev/null +++ b/packages/firestore/src/util/bundle_builder_impl.ts @@ -0,0 +1,285 @@ +/** + * @license + * Copyright 2025 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 { + JsonProtoSerializer, + fromTimestamp, + toName, + toQueryTarget, + toTimestamp +} from '../../src/remote/serializer'; +import { encoder } from '../../test/unit/util/bundle_data'; +import { Firestore } from '../api/database'; +import { DatabaseId } from '../core/database_info'; +import { Query, queryToTarget } from '../core/query'; +import { DocumentData } from '../lite-api/reference'; +import { Timestamp } from '../lite-api/timestamp'; +import { + parseObject, + UserDataReader, + UserDataSource +} from '../lite-api/user_data_reader'; +import { DocumentKey } from '../model/document_key'; +import { + BundledDocumentMetadata as ProtoBundledDocumentMetadata, + BundleElement as ProtoBundleElement, + BundleMetadata as ProtoBundleMetadata, + NamedQuery as ProtoNamedQuery +} from '../protos/firestore_bundle_proto'; +import { + Document as ProtoDocument, + Document +} from '../protos/firestore_proto_api'; +import { AutoId } from '../util/misc'; + +const BUNDLE_VERSION = 1; + +/** + * Builds a Firestore data bundle with results from the given document and query snapshots. + */ +export class BundleBuilder { + // Resulting documents for the bundle, keyed by full document path. + private documents: Map = new Map(); + // Named queries saved in the bundle, keyed by query name. + private namedQueries: Map = new Map(); + + // The latest read time among all bundled documents and queries. + private latestReadTime = new Timestamp(0, 0); + + // Database identifier which is part of the serialized bundle. + private databaseId: DatabaseId; + + // Tools to convert public data types into their serialized form. + private readonly serializer: JsonProtoSerializer; + private readonly userDataReader: UserDataReader; + + constructor(private firestore: Firestore, readonly bundleId: string) { + this.databaseId = firestore._databaseId; + + // useProto3Json is true because the objects will be serialized to JSON string + // before being written to the bundle buffer. + this.serializer = new JsonProtoSerializer( + this.databaseId, + /*useProto3Json=*/ true + ); + + this.userDataReader = new UserDataReader( + this.databaseId, + true, + this.serializer + ); + } + + /** + * Adds data from a DocumentSnapshot to the bundle. + * @internal + * @param docBundleData A DocumentSnapshotBundleData containing information from the + * DocumentSnapshot. Note we cannot accept a DocumentSnapshot directly due to a circular + * dependency error. + * @param queryName The name of the QuerySnapshot if this document is part of a Query. + */ + addBundleDocument( + docBundleData: DocumentSnapshotBundleData, + queryName?: string + ): void { + const originalDocument = this.documents.get(docBundleData.documentPath); + const originalQueries = originalDocument?.metadata.queries; + const docReadTime: Timestamp | undefined = docBundleData.readTime; + const origDocReadTime: Timestamp | null = !!originalDocument?.metadata + .readTime + ? fromTimestamp(originalDocument.metadata.readTime) + : null; + + const neitherHasReadTime: boolean = !docReadTime && origDocReadTime == null; + const docIsNewer: boolean = + docReadTime !== undefined && + (origDocReadTime == null || origDocReadTime < docReadTime); + if (neitherHasReadTime || docIsNewer) { + // Store document. + this.documents.set(docBundleData.documentPath, { + document: this.toBundleDocument(docBundleData), + metadata: { + name: toName(this.serializer, docBundleData.documentKey), + readTime: !!docReadTime + ? toTimestamp(this.serializer, docReadTime) // Convert Timestamp to proto format. + : undefined, + exists: docBundleData.documentExists + } + }); + } + if (docReadTime && docReadTime > this.latestReadTime) { + this.latestReadTime = docReadTime; + } + // Update `queries` to include both original and `queryName`. + if (queryName) { + const newDocument = this.documents.get(docBundleData.documentPath)!; + newDocument.metadata.queries = originalQueries || []; + newDocument.metadata.queries!.push(queryName); + } + } + + /** + * Adds data from a QuerySnapshot to the bundle. + * @internal + * @param docBundleData A QuerySnapshotBundleData containing information from the + * QuerySnapshot. Note we cannot accept a QuerySnapshot directly due to a circular + * dependency error. + */ + addBundleQuery(queryBundleData: QuerySnapshotBundleData): void { + const name = AutoId.newId(); + if (this.namedQueries.has(name)) { + throw new Error(`Query name conflict: ${name} has already been added.`); + } + let latestReadTime = new Timestamp(0, 0); + for (const docBundleData of queryBundleData.docBundleDataArray) { + this.addBundleDocument(docBundleData, name); + if (docBundleData.readTime && docBundleData.readTime > latestReadTime) { + latestReadTime = docBundleData.readTime; + } + } + const queryTarget = toQueryTarget( + this.serializer, + queryToTarget(queryBundleData.query) + ); + const bundledQuery = { + parent: queryBundleData.parent, + structuredQuery: queryTarget.queryTarget.structuredQuery + }; + this.namedQueries.set(name, { + name, + bundledQuery, + readTime: toTimestamp(this.serializer, latestReadTime) + }); + } + + /** + * Convert data from a DocumentSnapshot into the serialized form within a bundle. + * @private + * @internal + * @param docBundleData a DocumentSnapshotBundleData containing the data required to + * serialize a document. + */ + private toBundleDocument( + docBundleData: DocumentSnapshotBundleData + ): ProtoDocument { + // a parse context is typically used for validating and parsing user data, but in this + // case we are using it internally to convert DocumentData to Proto3 JSON + const context = this.userDataReader.createContext( + UserDataSource.ArrayArgument, + 'internal toBundledDocument' + ); + const proto3Fields = parseObject(docBundleData.documentData, context); + + return { + name: toName(this.serializer, docBundleData.documentKey), + fields: proto3Fields.mapValue.fields, + updateTime: toTimestamp(this.serializer, docBundleData.versionTime), + createTime: toTimestamp(this.serializer, docBundleData.createdTime) + }; + } + + /** + * Converts a IBundleElement to a Buffer whose content is the length prefixed JSON representation + * of the element. + * @private + * @internal + * @param bundleElement A ProtoBundleElement that is expected to be Proto3 JSON compatible. + */ + private lengthPrefixedString(bundleElement: ProtoBundleElement): string { + const str = JSON.stringify(bundleElement); + // TODO: it's not ideal to have to re-encode all of these strings multiple times + // It may be more performant to return a UInt8Array that is concatenated to other + // UInt8Arrays instead of returning and concatenating strings and then + // converting the full string to UInt8Array. + const l = encoder.encode(str).byteLength; + return `${l}${str}`; + } + + /** + * Construct a serialized string containing document and query information that has previously + * been added to the BundleBuilder through the addBundleDocument and addBundleQuery methods. + * @internal + */ + build(): string { + let bundleString = ''; + + for (const namedQuery of this.namedQueries.values()) { + bundleString += this.lengthPrefixedString({ namedQuery }); + } + + for (const bundledDocument of this.documents.values()) { + const documentMetadata: ProtoBundledDocumentMetadata = + bundledDocument.metadata; + + bundleString += this.lengthPrefixedString({ documentMetadata }); + // Write to the bundle if document exists. + const document = bundledDocument.document; + if (document) { + bundleString += this.lengthPrefixedString({ document }); + } + } + + const metadata: ProtoBundleMetadata = { + id: this.bundleId, + createTime: toTimestamp(this.serializer, this.latestReadTime), + version: BUNDLE_VERSION, + totalDocuments: this.documents.size, + // TODO: it's not ideal to have to re-encode all of these strings multiple times + totalBytes: encoder.encode(bundleString).length + }; + // Prepends the metadata element to the bundleBuffer: `bundleBuffer` is the second argument to `Buffer.concat`. + bundleString = this.lengthPrefixedString({ metadata }) + bundleString; + + return bundleString; + } +} + +/** + * Interface for an object that contains data required to bundle a DocumentSnapshot. + * @internal + */ +export interface DocumentSnapshotBundleData { + documentData: DocumentData; + documentKey: DocumentKey; + documentPath: string; + documentExists: boolean; + createdTime: Timestamp; + readTime?: Timestamp; + versionTime: Timestamp; +} + +/** + * Interface for an object that contains data required to bundle a QuerySnapshot. + * @internal + */ +export interface QuerySnapshotBundleData { + query: Query; + parent: string; + docBundleDataArray: DocumentSnapshotBundleData[]; +} + +/** + * Convenient class to hold both the metadata and the actual content of a document to be bundled. + * @private + * @internal + */ +class BundledDocument { + constructor( + readonly metadata: ProtoBundledDocumentMetadata, + readonly document?: Document + ) {} +} diff --git a/packages/firestore/test/integration/api/query.test.ts b/packages/firestore/test/integration/api/query.test.ts index 01fd0e47e35..9eb537db4c9 100644 --- a/packages/firestore/test/integration/api/query.test.ts +++ b/packages/firestore/test/integration/api/query.test.ts @@ -37,9 +37,11 @@ import { endAt, endBefore, GeoPoint, + getDocFromCache, getDocs, limit, limitToLast, + loadBundle, onSnapshot, or, orderBy, @@ -74,6 +76,45 @@ import { captureExistenceFilterMismatches } from '../util/testing_hooks_util'; apiDescribe('Queries', persistence => { addEqualityMatcher(); + it('QuerySnapshot.toJSON bundle getDocFromCache', async () => { + let path: string | null = null; + let jsonBundle: object | null = null; + const testDocs = { + a: { k: 'a' }, + b: { k: 'b' }, + c: { k: 'c' } + }; + // Write an initial document in an isolated Firestore instance so it's not stored in the cache. + await withTestCollection(persistence, testDocs, async collection => { + await getDocs(query(collection)).then(querySnapshot => { + expect(querySnapshot.docs.length).to.equal(3); + // Find the path to a known doc. + querySnapshot.docs.forEach(docSnapshot => { + if (docSnapshot.ref.path.endsWith('a')) { + path = docSnapshot.ref.path; + } + }); + expect(path).to.not.be.null; + jsonBundle = querySnapshot.toJSON(); + }); + }); + expect(jsonBundle).to.not.be.null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const json = (jsonBundle as any).bundle; + expect(json).to.exist; + expect(json.length).to.be.greaterThan(0); + + if (path !== null) { + await withTestDb(persistence, async db => { + const docRef = doc(db, path!); + await loadBundle(db, json); + const docSnap = await getDocFromCache(docRef); + expect(docSnap.exists); + expect(docSnap.data()).to.deep.equal(testDocs.a); + }); + } + }); + it('can issue limit queries', () => { const testDocs = { a: { k: 'a' }, diff --git a/packages/firestore/test/unit/api/database.test.ts b/packages/firestore/test/unit/api/database.test.ts index 1cc1df51063..b5e9dc1b673 100644 --- a/packages/firestore/test/unit/api/database.test.ts +++ b/packages/firestore/test/unit/api/database.test.ts @@ -19,6 +19,7 @@ import { expect } from 'chai'; import { connectFirestoreEmulator, + loadBundle, refEqual, snapshotEqual, queryEqual @@ -35,6 +36,15 @@ import { } from '../../util/api_helpers'; import { keys } from '../../util/helpers'; +describe('Bundle', () => { + it('loadBundle does not throw with an empty bundle string)', async () => { + const db = newTestFirestore(); + expect(async () => { + await loadBundle(db, ''); + }).to.not.throw; + }); +}); + describe('CollectionReference', () => { it('support equality checking with isEqual()', () => { expect(refEqual(collectionReference('foo'), collectionReference('foo'))).to @@ -107,6 +117,44 @@ describe('DocumentSnapshot', () => { it('JSON.stringify() does not throw', () => { JSON.stringify(documentSnapshot('foo/bar', { a: 1 }, true)); }); + + it('toJSON returns a bundle', () => { + const json = documentSnapshot( + 'foo/bar', + { a: 1 }, + /*fromCache=*/ true + ).toJSON(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((json as any).bundle).to.exist; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((json as any).bundle.length).to.be.greaterThan(0); + }); + + it('toJSON returns an empty bundle when there are no documents', () => { + const json = documentSnapshot( + 'foo/bar', + /*data=*/ null, + /*fromCache=*/ true + ).toJSON(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((json as any).bundle).to.exist; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((json as any).bundle.length).to.equal(0); + }); + + it('toJSON throws when there are pending writes', () => { + expect(() => { + documentSnapshot( + 'foo/bar', + {}, + /*fromCache=*/ true, + /*hasPendingWrites=*/ true + ).toJSON(); + }).to.throw( + `DocumentSnapshot.toJSON() attempted to serialize a document with pending writes. ` + + `Await waitForPendingWrites() before invoking toJSON().` + ); + }); }); describe('Query', () => { @@ -229,6 +277,45 @@ describe('QuerySnapshot', () => { querySnapshot('foo', {}, { a: { a: 1 } }, keys(), false, false) ); }); + + it('toJSON returns a bundle', () => { + const json = querySnapshot( + 'foo', + {}, + { a: { a: 1 } }, + keys(), + false, + false + ).toJSON(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((json as any).bundle).to.exist; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((json as any).bundle.length).to.be.greaterThan(0); + }); + + it('toJSON returns a bundle when there are no documents', () => { + const json = querySnapshot('foo', {}, {}, keys(), false, false).toJSON(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((json as any).bundle).to.exist; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((json as any).bundle.length).to.be.greaterThan(0); + }); + + it('toJSON throws when there are pending writes', () => { + expect(() => + querySnapshot( + 'foo', + {}, + { a: { a: 1 } }, + keys('foo/a'), + true, + true + ).toJSON() + ).to.throw( + `QuerySnapshot.toJSON() attempted to serialize a document with pending writes. ` + + `Await waitForPendingWrites() before invoking toJSON().` + ); + }); }); describe('SnapshotMetadata', () => { diff --git a/packages/firestore/test/util/api_helpers.ts b/packages/firestore/test/util/api_helpers.ts index 762b5258a29..fe398e4332b 100644 --- a/packages/firestore/test/util/api_helpers.ts +++ b/packages/firestore/test/util/api_helpers.ts @@ -79,8 +79,12 @@ export function documentReference(path: string): DocumentReference { export function documentSnapshot( path: string, data: JsonObject | null, - fromCache: boolean + fromCache: boolean, + hasPendingWrites?: boolean ): DocumentSnapshot { + if (hasPendingWrites === undefined) { + hasPendingWrites = false; + } const db = firestore(); const userDataWriter = new ExpUserDataWriter(db); if (data) { @@ -89,7 +93,7 @@ export function documentSnapshot( userDataWriter, key(path), doc(path, 1, data), - new SnapshotMetadata(/* hasPendingWrites= */ false, fromCache), + new SnapshotMetadata(hasPendingWrites, fromCache), /* converter= */ null ); } else { @@ -98,7 +102,7 @@ export function documentSnapshot( userDataWriter, key(path), null, - new SnapshotMetadata(/* hasPendingWrites= */ false, fromCache), + new SnapshotMetadata(hasPendingWrites, fromCache), /* converter= */ null ); } From e34353db304f4ba43539493788e96a2d02786695 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Mon, 14 Apr 2025 20:43:08 -0400 Subject: [PATCH 2/7] [Feat] Firestore onSnapshot support for bundles for feature branch. (#8896) Pull request to add onSnapshot listeners support for bundles to the Firestore SSR serialization feature branch. This change adds a series of onSnapshot overloads that match the existing onSnapshot variants (observer, individual callback functions, etc) but accepts a bundle instead of a QuerySnapshot or a DocumentSnapshot. The toJSON methods of `QuerySnapshot` and `DocumentSnapshot` have also been updated to explicitly name the fields in the object return type. Fixed an issue in the bundle builder where the bundleName for `DocumentSnapshots` didn't match the `ResourcePath` of the document, which is needed when reconstituting the `DocumentReference` in `onSnapshot`. Finally, I cleaned up the text wrapping for the `onSnapshot` reference doc comments so they take up fewer lines of source code. --- common/api-review/firestore.api.md | 40 ++ docs-devsite/firestore_.md | 252 +++++++ packages/firestore/src/api/reference_impl.ts | 669 +++++++++++++++--- packages/firestore/src/api/snapshot.ts | 10 +- .../firestore/src/util/bundle_builder_impl.ts | 11 +- .../test/integration/api/database.test.ts | 269 +++++++ 6 files changed, 1156 insertions(+), 95 deletions(-) diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index 3f17e415e52..ebd620e43ca 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -461,6 +461,46 @@ export function onSnapshot(query // @public export function onSnapshot(query: Query, options: SnapshotListenOptions, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void): Unsubscribe; +// @public +export function onSnapshot(firestore: Firestore, snapshotJson: object, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; + +// @public +export function onSnapshot(firestore: Firestore, snapshotJson: object, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; + +// @public +export function onSnapshot(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; + +// @public +export function onSnapshot(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; + +// @public +export function onSnapshot(firestore: Firestore, snapshotJson: object, observer: { + next: (snapshot: QuerySnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}, converter?: FirestoreDataConverter): Unsubscribe; + +// @public +export function onSnapshot(firestore: Firestore, snapshotJson: object, observer: { + next: (snapshot: DocumentSnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}, converter?: FirestoreDataConverter): Unsubscribe; + +// @public +export function onSnapshot(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, observer: { + next: (snapshot: QuerySnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}, converter?: FirestoreDataConverter): Unsubscribe; + +// @public +export function onSnapshot(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, observer: { + next: (snapshot: DocumentSnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}, converter?: FirestoreDataConverter): Unsubscribe; + // @public export function onSnapshotsInSync(firestore: Firestore, observer: { next?: (value: void) => void; diff --git a/docs-devsite/firestore_.md b/docs-devsite/firestore_.md index 91d21e32708..685958d364c 100644 --- a/docs-devsite/firestore_.md +++ b/docs-devsite/firestore_.md @@ -32,6 +32,14 @@ https://github.com/firebase/firebase-js-sdk | [getPersistentCacheIndexManager(firestore)](./firestore_.md#getpersistentcacheindexmanager_231a8e0) | Returns the PersistentCache Index Manager used by the given Firestore object. The PersistentCacheIndexManager instance, or null if local persistent storage is not in use. | | [loadBundle(firestore, bundleData)](./firestore_.md#loadbundle_bec5b75) | Loads a Firestore bundle into the local cache. | | [namedQuery(firestore, name)](./firestore_.md#namedquery_6438876) | Reads a Firestore [Query](./firestore_.query.md#query_class) from local cache, identified by the given name.The named queries are packaged into bundles on the server side (along with resulting documents), and loaded to local cache using loadBundle. Once in local cache, use this method to extract a [Query](./firestore_.query.md#query_class) by name. | +| [onSnapshot(firestore, snapshotJson, onNext, onError, onCompletion, converter)](./firestore_.md#onsnapshot_712362a) | Attaches a listener for DocumentSnapshot events based on data generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | +| [onSnapshot(firestore, snapshotJson, options, onNext, onError, onCompletion, converter)](./firestore_.md#onsnapshot_8807e6e) | Attaches a listener for QuerySnapshot events based on data generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | +| [onSnapshot(firestore, snapshotJson, options, onNext, onError, onCompletion, converter)](./firestore_.md#onsnapshot_301fcec) | Attaches a listener for DocumentSnapshot events based on data generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | +| [onSnapshot(firestore, snapshotJson, observer, converter)](./firestore_.md#onsnapshot_b8b5c9d) | Attaches a listener for QuerySnapshot events based on QuerySnapshot data generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | +| [onSnapshot(firestore, snapshotJson, observer, converter)](./firestore_.md#onsnapshot_9b75d28) | Attaches a listener for DocumentSnapshot events based on data generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | +| [onSnapshot(firestore, snapshotJson, options, observer, converter)](./firestore_.md#onsnapshot_fb80adf) | Attaches a listener for QuerySnapshot events based on QuerySnapshot data generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | +| [onSnapshot(firestore, snapshotJson, options, observer, converter)](./firestore_.md#onsnapshot_f76d912) | Attaches a listener for DocumentSnapshot events based on QuerySnapshot data generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | +| [onSnapshot(firestore, snapshotJson, onNext, onError, onCompletion, converter)](./firestore_.md#onsnapshot_7c84f5e) | Attaches a listener for QuerySnapshot events based on data generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | | [onSnapshotsInSync(firestore, observer)](./firestore_.md#onsnapshotsinsync_2f0dfa4) | Attaches a listener for a snapshots-in-sync event. The snapshots-in-sync event indicates that all listeners affected by a given change have fired, even if a single server-generated change affects multiple listeners.NOTE: The snapshots-in-sync event only indicates that listeners are in sync with each other, but does not relate to whether those snapshots are in sync with the server. Use SnapshotMetadata in the individual listeners to determine if a snapshot is from the cache or the server. | | [onSnapshotsInSync(firestore, onSync)](./firestore_.md#onsnapshotsinsync_1901c06) | Attaches a listener for a snapshots-in-sync event. The snapshots-in-sync event indicates that all listeners affected by a given change have fired, even if a single server-generated change affects multiple listeners.NOTE: The snapshots-in-sync event only indicates that listeners are in sync with each other, but does not relate to whether those snapshots are in sync with the server. Use SnapshotMetadata in the individual listeners to determine if a snapshot is from the cache or the server. | | [runTransaction(firestore, updateFunction, options)](./firestore_.md#runtransaction_6f03ec4) | Executes the given updateFunction and then attempts to commit the changes applied within the transaction. If any document read within the transaction has changed, Cloud Firestore retries the updateFunction. If it fails to commit after 5 attempts, the transaction fails.The maximum number of writes allowed in a single transaction is 500. | @@ -617,6 +625,250 @@ Promise<[Query](./firestore_.query.md#query_class) \| null> A `Promise` that is resolved with the Query or `null`. +### onSnapshot(firestore, snapshotJson, onNext, onError, onCompletion, converter) {:#onsnapshot_712362a} + +Attaches a listener for `DocumentSnapshot` events based on data generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson) You may either pass individual `onNext` and `onError` callbacks or pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by calling the function that is returned when `onSnapshot` is called. + +NOTE: Although an `onCompletion` callback can be provided, it will never be called because the snapshot stream is never-ending. + +Signature: + +```typescript +export declare function onSnapshot(firestore: Firestore, snapshotJson: object, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| firestore | [Firestore](./firestore_.firestore.md#firestore_class) | The [Firestore](./firestore_.firestore.md#firestore_class) instance to enable persistence for. | +| snapshotJson | object | A JSON object generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). | +| onNext | (snapshot: [DocumentSnapshot](./firestore_.documentsnapshot.md#documentsnapshot_class)<AppModelType, DbModelType>) => void | A callback to be called every time a new DocumentSnapshot is available. | +| onError | (error: [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class)) => void | A callback to be called if the listen fails or is cancelled. No fruther callbacks will occur. | +| onCompletion | () => void | Can be provided, but will not be called since streams are never ending. | +| converter | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface)<DbModelType> | An optional object that converts objects from Firestore before the onNext listener is invoked. | + +Returns: + +[Unsubscribe](./firestore_.unsubscribe.md#unsubscribe_interface) + +An unsubscribe function that can be called to cancel the snapshot listener. + +### onSnapshot(firestore, snapshotJson, options, onNext, onError, onCompletion, converter) {:#onsnapshot_8807e6e} + +Attaches a listener for `QuerySnapshot` events based on data generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) You may either pass individual `onNext` and `onError` callbacks or pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by calling the function that is returned when `onSnapshot` is called. + +NOTE: Although an `onCompletion` callback can be provided, it will never be called because the snapshot stream is never-ending. + +Signature: + +```typescript +export declare function onSnapshot(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| firestore | [Firestore](./firestore_.firestore.md#firestore_class) | The [Firestore](./firestore_.firestore.md#firestore_class) instance to enable persistence for. | +| snapshotJson | object | A JSON object generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). | +| options | [SnapshotListenOptions](./firestore_.snapshotlistenoptions.md#snapshotlistenoptions_interface) | Options controlling the listen behavior. | +| onNext | (snapshot: [QuerySnapshot](./firestore_.querysnapshot.md#querysnapshot_class)<AppModelType, DbModelType>) => void | A callback to be called every time a new QuerySnapshot is available. | +| onError | (error: [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class)) => void | A callback to be called if the listen fails or is cancelled. No further callbacks will occur. | +| onCompletion | () => void | Can be provided, but will not be called since streams are never ending. | +| converter | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface)<DbModelType> | An optional object that converts objects from Firestore before the onNext listener is invoked. | + +Returns: + +[Unsubscribe](./firestore_.unsubscribe.md#unsubscribe_interface) + +An unsubscribe function that can be called to cancel the snapshot listener. + +### onSnapshot(firestore, snapshotJson, options, onNext, onError, onCompletion, converter) {:#onsnapshot_301fcec} + +Attaches a listener for `DocumentSnapshot` events based on data generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson) You may either pass individual `onNext` and `onError` callbacks or pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by calling the function that is returned when `onSnapshot` is called. + +NOTE: Although an `onCompletion` callback can be provided, it will never be called because the snapshot stream is never-ending. + +Signature: + +```typescript +export declare function onSnapshot(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| firestore | [Firestore](./firestore_.firestore.md#firestore_class) | The [Firestore](./firestore_.firestore.md#firestore_class) instance to enable persistence for. | +| snapshotJson | object | A JSON object generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). | +| options | [SnapshotListenOptions](./firestore_.snapshotlistenoptions.md#snapshotlistenoptions_interface) | Options controlling the listen behavior. | +| onNext | (snapshot: [DocumentSnapshot](./firestore_.documentsnapshot.md#documentsnapshot_class)<AppModelType, DbModelType>) => void | A callback to be called every time a new DocumentSnapshot is available. | +| onError | (error: [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class)) => void | A callback to be called if the listen fails or is cancelled. No further callbacks will occur. | +| onCompletion | () => void | Can be provided, but will not be called since streams are never ending. | +| converter | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface)<DbModelType> | An optional object that converts objects from Firestore before the onNext listener is invoked. | + +Returns: + +[Unsubscribe](./firestore_.unsubscribe.md#unsubscribe_interface) + +An unsubscribe function that can be called to cancel the snapshot listener. + +### onSnapshot(firestore, snapshotJson, observer, converter) {:#onsnapshot_b8b5c9d} + +Attaches a listener for `QuerySnapshot` events based on QuerySnapshot data generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) You may either pass individual `onNext` and `onError` callbacks or pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by calling the function that is returned when `onSnapshot` is called. + +NOTE: Although an `onCompletion` callback can be provided, it will never be called because the snapshot stream is never-ending. + +Signature: + +```typescript +export declare function onSnapshot(firestore: Firestore, snapshotJson: object, observer: { + next: (snapshot: QuerySnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}, converter?: FirestoreDataConverter): Unsubscribe; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| firestore | [Firestore](./firestore_.firestore.md#firestore_class) | The [Firestore](./firestore_.firestore.md#firestore_class) instance to enable persistence for. | +| snapshotJson | object | A JSON object generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). | +| observer | { next: (snapshot: [QuerySnapshot](./firestore_.querysnapshot.md#querysnapshot_class)<AppModelType, DbModelType>) => void; error?: (error: [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class)) => void; complete?: () => void; } | A single object containing next and error callbacks. | +| converter | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface)<DbModelType> | An optional object that converts objects from Firestore before the onNext listener is invoked. | + +Returns: + +[Unsubscribe](./firestore_.unsubscribe.md#unsubscribe_interface) + +An unsubscribe function that can be called to cancel the snapshot listener. + +### onSnapshot(firestore, snapshotJson, observer, converter) {:#onsnapshot_9b75d28} + +Attaches a listener for `DocumentSnapshot` events based on data generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson) You may either pass individual `onNext` and `onError` callbacks or pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by calling the function that is returned when `onSnapshot` is called. + +NOTE: Although an `onCompletion` callback can be provided, it will never be called because the snapshot stream is never-ending. + +Signature: + +```typescript +export declare function onSnapshot(firestore: Firestore, snapshotJson: object, observer: { + next: (snapshot: DocumentSnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}, converter?: FirestoreDataConverter): Unsubscribe; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| firestore | [Firestore](./firestore_.firestore.md#firestore_class) | The [Firestore](./firestore_.firestore.md#firestore_class) instance to enable persistence for. | +| snapshotJson | object | A JSON object generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). | +| observer | { next: (snapshot: [DocumentSnapshot](./firestore_.documentsnapshot.md#documentsnapshot_class)<AppModelType, DbModelType>) => void; error?: (error: [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class)) => void; complete?: () => void; } | A single object containing next and error callbacks. | +| converter | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface)<DbModelType> | An optional object that converts objects from Firestore before the onNext listener is invoked. | + +Returns: + +[Unsubscribe](./firestore_.unsubscribe.md#unsubscribe_interface) + +An unsubscribe function that can be called to cancel the snapshot listener. + +### onSnapshot(firestore, snapshotJson, options, observer, converter) {:#onsnapshot_fb80adf} + +Attaches a listener for `QuerySnapshot` events based on QuerySnapshot data generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) You may either pass individual `onNext` and `onError` callbacks or pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by calling the function that is returned when `onSnapshot` is called. + +NOTE: Although an `onCompletion` callback can be provided, it will never be called because the snapshot stream is never-ending. + +Signature: + +```typescript +export declare function onSnapshot(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, observer: { + next: (snapshot: QuerySnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}, converter?: FirestoreDataConverter): Unsubscribe; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| firestore | [Firestore](./firestore_.firestore.md#firestore_class) | The [Firestore](./firestore_.firestore.md#firestore_class) instance to enable persistence for. | +| snapshotJson | object | A JSON object generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). | +| options | [SnapshotListenOptions](./firestore_.snapshotlistenoptions.md#snapshotlistenoptions_interface) | Options controlling the listen behavior. | +| observer | { next: (snapshot: [QuerySnapshot](./firestore_.querysnapshot.md#querysnapshot_class)<AppModelType, DbModelType>) => void; error?: (error: [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class)) => void; complete?: () => void; } | A single object containing next and error callbacks. | +| converter | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface)<DbModelType> | An optional object that converts objects from Firestore before the onNext listener is invoked. | + +Returns: + +[Unsubscribe](./firestore_.unsubscribe.md#unsubscribe_interface) + +An unsubscribe function that can be called to cancel the snapshot listener. + +### onSnapshot(firestore, snapshotJson, options, observer, converter) {:#onsnapshot_f76d912} + +Attaches a listener for `DocumentSnapshot` events based on QuerySnapshot data generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson) You may either pass individual `onNext` and `onError` callbacks or pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by calling the function that is returned when `onSnapshot` is called. + +NOTE: Although an `onCompletion` callback can be provided, it will never be called because the snapshot stream is never-ending. + +Signature: + +```typescript +export declare function onSnapshot(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, observer: { + next: (snapshot: DocumentSnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; +}, converter?: FirestoreDataConverter): Unsubscribe; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| firestore | [Firestore](./firestore_.firestore.md#firestore_class) | The [Firestore](./firestore_.firestore.md#firestore_class) instance to enable persistence for. | +| snapshotJson | object | A JSON object generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). | +| options | [SnapshotListenOptions](./firestore_.snapshotlistenoptions.md#snapshotlistenoptions_interface) | Options controlling the listen behavior. | +| observer | { next: (snapshot: [DocumentSnapshot](./firestore_.documentsnapshot.md#documentsnapshot_class)<AppModelType, DbModelType>) => void; error?: (error: [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class)) => void; complete?: () => void; } | A single object containing next and error callbacks. | +| converter | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface)<DbModelType> | An optional object that converts objects from Firestore before the onNext listener is invoked. | + +Returns: + +[Unsubscribe](./firestore_.unsubscribe.md#unsubscribe_interface) + +An unsubscribe function that can be called to cancel the snapshot listener. + +### onSnapshot(firestore, snapshotJson, onNext, onError, onCompletion, converter) {:#onsnapshot_7c84f5e} + +Attaches a listener for `QuerySnapshot` events based on data generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) You may either pass individual `onNext` and `onError` callbacks or pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by calling the function that is returned when `onSnapshot` is called. + +NOTE: Although an `onCompletion` callback can be provided, it will never be called because the snapshot stream is never-ending. + +Signature: + +```typescript +export declare function onSnapshot(firestore: Firestore, snapshotJson: object, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| firestore | [Firestore](./firestore_.firestore.md#firestore_class) | The [Firestore](./firestore_.firestore.md#firestore_class) instance to enable persistence for. | +| snapshotJson | object | A JSON object generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). | +| onNext | (snapshot: [QuerySnapshot](./firestore_.querysnapshot.md#querysnapshot_class)<AppModelType, DbModelType>) => void | A callback to be called every time a new QuerySnapshot is available. | +| onError | (error: [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class)) => void | A callback to be called if the listen fails or is cancelled. No further callbacks will occur. | +| onCompletion | () => void | Can be provided, but will not be called since streams are never ending. | +| converter | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface)<DbModelType> | An optional object that converts objects from Firestore before the onNext listener is invoked. | + +Returns: + +[Unsubscribe](./firestore_.unsubscribe.md#unsubscribe_interface) + +An unsubscribe function that can be called to cancel the snapshot listener. + ### onSnapshotsInSync(firestore, observer) {:#onsnapshotsinsync_2f0dfa4} Attaches a listener for a snapshots-in-sync event. The snapshots-in-sync event indicates that all listeners affected by a given change have fired, even if a single server-generated change affects multiple listeners. diff --git a/packages/firestore/src/api/reference_impl.ts b/packages/firestore/src/api/reference_impl.ts index e730fb40da7..b920c479d0b 100644 --- a/packages/firestore/src/api/reference_impl.ts +++ b/packages/firestore/src/api/reference_impl.ts @@ -17,6 +17,7 @@ import { getModularInstance } from '@firebase/util'; +import { loadBundle, namedQuery } from '../api/database'; import { CompleteFn, ErrorFn, @@ -59,14 +60,20 @@ import { parseUpdateVarargs } from '../lite-api/user_data_reader'; import { AbstractUserDataWriter } from '../lite-api/user_data_writer'; +import { DocumentKey } from '../model/document_key'; import { DeleteMutation, Mutation, Precondition } from '../model/mutation'; import { debugAssert } from '../util/assert'; import { ByteString } from '../util/byte_string'; -import { FirestoreError } from '../util/error'; +import { Code, FirestoreError } from '../util/error'; import { cast } from '../util/input_validation'; import { ensureFirestoreConfigured, Firestore } from './database'; -import { DocumentSnapshot, QuerySnapshot, SnapshotMetadata } from './snapshot'; +import { + DocumentSnapshot, + FirestoreDataConverter, + QuerySnapshot, + SnapshotMetadata +} from './snapshot'; /** * An options object that can be passed to {@link (onSnapshot:1)} and {@link @@ -483,12 +490,11 @@ export interface Unsubscribe { // integration tests /** - * Attaches a listener for `DocumentSnapshot` events. You may either pass - * individual `onNext` and `onError` callbacks or pass a single observer - * object with `next` and `error` callbacks. + * Attaches a listener for `DocumentSnapshot` events. You may either pass individual `onNext` and + * `onError` callbacks or pass a single observer object with `next` and `error` callbacks. * - * NOTE: Although an `onCompletion` callback can be provided, it will - * never be called because the snapshot stream is never-ending. + * NOTE: Although an `onCompletion` callback can be provided, it will never be called because the + * snapshot stream is never-ending. * * @param reference - A reference to the document to listen to. * @param observer - A single object containing `next` and `error` callbacks. @@ -504,12 +510,11 @@ export function onSnapshot( } ): Unsubscribe; /** - * Attaches a listener for `DocumentSnapshot` events. You may either pass - * individual `onNext` and `onError` callbacks or pass a single observer - * object with `next` and `error` callbacks. + * Attaches a listener for `DocumentSnapshot` events. You may either pass individual `onNext` and + * `onError` callbacks or pass a single observer object with `next` and `error` callbacks. * - * NOTE: Although an `onCompletion` callback can be provided, it will - * never be called because the snapshot stream is never-ending. + * NOTE: Although an `onCompletion` callback can be provided, it will never be called because the + * snapshot stream is never-ending. * * @param reference - A reference to the document to listen to. * @param options - Options controlling the listen behavior. @@ -527,22 +532,18 @@ export function onSnapshot( } ): Unsubscribe; /** - * Attaches a listener for `DocumentSnapshot` events. You may either pass - * individual `onNext` and `onError` callbacks or pass a single observer - * object with `next` and `error` callbacks. + * Attaches a listener for `DocumentSnapshot` events. You may either pass individual `onNext` and + * `onError` callbacks or pass a single observer object with `next` and `error` callbacks. * - * NOTE: Although an `onCompletion` callback can be provided, it will - * never be called because the snapshot stream is never-ending. + * NOTE: Although an `onCompletion` callback can be provided, it will never be called because the + * snapshot stream is never-ending. * * @param reference - A reference to the document to listen to. - * @param onNext - A callback to be called every time a new `DocumentSnapshot` - * is available. - * @param onError - A callback to be called if the listen fails or is - * cancelled. No further callbacks will occur. - * @param onCompletion - Can be provided, but will not be called since streams are - * never ending. - * @returns An unsubscribe function that can be called to cancel - * the snapshot listener. + * @param onNext - A callback to be called every time a new `DocumentSnapshot` is available. + * @param onError - A callback to be called if the listen fails or is cancelled. No further + * callbacks will occur. + * @param onCompletion - Can be provided, but will not be called since streams are never ending. + * @returns An unsubscribe function that can be called to cancel the snapshot listener. */ export function onSnapshot( reference: DocumentReference, @@ -551,23 +552,19 @@ export function onSnapshot( onCompletion?: () => void ): Unsubscribe; /** - * Attaches a listener for `DocumentSnapshot` events. You may either pass - * individual `onNext` and `onError` callbacks or pass a single observer - * object with `next` and `error` callbacks. + * Attaches a listener for `DocumentSnapshot` events. You may either pass individual `onNext` and + * `onError` callbacks or pass a single observer object with `next` and `error` callbacks. * - * NOTE: Although an `onCompletion` callback can be provided, it will - * never be called because the snapshot stream is never-ending. + * NOTE: Although an `onCompletion` callback can be provided, it will never be called because the + * snapshot stream is never-ending. * * @param reference - A reference to the document to listen to. * @param options - Options controlling the listen behavior. - * @param onNext - A callback to be called every time a new `DocumentSnapshot` - * is available. - * @param onError - A callback to be called if the listen fails or is - * cancelled. No further callbacks will occur. - * @param onCompletion - Can be provided, but will not be called since streams are - * never ending. - * @returns An unsubscribe function that can be called to cancel - * the snapshot listener. + * @param onNext - A callback to be called every time a new `DocumentSnapshot` is available. + * @param onError - A callback to be called if the listen fails or is cancelled. No further + * callbacks will occur. + * @param onCompletion - Can be provided, but will not be called since streams are never ending. + * @returns An unsubscribe function that can be called to cancel the snapshot listener. */ export function onSnapshot( reference: DocumentReference, @@ -577,18 +574,16 @@ export function onSnapshot( onCompletion?: () => void ): Unsubscribe; /** - * Attaches a listener for `QuerySnapshot` events. You may either pass - * individual `onNext` and `onError` callbacks or pass a single observer - * object with `next` and `error` callbacks. The listener can be cancelled by - * calling the function that is returned when `onSnapshot` is called. + * Attaches a listener for `QuerySnapshot` events. You may either pass individual `onNext` and + * `onError` callbacks or pass a single observer object with `next` and `error` callbacks. The + * listener can be cancelled by calling the function that is returned when `onSnapshot` is called. * - * NOTE: Although an `onCompletion` callback can be provided, it will - * never be called because the snapshot stream is never-ending. + * NOTE: Although an `onCompletion` callback can be provided, it will never be called because the + * snapshot stream is never-ending. * * @param query - The query to listen to. * @param observer - A single object containing `next` and `error` callbacks. - * @returns An unsubscribe function that can be called to cancel - * the snapshot listener. + * @returns An unsubscribe function that can be called to cancel the snapshot listener. */ export function onSnapshot( query: Query, @@ -599,19 +594,17 @@ export function onSnapshot( } ): Unsubscribe; /** - * Attaches a listener for `QuerySnapshot` events. You may either pass - * individual `onNext` and `onError` callbacks or pass a single observer - * object with `next` and `error` callbacks. The listener can be cancelled by - * calling the function that is returned when `onSnapshot` is called. + * Attaches a listener for `QuerySnapshot` events. You may either pass individual `onNext` and + * `onError` callbacks or pass a single observer object with `next` and `error` callbacks. The + * listener can be cancelled by calling the function that is returned when `onSnapshot` is called. * - * NOTE: Although an `onCompletion` callback can be provided, it will - * never be called because the snapshot stream is never-ending. + * NOTE: Although an `onCompletion` callback can be provided, it will never be called because the + * snapshot stream is never-ending. * * @param query - The query to listen to. * @param options - Options controlling the listen behavior. * @param observer - A single object containing `next` and `error` callbacks. - * @returns An unsubscribe function that can be called to cancel - * the snapshot listener. + * @returns An unsubscribe function that can be called to cancel the snapshot listener. */ export function onSnapshot( query: Query, @@ -623,23 +616,19 @@ export function onSnapshot( } ): Unsubscribe; /** - * Attaches a listener for `QuerySnapshot` events. You may either pass - * individual `onNext` and `onError` callbacks or pass a single observer - * object with `next` and `error` callbacks. The listener can be cancelled by - * calling the function that is returned when `onSnapshot` is called. + * Attaches a listener for `QuerySnapshot` events. You may either pass individual `onNext` and + * `onError` callbacks or pass a single observer object with `next` and `error` callbacks. The + * listener can be cancelled by calling the function that is returned when `onSnapshot` is called. * - * NOTE: Although an `onCompletion` callback can be provided, it will - * never be called because the snapshot stream is never-ending. + * NOTE: Although an `onCompletion` callback can be provided, it will never be called because the + * snapshot stream is never-ending. * * @param query - The query to listen to. - * @param onNext - A callback to be called every time a new `QuerySnapshot` - * is available. - * @param onCompletion - Can be provided, but will not be called since streams are - * never ending. - * @param onError - A callback to be called if the listen fails or is - * cancelled. No further callbacks will occur. - * @returns An unsubscribe function that can be called to cancel - * the snapshot listener. + * @param onNext - A callback to be called every time a new `QuerySnapshot` is available. + * @param onCompletion - Can be provided, but will not be called since streams are never ending. + * @param onError - A callback to be called if the listen fails or is cancelled. No further + * callbacks will occur. + * @returns An unsubscribe function that can be called to cancel the snapshot listener. */ export function onSnapshot( query: Query, @@ -648,48 +637,275 @@ export function onSnapshot( onCompletion?: () => void ): Unsubscribe; /** - * Attaches a listener for `QuerySnapshot` events. You may either pass - * individual `onNext` and `onError` callbacks or pass a single observer - * object with `next` and `error` callbacks. The listener can be cancelled by - * calling the function that is returned when `onSnapshot` is called. + * Attaches a listener for `QuerySnapshot` events. You may either pass individual `onNext` and + * `onError` callbacks or pass a single observer object with `next` and `error` callbacks. The + * listener can be cancelled by calling the function that is returned when `onSnapshot` is called. * - * NOTE: Although an `onCompletion` callback can be provided, it will - * never be called because the snapshot stream is never-ending. + * NOTE: Although an `onCompletion` callback can be provided, it will never be called because the + * snapshot stream is never-ending. * * @param query - The query to listen to. * @param options - Options controlling the listen behavior. - * @param onNext - A callback to be called every time a new `QuerySnapshot` - * is available. + * @param onNext - A callback to be called every time a new `QuerySnapshot` is available. + * @param onCompletion - Can be provided, but will not be called since streams are never ending. + * @param onError - A callback to be called if the listen fails or is cancelled. No further + * callbacks will occur. + * @returns An unsubscribe function that can be called to cancel the snapshot listener. + */ +export function onSnapshot( + query: Query, + options: SnapshotListenOptions, + onNext: (snapshot: QuerySnapshot) => void, + onError?: (error: FirestoreError) => void, + onCompletion?: () => void +): Unsubscribe; + +/** + * Attaches a listener for `QuerySnapshot` events based on data generated by invoking + * {@link QuerySnapshot.toJSON} You may either pass individual `onNext` and `onError` callbacks or + * pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by + * calling the function that is returned when `onSnapshot` is called. + * + * NOTE: Although an `onCompletion` callback can be provided, it will never be called because the + * snapshot stream is never-ending. + * + * @param firestore - The {@link Firestore} instance to enable persistence for. + * @param snapshotJson - A JSON object generated by invoking {@link QuerySnapshot.toJSON}. + * @param onNext - A callback to be called every time a new `QuerySnapshot` is available. + * @param onError - A callback to be called if the listen fails or is cancelled. No further + * callbacks will occur. + * @param onCompletion - Can be provided, but will not be called since streams are never ending. + * @param converter - An optional object that converts objects from Firestore before the onNext + * listener is invoked. + * @returns An unsubscribe function that can be called to cancel the snapshot listener. + */ +export function onSnapshot( + firestore: Firestore, + snapshotJson: object, + onNext: (snapshot: QuerySnapshot) => void, + onError?: (error: FirestoreError) => void, + onCompletion?: () => void, + converter?: FirestoreDataConverter +): Unsubscribe; +/** + * Attaches a listener for `DocumentSnapshot` events based on data generated by invoking + * {@link DocumentSnapshot.toJSON} You may either pass individual `onNext` and `onError` callbacks or + * pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by + * calling the function that is returned when `onSnapshot` is called. + * + * NOTE: Although an `onCompletion` callback can be provided, it will never be called because the + * snapshot stream is never-ending. + * + * @param firestore - The {@link Firestore} instance to enable persistence for. + * @param snapshotJson - A JSON object generated by invoking {@link DocumentSnapshot.toJSON}. + * @param onNext - A callback to be called every time a new `DocumentSnapshot` is available. + * @param onError - A callback to be called if the listen fails or is cancelled. No fruther + * callbacks will occur. * @param onCompletion - Can be provided, but will not be called since streams are * never ending. - * @param onError - A callback to be called if the listen fails or is - * cancelled. No further callbacks will occur. + * @param converter - An optional object that converts objects from Firestore before the onNext + * listener is invoked. + * @returns An unsubscribe function that can be called to cancel the snapshot listener. + */ +export function onSnapshot( + firestore: Firestore, + snapshotJson: object, + onNext: (snapshot: DocumentSnapshot) => void, + onError?: (error: FirestoreError) => void, + onCompletion?: () => void, + converter?: FirestoreDataConverter +): Unsubscribe; +/** + * Attaches a listener for `QuerySnapshot` events based on data generated by invoking + * {@link QuerySnapshot.toJSON} You may either pass individual `onNext` and `onError` callbacks or + * pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by + * calling the function that is returned when `onSnapshot` is called. + * + * NOTE: Although an `onCompletion` callback can be provided, it will never be called because the + * snapshot stream is never-ending. + * + * @param firestore - The {@link Firestore} instance to enable persistence for. + * @param snapshotJson - A JSON object generated by invoking {@link QuerySnapshot.toJSON}. + * @param options - Options controlling the listen behavior. + * @param onNext - A callback to be called every time a new `QuerySnapshot` is available. + * @param onError - A callback to be called if the listen fails or is cancelled. No further + * callbacks will occur. + * @param onCompletion - Can be provided, but will not be called since streams are never ending. + * @param converter - An optional object that converts objects from Firestore before the onNext + * listener is invoked. + * @returns An unsubscribe function that can be called to cancel the snapshot listener. + */ +export function onSnapshot( + firestore: Firestore, + snapshotJson: object, + options: SnapshotListenOptions, + onNext: (snapshot: QuerySnapshot) => void, + onError?: (error: FirestoreError) => void, + onCompletion?: () => void, + converter?: FirestoreDataConverter +): Unsubscribe; +/** + * Attaches a listener for `DocumentSnapshot` events based on data generated by invoking + * {@link DocumentSnapshot.toJSON} You may either pass individual `onNext` and `onError` callbacks + * or pass a single observer object with `next` and `error` callbacks. The listener can be cancelled + * by calling the function that is returned when `onSnapshot` is called. + * + * NOTE: Although an `onCompletion` callback can be provided, it will never be called because the + * snapshot stream is never-ending. + * + * @param firestore - The {@link Firestore} instance to enable persistence for. + * @param snapshotJson - A JSON object generated by invoking {@link DocumentSnapshot.toJSON}. + * @param options - Options controlling the listen behavior. + * @param onNext - A callback to be called every time a new `DocumentSnapshot` is available. + * @param onError - A callback to be called if the listen fails or is cancelled. No further + * callbacks will occur. + * @param onCompletion - Can be provided, but will not be called since streams are never ending. + * @param converter - An optional object that converts objects from Firestore before the onNext + * listener is invoked. * @returns An unsubscribe function that can be called to cancel * the snapshot listener. */ export function onSnapshot( - query: Query, + firestore: Firestore, + snapshotJson: object, options: SnapshotListenOptions, - onNext: (snapshot: QuerySnapshot) => void, + onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, - onCompletion?: () => void + onCompletion?: () => void, + converter?: FirestoreDataConverter +): Unsubscribe; + +/** + * Attaches a listener for `QuerySnapshot` events based on QuerySnapshot data generated by invoking + * {@link QuerySnapshot.toJSON} You may either pass individual `onNext` and `onError` callbacks or + * pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by + * calling the function that is returned when `onSnapshot` is called. + * + * NOTE: Although an `onCompletion` callback can be provided, it will never be called because the + * snapshot stream is never-ending. + * + * @param firestore - The {@link Firestore} instance to enable persistence for. + * @param snapshotJson - A JSON object generated by invoking {@link QuerySnapshot.toJSON}. + * @param observer - A single object containing `next` and `error` callbacks. + * @param converter - An optional object that converts objects from Firestore before the onNext + * listener is invoked. + * @returns An unsubscribe function that can be called to cancel + * the snapshot listener. + */ +export function onSnapshot( + firestore: Firestore, + snapshotJson: object, + observer: { + next: (snapshot: QuerySnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; + }, + converter?: FirestoreDataConverter +): Unsubscribe; +/** + * Attaches a listener for `DocumentSnapshot` events based on data generated by invoking + * {@link DocumentSnapshot.toJSON} You may either pass individual `onNext` and `onError` callbacks + * or pass a single observer object with `next` and `error` callbacks. The listener can be cancelled + * by calling the function that is returned when `onSnapshot` is called. + * + * NOTE: Although an `onCompletion` callback can be provided, it will never be called because the + * snapshot stream is never-ending. + * + * @param firestore - The {@link Firestore} instance to enable persistence for. + * @param snapshotJson - A JSON object generated by invoking {@link DocumentSnapshot.toJSON}. + * @param observer - A single object containing `next` and `error` callbacks. + * @param converter - An optional object that converts objects from Firestore before the onNext + * listener is invoked. + * @returns An unsubscribe function that can be called to cancel + * the snapshot listener. + */ +export function onSnapshot( + firestore: Firestore, + snapshotJson: object, + observer: { + next: (snapshot: DocumentSnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; + }, + converter?: FirestoreDataConverter +): Unsubscribe; +/** + * Attaches a listener for `QuerySnapshot` events based on QuerySnapshot data generated by invoking + * {@link QuerySnapshot.toJSON} You may either pass individual `onNext` and `onError` callbacks or + * pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by + * calling the function that is returned when `onSnapshot` is called. + * + * NOTE: Although an `onCompletion` callback can be provided, it will never be called because the + * snapshot stream is never-ending. + * + * @param firestore - The {@link Firestore} instance to enable persistence for. + * @param snapshotJson - A JSON object generated by invoking {@link QuerySnapshot.toJSON}. + * @param options - Options controlling the listen behavior. + * @param observer - A single object containing `next` and `error` callbacks. + * @param converter - An optional object that converts objects from Firestore before the onNext + * listener is invoked. + * @returns An unsubscribe function that can be called to cancel + * the snapshot listener. + */ +export function onSnapshot( + firestore: Firestore, + snapshotJson: object, + options: SnapshotListenOptions, + observer: { + next: (snapshot: QuerySnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; + }, + converter?: FirestoreDataConverter +): Unsubscribe; +/** + * Attaches a listener for `DocumentSnapshot` events based on QuerySnapshot data generated by + * invoking {@link DocumentSnapshot.toJSON} You may either pass individual `onNext` and `onError` + * callbacks or pass a single observer object with `next` and `error` callbacks. The listener can be + * cancelled by calling the function that is returned when `onSnapshot` is called. + * + * NOTE: Although an `onCompletion` callback can be provided, it will never be called because the + * snapshot stream is never-ending. + * + * @param firestore - The {@link Firestore} instance to enable persistence for. + * @param snapshotJson - A JSON object generated by invoking {@link DocumentSnapshot.toJSON}. + * @param options - Options controlling the listen behavior. + * @param observer - A single object containing `next` and `error` callbacks. + * @param converter - An optional object that converts objects from Firestore before the onNext + * listener is invoked. + * @returns An unsubscribe function that can be called to cancel the snapshot listener. + */ +export function onSnapshot( + firestore: Firestore, + snapshotJson: object, + options: SnapshotListenOptions, + observer: { + next: (snapshot: DocumentSnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; + }, + converter?: FirestoreDataConverter ): Unsubscribe; export function onSnapshot( reference: | Query - | DocumentReference, + | DocumentReference + | Firestore, ...args: unknown[] ): Unsubscribe { - reference = getModularInstance(reference); + if (reference instanceof Firestore) { + return onSnapshotBundle(reference as Firestore, ...args); + } + // onSnapshot for Query or Document. + reference = getModularInstance(reference); let options: SnapshotListenOptions = { includeMetadataChanges: false, source: 'default' }; let currArg = 0; if (typeof args[currArg] === 'object' && !isPartialObserver(args[currArg])) { - options = args[currArg] as SnapshotListenOptions; - currArg++; + options = args[currArg++] as SnapshotListenOptions; } const internalOptions = { @@ -736,7 +952,6 @@ export function onSnapshot( firestore = cast(query.firestore, Firestore); internalQuery = query._query; const userDataWriter = new ExpUserDataWriter(firestore); - observer = { next: snapshot => { if (args[currArg]) { @@ -859,3 +1074,285 @@ function convertToDocSnapshot( ref.converter ); } + +/** + * Handles {@link onSnapshot} for a bundle generated by calling {@link QuerySnapshot.toJSON} or + * {@link DocumentSnapshot.toJSON}. Parse the JSON object containing the bundle to determine the + * `bundleSource` (either form a {@link DocumentSnapshot} or {@link QuerySnapshot}, and marshall the + * other optional parameters before sending the request to either + * {@link onSnapshotDocumentSnapshotBundle} or {@link onSnapshotQuerySnapshotBundle}, respectively. + * + * @param firestore - The {@link Firestore} instance for the {@link onSnapshot} operation request. + * @param args - The variadic arguments passed to {@link onSnapshot}. + * @returns An unsubscribe function that can be called to cancel the snapshot + * listener. + * + * @internal + */ +function onSnapshotBundle( + reference: Firestore, + ...args: unknown[] +): Unsubscribe { + const db = getModularInstance(reference); + let curArg = 0; + const snapshotJson = normalizeSnapshotJsonFields(args[curArg++] as object); + if (snapshotJson.error) { + throw new FirestoreError(Code.INVALID_ARGUMENT, snapshotJson.error); + } + let options: SnapshotListenOptions | undefined = undefined; + if (typeof args[curArg] === 'object' && !isPartialObserver(args[curArg])) { + options = args[curArg++] as SnapshotListenOptions; + } + + if (snapshotJson.bundleSource === 'QuerySnapshot') { + let observer: { + next: (snapshot: QuerySnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; + } | null = null; + if (typeof args[curArg] === 'object' && isPartialObserver(args[1])) { + const userObserver = args[curArg++] as PartialObserver< + QuerySnapshot + >; + observer = { + next: userObserver.next!, + error: userObserver.error, + complete: userObserver.complete + }; + } else { + observer = { + next: args[curArg++] as ( + snapshot: QuerySnapshot + ) => void, + error: args[curArg++] as (error: FirestoreError) => void, + complete: args[curArg++] as () => void + }; + } + return onSnapshotQuerySnapshotBundle( + db, + snapshotJson, + options, + observer!, + args[curArg] as FirestoreDataConverter + ); + } else if (snapshotJson.bundleSource === 'DocumentSnapshot') { + let observer: { + next: (snapshot: DocumentSnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; + } | null = null; + if (typeof args[curArg] === 'object' && isPartialObserver(args[1])) { + const userObserver = args[curArg++] as PartialObserver< + DocumentSnapshot + >; + observer = { + next: userObserver.next!, + error: userObserver.error, + complete: userObserver.complete + }; + } else { + observer = { + next: args[curArg++] as ( + snapshot: DocumentSnapshot + ) => void, + error: args[curArg++] as (error: FirestoreError) => void, + complete: args[curArg++] as () => void + }; + } + return onSnapshotDocumentSnapshotBundle( + db, + snapshotJson, + options, + observer!, + args[curArg] as FirestoreDataConverter + ); + } else { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + `unsupported bundle source: ${snapshotJson.bundleSource}` + ); + } +} + +/** + * Ensures the data required to construct an {@link onSnapshot} listener exist in a `snapshotJson` + * object that originates from {@link DocumentSnapshot.toJSON} or {@link Querysnapshot.toJSON}. The + * data is normalized into a typed object. + * + * @param snapshotJson - The JSON object that the app provided to {@link onSnapshot}. + * @returns A normalized object that contains all of the required bundle JSON fields. If + * {@link snapshotJson} doesn't contain the required fields, or if the fields exist as empty + * strings, then the {@link snapshotJson.error} field will be a non empty string. + * + * @internal + */ +function normalizeSnapshotJsonFields(snapshotJson: object): { + bundle: string; + bundleName: string; + bundleSource: string; + error?: string; +} { + const result: { + bundle: string; + bundleName: string; + bundleSource: string; + error?: string; + } = { + bundle: '', + bundleName: '', + bundleSource: '' + }; + const requiredKeys = ['bundle', 'bundleName', 'bundleSource']; + for (const key of requiredKeys) { + if (!(key in snapshotJson)) { + result.error = `snapshotJson missing required field: ${key}`; + break; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const value = (snapshotJson as any)[key]; + if (typeof value !== 'string') { + result.error = `snapshotJson field '${key}' must be a string.`; + break; + } + if (value.length === 0) { + result.error = `snapshotJson field '${key}' cannot be an empty string.`; + break; + } + if (key === 'bundle') { + result.bundle = value; + } else if (key === 'bundleName') { + result.bundleName = value; + } else if (key === 'bundleSource') { + result.bundleSource = value; + } + } + return result; +} + +/** + * Loads the bundle in a separate task and then invokes {@link onSnapshot} with a + * {@link DocumentReference} for the document in the bundle. + * + * @param firestore - The {@link Firestore} instance for the {@link onSnapshot} operation request. + * @param json - The JSON bundle to load, produced by {@link DocumentSnapshot.toJSON}. + * @param options - Options controlling the listen behavior. + * @param observer - A single object containing `next` and `error` callbacks. + * @param converter - An optional object that converts objects from Firestore before the onNext + * listener is invoked. + * @returns An unsubscribe function that can be called to cancel the snapshot + * listener. + * + * @internal + */ +function onSnapshotDocumentSnapshotBundle< + AppModelType, + DbModelType extends DocumentData +>( + db: Firestore, + json: { bundle: string; bundleName: string; bundleSource: string }, + options: SnapshotListenOptions | undefined, + observer: { + next: (snapshot: DocumentSnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; + }, + converter?: FirestoreDataConverter +): Unsubscribe { + let unsubscribed: boolean = false; + let internalUnsubscribe: Unsubscribe | undefined; + const loadTask = loadBundle(db, json.bundle); + loadTask + .then(() => { + if (!unsubscribed) { + const docReference = new DocumentReference( + db, + converter ? converter : null, + DocumentKey.fromPath(json.bundleName) + ); + internalUnsubscribe = onSnapshot( + docReference as DocumentReference, + options ? options : {}, + observer + ); + } + }) + .catch(e => { + if (observer.error) { + observer.error(e); + } + return () => {}; + }); + return () => { + if (unsubscribed) { + return; + } + unsubscribed = true; + if (internalUnsubscribe) { + internalUnsubscribe(); + } + }; +} + +/** + * Loads the bundle in a separate task and then invokes {@link onSnapshot} with a + * {@link Query} that represents the Query in the bundle. + * + * @param firestore - The {@link Firestore} instance for the {@link onSnapshot} operation request. + * @param json - The JSON bundle to load, produced by {@link QuerySnapshot.toJSON}. + * @param options - Options controlling the listen behavior. + * @param observer - A single object containing `next` and `error` callbacks. + * @param converter - An optional object that converts objects from Firestore before the onNext + * listener is invoked. + * @returns An unsubscribe function that can be called to cancel the snapshot + * listener. + * + * @internal + */ +function onSnapshotQuerySnapshotBundle< + AppModelType, + DbModelType extends DocumentData +>( + db: Firestore, + json: { bundle: string; bundleName: string; bundleSource: string }, + options: SnapshotListenOptions | undefined, + observer: { + next: (snapshot: QuerySnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; + }, + converter?: FirestoreDataConverter +): Unsubscribe { + let unsubscribed: boolean = false; + let internalUnsubscribe: Unsubscribe | undefined; + const loadTask = loadBundle(db, json.bundle); + loadTask + .then(() => namedQuery(db, json.bundleName)) + .then(query => { + if (query && !unsubscribed) { + const realQuery: Query = (query as Query)!; + if (converter) { + realQuery.withConverter(converter); + } + internalUnsubscribe = onSnapshot( + query as Query, + options ? options : {}, + observer + ); + } + }) + .catch(e => { + if (observer.error) { + observer.error(e); + } + return () => {}; + }); + return () => { + if (unsubscribed) { + return; + } + unsubscribed = true; + if (internalUnsubscribe) { + internalUnsubscribe(); + } + }; +} diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index 9d2ddf41a7e..4f2f056d720 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -508,7 +508,8 @@ export class DocumentSnapshot< // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: any = {}; result['bundle'] = ''; - result['source'] = 'DocumentSnapshot'; + result['bundleSource'] = 'DocumentSnapshot'; + result['bundleName'] = this._key.toString(); if ( !document || @@ -701,10 +702,12 @@ export class QuerySnapshot< toJSON(): object { // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: any = {}; - result['source'] = 'QuerySnapshot'; + result['bundleSource'] = 'QuerySnapshot'; + result['bundleName'] = AutoId.newId(); + const builder: BundleBuilder = new BundleBuilder( this._firestore, - AutoId.newId() + result['bundleName'] ); const databaseId = this._firestore._databaseId.database; const projectId = this._firestore._databaseId.projectId; @@ -735,6 +738,7 @@ export class QuerySnapshot< ); }); const bundleData: QuerySnapshotBundleData = { + name: result['bundleName'], query: this.query._query, parent, docBundleDataArray diff --git a/packages/firestore/src/util/bundle_builder_impl.ts b/packages/firestore/src/util/bundle_builder_impl.ts index d516e512db0..dc94ebca495 100644 --- a/packages/firestore/src/util/bundle_builder_impl.ts +++ b/packages/firestore/src/util/bundle_builder_impl.ts @@ -44,7 +44,6 @@ import { Document as ProtoDocument, Document } from '../protos/firestore_proto_api'; -import { AutoId } from '../util/misc'; const BUNDLE_VERSION = 1; @@ -140,13 +139,12 @@ export class BundleBuilder { * dependency error. */ addBundleQuery(queryBundleData: QuerySnapshotBundleData): void { - const name = AutoId.newId(); - if (this.namedQueries.has(name)) { + if (this.namedQueries.has(queryBundleData.name)) { throw new Error(`Query name conflict: ${name} has already been added.`); } let latestReadTime = new Timestamp(0, 0); for (const docBundleData of queryBundleData.docBundleDataArray) { - this.addBundleDocument(docBundleData, name); + this.addBundleDocument(docBundleData, queryBundleData.name); if (docBundleData.readTime && docBundleData.readTime > latestReadTime) { latestReadTime = docBundleData.readTime; } @@ -159,8 +157,8 @@ export class BundleBuilder { parent: queryBundleData.parent, structuredQuery: queryTarget.queryTarget.structuredQuery }; - this.namedQueries.set(name, { - name, + this.namedQueries.set(queryBundleData.name, { + name: queryBundleData.name, bundledQuery, readTime: toTimestamp(this.serializer, latestReadTime) }); @@ -267,6 +265,7 @@ export interface DocumentSnapshotBundleData { * @internal */ export interface QuerySnapshotBundleData { + name: string; query: Query; parent: string; docBundleDataArray: DocumentSnapshotBundleData[]; diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 8cbe99b3cd9..b6320169582 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -1200,6 +1200,275 @@ apiDescribe('Database', persistence => { }); }); + it('DocumentSnapshot events for snapshot created by a bundle', async () => { + const initialData = { a: 0 }; + const finalData = { a: 1 }; + await withTestDocAndInitialData( + persistence, + initialData, + async (docRef, db) => { + const doc = await getDoc(docRef); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshot( + db, + doc.toJSON(), + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(initialData); + }) + .then(() => setDoc(docRef, finalData)) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(finalData); + }); + unsubscribe(); + } + ); + }); + + it('DocumentSnapshot updated doc events in snapshot created by a bundle', async () => { + const initialData = { a: 0 }; + const finalData = { a: 1 }; + await withTestDocAndInitialData( + persistence, + initialData, + async (docRef, db) => { + const doc = await getDoc(docRef); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshot( + db, + doc.toJSON(), + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(initialData); + }) + .then(() => setDoc(docRef, finalData)) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(finalData); + }); + unsubscribe(); + } + ); + }); + + it('DocumentSnapshot observer events for snapshot created by a bundle', async () => { + const initialData = { a: 0 }; + const finalData = { a: 1 }; + await withTestDocAndInitialData( + persistence, + initialData, + async (docRef, db) => { + const doc = await getDoc(docRef); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshot(db, doc.toJSON(), { + next: accumulator.storeEvent + }); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(initialData); + }) + .then(() => setDoc(docRef, finalData)) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(finalData); + }); + unsubscribe(); + } + ); + }); + + it('DocumentSnapshot error events for snapshot created by a bundle', async () => { + return withTestDb(persistence, async db => { + const json = { + bundle: 'BadData', + bundleName: 'bundleName', + bundleSource: 'DocumentSnapshot' + }; + const deferred = new Deferred(); + const unsubscribe = onSnapshot( + db, + json, + ds => { + expect(ds).to.not.exist; + deferred.resolve(); + }, + err => { + expect(err.name).to.exist; + expect(err.message).to.exist; + deferred.resolve(); + } + ); + await deferred.promise; + unsubscribe(); + }); + }); + + it('DocumentSnapshot observer error events for snapshot created by a bundle', async () => { + return withTestDb(persistence, async db => { + const json = { + bundle: 'BadData', + bundleName: 'bundleName', + bundleSource: 'QuerySnapshot' + }; + const deferred = new Deferred(); + const unsubscribe = onSnapshot(db, json, { + next: ds => { + expect(ds).to.not.exist; + deferred.resolve(); + }, + error: err => { + expect(err.name).to.exist; + expect(err.message).to.exist; + deferred.resolve(); + } + }); + await deferred.promise; + unsubscribe(); + }); + }); + + it('Querysnapshot events for snapshot created by a bundle', async () => { + const testDocs = { + a: { foo: 1 }, + b: { bar: 2 } + }; + await withTestCollection(persistence, testDocs, async (coll, db) => { + const querySnap = await getDocs(query(coll, orderBy(documentId()))); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshot( + db, + querySnap.toJSON(), + accumulator.storeEvent + ); + await accumulator.awaitEvent().then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal(testDocs.a); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }); + unsubscribe(); + }); + }); + + it('Querysnapshot observer events for snapshot created by a bundle', async () => { + const testDocs = { + a: { foo: 1 }, + b: { bar: 2 } + }; + await withTestCollection(persistence, testDocs, async (coll, db) => { + const querySnap = await getDocs(query(coll, orderBy(documentId()))); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshot(db, querySnap.toJSON(), { + next: accumulator.storeEvent + }); + await accumulator.awaitEvent().then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal(testDocs.a); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }); + unsubscribe(); + }); + }); + + it('QuerySnapshot error events for snapshot created by a bundle', async () => { + return withTestDb(persistence, async db => { + const json = { + bundle: 'BadData', + bundleName: 'bundleName', + bundleSource: 'QuerySnapshot' + }; + const deferred = new Deferred(); + const unsubscribe = onSnapshot( + db, + json, + qs => { + expect(qs).to.not.exist; + deferred.resolve(); + }, + err => { + expect(err.name).to.exist; + expect(err.message).to.exist; + deferred.resolve(); + } + ); + await deferred.promise; + unsubscribe(); + }); + }); + + it('QuerySnapshot observer error events for snapshot created by a bundle', async () => { + return withTestDb(persistence, async db => { + const json = { + bundle: 'BadData', + bundleName: 'bundleName', + bundleSource: 'QuerySnapshot' + }; + const deferred = new Deferred(); + const unsubscribe = onSnapshot(db, json, { + next: qs => { + expect(qs).to.not.exist; + deferred.resolve(); + }, + error: err => { + expect(err.name).to.exist; + expect(err.message).to.exist; + deferred.resolve(); + } + }); + await deferred.promise; + unsubscribe(); + }); + }); + + it('QuerySnapshot updated doc events in snapshot created by a bundle', async () => { + const testDocs = { + a: { foo: 1 }, + b: { bar: 2 } + }; + await withTestCollection(persistence, testDocs, async (coll, db) => { + const querySnap = await getDocs(query(coll, orderBy(documentId()))); + const refForDocA = querySnap.docs[0].ref; + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshot( + db, + querySnap.toJSON(), + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal(testDocs.a); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }) + .then(() => setDoc(refForDocA, { foo: 0 })) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal({ foo: 0 }); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }); + unsubscribe(); + }); + }); + it('Metadata only changes are not fired when no options provided', () => { return withTestDoc(persistence, docRef => { const secondUpdateFound = new Deferred(); From 41b82e9a992596cd267e30bb7d55d0abbcc9cdc1 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Thu, 24 Apr 2025 15:59:08 -0400 Subject: [PATCH 3/7] Squashed commit of the following: commit 05338d8c0ffa61edc164515a77864bf6147f1e1e Author: DellaBitta Date: Thu Apr 24 15:58:18 2025 -0400 test fixes commit 28c6da9967ee3dd0868e68cd5c8ead68799da073 Author: DellaBitta Date: Thu Apr 24 15:39:22 2025 -0400 update vector_value commit 0aa81ce68ef964ce6d0487676a88da9635124208 Author: DellaBitta Date: Thu Apr 24 15:39:11 2025 -0400 update reference commit e20bac9dab1689027154aeae8744971735e4354b Author: DellaBitta Date: Thu Apr 24 15:24:50 2025 -0400 update timestamp commit 05b29c41b28828e99be5203e9975ca611400cf30 Author: DellaBitta Date: Thu Apr 24 15:24:39 2025 -0400 update geopoint commit 4cf66f2965697f89c063fc071e08661c4c28e38a Author: DellaBitta Date: Thu Apr 24 15:16:15 2025 -0400 convert DocumentReference to new JSON validator. commit 00a2423f8665540f2b38ad222596eb58b15003e5 Author: DellaBitta Date: Thu Apr 24 13:44:27 2025 -0400 format fix. commit 09eb5cf2238fe7d66aabbb1583974b72cadc598e Author: DellaBitta Date: Thu Apr 24 13:43:13 2025 -0400 Bytes prototype. commit b753f4980856c90f21b68f4a9033263698c4c5a9 Author: DellaBitta Date: Thu Apr 24 13:26:41 2025 -0400 Linter doesn't like it, but API extractor does commit 775cc69c02ef4e6331b3f7f3bc0e6d27cecd72ae Author: DellaBitta Date: Thu Apr 17 13:13:52 2025 -0400 lower case types commit 188c86f33f054c9dd18990627c8105db136558d5 Author: DellaBitta Date: Thu Apr 17 10:41:58 2025 -0400 DocumentReference unit and integration tests commit 44d80459a3c2a05fa1946627674aa185d05a82eb Author: DellaBitta Date: Wed Apr 16 20:39:55 2025 -0400 map Timestamp fields to DbTimestamp fields commit f305182289fad0f964811050616792dd416702dd Author: DellaBitta Date: Wed Apr 16 15:54:23 2025 -0400 DocumentReference impl commit 05a49aabcd553b003681680f069444c03f98d07d Author: DellaBitta Date: Wed Apr 16 11:14:00 2025 -0400 Timestamp, GeoPoint, VectorValue commit 1652e8bdc51f23b1048c7d730598f9f39faeb349 Author: DellaBitta Date: Tue Apr 15 17:33:51 2025 -0400 format commit 8a081e183d9b25c8123ed778faed6169995ffe96 Author: DellaBitta Date: Tue Apr 15 17:26:09 2025 -0400 Bytes and VectorValue to/from JSON commit 2a07791fb34239f26114877e12ba7ecfef5ec799 Author: DellaBitta Date: Tue Apr 15 14:07:25 2025 -0400 Bytes --- common/api-review/firestore-lite.api.md | 10 ++ common/api-review/firestore.api.md | 10 ++ docs-devsite/firestore_.bytes.md | 35 +++++ docs-devsite/firestore_.documentreference.md | 37 +++++ docs-devsite/firestore_.geopoint.md | 24 ++- docs-devsite/firestore_.timestamp.md | 24 ++- docs-devsite/firestore_.vectorvalue.md | 35 +++++ docs-devsite/firestore_lite.bytes.md | 35 +++++ .../firestore_lite.documentreference.md | 37 +++++ docs-devsite/firestore_lite.geopoint.md | 24 ++- docs-devsite/firestore_lite.timestamp.md | 24 ++- docs-devsite/firestore_lite.vectorvalue.md | 35 +++++ packages/firestore/src/lite-api/bytes.ts | 28 ++++ packages/firestore/src/lite-api/geo_point.ts | 35 ++++- packages/firestore/src/lite-api/reference.ts | 39 +++++ packages/firestore/src/lite-api/timestamp.ts | 29 +++- .../firestore/src/lite-api/vector_value.ts | 37 +++++ .../firestore/src/util/json_validation.ts | 141 ++++++++++++++++++ .../firestore/test/lite/integration.test.ts | 14 ++ .../firestore/test/unit/api/bytes.test.ts | 48 ++++++ .../firestore/test/unit/api/database.test.ts | 35 +++++ .../firestore/test/unit/api/geo_point.test.ts | 87 ++++++++++- .../firestore/test/unit/api/timestamp.test.ts | 92 +++++++++++- .../test/unit/api/vector_value.test.ts | 73 +++++++++ .../unit/local/indexeddb_persistence.test.ts | 6 +- 25 files changed, 976 insertions(+), 18 deletions(-) create mode 100644 packages/firestore/src/util/json_validation.ts create mode 100644 packages/firestore/test/unit/api/vector_value.test.ts diff --git a/common/api-review/firestore-lite.api.md b/common/api-review/firestore-lite.api.md index 4a9ef4c0171..0dd52644d8f 100644 --- a/common/api-review/firestore-lite.api.md +++ b/common/api-review/firestore-lite.api.md @@ -68,9 +68,11 @@ export function average(field: string | FieldPath): AggregateField { readonly converter: FirestoreDataConverter | null; readonly firestore: Firestore; + static fromJSON(firestore: Firestore, json: object, converter?: FirestoreDataConverter): DocumentReference; get id(): string; get parent(): CollectionReference; get path(): string; + toJSON(): object; readonly type = "document"; withConverter(converter: FirestoreDataConverter): DocumentReference; withConverter(converter: null): DocumentReference; @@ -205,12 +209,14 @@ export type FirestoreErrorCode = 'cancelled' | 'unknown' | 'invalid-argument' | // @public export class GeoPoint { constructor(latitude: number, longitude: number); + static fromJSON(json: object): GeoPoint; isEqual(other: GeoPoint): boolean; get latitude(): number; get longitude(): number; toJSON(): { latitude: number; longitude: number; + type: string; }; } @@ -416,6 +422,7 @@ export class Timestamp { seconds: number, nanoseconds: number); static fromDate(date: Date): Timestamp; + static fromJSON(json: object): Timestamp; static fromMillis(milliseconds: number): Timestamp; isEqual(other: Timestamp): boolean; readonly nanoseconds: number; @@ -425,6 +432,7 @@ export class Timestamp { toJSON(): { seconds: number; nanoseconds: number; + type: string; }; toMillis(): number; toString(): string; @@ -466,8 +474,10 @@ export function vector(values?: number[]): VectorValue; // @public export class VectorValue { /* Excluded from this release type: __constructor */ + static fromJSON(json: object): VectorValue; isEqual(other: VectorValue): boolean; toArray(): number[]; + toJSON(): object; } // @public diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index ebd620e43ca..7bce21a7cc4 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -68,9 +68,11 @@ export function average(field: string | FieldPath): AggregateField { readonly converter: FirestoreDataConverter | null; readonly firestore: Firestore; + static fromJSON(firestore: Firestore, json: object, converter?: FirestoreDataConverter): DocumentReference; get id(): string; get parent(): CollectionReference; get path(): string; + toJSON(): object; readonly type = "document"; withConverter(converter: FirestoreDataConverter): DocumentReference; withConverter(converter: null): DocumentReference; @@ -266,12 +270,14 @@ export interface FirestoreSettings { // @public export class GeoPoint { constructor(latitude: number, longitude: number); + static fromJSON(json: object): GeoPoint; isEqual(other: GeoPoint): boolean; get latitude(): number; get longitude(): number; toJSON(): { latitude: number; longitude: number; + type: string; }; } @@ -740,6 +746,7 @@ export class Timestamp { seconds: number, nanoseconds: number); static fromDate(date: Date): Timestamp; + static fromJSON(json: object): Timestamp; static fromMillis(milliseconds: number): Timestamp; isEqual(other: Timestamp): boolean; readonly nanoseconds: number; @@ -749,6 +756,7 @@ export class Timestamp { toJSON(): { seconds: number; nanoseconds: number; + type: string; }; toMillis(): number; toString(): string; @@ -795,8 +803,10 @@ export function vector(values?: number[]): VectorValue; // @public export class VectorValue { /* Excluded from this release type: __constructor */ + static fromJSON(json: object): VectorValue; isEqual(other: VectorValue): boolean; toArray(): number[]; + toJSON(): object; } // @public diff --git a/docs-devsite/firestore_.bytes.md b/docs-devsite/firestore_.bytes.md index 8060d394a45..ac77ccb6458 100644 --- a/docs-devsite/firestore_.bytes.md +++ b/docs-devsite/firestore_.bytes.md @@ -23,9 +23,11 @@ export declare class Bytes | Method | Modifiers | Description | | --- | --- | --- | | [fromBase64String(base64)](./firestore_.bytes.md#bytesfrombase64string) | static | Creates a new Bytes object from the given Base64 string, converting it to bytes. | +| [fromJSON(json)](./firestore_.bytes.md#bytesfromjson) | static | Builds a Bytes instance from a JSON serialized version of Bytes. | | [fromUint8Array(array)](./firestore_.bytes.md#bytesfromuint8array) | static | Creates a new Bytes object from the given Uint8Array. | | [isEqual(other)](./firestore_.bytes.md#bytesisequal) | | Returns true if this Bytes object is equal to the provided one. | | [toBase64()](./firestore_.bytes.md#bytestobase64) | | Returns the underlying bytes as a Base64-encoded string. | +| [toJSON()](./firestore_.bytes.md#bytestojson) | | Returns a JSON-serializable representation of this Bytes instance. | | [toString()](./firestore_.bytes.md#bytestostring) | | Returns a string representation of the Bytes object. | | [toUint8Array()](./firestore_.bytes.md#bytestouint8array) | | Returns the underlying bytes in a new Uint8Array. | @@ -49,6 +51,26 @@ static fromBase64String(base64: string): Bytes; [Bytes](./firestore_.bytes.md#bytes_class) +## Bytes.fromJSON() + +Builds a `Bytes` instance from a JSON serialized version of `Bytes`. + +Signature: + +```typescript +static fromJSON(json: object): Bytes; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| json | object | | + +Returns: + +[Bytes](./firestore_.bytes.md#bytes_class) + ## Bytes.fromUint8Array() Creates a new `Bytes` object from the given Uint8Array. @@ -106,6 +128,19 @@ string The Base64-encoded string created from the `Bytes` object. +## Bytes.toJSON() + +Returns a JSON-serializable representation of this `Bytes` instance. + +Signature: + +```typescript +toJSON(): object; +``` +Returns: + +object + ## Bytes.toString() Returns a string representation of the `Bytes` object. diff --git a/docs-devsite/firestore_.documentreference.md b/docs-devsite/firestore_.documentreference.md index c63ba6eab0a..e786b45a086 100644 --- a/docs-devsite/firestore_.documentreference.md +++ b/docs-devsite/firestore_.documentreference.md @@ -33,6 +33,8 @@ export declare class DocumentReferencestatic | Builds a DocumentReference instance from a JSON serialized version of DocumentReference. | +| [toJSON()](./firestore_.documentreference.md#documentreferencetojson) | | Returns a JSON-serializable representation of this DocumentReference. | | [withConverter(converter)](./firestore_.documentreference.md#documentreferencewithconverter) | | Applies a custom data converter to this DocumentReference, allowing you to use your own custom model objects with Firestore. When you call [setDoc()](./firestore_lite.md#setdoc_ee215ad), [getDoc()](./firestore_lite.md#getdoc_4569087), etc. with the returned DocumentReference instance, the provided converter will convert between Firestore data of type NewDbModelType and your custom type NewAppModelType. | | [withConverter(converter)](./firestore_.documentreference.md#documentreferencewithconverter) | | Removes the current converter. | @@ -96,6 +98,41 @@ The type of this Firestore reference. readonly type = "document"; ``` +## DocumentReference.fromJSON() + +Builds a `DocumentReference` instance from a JSON serialized version of `DocumentReference`. + +Signature: + +```typescript +static fromJSON(firestore: Firestore, json: object, converter?: FirestoreDataConverter): DocumentReference; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| firestore | [Firestore](./firestore_.firestore.md#firestore_class) | | +| json | object | | +| converter | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface)<NewAppModelType, NewDbModelType> | | + +Returns: + +[DocumentReference](./firestore_.documentreference.md#documentreference_class)<NewAppModelType, NewDbModelType> + +## DocumentReference.toJSON() + +Returns a JSON-serializable representation of this DocumentReference. + +Signature: + +```typescript +toJSON(): object; +``` +Returns: + +object + ## DocumentReference.withConverter() Applies a custom data converter to this `DocumentReference`, allowing you to use your own custom model objects with Firestore. When you call [setDoc()](./firestore_lite.md#setdoc_ee215ad), [getDoc()](./firestore_lite.md#getdoc_4569087), etc. with the returned `DocumentReference` instance, the provided converter will convert between Firestore data of type `NewDbModelType` and your custom type `NewAppModelType`. diff --git a/docs-devsite/firestore_.geopoint.md b/docs-devsite/firestore_.geopoint.md index d4264a39f5f..253b0ae4140 100644 --- a/docs-devsite/firestore_.geopoint.md +++ b/docs-devsite/firestore_.geopoint.md @@ -37,6 +37,7 @@ export declare class GeoPoint | Method | Modifiers | Description | | --- | --- | --- | +| [fromJSON(json)](./firestore_.geopoint.md#geopointfromjson) | static | Builds a Timestamp instance from a JSON serialized version of Bytes. | | [isEqual(other)](./firestore_.geopoint.md#geopointisequal) | | Returns true if this GeoPoint is equal to the provided one. | | [toJSON()](./firestore_.geopoint.md#geopointtojson) | | Returns a JSON-serializable representation of this GeoPoint. | @@ -77,6 +78,26 @@ The longitude of this `GeoPoint` instance. get longitude(): number; ``` +## GeoPoint.fromJSON() + +Builds a `Timestamp` instance from a JSON serialized version of `Bytes`. + +Signature: + +```typescript +static fromJSON(json: object): GeoPoint; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| json | object | | + +Returns: + +[GeoPoint](./firestore_.geopoint.md#geopoint_class) + ## GeoPoint.isEqual() Returns true if this `GeoPoint` is equal to the provided one. @@ -109,9 +130,10 @@ Returns a JSON-serializable representation of this GeoPoint. toJSON(): { latitude: number; longitude: number; + type: string; }; ``` Returns: -{ latitude: number; longitude: number; } +{ latitude: number; longitude: number; type: string; } diff --git a/docs-devsite/firestore_.timestamp.md b/docs-devsite/firestore_.timestamp.md index 6f7a7dd011b..dd902aeba75 100644 --- a/docs-devsite/firestore_.timestamp.md +++ b/docs-devsite/firestore_.timestamp.md @@ -40,6 +40,7 @@ export declare class Timestamp | Method | Modifiers | Description | | --- | --- | --- | | [fromDate(date)](./firestore_.timestamp.md#timestampfromdate) | static | Creates a new timestamp from the given date. | +| [fromJSON(json)](./firestore_.timestamp.md#timestampfromjson) | static | Builds a Timestamp instance from a JSON serialized version of Bytes. | | [fromMillis(milliseconds)](./firestore_.timestamp.md#timestampfrommillis) | static | Creates a new timestamp from the given number of milliseconds. | | [isEqual(other)](./firestore_.timestamp.md#timestampisequal) | | Returns true if this Timestamp is equal to the provided one. | | [now()](./firestore_.timestamp.md#timestampnow) | static | Creates a new timestamp with the current date, with millisecond precision. | @@ -110,6 +111,26 @@ static fromDate(date: Date): Timestamp; A new `Timestamp` representing the same point in time as the given date. +## Timestamp.fromJSON() + +Builds a `Timestamp` instance from a JSON serialized version of `Bytes`. + +Signature: + +```typescript +static fromJSON(json: object): Timestamp; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| json | object | | + +Returns: + +[Timestamp](./firestore_.timestamp.md#timestamp_class) + ## Timestamp.fromMillis() Creates a new timestamp from the given number of milliseconds. @@ -194,11 +215,12 @@ Returns a JSON-serializable representation of this `Timestamp`. toJSON(): { seconds: number; nanoseconds: number; + type: string; }; ``` Returns: -{ seconds: number; nanoseconds: number; } +{ seconds: number; nanoseconds: number; type: string; } ## Timestamp.toMillis() diff --git a/docs-devsite/firestore_.vectorvalue.md b/docs-devsite/firestore_.vectorvalue.md index e35e96ec9ec..c3e346a4855 100644 --- a/docs-devsite/firestore_.vectorvalue.md +++ b/docs-devsite/firestore_.vectorvalue.md @@ -24,8 +24,30 @@ export declare class VectorValue | Method | Modifiers | Description | | --- | --- | --- | +| [fromJSON(json)](./firestore_.vectorvalue.md#vectorvaluefromjson) | static | Builds a Bytes instance from a JSON serialized version of Bytes. | | [isEqual(other)](./firestore_.vectorvalue.md#vectorvalueisequal) | | Returns true if the two VectorValue values have the same raw number arrays, returns false otherwise. | | [toArray()](./firestore_.vectorvalue.md#vectorvaluetoarray) | | Returns a copy of the raw number array form of the vector. | +| [toJSON()](./firestore_.vectorvalue.md#vectorvaluetojson) | | Returns a JSON-serializable representation of this VectorValue instance. | + +## VectorValue.fromJSON() + +Builds a `Bytes` instance from a JSON serialized version of `Bytes`. + +Signature: + +```typescript +static fromJSON(json: object): VectorValue; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| json | object | | + +Returns: + +[VectorValue](./firestore_.vectorvalue.md#vectorvalue_class) ## VectorValue.isEqual() @@ -60,3 +82,16 @@ toArray(): number[]; number\[\] +## VectorValue.toJSON() + +Returns a JSON-serializable representation of this `VectorValue` instance. + +Signature: + +```typescript +toJSON(): object; +``` +Returns: + +object + diff --git a/docs-devsite/firestore_lite.bytes.md b/docs-devsite/firestore_lite.bytes.md index 51cfb0a9bf0..87d5076cfb1 100644 --- a/docs-devsite/firestore_lite.bytes.md +++ b/docs-devsite/firestore_lite.bytes.md @@ -23,9 +23,11 @@ export declare class Bytes | Method | Modifiers | Description | | --- | --- | --- | | [fromBase64String(base64)](./firestore_lite.bytes.md#bytesfrombase64string) | static | Creates a new Bytes object from the given Base64 string, converting it to bytes. | +| [fromJSON(json)](./firestore_lite.bytes.md#bytesfromjson) | static | Builds a Bytes instance from a JSON serialized version of Bytes. | | [fromUint8Array(array)](./firestore_lite.bytes.md#bytesfromuint8array) | static | Creates a new Bytes object from the given Uint8Array. | | [isEqual(other)](./firestore_lite.bytes.md#bytesisequal) | | Returns true if this Bytes object is equal to the provided one. | | [toBase64()](./firestore_lite.bytes.md#bytestobase64) | | Returns the underlying bytes as a Base64-encoded string. | +| [toJSON()](./firestore_lite.bytes.md#bytestojson) | | Returns a JSON-serializable representation of this Bytes instance. | | [toString()](./firestore_lite.bytes.md#bytestostring) | | Returns a string representation of the Bytes object. | | [toUint8Array()](./firestore_lite.bytes.md#bytestouint8array) | | Returns the underlying bytes in a new Uint8Array. | @@ -49,6 +51,26 @@ static fromBase64String(base64: string): Bytes; [Bytes](./firestore_lite.bytes.md#bytes_class) +## Bytes.fromJSON() + +Builds a `Bytes` instance from a JSON serialized version of `Bytes`. + +Signature: + +```typescript +static fromJSON(json: object): Bytes; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| json | object | | + +Returns: + +[Bytes](./firestore_lite.bytes.md#bytes_class) + ## Bytes.fromUint8Array() Creates a new `Bytes` object from the given Uint8Array. @@ -106,6 +128,19 @@ string The Base64-encoded string created from the `Bytes` object. +## Bytes.toJSON() + +Returns a JSON-serializable representation of this `Bytes` instance. + +Signature: + +```typescript +toJSON(): object; +``` +Returns: + +object + ## Bytes.toString() Returns a string representation of the `Bytes` object. diff --git a/docs-devsite/firestore_lite.documentreference.md b/docs-devsite/firestore_lite.documentreference.md index 2239850b829..80cef914232 100644 --- a/docs-devsite/firestore_lite.documentreference.md +++ b/docs-devsite/firestore_lite.documentreference.md @@ -33,6 +33,8 @@ export declare class DocumentReferencestatic | Builds a DocumentReference instance from a JSON serialized version of DocumentReference. | +| [toJSON()](./firestore_lite.documentreference.md#documentreferencetojson) | | Returns a JSON-serializable representation of this DocumentReference. | | [withConverter(converter)](./firestore_lite.documentreference.md#documentreferencewithconverter) | | Applies a custom data converter to this DocumentReference, allowing you to use your own custom model objects with Firestore. When you call [setDoc()](./firestore_lite.md#setdoc_ee215ad), [getDoc()](./firestore_lite.md#getdoc_4569087), etc. with the returned DocumentReference instance, the provided converter will convert between Firestore data of type NewDbModelType and your custom type NewAppModelType. | | [withConverter(converter)](./firestore_lite.documentreference.md#documentreferencewithconverter) | | Removes the current converter. | @@ -96,6 +98,41 @@ The type of this Firestore reference. readonly type = "document"; ``` +## DocumentReference.fromJSON() + +Builds a `DocumentReference` instance from a JSON serialized version of `DocumentReference`. + +Signature: + +```typescript +static fromJSON(firestore: Firestore, json: object, converter?: FirestoreDataConverter): DocumentReference; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| firestore | [Firestore](./firestore_lite.firestore.md#firestore_class) | | +| json | object | | +| converter | [FirestoreDataConverter](./firestore_lite.firestoredataconverter.md#firestoredataconverter_interface)<NewAppModelType, NewDbModelType> | | + +Returns: + +[DocumentReference](./firestore_lite.documentreference.md#documentreference_class)<NewAppModelType, NewDbModelType> + +## DocumentReference.toJSON() + +Returns a JSON-serializable representation of this DocumentReference. + +Signature: + +```typescript +toJSON(): object; +``` +Returns: + +object + ## DocumentReference.withConverter() Applies a custom data converter to this `DocumentReference`, allowing you to use your own custom model objects with Firestore. When you call [setDoc()](./firestore_lite.md#setdoc_ee215ad), [getDoc()](./firestore_lite.md#getdoc_4569087), etc. with the returned `DocumentReference` instance, the provided converter will convert between Firestore data of type `NewDbModelType` and your custom type `NewAppModelType`. diff --git a/docs-devsite/firestore_lite.geopoint.md b/docs-devsite/firestore_lite.geopoint.md index fdd760520c8..5d600f258f7 100644 --- a/docs-devsite/firestore_lite.geopoint.md +++ b/docs-devsite/firestore_lite.geopoint.md @@ -37,6 +37,7 @@ export declare class GeoPoint | Method | Modifiers | Description | | --- | --- | --- | +| [fromJSON(json)](./firestore_lite.geopoint.md#geopointfromjson) | static | Builds a Timestamp instance from a JSON serialized version of Bytes. | | [isEqual(other)](./firestore_lite.geopoint.md#geopointisequal) | | Returns true if this GeoPoint is equal to the provided one. | | [toJSON()](./firestore_lite.geopoint.md#geopointtojson) | | Returns a JSON-serializable representation of this GeoPoint. | @@ -77,6 +78,26 @@ The longitude of this `GeoPoint` instance. get longitude(): number; ``` +## GeoPoint.fromJSON() + +Builds a `Timestamp` instance from a JSON serialized version of `Bytes`. + +Signature: + +```typescript +static fromJSON(json: object): GeoPoint; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| json | object | | + +Returns: + +[GeoPoint](./firestore_lite.geopoint.md#geopoint_class) + ## GeoPoint.isEqual() Returns true if this `GeoPoint` is equal to the provided one. @@ -109,9 +130,10 @@ Returns a JSON-serializable representation of this GeoPoint. toJSON(): { latitude: number; longitude: number; + type: string; }; ``` Returns: -{ latitude: number; longitude: number; } +{ latitude: number; longitude: number; type: string; } diff --git a/docs-devsite/firestore_lite.timestamp.md b/docs-devsite/firestore_lite.timestamp.md index 506a6c66ade..5dbbbfda0bc 100644 --- a/docs-devsite/firestore_lite.timestamp.md +++ b/docs-devsite/firestore_lite.timestamp.md @@ -40,6 +40,7 @@ export declare class Timestamp | Method | Modifiers | Description | | --- | --- | --- | | [fromDate(date)](./firestore_lite.timestamp.md#timestampfromdate) | static | Creates a new timestamp from the given date. | +| [fromJSON(json)](./firestore_lite.timestamp.md#timestampfromjson) | static | Builds a Timestamp instance from a JSON serialized version of Bytes. | | [fromMillis(milliseconds)](./firestore_lite.timestamp.md#timestampfrommillis) | static | Creates a new timestamp from the given number of milliseconds. | | [isEqual(other)](./firestore_lite.timestamp.md#timestampisequal) | | Returns true if this Timestamp is equal to the provided one. | | [now()](./firestore_lite.timestamp.md#timestampnow) | static | Creates a new timestamp with the current date, with millisecond precision. | @@ -110,6 +111,26 @@ static fromDate(date: Date): Timestamp; A new `Timestamp` representing the same point in time as the given date. +## Timestamp.fromJSON() + +Builds a `Timestamp` instance from a JSON serialized version of `Bytes`. + +Signature: + +```typescript +static fromJSON(json: object): Timestamp; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| json | object | | + +Returns: + +[Timestamp](./firestore_lite.timestamp.md#timestamp_class) + ## Timestamp.fromMillis() Creates a new timestamp from the given number of milliseconds. @@ -194,11 +215,12 @@ Returns a JSON-serializable representation of this `Timestamp`. toJSON(): { seconds: number; nanoseconds: number; + type: string; }; ``` Returns: -{ seconds: number; nanoseconds: number; } +{ seconds: number; nanoseconds: number; type: string; } ## Timestamp.toMillis() diff --git a/docs-devsite/firestore_lite.vectorvalue.md b/docs-devsite/firestore_lite.vectorvalue.md index 28eaf7f5f01..05d4755a196 100644 --- a/docs-devsite/firestore_lite.vectorvalue.md +++ b/docs-devsite/firestore_lite.vectorvalue.md @@ -24,8 +24,30 @@ export declare class VectorValue | Method | Modifiers | Description | | --- | --- | --- | +| [fromJSON(json)](./firestore_lite.vectorvalue.md#vectorvaluefromjson) | static | Builds a Bytes instance from a JSON serialized version of Bytes. | | [isEqual(other)](./firestore_lite.vectorvalue.md#vectorvalueisequal) | | Returns true if the two VectorValue values have the same raw number arrays, returns false otherwise. | | [toArray()](./firestore_lite.vectorvalue.md#vectorvaluetoarray) | | Returns a copy of the raw number array form of the vector. | +| [toJSON()](./firestore_lite.vectorvalue.md#vectorvaluetojson) | | Returns a JSON-serializable representation of this VectorValue instance. | + +## VectorValue.fromJSON() + +Builds a `Bytes` instance from a JSON serialized version of `Bytes`. + +Signature: + +```typescript +static fromJSON(json: object): VectorValue; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| json | object | | + +Returns: + +[VectorValue](./firestore_lite.vectorvalue.md#vectorvalue_class) ## VectorValue.isEqual() @@ -60,3 +82,16 @@ toArray(): number[]; number\[\] +## VectorValue.toJSON() + +Returns a JSON-serializable representation of this `VectorValue` instance. + +Signature: + +```typescript +toJSON(): object; +``` +Returns: + +object + diff --git a/packages/firestore/src/lite-api/bytes.ts b/packages/firestore/src/lite-api/bytes.ts index ef16bc54463..eacc797e684 100644 --- a/packages/firestore/src/lite-api/bytes.ts +++ b/packages/firestore/src/lite-api/bytes.ts @@ -17,6 +17,9 @@ import { ByteString } from '../util/byte_string'; import { Code, FirestoreError } from '../util/error'; +// API extractor fails importing property unless we also explicitly import Property. +// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports-ts +import { Property, property, validateJSON } from '../util/json_validation'; /** * An immutable object representing an array of bytes. @@ -91,4 +94,29 @@ export class Bytes { isEqual(other: Bytes): boolean { return this._byteString.isEqual(other._byteString); } + + static _jsonSchemaVersion: string = 'firestore/bytes/1.0'; + static _jsonSchema = { + type: property('string', Bytes._jsonSchemaVersion), + bytes: property('string') + }; + + /** Returns a JSON-serializable representation of this `Bytes` instance. */ + toJSON(): object { + return { + type: Bytes._jsonSchemaVersion, + bytes: this.toBase64() + }; + } + + /** Builds a `Bytes` instance from a JSON serialized version of `Bytes`. */ + static fromJSON(json: object): Bytes { + if (validateJSON(json, Bytes._jsonSchema)) { + return Bytes.fromBase64String(json.bytes); + } + throw new FirestoreError( + Code.INTERNAL, + 'Unexpected error creating Bytes from JSON.' + ); + } } diff --git a/packages/firestore/src/lite-api/geo_point.ts b/packages/firestore/src/lite-api/geo_point.ts index 3e2944cde29..b5e92b39cce 100644 --- a/packages/firestore/src/lite-api/geo_point.ts +++ b/packages/firestore/src/lite-api/geo_point.ts @@ -16,6 +16,9 @@ */ import { Code, FirestoreError } from '../util/error'; +// API extractor fails importing 'property' unless we also explicitly import 'Property'. +// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports-ts +import { Property, property, validateJSON } from '../util/json_validation'; import { primitiveComparator } from '../util/misc'; /** @@ -79,11 +82,6 @@ export class GeoPoint { return this._lat === other._lat && this._long === other._long; } - /** Returns a JSON-serializable representation of this GeoPoint. */ - toJSON(): { latitude: number; longitude: number } { - return { latitude: this._lat, longitude: this._long }; - } - /** * Actually private to JS consumers of our API, so this function is prefixed * with an underscore. @@ -94,4 +92,31 @@ export class GeoPoint { primitiveComparator(this._long, other._long) ); } + + static _jsonSchemaVersion: string = 'firestore/geoPoint/1.0'; + static _jsonSchema = { + type: property('string', GeoPoint._jsonSchemaVersion), + latitude: property('number'), + longitude: property('number') + }; + + /** Returns a JSON-serializable representation of this GeoPoint. */ + toJSON(): { latitude: number; longitude: number; type: string } { + return { + latitude: this._lat, + longitude: this._long, + type: GeoPoint._jsonSchemaVersion + }; + } + + /** Builds a `Timestamp` instance from a JSON serialized version of `Bytes`. */ + static fromJSON(json: object): GeoPoint { + if (validateJSON(json, GeoPoint._jsonSchema)) { + return new GeoPoint(json.latitude, json.longitude); + } + throw new FirestoreError( + Code.INTERNAL, + 'Unexpected error creating GeoPoint from JSON.' + ); + } } diff --git a/packages/firestore/src/lite-api/reference.ts b/packages/firestore/src/lite-api/reference.ts index 26ae2fbd433..7a7e64ed3ee 100644 --- a/packages/firestore/src/lite-api/reference.ts +++ b/packages/firestore/src/lite-api/reference.ts @@ -32,6 +32,9 @@ import { validateDocumentPath, validateNonEmptyArgument } from '../util/input_validation'; +// API extractor fails importing property unless we also explicitly import Property. +// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports-ts +import { Property, property, validateJSON } from '../util/json_validation'; import { AutoId } from '../util/misc'; import { Firestore } from './database'; @@ -278,6 +281,42 @@ export class DocumentReference< this._key ); } + + static _jsonSchemaVersion: string = 'firestore/documentReference/1.0'; + static _jsonSchema = { + type: property('string', DocumentReference._jsonSchemaVersion), + referencePath: property('string') + }; + + /** Returns a JSON-serializable representation of this DocumentReference. */ + toJSON(): object { + return { + type: DocumentReference._jsonSchemaVersion, + referencePath: this._key.toString() + }; + } + + /** Builds a `DocumentReference` instance from a JSON serialized version of `DocumentReference`. */ + static fromJSON< + NewAppModelType = DocumentData, + NewDbModelType extends DocumentData = DocumentData + >( + firestore: Firestore, + json: object, + converter?: FirestoreDataConverter + ): DocumentReference { + if (validateJSON(json, DocumentReference._jsonSchema)) { + return new DocumentReference( + firestore, + converter ? converter : null, + new DocumentKey(ResourcePath.fromString(json.referencePath)) + ); + } + throw new FirestoreError( + Code.INTERNAL, + 'Unexpected error creating Bytes from JSON.' + ); + } } /** diff --git a/packages/firestore/src/lite-api/timestamp.ts b/packages/firestore/src/lite-api/timestamp.ts index e3d945aaf30..5dde51b0e28 100644 --- a/packages/firestore/src/lite-api/timestamp.ts +++ b/packages/firestore/src/lite-api/timestamp.ts @@ -16,6 +16,9 @@ */ import { Code, FirestoreError } from '../util/error'; +// API extractor fails importing 'property' unless we also explicitly import 'Property'. +// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports-ts +import { Property, property, validateJSON } from '../util/json_validation'; import { primitiveComparator } from '../util/misc'; // The earliest date supported by Firestore timestamps (0001-01-01T00:00:00Z). @@ -174,9 +177,31 @@ export class Timestamp { ); } + static _jsonSchemaVersion: string = 'firestore/timestamp/1.0'; + static _jsonSchema = { + type: property('string', Timestamp._jsonSchemaVersion), + seconds: property('number'), + nanoseconds: property('number') + }; + /** Returns a JSON-serializable representation of this `Timestamp`. */ - toJSON(): { seconds: number; nanoseconds: number } { - return { seconds: this.seconds, nanoseconds: this.nanoseconds }; + toJSON(): { seconds: number; nanoseconds: number; type: string } { + return { + type: Timestamp._jsonSchemaVersion, + seconds: this.seconds, + nanoseconds: this.nanoseconds + }; + } + + /** Builds a `Timestamp` instance from a JSON serialized version of `Bytes`. */ + static fromJSON(json: object): Timestamp { + if (validateJSON(json, Timestamp._jsonSchema)) { + return new Timestamp(json.seconds, json.nanoseconds); + } + throw new FirestoreError( + Code.INTERNAL, + 'Unexpected error creating Timestamp from JSON.' + ); } /** diff --git a/packages/firestore/src/lite-api/vector_value.ts b/packages/firestore/src/lite-api/vector_value.ts index 9ac9753fef5..211676b4be5 100644 --- a/packages/firestore/src/lite-api/vector_value.ts +++ b/packages/firestore/src/lite-api/vector_value.ts @@ -16,6 +16,10 @@ */ import { isPrimitiveArrayEqual } from '../util/array'; +import { Code, FirestoreError } from '../util/error'; +// API extractor fails importing 'property' unless we also explicitly import 'Property'. +// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports-ts +import { Property, property, validateJSON } from '../util/json_validation'; /** * Represents a vector type in Firestore documents. @@ -48,4 +52,37 @@ export class VectorValue { isEqual(other: VectorValue): boolean { return isPrimitiveArrayEqual(this._values, other._values); } + + static _jsonSchemaVersion: string = 'firestore/vectorValue/1.0'; + static _jsonSchema = { + type: property('string', VectorValue._jsonSchemaVersion), + vectorValues: property('object') + }; + + /** Returns a JSON-serializable representation of this `VectorValue` instance. */ + toJSON(): object { + return { + type: VectorValue._jsonSchemaVersion, + vectorValues: this._values + }; + } + /** Builds a `Bytes` instance from a JSON serialized version of `Bytes`. */ + static fromJSON(json: object): VectorValue { + if (validateJSON(json, VectorValue._jsonSchema)) { + if ( + Array.isArray(json.vectorValues) && + json.vectorValues.every(element => typeof element === 'number') + ) { + return new VectorValue(json.vectorValues); + } + throw new FirestoreError( + Code.INVALID_ARGUMENT, + "Expected 'vectorValues' field to be a number array" + ); + } + throw new FirestoreError( + Code.INTERNAL, + 'Unexpected error creating Timestamp from JSON.' + ); + } } diff --git a/packages/firestore/src/util/json_validation.ts b/packages/firestore/src/util/json_validation.ts new file mode 100644 index 00000000000..f16e28cacff --- /dev/null +++ b/packages/firestore/src/util/json_validation.ts @@ -0,0 +1,141 @@ +/** + * @license + * Copyright 2025 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 { isPlainObject } from '../util/input_validation'; + +import { Code, FirestoreError } from './error'; + +/** + * A list of data types Firestore objects may serialize in their toJSON implemenetations. + * @private + * @internal + */ +export type JsonTypeDesc = + | 'object' + | 'string' + | 'number' + | 'boolean' + | 'null' + | 'undefined'; + +/** + * An association of JsonTypeDesc values to their native types. + * @private + * @internal + */ +export type TSType = T extends 'object' + ? object + : T extends 'string' + ? string + : T extends 'number' + ? number + : T extends 'boolean' + ? boolean + : T extends 'null' + ? null + : T extends 'undefined' + ? undefined + : never; + +/** + * The representation of a JSON object property name and its type value. + * @private + * @internal + */ +export interface Property { + value?: TSType; + typeString: JsonTypeDesc; +} + +/** + * A type Firestore data types may use to define the fields used in their JSON serialization. + * @private + * @internal + */ +export interface JsonSchema { + [key: string]: Property; +} + +/** + * Associates the JSON property type to the native type and sets them to be Required. + * @private + * @internal + */ +export type Json = { + [K in keyof T]: Required['value']; +}; + +/** + * Helper function to define a JSON schema {@link Property}. + * @private + * @internal + */ +export function property( + typeString: T, + optionalValue?: TSType +): Property { + const result: Property = { + typeString + }; + if (optionalValue) { + result.value = optionalValue; + } + return result; +} + +/** + * Validates the JSON object based on the provided schema, and narrows the type to the provided + * JSON schaem. + * @private + * @internal + * + * @param json A JSON object to validate. + * @param scheme a {@link JsonSchema} that defines the properties to validate. + * @returns true if the JSON schema exists within the object. Throws a FirestoreError otherwise. + */ +export function validateJSON( + json: object, + schema: S +): json is Json { + if (!isPlainObject(json)) { + throw new FirestoreError(Code.INVALID_ARGUMENT, 'json must be an object'); + } + let error: string | undefined = undefined; + for (const key in schema) { + if (schema[key]) { + const typeString = schema[key].typeString; + const value: { value: unknown } | undefined = + 'value' in schema[key] ? { value: schema[key].value } : undefined; + if (!(key in json)) { + error = `json missing required field: ${key}`; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fieldValue = (json as any)[key]; + if (typeString && typeof fieldValue !== typeString) { + error = `json field '${key}' must be a ${typeString}.`; + break; + } else if (value !== undefined && fieldValue !== value.value) { + error = `Expected '${key}' field to equal '${value.value}'`; + break; + } + } + } + if (error) { + throw new FirestoreError(Code.INVALID_ARGUMENT, error); + } + return true; +} diff --git a/packages/firestore/test/lite/integration.test.ts b/packages/firestore/test/lite/integration.test.ts index 780db5f4f9c..7fb7eafcb1e 100644 --- a/packages/firestore/test/lite/integration.test.ts +++ b/packages/firestore/test/lite/integration.test.ts @@ -423,6 +423,20 @@ describe('getDoc()', () => { expect(docSnap.exists()).to.be.true; }); }); + + it('can get doc with a deserialized reference', () => { + return withTestDocAndInitialData({ val: 1 }, async docRef => { + const docSnap = await getDoc(docRef); + expect(docSnap.exists()).to.be.true; + const json = docRef.toJSON(); + const deserializedDocRef = DocumentReference.fromJSON( + docSnap._firestore, + json + ); + const docSnap2 = await getDoc(deserializedDocRef); + expect(docSnap2.exists()).to.be.true; + }); + }); }); /** diff --git a/packages/firestore/test/unit/api/bytes.test.ts b/packages/firestore/test/unit/api/bytes.test.ts index afc37400d8e..8fa8919b1e2 100644 --- a/packages/firestore/test/unit/api/bytes.test.ts +++ b/packages/firestore/test/unit/api/bytes.test.ts @@ -55,4 +55,52 @@ describe('Bytes', () => { expectEqual(blob(1, 2, 3), blob(1, 2, 3)); expectNotEqual(blob(1, 2, 3), blob(4, 5, 6)); }); + + it('fromJSON reconstructs the value from toJSON', () => { + const bytes = Bytes.fromUint8Array(new Uint8Array([0, 1, 2, 3, 4, 5])); + expect(() => { + Bytes.fromJSON(bytes.toJSON()); + }).to.not.throw; + expect(Bytes.fromJSON(bytes.toJSON()).isEqual(bytes)).to.be.true; + }); + + it('fromJSON parameter order does not matter', () => { + const type = 'firestore/bytes/1.0'; + const bytes = 'AA=='; + expect(() => { + Bytes.fromJSON({ bytes, type }); + }).to.not.throw; + expect(() => { + Bytes.fromJSON({ type, bytes }); + }).to.not.throw; + }); + + it('toJSON -> fromJSON bytes comparison', () => { + Object.keys(base64Mappings).forEach(base64Str => { + const bytesToSerialize = Bytes.fromBase64String(base64Str); + const deserializedBytes = Bytes.fromJSON(bytesToSerialize.toJSON()); + expectEqual(bytesToSerialize, deserializedBytes); + const expectedUint8Array = base64Mappings[base64Str]; + const actualUint8Array = deserializedBytes.toUint8Array(); + expect(actualUint8Array.length).to.equal(expectedUint8Array.length); + for (let i = 0; i < actualUint8Array.length; i++) { + expect(actualUint8Array[i]).to.equal(expectedUint8Array[i]); + } + }); + }); + + it('fromJSON misisng fields throws', () => { + expect(() => { + Bytes.fromJSON({ type: 'firestore/bytes/1.0' /* missing bytes data */ }); + }).to.throw; + expect(() => { + Bytes.fromJSON({ bytes: 'AA==' /* missing type */ }); + }).to.throw; + expect(() => { + Bytes.fromJSON({ type: 1, bytes: 'AA==' }); + }).to.throw; + expect(() => { + Bytes.fromJSON({ type: 'firestore/bytes/1.0', bytes: 1 }); + }).to.throw; + }); }); diff --git a/packages/firestore/test/unit/api/database.test.ts b/packages/firestore/test/unit/api/database.test.ts index b5e9dc1b673..308835c8855 100644 --- a/packages/firestore/test/unit/api/database.test.ts +++ b/packages/firestore/test/unit/api/database.test.ts @@ -18,6 +18,7 @@ import { expect } from 'chai'; import { + DocumentReference, connectFirestoreEmulator, loadBundle, refEqual, @@ -71,6 +72,40 @@ describe('DocumentReference', () => { it('JSON.stringify() does not throw', () => { JSON.stringify(documentReference('foo/bar')); }); + + it('toJSON() does not throw', () => { + expect(() => { + documentReference('foo/bar').toJSON(); + }).to.not.throw; + }); + + it('toJSON() includes correct JSON fields', () => { + const docRef = documentReference('foo/bar'); + const json = docRef.toJSON(); + expect(json).to.deep.equal({ + type: 'firestore/documentReference/1.0', + referencePath: 'foo/bar' + }); + }); + + it('fromJSON() does not throw', () => { + const db = newTestFirestore(); + const docRef = documentReference('foo/bar'); + const json = docRef.toJSON(); + expect(() => { + DocumentReference.fromJSON(db, json); + }).to.not.throw; + }); + + it('fromJSON() equals original docRef', () => { + const db = newTestFirestore(); + const docRef = documentReference('foo/bar'); + const json = docRef.toJSON(); + const deserializedDocRef = DocumentReference.fromJSON(db, json); + expect(docRef.id).to.equal(deserializedDocRef.id); + expect(docRef.path).to.equal(deserializedDocRef.path); + expect(docRef.toJSON()).to.deep.equal(deserializedDocRef.toJSON()); + }); }); describe('DocumentSnapshot', () => { diff --git a/packages/firestore/test/unit/api/geo_point.test.ts b/packages/firestore/test/unit/api/geo_point.test.ts index f2cdb4c27f5..dc744ceb63f 100644 --- a/packages/firestore/test/unit/api/geo_point.test.ts +++ b/packages/firestore/test/unit/api/geo_point.test.ts @@ -105,15 +105,96 @@ describe('GeoPoint', () => { it('serializes to JSON', () => { expect(new GeoPoint(1, 2).toJSON()).to.deep.equal({ latitude: 1, - longitude: 2 + longitude: 2, + 'type': GeoPoint._jsonSchemaVersion }); expect(new GeoPoint(0, 0).toJSON()).to.deep.equal({ latitude: 0, - longitude: 0 + longitude: 0, + 'type': GeoPoint._jsonSchemaVersion }); expect(new GeoPoint(90, 180).toJSON()).to.deep.equal({ latitude: 90, - longitude: 180 + longitude: 180, + 'type': GeoPoint._jsonSchemaVersion }); }); + it('fromJSON does not throw', () => { + const geoPoint = new GeoPoint(1, 2); + expect(() => { + GeoPoint.fromJSON(geoPoint.toJSON()); + }).to.not.throw; + }); + + it('fromJSON reconstructs seconds and nanoseconds', () => { + const geoPoint = new GeoPoint(1, 2); + const deserializedGeoPoint = GeoPoint.fromJSON(geoPoint.toJSON()); + expect(deserializedGeoPoint).to.exist; + expect(geoPoint.latitude).to.equal(deserializedGeoPoint.latitude); + expect(geoPoint.longitude).to.equal(deserializedGeoPoint.longitude); + }); + + it('toJSON -> fromJSON timestamp comparison', () => { + const geoPoint = new GeoPoint(1, 2); + const deserializedGeoPoint = GeoPoint.fromJSON(geoPoint.toJSON()); + expect(deserializedGeoPoint.isEqual(geoPoint)).to.be.true; + }); + + it('fromJSON parameter order does not matter', () => { + const type = 'firestore/geopoint/1.0'; + const latitude = 90; + const longitude = 180; + const control = new GeoPoint(90, 180); + expect(() => { + expect(GeoPoint.fromJSON({ latitude, longitude, type }).isEqual(control)) + .to.be.true; + }).to.not.throw; + expect(() => { + expect(GeoPoint.fromJSON({ longitude, type, latitude }).isEqual(control)) + .to.be.true; + }).to.not.throw; + expect(() => { + expect(GeoPoint.fromJSON({ type, latitude, longitude }).isEqual(control)) + .to.be.true; + }).to.not.throw; + expect(() => { + expect(GeoPoint.fromJSON({ latitude, type, longitude }).isEqual(control)) + .to.be.true; + }).to.not.throw; + }); + + it('fromJSON missing fields throws', () => { + const type = 'firestore/geopoint/1.0'; + const latitude = 90; + const longitude = 180; + + expect(() => { + GeoPoint.fromJSON({ type, latitude }); + }).to.throw; + expect(() => { + GeoPoint.fromJSON({ type, longitude }); + }).to.throw; + expect(() => { + GeoPoint.fromJSON({ latitude, longitude }); + }).to.throw; + }); + + it('fromJSON field errant field type throws', () => { + const type = 'firestore/geopoint/1.0'; + const latitude = 90; + const longitude = 180; + + expect(() => { + GeoPoint.fromJSON({ type, latitude, longitude: 'wrong' }); + }).to.throw; + expect(() => { + GeoPoint.fromJSON({ type, longitude, latitude: 'wrong' }); + }).to.throw; + expect(() => { + GeoPoint.fromJSON({ latitude, longitude, type: 1 }); + }).to.throw; + expect(() => { + GeoPoint.fromJSON({ latitude, longitude, type: 'firestore/wrong/1.0' }); + }).to.throw; + }); }); diff --git a/packages/firestore/test/unit/api/timestamp.test.ts b/packages/firestore/test/unit/api/timestamp.test.ts index ef883f33a92..81773520698 100644 --- a/packages/firestore/test/unit/api/timestamp.test.ts +++ b/packages/firestore/test/unit/api/timestamp.test.ts @@ -143,15 +143,101 @@ describe('Timestamp', () => { it('serializes to JSON', () => { expect(new Timestamp(123, 456).toJSON()).to.deep.equal({ seconds: 123, - nanoseconds: 456 + nanoseconds: 456, + type: 'firestore/timestamp/1.0' }); expect(new Timestamp(0, 0).toJSON()).to.deep.equal({ seconds: 0, - nanoseconds: 0 + nanoseconds: 0, + type: 'firestore/timestamp/1.0' }); expect(new Timestamp(-123, 456).toJSON()).to.deep.equal({ seconds: -123, - nanoseconds: 456 + nanoseconds: 456, + type: 'firestore/timestamp/1.0' }); }); + + it('fromJSON does not throw', () => { + const timestamp = new Timestamp(123, 456); + expect(() => { + Timestamp.fromJSON(timestamp.toJSON()); + }).to.not.throw; + }); + + it('fromJSON reconstructs seconds and nanoseconds', () => { + const timestamp = new Timestamp(123, 456); + const deserializedTimestamp = Timestamp.fromJSON(timestamp.toJSON()); + expect(deserializedTimestamp).to.exist; + expect(timestamp.nanoseconds).to.equal(deserializedTimestamp.nanoseconds); + expect(timestamp.seconds).to.equal(deserializedTimestamp.seconds); + }); + + it('toJSON -> fromJSON timestamp comparison', () => { + const timestamp = new Timestamp(123, 456); + const deserializedTimestamp = Timestamp.fromJSON(timestamp.toJSON()); + expect(deserializedTimestamp.isEqual(timestamp)).to.be.true; + }); + + it('fromJSON parameter order does not matter', () => { + const type = 'firestore/timestamp/1.0'; + const seconds = 123; + const nanoseconds = 456; + const control = new Timestamp(seconds, nanoseconds); + expect(() => { + expect( + Timestamp.fromJSON({ seconds, nanoseconds, type }).isEqual(control) + ).to.be.true; + }).to.not.throw; + expect(() => { + expect( + Timestamp.fromJSON({ nanoseconds, type, seconds }).isEqual(control) + ).to.be.true; + }).to.not.throw; + expect(() => { + expect( + Timestamp.fromJSON({ type, seconds, nanoseconds }).isEqual(control) + ).to.be.true; + }).to.not.throw; + expect(() => { + expect( + Timestamp.fromJSON({ seconds, type, nanoseconds }).isEqual(control) + ).to.be.true; + }).to.not.throw; + }); + + it('fromJSON missing fields throws', () => { + const type = 'firestore/timestamp/1.0'; + const seconds = 123; + const nanoseconds = 456; + + expect(() => { + Timestamp.fromJSON({ type, seconds }); + }).to.throw; + expect(() => { + Timestamp.fromJSON({ type, nanoseconds }); + }).to.throw; + expect(() => { + Timestamp.fromJSON({ seconds, nanoseconds }); + }).to.throw; + }); + + it('fromJSON field errant field type throws', () => { + const type = 'firestore/timestamp/1.0'; + const seconds = 123; + const nanoseconds = 456; + + expect(() => { + Timestamp.fromJSON({ type, seconds, nanoseconds: 'wrong' }); + }).to.throw; + expect(() => { + Timestamp.fromJSON({ type, nanoseconds, seconds: 'wrong' }); + }).to.throw; + expect(() => { + Timestamp.fromJSON({ seconds, nanoseconds, type: 1 }); + }).to.throw; + expect(() => { + Timestamp.fromJSON({ seconds, nanoseconds, type: 'firestore/wrong/1.0' }); + }).to.throw; + }); }); diff --git a/packages/firestore/test/unit/api/vector_value.test.ts b/packages/firestore/test/unit/api/vector_value.test.ts new file mode 100644 index 00000000000..c0944a64934 --- /dev/null +++ b/packages/firestore/test/unit/api/vector_value.test.ts @@ -0,0 +1,73 @@ +/** + * @license + * Copyright 2025 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 { VectorValue } from '../../../src'; + +describe('VectorValue', () => { + it('fromJSON reconstructs the value from toJSON', () => { + const num: number[] = [1, 2, 3]; + const vectorValue = new VectorValue(num); + const json = vectorValue.toJSON(); + const parsedVectorValue = VectorValue.fromJSON(json); + expect(vectorValue.isEqual(parsedVectorValue)).to.be.true; + }); + + it('fromJSON parameter order does not matter', () => { + const type = VectorValue._jsonSchemaVersion; + const vectorValues = [1, 2, 3]; + const control = new VectorValue(vectorValues); + + expect(() => { + expect(VectorValue.fromJSON({ vectorValues, type }).isEqual(control)).to + .be.true; + }).to.not.throw; + expect(() => { + expect(VectorValue.fromJSON({ type, vectorValues }).isEqual(control)).to + .be.true; + }).to.not.throw; + }); + + it('fromJSON empty array does not throw', () => { + const type = VectorValue._jsonSchemaVersion; + const vectorValues = [1, 2, 3]; + expect(() => { + VectorValue.fromJSON({ type, vectorValues }); + }).to.not.throw; + }); + + it('fromJSON missing fields throws', () => { + const type = VectorValue._jsonSchemaVersion; + const vectorValues = [1, 2, 3]; + expect(() => { + VectorValue.fromJSON({ type /* missing data */ }); + }).to.throw; + expect(() => { + VectorValue.fromJSON({ vectorValues /* missing type */ }); + }).to.throw; + expect(() => { + VectorValue.fromJSON({ type: 1, vectorValues }); + }).to.throw; + expect(() => { + VectorValue.fromJSON({ type: 'firestore/bytes/1.0', vectorValues }); + }).to.throw; + expect(() => { + VectorValue.fromJSON({ type, vectorValues: 'not a number' }); + }); + }); +}); diff --git a/packages/firestore/test/unit/local/indexeddb_persistence.test.ts b/packages/firestore/test/unit/local/indexeddb_persistence.test.ts index 1240c977cee..9fa872101b1 100644 --- a/packages/firestore/test/unit/local/indexeddb_persistence.test.ts +++ b/packages/firestore/test/unit/local/indexeddb_persistence.test.ts @@ -316,10 +316,14 @@ describe('IndexedDbSchema: createOrUpgradeDb', () => { lastRemoteSnapshotVersion: { seconds: 1, nanoseconds: 1 }, targetCount: 1 }; + const timestamp = SnapshotVersion.min().toTimestamp(); const resetTargetGlobal: DbTargetGlobal = { highestTargetId: 0, highestListenSequenceNumber: 0, - lastRemoteSnapshotVersion: SnapshotVersion.min().toTimestamp(), + lastRemoteSnapshotVersion: { + seconds: timestamp.seconds, + nanoseconds: timestamp.nanoseconds + }, targetCount: 0 }; From 4c8e431b9f8dfe0ac028c3268d84487e4c24d5b2 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Thu, 24 Apr 2025 16:25:38 -0400 Subject: [PATCH 4/7] update docs --- packages/firestore/src/lite-api/bytes.ts | 14 ++++++++++++-- packages/firestore/src/lite-api/geo_point.ts | 14 ++++++++++++-- packages/firestore/src/lite-api/reference.ts | 15 +++++++++++++-- packages/firestore/src/lite-api/vector_value.ts | 15 +++++++++++++-- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/packages/firestore/src/lite-api/bytes.ts b/packages/firestore/src/lite-api/bytes.ts index eacc797e684..191d104219b 100644 --- a/packages/firestore/src/lite-api/bytes.ts +++ b/packages/firestore/src/lite-api/bytes.ts @@ -101,7 +101,11 @@ export class Bytes { bytes: property('string') }; - /** Returns a JSON-serializable representation of this `Bytes` instance. */ + /** + * Returns a JSON-serializable representation of this `Bytes` instance. + * + * @returns a JSON representation of this object. + */ toJSON(): object { return { type: Bytes._jsonSchemaVersion, @@ -109,7 +113,13 @@ export class Bytes { }; } - /** Builds a `Bytes` instance from a JSON serialized version of `Bytes`. */ + /** + * Builds a `Byes` instance from a JSON object created by {@link Bytes.toJSON}. + * + * @param json a JSON object represention of a `Bytes` instance + * @returns an instance of {@link Bytes} if the JSON object could be parsed. Throws a + * {@link FirestoreError} if an error occurs. + */ static fromJSON(json: object): Bytes { if (validateJSON(json, Bytes._jsonSchema)) { return Bytes.fromBase64String(json.bytes); diff --git a/packages/firestore/src/lite-api/geo_point.ts b/packages/firestore/src/lite-api/geo_point.ts index b5e92b39cce..b16a7683bba 100644 --- a/packages/firestore/src/lite-api/geo_point.ts +++ b/packages/firestore/src/lite-api/geo_point.ts @@ -100,7 +100,11 @@ export class GeoPoint { longitude: property('number') }; - /** Returns a JSON-serializable representation of this GeoPoint. */ + /** + * Returns a JSON-serializable representation of this `GeoPoint` instance. + * + * @returns a JSON representation of this object. + */ toJSON(): { latitude: number; longitude: number; type: string } { return { latitude: this._lat, @@ -109,7 +113,13 @@ export class GeoPoint { }; } - /** Builds a `Timestamp` instance from a JSON serialized version of `Bytes`. */ + /** + * Builds a `GeoPoint` instance from a JSON object created by {@link GeoPoint.toJSON}. + * + * @param json a JSON object represention of a `GeoPoint` instance + * @returns an instance of {@link GeoPoint} if the JSON object could be parsed. Throws a + * {@link FirestoreError} if an error occurs. + */ static fromJSON(json: object): GeoPoint { if (validateJSON(json, GeoPoint._jsonSchema)) { return new GeoPoint(json.latitude, json.longitude); diff --git a/packages/firestore/src/lite-api/reference.ts b/packages/firestore/src/lite-api/reference.ts index 7a7e64ed3ee..ed66eae7b70 100644 --- a/packages/firestore/src/lite-api/reference.ts +++ b/packages/firestore/src/lite-api/reference.ts @@ -288,7 +288,11 @@ export class DocumentReference< referencePath: property('string') }; - /** Returns a JSON-serializable representation of this DocumentReference. */ + /** + * Returns a JSON-serializable representation of this `DocumentReference` instance. + * + * @returns a JSON representation of this object. + */ toJSON(): object { return { type: DocumentReference._jsonSchemaVersion, @@ -296,7 +300,14 @@ export class DocumentReference< }; } - /** Builds a `DocumentReference` instance from a JSON serialized version of `DocumentReference`. */ + /** + * Builds a `DocumentReference` instance from a JSON object created by + * {@link DocumentReference.toJSON}. + * + * @param json a JSON object represention of a `DocumentReference` instance + * @returns an instance of {@link DocumentReference} if the JSON object could be parsed. Throws a + * {@link FirestoreError} if an error occurs. + */ static fromJSON< NewAppModelType = DocumentData, NewDbModelType extends DocumentData = DocumentData diff --git a/packages/firestore/src/lite-api/vector_value.ts b/packages/firestore/src/lite-api/vector_value.ts index 211676b4be5..88bc55ef032 100644 --- a/packages/firestore/src/lite-api/vector_value.ts +++ b/packages/firestore/src/lite-api/vector_value.ts @@ -59,14 +59,25 @@ export class VectorValue { vectorValues: property('object') }; - /** Returns a JSON-serializable representation of this `VectorValue` instance. */ + /** + * Returns a JSON-serializable representation of this `VectorValue` instance. + * + * @returns a JSON representation of this object. + */ toJSON(): object { return { type: VectorValue._jsonSchemaVersion, vectorValues: this._values }; } - /** Builds a `Bytes` instance from a JSON serialized version of `Bytes`. */ + + /** + * Builds a `VectorValue` instance from a JSON object created by {@link VectorValue.toJSON}. + * + * @param json a JSON object represention of a `VectorValue` instance + * @returns an instance of {@link VectorValue} if the JSON object could be parsed. Throws a + * {@link FirestoreError} if an error occurs. + */ static fromJSON(json: object): VectorValue { if (validateJSON(json, VectorValue._jsonSchema)) { if ( From c211ea2fe6853094be49b6fbf86ae59481b63584 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Thu, 24 Apr 2025 16:29:12 -0400 Subject: [PATCH 5/7] Docgen --- docs-devsite/firestore_.bytes.md | 10 +++++++--- docs-devsite/firestore_.documentreference.md | 14 +++++++++----- docs-devsite/firestore_.geopoint.md | 14 +++++++++----- docs-devsite/firestore_.vectorvalue.md | 10 +++++++--- docs-devsite/firestore_lite.bytes.md | 10 +++++++--- docs-devsite/firestore_lite.documentreference.md | 14 +++++++++----- docs-devsite/firestore_lite.geopoint.md | 14 +++++++++----- docs-devsite/firestore_lite.vectorvalue.md | 10 +++++++--- 8 files changed, 64 insertions(+), 32 deletions(-) diff --git a/docs-devsite/firestore_.bytes.md b/docs-devsite/firestore_.bytes.md index ac77ccb6458..d06d4ba30a8 100644 --- a/docs-devsite/firestore_.bytes.md +++ b/docs-devsite/firestore_.bytes.md @@ -23,7 +23,7 @@ export declare class Bytes | Method | Modifiers | Description | | --- | --- | --- | | [fromBase64String(base64)](./firestore_.bytes.md#bytesfrombase64string) | static | Creates a new Bytes object from the given Base64 string, converting it to bytes. | -| [fromJSON(json)](./firestore_.bytes.md#bytesfromjson) | static | Builds a Bytes instance from a JSON serialized version of Bytes. | +| [fromJSON(json)](./firestore_.bytes.md#bytesfromjson) | static | Builds a Byes instance from a JSON object created by [Bytes.toJSON()](./firestore_.bytes.md#bytestojson). | | [fromUint8Array(array)](./firestore_.bytes.md#bytesfromuint8array) | static | Creates a new Bytes object from the given Uint8Array. | | [isEqual(other)](./firestore_.bytes.md#bytesisequal) | | Returns true if this Bytes object is equal to the provided one. | | [toBase64()](./firestore_.bytes.md#bytestobase64) | | Returns the underlying bytes as a Base64-encoded string. | @@ -53,7 +53,7 @@ static fromBase64String(base64: string): Bytes; ## Bytes.fromJSON() -Builds a `Bytes` instance from a JSON serialized version of `Bytes`. +Builds a `Byes` instance from a JSON object created by [Bytes.toJSON()](./firestore_.bytes.md#bytestojson). Signature: @@ -65,12 +65,14 @@ static fromJSON(json: object): Bytes; | Parameter | Type | Description | | --- | --- | --- | -| json | object | | +| json | object | a JSON object represention of a Bytes instance | Returns: [Bytes](./firestore_.bytes.md#bytes_class) +an instance of [Bytes](./firestore_.bytes.md#bytes_class) if the JSON object could be parsed. Throws a [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class) if an error occurs. + ## Bytes.fromUint8Array() Creates a new `Bytes` object from the given Uint8Array. @@ -141,6 +143,8 @@ toJSON(): object; object +a JSON representation of this object. + ## Bytes.toString() Returns a string representation of the `Bytes` object. diff --git a/docs-devsite/firestore_.documentreference.md b/docs-devsite/firestore_.documentreference.md index e786b45a086..53a38cd8dd7 100644 --- a/docs-devsite/firestore_.documentreference.md +++ b/docs-devsite/firestore_.documentreference.md @@ -33,8 +33,8 @@ export declare class DocumentReferencestatic | Builds a DocumentReference instance from a JSON serialized version of DocumentReference. | -| [toJSON()](./firestore_.documentreference.md#documentreferencetojson) | | Returns a JSON-serializable representation of this DocumentReference. | +| [fromJSON(firestore, json, converter)](./firestore_.documentreference.md#documentreferencefromjson) | static | Builds a DocumentReference instance from a JSON object created by [DocumentReference.toJSON()](./firestore_.documentreference.md#documentreferencetojson). | +| [toJSON()](./firestore_.documentreference.md#documentreferencetojson) | | Returns a JSON-serializable representation of this DocumentReference instance. | | [withConverter(converter)](./firestore_.documentreference.md#documentreferencewithconverter) | | Applies a custom data converter to this DocumentReference, allowing you to use your own custom model objects with Firestore. When you call [setDoc()](./firestore_lite.md#setdoc_ee215ad), [getDoc()](./firestore_lite.md#getdoc_4569087), etc. with the returned DocumentReference instance, the provided converter will convert between Firestore data of type NewDbModelType and your custom type NewAppModelType. | | [withConverter(converter)](./firestore_.documentreference.md#documentreferencewithconverter) | | Removes the current converter. | @@ -100,7 +100,7 @@ readonly type = "document"; ## DocumentReference.fromJSON() -Builds a `DocumentReference` instance from a JSON serialized version of `DocumentReference`. +Builds a `DocumentReference` instance from a JSON object created by [DocumentReference.toJSON()](./firestore_.documentreference.md#documentreferencetojson). Signature: @@ -113,16 +113,18 @@ static fromJSONDocumentReference instance | | converter | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface)<NewAppModelType, NewDbModelType> | | Returns: [DocumentReference](./firestore_.documentreference.md#documentreference_class)<NewAppModelType, NewDbModelType> +an instance of [DocumentReference](./firestore_.documentreference.md#documentreference_class) if the JSON object could be parsed. Throws a [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class) if an error occurs. + ## DocumentReference.toJSON() -Returns a JSON-serializable representation of this DocumentReference. +Returns a JSON-serializable representation of this `DocumentReference` instance. Signature: @@ -133,6 +135,8 @@ toJSON(): object; object +a JSON representation of this object. + ## DocumentReference.withConverter() Applies a custom data converter to this `DocumentReference`, allowing you to use your own custom model objects with Firestore. When you call [setDoc()](./firestore_lite.md#setdoc_ee215ad), [getDoc()](./firestore_lite.md#getdoc_4569087), etc. with the returned `DocumentReference` instance, the provided converter will convert between Firestore data of type `NewDbModelType` and your custom type `NewAppModelType`. diff --git a/docs-devsite/firestore_.geopoint.md b/docs-devsite/firestore_.geopoint.md index 253b0ae4140..c88a5289c64 100644 --- a/docs-devsite/firestore_.geopoint.md +++ b/docs-devsite/firestore_.geopoint.md @@ -37,9 +37,9 @@ export declare class GeoPoint | Method | Modifiers | Description | | --- | --- | --- | -| [fromJSON(json)](./firestore_.geopoint.md#geopointfromjson) | static | Builds a Timestamp instance from a JSON serialized version of Bytes. | +| [fromJSON(json)](./firestore_.geopoint.md#geopointfromjson) | static | Builds a GeoPoint instance from a JSON object created by [GeoPoint.toJSON()](./firestore_.geopoint.md#geopointtojson). | | [isEqual(other)](./firestore_.geopoint.md#geopointisequal) | | Returns true if this GeoPoint is equal to the provided one. | -| [toJSON()](./firestore_.geopoint.md#geopointtojson) | | Returns a JSON-serializable representation of this GeoPoint. | +| [toJSON()](./firestore_.geopoint.md#geopointtojson) | | Returns a JSON-serializable representation of this GeoPoint instance. | ## GeoPoint.(constructor) @@ -80,7 +80,7 @@ get longitude(): number; ## GeoPoint.fromJSON() -Builds a `Timestamp` instance from a JSON serialized version of `Bytes`. +Builds a `GeoPoint` instance from a JSON object created by [GeoPoint.toJSON()](./firestore_.geopoint.md#geopointtojson). Signature: @@ -92,12 +92,14 @@ static fromJSON(json: object): GeoPoint; | Parameter | Type | Description | | --- | --- | --- | -| json | object | | +| json | object | a JSON object represention of a GeoPoint instance | Returns: [GeoPoint](./firestore_.geopoint.md#geopoint_class) +an instance of [GeoPoint](./firestore_.geopoint.md#geopoint_class) if the JSON object could be parsed. Throws a [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class) if an error occurs. + ## GeoPoint.isEqual() Returns true if this `GeoPoint` is equal to the provided one. @@ -122,7 +124,7 @@ true if this `GeoPoint` is equal to the provided one. ## GeoPoint.toJSON() -Returns a JSON-serializable representation of this GeoPoint. +Returns a JSON-serializable representation of this `GeoPoint` instance. Signature: @@ -137,3 +139,5 @@ toJSON(): { { latitude: number; longitude: number; type: string; } +a JSON representation of this object. + diff --git a/docs-devsite/firestore_.vectorvalue.md b/docs-devsite/firestore_.vectorvalue.md index c3e346a4855..265251142ce 100644 --- a/docs-devsite/firestore_.vectorvalue.md +++ b/docs-devsite/firestore_.vectorvalue.md @@ -24,14 +24,14 @@ export declare class VectorValue | Method | Modifiers | Description | | --- | --- | --- | -| [fromJSON(json)](./firestore_.vectorvalue.md#vectorvaluefromjson) | static | Builds a Bytes instance from a JSON serialized version of Bytes. | +| [fromJSON(json)](./firestore_.vectorvalue.md#vectorvaluefromjson) | static | Builds a VectorValue instance from a JSON object created by [VectorValue.toJSON()](./firestore_.vectorvalue.md#vectorvaluetojson). | | [isEqual(other)](./firestore_.vectorvalue.md#vectorvalueisequal) | | Returns true if the two VectorValue values have the same raw number arrays, returns false otherwise. | | [toArray()](./firestore_.vectorvalue.md#vectorvaluetoarray) | | Returns a copy of the raw number array form of the vector. | | [toJSON()](./firestore_.vectorvalue.md#vectorvaluetojson) | | Returns a JSON-serializable representation of this VectorValue instance. | ## VectorValue.fromJSON() -Builds a `Bytes` instance from a JSON serialized version of `Bytes`. +Builds a `VectorValue` instance from a JSON object created by [VectorValue.toJSON()](./firestore_.vectorvalue.md#vectorvaluetojson). Signature: @@ -43,12 +43,14 @@ static fromJSON(json: object): VectorValue; | Parameter | Type | Description | | --- | --- | --- | -| json | object | | +| json | object | a JSON object represention of a VectorValue instance | Returns: [VectorValue](./firestore_.vectorvalue.md#vectorvalue_class) +an instance of [VectorValue](./firestore_.vectorvalue.md#vectorvalue_class) if the JSON object could be parsed. Throws a [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class) if an error occurs. + ## VectorValue.isEqual() Returns `true` if the two `VectorValue` values have the same raw number arrays, returns `false` otherwise. @@ -95,3 +97,5 @@ toJSON(): object; object +a JSON representation of this object. + diff --git a/docs-devsite/firestore_lite.bytes.md b/docs-devsite/firestore_lite.bytes.md index 87d5076cfb1..54bd8c7c903 100644 --- a/docs-devsite/firestore_lite.bytes.md +++ b/docs-devsite/firestore_lite.bytes.md @@ -23,7 +23,7 @@ export declare class Bytes | Method | Modifiers | Description | | --- | --- | --- | | [fromBase64String(base64)](./firestore_lite.bytes.md#bytesfrombase64string) | static | Creates a new Bytes object from the given Base64 string, converting it to bytes. | -| [fromJSON(json)](./firestore_lite.bytes.md#bytesfromjson) | static | Builds a Bytes instance from a JSON serialized version of Bytes. | +| [fromJSON(json)](./firestore_lite.bytes.md#bytesfromjson) | static | Builds a Byes instance from a JSON object created by [Bytes.toJSON()](./firestore_.bytes.md#bytestojson). | | [fromUint8Array(array)](./firestore_lite.bytes.md#bytesfromuint8array) | static | Creates a new Bytes object from the given Uint8Array. | | [isEqual(other)](./firestore_lite.bytes.md#bytesisequal) | | Returns true if this Bytes object is equal to the provided one. | | [toBase64()](./firestore_lite.bytes.md#bytestobase64) | | Returns the underlying bytes as a Base64-encoded string. | @@ -53,7 +53,7 @@ static fromBase64String(base64: string): Bytes; ## Bytes.fromJSON() -Builds a `Bytes` instance from a JSON serialized version of `Bytes`. +Builds a `Byes` instance from a JSON object created by [Bytes.toJSON()](./firestore_.bytes.md#bytestojson). Signature: @@ -65,12 +65,14 @@ static fromJSON(json: object): Bytes; | Parameter | Type | Description | | --- | --- | --- | -| json | object | | +| json | object | a JSON object represention of a Bytes instance | Returns: [Bytes](./firestore_lite.bytes.md#bytes_class) +an instance of [Bytes](./firestore_.bytes.md#bytes_class) if the JSON object could be parsed. Throws a [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class) if an error occurs. + ## Bytes.fromUint8Array() Creates a new `Bytes` object from the given Uint8Array. @@ -141,6 +143,8 @@ toJSON(): object; object +a JSON representation of this object. + ## Bytes.toString() Returns a string representation of the `Bytes` object. diff --git a/docs-devsite/firestore_lite.documentreference.md b/docs-devsite/firestore_lite.documentreference.md index 80cef914232..4dd970afb7e 100644 --- a/docs-devsite/firestore_lite.documentreference.md +++ b/docs-devsite/firestore_lite.documentreference.md @@ -33,8 +33,8 @@ export declare class DocumentReferencestatic | Builds a DocumentReference instance from a JSON serialized version of DocumentReference. | -| [toJSON()](./firestore_lite.documentreference.md#documentreferencetojson) | | Returns a JSON-serializable representation of this DocumentReference. | +| [fromJSON(firestore, json, converter)](./firestore_lite.documentreference.md#documentreferencefromjson) | static | Builds a DocumentReference instance from a JSON object created by [DocumentReference.toJSON()](./firestore_.documentreference.md#documentreferencetojson). | +| [toJSON()](./firestore_lite.documentreference.md#documentreferencetojson) | | Returns a JSON-serializable representation of this DocumentReference instance. | | [withConverter(converter)](./firestore_lite.documentreference.md#documentreferencewithconverter) | | Applies a custom data converter to this DocumentReference, allowing you to use your own custom model objects with Firestore. When you call [setDoc()](./firestore_lite.md#setdoc_ee215ad), [getDoc()](./firestore_lite.md#getdoc_4569087), etc. with the returned DocumentReference instance, the provided converter will convert between Firestore data of type NewDbModelType and your custom type NewAppModelType. | | [withConverter(converter)](./firestore_lite.documentreference.md#documentreferencewithconverter) | | Removes the current converter. | @@ -100,7 +100,7 @@ readonly type = "document"; ## DocumentReference.fromJSON() -Builds a `DocumentReference` instance from a JSON serialized version of `DocumentReference`. +Builds a `DocumentReference` instance from a JSON object created by [DocumentReference.toJSON()](./firestore_.documentreference.md#documentreferencetojson). Signature: @@ -113,16 +113,18 @@ static fromJSONDocumentReference instance | | converter | [FirestoreDataConverter](./firestore_lite.firestoredataconverter.md#firestoredataconverter_interface)<NewAppModelType, NewDbModelType> | | Returns: [DocumentReference](./firestore_lite.documentreference.md#documentreference_class)<NewAppModelType, NewDbModelType> +an instance of [DocumentReference](./firestore_.documentreference.md#documentreference_class) if the JSON object could be parsed. Throws a [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class) if an error occurs. + ## DocumentReference.toJSON() -Returns a JSON-serializable representation of this DocumentReference. +Returns a JSON-serializable representation of this `DocumentReference` instance. Signature: @@ -133,6 +135,8 @@ toJSON(): object; object +a JSON representation of this object. + ## DocumentReference.withConverter() Applies a custom data converter to this `DocumentReference`, allowing you to use your own custom model objects with Firestore. When you call [setDoc()](./firestore_lite.md#setdoc_ee215ad), [getDoc()](./firestore_lite.md#getdoc_4569087), etc. with the returned `DocumentReference` instance, the provided converter will convert between Firestore data of type `NewDbModelType` and your custom type `NewAppModelType`. diff --git a/docs-devsite/firestore_lite.geopoint.md b/docs-devsite/firestore_lite.geopoint.md index 5d600f258f7..6b3396107d2 100644 --- a/docs-devsite/firestore_lite.geopoint.md +++ b/docs-devsite/firestore_lite.geopoint.md @@ -37,9 +37,9 @@ export declare class GeoPoint | Method | Modifiers | Description | | --- | --- | --- | -| [fromJSON(json)](./firestore_lite.geopoint.md#geopointfromjson) | static | Builds a Timestamp instance from a JSON serialized version of Bytes. | +| [fromJSON(json)](./firestore_lite.geopoint.md#geopointfromjson) | static | Builds a GeoPoint instance from a JSON object created by [GeoPoint.toJSON()](./firestore_.geopoint.md#geopointtojson). | | [isEqual(other)](./firestore_lite.geopoint.md#geopointisequal) | | Returns true if this GeoPoint is equal to the provided one. | -| [toJSON()](./firestore_lite.geopoint.md#geopointtojson) | | Returns a JSON-serializable representation of this GeoPoint. | +| [toJSON()](./firestore_lite.geopoint.md#geopointtojson) | | Returns a JSON-serializable representation of this GeoPoint instance. | ## GeoPoint.(constructor) @@ -80,7 +80,7 @@ get longitude(): number; ## GeoPoint.fromJSON() -Builds a `Timestamp` instance from a JSON serialized version of `Bytes`. +Builds a `GeoPoint` instance from a JSON object created by [GeoPoint.toJSON()](./firestore_.geopoint.md#geopointtojson). Signature: @@ -92,12 +92,14 @@ static fromJSON(json: object): GeoPoint; | Parameter | Type | Description | | --- | --- | --- | -| json | object | | +| json | object | a JSON object represention of a GeoPoint instance | Returns: [GeoPoint](./firestore_lite.geopoint.md#geopoint_class) +an instance of [GeoPoint](./firestore_.geopoint.md#geopoint_class) if the JSON object could be parsed. Throws a [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class) if an error occurs. + ## GeoPoint.isEqual() Returns true if this `GeoPoint` is equal to the provided one. @@ -122,7 +124,7 @@ true if this `GeoPoint` is equal to the provided one. ## GeoPoint.toJSON() -Returns a JSON-serializable representation of this GeoPoint. +Returns a JSON-serializable representation of this `GeoPoint` instance. Signature: @@ -137,3 +139,5 @@ toJSON(): { { latitude: number; longitude: number; type: string; } +a JSON representation of this object. + diff --git a/docs-devsite/firestore_lite.vectorvalue.md b/docs-devsite/firestore_lite.vectorvalue.md index 05d4755a196..99b27bf8553 100644 --- a/docs-devsite/firestore_lite.vectorvalue.md +++ b/docs-devsite/firestore_lite.vectorvalue.md @@ -24,14 +24,14 @@ export declare class VectorValue | Method | Modifiers | Description | | --- | --- | --- | -| [fromJSON(json)](./firestore_lite.vectorvalue.md#vectorvaluefromjson) | static | Builds a Bytes instance from a JSON serialized version of Bytes. | +| [fromJSON(json)](./firestore_lite.vectorvalue.md#vectorvaluefromjson) | static | Builds a VectorValue instance from a JSON object created by [VectorValue.toJSON()](./firestore_.vectorvalue.md#vectorvaluetojson). | | [isEqual(other)](./firestore_lite.vectorvalue.md#vectorvalueisequal) | | Returns true if the two VectorValue values have the same raw number arrays, returns false otherwise. | | [toArray()](./firestore_lite.vectorvalue.md#vectorvaluetoarray) | | Returns a copy of the raw number array form of the vector. | | [toJSON()](./firestore_lite.vectorvalue.md#vectorvaluetojson) | | Returns a JSON-serializable representation of this VectorValue instance. | ## VectorValue.fromJSON() -Builds a `Bytes` instance from a JSON serialized version of `Bytes`. +Builds a `VectorValue` instance from a JSON object created by [VectorValue.toJSON()](./firestore_.vectorvalue.md#vectorvaluetojson). Signature: @@ -43,12 +43,14 @@ static fromJSON(json: object): VectorValue; | Parameter | Type | Description | | --- | --- | --- | -| json | object | | +| json | object | a JSON object represention of a VectorValue instance | Returns: [VectorValue](./firestore_lite.vectorvalue.md#vectorvalue_class) +an instance of [VectorValue](./firestore_.vectorvalue.md#vectorvalue_class) if the JSON object could be parsed. Throws a [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class) if an error occurs. + ## VectorValue.isEqual() Returns `true` if the two `VectorValue` values have the same raw number arrays, returns `false` otherwise. @@ -95,3 +97,5 @@ toJSON(): object; object +a JSON representation of this object. + From c88ea8e21afca2e71b0f16f6f671221e825a70c9 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Fri, 16 May 2025 10:11:35 -0400 Subject: [PATCH 6/7] [FEAT] Doc/QuerySnapshot fromJSON (#8960) Add a methods to parse bundles synchronously to extract single `DocumentSnapshots` or `QuerySnapshots` containing documents. Hook the new bundle parser up the `documentSnapshotFromJSON` and `querySnapshotFromJSON`. ### Testing Local testing with Next.js app. Added unit tests and integration tests. ### API Changes go/fs-js-json-serialization --- common/api-review/firestore-lite.api.md | 3 +- common/api-review/firestore.api.md | 33 +- docs-devsite/firestore_.documentreference.md | 30 +- docs-devsite/firestore_.documentsnapshot.md | 6 +- docs-devsite/firestore_.md | 213 +++++++--- docs-devsite/firestore_.querysnapshot.md | 6 +- docs-devsite/firestore_.timestamp.md | 4 +- docs-devsite/firestore_.vectorvalue.md | 2 +- .../firestore_lite.documentreference.md | 30 +- docs-devsite/firestore_lite.timestamp.md | 4 +- docs-devsite/firestore_lite.vectorvalue.md | 2 +- packages/firestore/src/api.ts | 3 + packages/firestore/src/api/reference_impl.ts | 395 +++++++++--------- packages/firestore/src/api/snapshot.ts | 225 ++++++++++ packages/firestore/src/core/bundle_impl.ts | 49 ++- .../firestore/src/core/firestore_client.ts | 12 +- .../firestore/src/core/sync_engine_impl.ts | 8 +- packages/firestore/src/lite-api/reference.ts | 20 + packages/firestore/src/lite-api/timestamp.ts | 8 +- .../firestore/src/lite-api/vector_value.ts | 2 +- packages/firestore/src/util/bundle_reader.ts | 21 + .../src/util/bundle_reader_sync_impl.ts | 129 ++++++ .../firestore/src/util/json_validation.ts | 7 +- .../test/integration/api/database.test.ts | 152 ++++++- .../firestore/test/unit/api/database.test.ts | 310 +++++++++++++- 25 files changed, 1352 insertions(+), 322 deletions(-) create mode 100644 packages/firestore/src/util/bundle_reader_sync_impl.ts diff --git a/common/api-review/firestore-lite.api.md b/common/api-review/firestore-lite.api.md index 0dd52644d8f..46b85a0efc5 100644 --- a/common/api-review/firestore-lite.api.md +++ b/common/api-review/firestore-lite.api.md @@ -137,7 +137,8 @@ export function documentId(): FieldPath; export class DocumentReference { readonly converter: FirestoreDataConverter | null; readonly firestore: Firestore; - static fromJSON(firestore: Firestore, json: object, converter?: FirestoreDataConverter): DocumentReference; + static fromJSON(firestore: Firestore, json: object): DocumentReference; + static fromJSON(firestore: Firestore, json: object, converter: FirestoreDataConverter): DocumentReference; get id(): string; get parent(): CollectionReference; get path(): string; diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index 7bce21a7cc4..292d81d7a75 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -163,7 +163,8 @@ export function documentId(): FieldPath; export class DocumentReference { readonly converter: FirestoreDataConverter | null; readonly firestore: Firestore; - static fromJSON(firestore: Firestore, json: object, converter?: FirestoreDataConverter): DocumentReference; + static fromJSON(firestore: Firestore, json: object): DocumentReference; + static fromJSON(firestore: Firestore, json: object, converter: FirestoreDataConverter): DocumentReference; get id(): string; get parent(): CollectionReference; get path(): string; @@ -182,10 +183,15 @@ export class DocumentSnapshot; - // (undocumented) toJSON(): object; } +// @public +export function documentSnapshotFromJSON(db: Firestore, json: object): DocumentSnapshot; + +// @public +export function documentSnapshotFromJSON(db: Firestore, json: object, converter: FirestoreDataConverter): DocumentSnapshot; + export { EmulatorMockTokenOptions } // @public @deprecated @@ -468,40 +474,40 @@ export function onSnapshot(query export function onSnapshot(query: Query, options: SnapshotListenOptions, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void): Unsubscribe; // @public -export function onSnapshot(firestore: Firestore, snapshotJson: object, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; +export function onSnapshotResume(firestore: Firestore, snapshotJson: object, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; // @public -export function onSnapshot(firestore: Firestore, snapshotJson: object, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; +export function onSnapshotResume(firestore: Firestore, snapshotJson: object, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; // @public -export function onSnapshot(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; +export function onSnapshotResume(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; // @public -export function onSnapshot(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; +export function onSnapshotResume(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; // @public -export function onSnapshot(firestore: Firestore, snapshotJson: object, observer: { +export function onSnapshotResume(firestore: Firestore, snapshotJson: object, observer: { next: (snapshot: QuerySnapshot) => void; error?: (error: FirestoreError) => void; complete?: () => void; }, converter?: FirestoreDataConverter): Unsubscribe; // @public -export function onSnapshot(firestore: Firestore, snapshotJson: object, observer: { +export function onSnapshotResume(firestore: Firestore, snapshotJson: object, observer: { next: (snapshot: DocumentSnapshot) => void; error?: (error: FirestoreError) => void; complete?: () => void; }, converter?: FirestoreDataConverter): Unsubscribe; // @public -export function onSnapshot(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, observer: { +export function onSnapshotResume(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, observer: { next: (snapshot: QuerySnapshot) => void; error?: (error: FirestoreError) => void; complete?: () => void; }, converter?: FirestoreDataConverter): Unsubscribe; // @public -export function onSnapshot(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, observer: { +export function onSnapshotResume(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, observer: { next: (snapshot: DocumentSnapshot) => void; error?: (error: FirestoreError) => void; complete?: () => void; @@ -658,10 +664,15 @@ export class QuerySnapshot; get size(): number; - // (undocumented) toJSON(): object; } +// @public +export function querySnapshotFromJSON(db: Firestore, json: object): QuerySnapshot; + +// @public +export function querySnapshotFromJSON(db: Firestore, json: object, converter: FirestoreDataConverter): QuerySnapshot; + // @public export class QueryStartAtConstraint extends QueryConstraint { readonly type: 'startAt' | 'startAfter'; diff --git a/docs-devsite/firestore_.documentreference.md b/docs-devsite/firestore_.documentreference.md index 53a38cd8dd7..ee4be972b0c 100644 --- a/docs-devsite/firestore_.documentreference.md +++ b/docs-devsite/firestore_.documentreference.md @@ -33,6 +33,7 @@ export declare class DocumentReferencestatic | Builds a DocumentReference instance from a JSON object created by [DocumentReference.toJSON()](./firestore_.documentreference.md#documentreferencetojson). | | [fromJSON(firestore, json, converter)](./firestore_.documentreference.md#documentreferencefromjson) | static | Builds a DocumentReference instance from a JSON object created by [DocumentReference.toJSON()](./firestore_.documentreference.md#documentreferencetojson). | | [toJSON()](./firestore_.documentreference.md#documentreferencetojson) | | Returns a JSON-serializable representation of this DocumentReference instance. | | [withConverter(converter)](./firestore_.documentreference.md#documentreferencewithconverter) | | Applies a custom data converter to this DocumentReference, allowing you to use your own custom model objects with Firestore. When you call [setDoc()](./firestore_lite.md#setdoc_ee215ad), [getDoc()](./firestore_lite.md#getdoc_4569087), etc. with the returned DocumentReference instance, the provided converter will convert between Firestore data of type NewDbModelType and your custom type NewAppModelType. | @@ -105,16 +106,39 @@ Builds a `DocumentReference` instance from a JSON object created by [DocumentRef Signature: ```typescript -static fromJSON(firestore: Firestore, json: object, converter?: FirestoreDataConverter): DocumentReference; +static fromJSON(firestore: Firestore, json: object): DocumentReference; ``` #### Parameters | Parameter | Type | Description | | --- | --- | --- | -| firestore | [Firestore](./firestore_.firestore.md#firestore_class) | | +| firestore | [Firestore](./firestore_.firestore.md#firestore_class) | The [Firestore](./firestore_.firestore.md#firestore_class) instance the snapshot should be loaded for. | | json | object | a JSON object represention of a DocumentReference instance | -| converter | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface)<NewAppModelType, NewDbModelType> | | + +Returns: + +[DocumentReference](./firestore_.documentreference.md#documentreference_class) + +an instance of [DocumentReference](./firestore_.documentreference.md#documentreference_class) if the JSON object could be parsed. Throws a [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class) if an error occurs. + +## DocumentReference.fromJSON() + +Builds a `DocumentReference` instance from a JSON object created by [DocumentReference.toJSON()](./firestore_.documentreference.md#documentreferencetojson). + +Signature: + +```typescript +static fromJSON(firestore: Firestore, json: object, converter: FirestoreDataConverter): DocumentReference; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| firestore | [Firestore](./firestore_.firestore.md#firestore_class) | The [Firestore](./firestore_.firestore.md#firestore_class) instance the snapshot should be loaded for. | +| json | object | a JSON object represention of a DocumentReference instance | +| converter | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface)<NewAppModelType, NewDbModelType> | Converts objects to and from Firestore. | Returns: diff --git a/docs-devsite/firestore_.documentsnapshot.md b/docs-devsite/firestore_.documentsnapshot.md index 8c4825593dc..3281873c525 100644 --- a/docs-devsite/firestore_.documentsnapshot.md +++ b/docs-devsite/firestore_.documentsnapshot.md @@ -41,7 +41,7 @@ export declare class DocumentSnapshotObject. Returns undefined if the document doesn't exist.By default, serverTimestamp() values that have not yet been set to their final value will be returned as null. You can override this by passing an options object. | | [exists()](./firestore_.documentsnapshot.md#documentsnapshotexists) | | Returns whether or not the data exists. True if the document exists. | | [get(fieldPath, options)](./firestore_.documentsnapshot.md#documentsnapshotget) | | Retrieves the field specified by fieldPath. Returns undefined if the document or field doesn't exist.By default, a serverTimestamp() that has not yet been set to its final value will be returned as null. You can override this by passing an options object. | -| [toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson) | | | +| [toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson) | | Returns a JSON-serializable representation of this DocumentSnapshot instance. | ## DocumentSnapshot.(constructor) @@ -147,6 +147,8 @@ The data at the specified field location or undefined if no such field exists in ## DocumentSnapshot.toJSON() +Returns a JSON-serializable representation of this `DocumentSnapshot` instance. + Signature: ```typescript @@ -156,3 +158,5 @@ toJSON(): object; object +a JSON representation of this object. + diff --git a/docs-devsite/firestore_.md b/docs-devsite/firestore_.md index 685958d364c..ac00fe33aaa 100644 --- a/docs-devsite/firestore_.md +++ b/docs-devsite/firestore_.md @@ -19,6 +19,11 @@ https://github.com/firebase/firebase-js-sdk | [getFirestore(app)](./firestore_.md#getfirestore_cf608e1) | Returns the existing default [Firestore](./firestore_.firestore.md#firestore_class) instance that is associated with the provided [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface). If no instance exists, initializes a new instance with default settings. | | [getFirestore(app, databaseId)](./firestore_.md#getfirestore_48de6cb) | (Public Preview) Returns the existing named [Firestore](./firestore_.firestore.md#firestore_class) instance that is associated with the provided [FirebaseApp](./app.firebaseapp.md#firebaseapp_interface). If no instance exists, initializes a new instance with default settings. | | [initializeFirestore(app, settings, databaseId)](./firestore_.md#initializefirestore_fc7d200) | Initializes a new instance of [Firestore](./firestore_.firestore.md#firestore_class) with the provided settings. Can only be called before any other function, including [getFirestore()](./firestore_.md#getfirestore). If the custom settings are empty, this function is equivalent to calling [getFirestore()](./firestore_.md#getfirestore). | +| function(db, ...) | +| [documentSnapshotFromJSON(db, json)](./firestore_.md#documentsnapshotfromjson_a318ff2) | Builds a DocumentSnapshot instance from a JSON object created by [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). | +| [documentSnapshotFromJSON(db, json, converter)](./firestore_.md#documentsnapshotfromjson_ddb369d) | Builds a DocumentSnapshot instance from a JSON object created by [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). | +| [querySnapshotFromJSON(db, json)](./firestore_.md#querysnapshotfromjson_a318ff2) | Builds a QuerySnapshot instance from a JSON object created by [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). | +| [querySnapshotFromJSON(db, json, converter)](./firestore_.md#querysnapshotfromjson_ddb369d) | Builds a QuerySnapshot instance from a JSON object created by [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). | | function(firestore, ...) | | [clearIndexedDbPersistence(firestore)](./firestore_.md#clearindexeddbpersistence_231a8e0) | Clears the persistent storage. This includes pending writes and cached documents.Must be called while the [Firestore](./firestore_.firestore.md#firestore_class) instance is not started (after the app is terminated or when the app is first initialized). On startup, this function must be called before other functions (other than [initializeFirestore()](./firestore_.md#initializefirestore_fc7d200) or [getFirestore()](./firestore_.md#getfirestore))). If the [Firestore](./firestore_.firestore.md#firestore_class) instance is still running, the promise will be rejected with the error code of failed-precondition.Note: clearIndexedDbPersistence() is primarily intended to help write reliable tests that use Cloud Firestore. It uses an efficient mechanism for dropping existing data but does not attempt to securely overwrite or otherwise make cached data unrecoverable. For applications that are sensitive to the disclosure of cached data in between user sessions, we strongly recommend not enabling persistence at all. | | [collection(firestore, path, pathSegments)](./firestore_.md#collection_1eb4c23) | Gets a CollectionReference instance that refers to the collection at the specified absolute path. | @@ -32,14 +37,14 @@ https://github.com/firebase/firebase-js-sdk | [getPersistentCacheIndexManager(firestore)](./firestore_.md#getpersistentcacheindexmanager_231a8e0) | Returns the PersistentCache Index Manager used by the given Firestore object. The PersistentCacheIndexManager instance, or null if local persistent storage is not in use. | | [loadBundle(firestore, bundleData)](./firestore_.md#loadbundle_bec5b75) | Loads a Firestore bundle into the local cache. | | [namedQuery(firestore, name)](./firestore_.md#namedquery_6438876) | Reads a Firestore [Query](./firestore_.query.md#query_class) from local cache, identified by the given name.The named queries are packaged into bundles on the server side (along with resulting documents), and loaded to local cache using loadBundle. Once in local cache, use this method to extract a [Query](./firestore_.query.md#query_class) by name. | -| [onSnapshot(firestore, snapshotJson, onNext, onError, onCompletion, converter)](./firestore_.md#onsnapshot_712362a) | Attaches a listener for DocumentSnapshot events based on data generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | -| [onSnapshot(firestore, snapshotJson, options, onNext, onError, onCompletion, converter)](./firestore_.md#onsnapshot_8807e6e) | Attaches a listener for QuerySnapshot events based on data generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | -| [onSnapshot(firestore, snapshotJson, options, onNext, onError, onCompletion, converter)](./firestore_.md#onsnapshot_301fcec) | Attaches a listener for DocumentSnapshot events based on data generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | -| [onSnapshot(firestore, snapshotJson, observer, converter)](./firestore_.md#onsnapshot_b8b5c9d) | Attaches a listener for QuerySnapshot events based on QuerySnapshot data generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | -| [onSnapshot(firestore, snapshotJson, observer, converter)](./firestore_.md#onsnapshot_9b75d28) | Attaches a listener for DocumentSnapshot events based on data generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | -| [onSnapshot(firestore, snapshotJson, options, observer, converter)](./firestore_.md#onsnapshot_fb80adf) | Attaches a listener for QuerySnapshot events based on QuerySnapshot data generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | -| [onSnapshot(firestore, snapshotJson, options, observer, converter)](./firestore_.md#onsnapshot_f76d912) | Attaches a listener for DocumentSnapshot events based on QuerySnapshot data generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | -| [onSnapshot(firestore, snapshotJson, onNext, onError, onCompletion, converter)](./firestore_.md#onsnapshot_7c84f5e) | Attaches a listener for QuerySnapshot events based on data generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | +| [onSnapshotResume(firestore, snapshotJson, onNext, onError, onCompletion, converter)](./firestore_.md#onsnapshotresume_7c84f5e) | Attaches a listener for QuerySnapshot events based on data generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | +| [onSnapshotResume(firestore, snapshotJson, onNext, onError, onCompletion, converter)](./firestore_.md#onsnapshotresume_712362a) | Attaches a listener for DocumentSnapshot events based on data generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | +| [onSnapshotResume(firestore, snapshotJson, options, onNext, onError, onCompletion, converter)](./firestore_.md#onsnapshotresume_8807e6e) | Attaches a listener for QuerySnapshot events based on data generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | +| [onSnapshotResume(firestore, snapshotJson, options, onNext, onError, onCompletion, converter)](./firestore_.md#onsnapshotresume_301fcec) | Attaches a listener for DocumentSnapshot events based on data generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | +| [onSnapshotResume(firestore, snapshotJson, observer, converter)](./firestore_.md#onsnapshotresume_b8b5c9d) | Attaches a listener for QuerySnapshot events based on QuerySnapshot data generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | +| [onSnapshotResume(firestore, snapshotJson, observer, converter)](./firestore_.md#onsnapshotresume_9b75d28) | Attaches a listener for DocumentSnapshot events based on data generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | +| [onSnapshotResume(firestore, snapshotJson, options, observer, converter)](./firestore_.md#onsnapshotresume_fb80adf) | Attaches a listener for QuerySnapshot events based on QuerySnapshot data generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | +| [onSnapshotResume(firestore, snapshotJson, options, observer, converter)](./firestore_.md#onsnapshotresume_f76d912) | Attaches a listener for DocumentSnapshot events based on QuerySnapshot data generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson) You may either pass individual onNext and onError callbacks or pass a single observer object with next and error callbacks. The listener can be cancelled by calling the function that is returned when onSnapshot is called.NOTE: Although an onCompletion callback can be provided, it will never be called because the snapshot stream is never-ending. | | [onSnapshotsInSync(firestore, observer)](./firestore_.md#onsnapshotsinsync_2f0dfa4) | Attaches a listener for a snapshots-in-sync event. The snapshots-in-sync event indicates that all listeners affected by a given change have fired, even if a single server-generated change affects multiple listeners.NOTE: The snapshots-in-sync event only indicates that listeners are in sync with each other, but does not relate to whether those snapshots are in sync with the server. Use SnapshotMetadata in the individual listeners to determine if a snapshot is from the cache or the server. | | [onSnapshotsInSync(firestore, onSync)](./firestore_.md#onsnapshotsinsync_1901c06) | Attaches a listener for a snapshots-in-sync event. The snapshots-in-sync event indicates that all listeners affected by a given change have fired, even if a single server-generated change affects multiple listeners.NOTE: The snapshots-in-sync event only indicates that listeners are in sync with each other, but does not relate to whether those snapshots are in sync with the server. Use SnapshotMetadata in the individual listeners to determine if a snapshot is from the cache or the server. | | [runTransaction(firestore, updateFunction, options)](./firestore_.md#runtransaction_6f03ec4) | Executes the given updateFunction and then attempts to commit the changes applied within the transaction. If any document read within the transaction has changed, Cloud Firestore retries the updateFunction. If it fails to commit after 5 attempts, the transaction fails.The maximum number of writes allowed in a single transaction is 500. | @@ -306,6 +311,102 @@ export declare function initializeFirestore(app: FirebaseApp, settings: Firestor A newly initialized [Firestore](./firestore_.firestore.md#firestore_class) instance. +## function(db, ...) + +### documentSnapshotFromJSON(db, json) {:#documentsnapshotfromjson_a318ff2} + +Builds a `DocumentSnapshot` instance from a JSON object created by [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). + +Signature: + +```typescript +export declare function documentSnapshotFromJSON(db: Firestore, json: object): DocumentSnapshot; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| db | [Firestore](./firestore_.firestore.md#firestore_class) | | +| json | object | a JSON object represention of a DocumentSnapshot instance. | + +Returns: + +[DocumentSnapshot](./firestore_.documentsnapshot.md#documentsnapshot_class) + +an instance of [DocumentSnapshot](./firestore_.documentsnapshot.md#documentsnapshot_class) if the JSON object could be parsed. Throws a [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class) if an error occurs. + +### documentSnapshotFromJSON(db, json, converter) {:#documentsnapshotfromjson_ddb369d} + +Builds a `DocumentSnapshot` instance from a JSON object created by [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). + +Signature: + +```typescript +export declare function documentSnapshotFromJSON(db: Firestore, json: object, converter: FirestoreDataConverter): DocumentSnapshot; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| db | [Firestore](./firestore_.firestore.md#firestore_class) | | +| json | object | a JSON object represention of a DocumentSnapshot instance. | +| converter | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface)<AppModelType, DbModelType> | Converts objects to and from Firestore. | + +Returns: + +[DocumentSnapshot](./firestore_.documentsnapshot.md#documentsnapshot_class)<AppModelType, DbModelType> + +an instance of [DocumentSnapshot](./firestore_.documentsnapshot.md#documentsnapshot_class) if the JSON object could be parsed. Throws a [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class) if an error occurs. + +### querySnapshotFromJSON(db, json) {:#querysnapshotfromjson_a318ff2} + +Builds a `QuerySnapshot` instance from a JSON object created by [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). + +Signature: + +```typescript +export declare function querySnapshotFromJSON(db: Firestore, json: object): QuerySnapshot; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| db | [Firestore](./firestore_.firestore.md#firestore_class) | | +| json | object | a JSON object represention of a QuerySnapshot instance. | + +Returns: + +[QuerySnapshot](./firestore_.querysnapshot.md#querysnapshot_class) + +an instance of [QuerySnapshot](./firestore_.querysnapshot.md#querysnapshot_class) if the JSON object could be parsed. Throws a [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class) if an error occurs. + +### querySnapshotFromJSON(db, json, converter) {:#querysnapshotfromjson_ddb369d} + +Builds a `QuerySnapshot` instance from a JSON object created by [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). + +Signature: + +```typescript +export declare function querySnapshotFromJSON(db: Firestore, json: object, converter: FirestoreDataConverter): QuerySnapshot; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| db | [Firestore](./firestore_.firestore.md#firestore_class) | | +| json | object | a JSON object represention of a QuerySnapshot instance. | +| converter | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface)<AppModelType, DbModelType> | Converts objects to and from Firestore. | + +Returns: + +[QuerySnapshot](./firestore_.querysnapshot.md#querysnapshot_class)<AppModelType, DbModelType> + +an instance of [QuerySnapshot](./firestore_.querysnapshot.md#querysnapshot_class) if the JSON object could be parsed. Throws a [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class) if an error occurs. + ## function(firestore, ...) ### clearIndexedDbPersistence(firestore) {:#clearindexeddbpersistence_231a8e0} @@ -625,16 +726,45 @@ Promise<[Query](./firestore_.query.md#query_class) \| null> A `Promise` that is resolved with the Query or `null`. -### onSnapshot(firestore, snapshotJson, onNext, onError, onCompletion, converter) {:#onsnapshot_712362a} +### onSnapshotResume(firestore, snapshotJson, onNext, onError, onCompletion, converter) {:#onsnapshotresume_7c84f5e} -Attaches a listener for `DocumentSnapshot` events based on data generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson) You may either pass individual `onNext` and `onError` callbacks or pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by calling the function that is returned when `onSnapshot` is called. +Attaches a listener for `QuerySnapshot` events based on data generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) You may either pass individual `onNext` and `onError` callbacks or pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by calling the function that is returned when `onSnapshot` is called. + +NOTE: Although an `onCompletion` callback can be provided, it will never be called because the snapshot stream is never-ending. + +Signature: + +```typescript +export declare function onSnapshotResume(firestore: Firestore, snapshotJson: object, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| firestore | [Firestore](./firestore_.firestore.md#firestore_class) | The [Firestore](./firestore_.firestore.md#firestore_class) instance to enable persistence for. | +| snapshotJson | object | A JSON object generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). | +| onNext | (snapshot: [QuerySnapshot](./firestore_.querysnapshot.md#querysnapshot_class)<AppModelType, DbModelType>) => void | A callback to be called every time a new QuerySnapshot is available. | +| onError | (error: [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class)) => void | A callback to be called if the listen fails or is cancelled. No further callbacks will occur. | +| onCompletion | () => void | Can be provided, but will not be called since streams are never ending. | +| converter | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface)<DbModelType> | An optional object that converts objects from Firestore before the onNext listener is invoked. | + +Returns: + +[Unsubscribe](./firestore_.unsubscribe.md#unsubscribe_interface) + +An unsubscribe function that can be called to cancel the snapshot listener. + +### onSnapshotResume(firestore, snapshotJson, onNext, onError, onCompletion, converter) {:#onsnapshotresume_712362a} + +Attaches a listener for `DocumentSnapshot` events based on data generated by invoking [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). You may either pass individual `onNext` and `onError` callbacks or pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by calling the function that is returned when `onSnapshot` is called. NOTE: Although an `onCompletion` callback can be provided, it will never be called because the snapshot stream is never-ending. Signature: ```typescript -export declare function onSnapshot(firestore: Firestore, snapshotJson: object, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; +export declare function onSnapshotResume(firestore: Firestore, snapshotJson: object, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; ``` #### Parameters @@ -644,7 +774,7 @@ export declare function onSnapshot. | | onNext | (snapshot: [DocumentSnapshot](./firestore_.documentsnapshot.md#documentsnapshot_class)<AppModelType, DbModelType>) => void | A callback to be called every time a new DocumentSnapshot is available. | -| onError | (error: [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class)) => void | A callback to be called if the listen fails or is cancelled. No fruther callbacks will occur. | +| onError | (error: [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class)) => void | A callback to be called if the listen fails or is cancelled. No further callbacks will occur. | | onCompletion | () => void | Can be provided, but will not be called since streams are never ending. | | converter | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface)<DbModelType> | An optional object that converts objects from Firestore before the onNext listener is invoked. | @@ -654,16 +784,16 @@ export declare function onSnapshot. You may either pass individual `onNext` and `onError` callbacks or pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by calling the function that is returned when `onSnapshot` is called. NOTE: Although an `onCompletion` callback can be provided, it will never be called because the snapshot stream is never-ending. Signature: ```typescript -export declare function onSnapshot(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; +export declare function onSnapshotResume(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; ``` #### Parameters @@ -684,16 +814,16 @@ export declare function onSnapshot. You may either pass individual `onNext` and `onError` callbacks or pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by calling the function that is returned when `onSnapshot` is called. NOTE: Although an `onCompletion` callback can be provided, it will never be called because the snapshot stream is never-ending. Signature: ```typescript -export declare function onSnapshot(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; +export declare function onSnapshotResume(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; ``` #### Parameters @@ -714,16 +844,16 @@ export declare function onSnapshot. You may either pass individual `onNext` and `onError` callbacks or pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by calling the function that is returned when `onSnapshot` is called. NOTE: Although an `onCompletion` callback can be provided, it will never be called because the snapshot stream is never-ending. Signature: ```typescript -export declare function onSnapshot(firestore: Firestore, snapshotJson: object, observer: { +export declare function onSnapshotResume(firestore: Firestore, snapshotJson: object, observer: { next: (snapshot: QuerySnapshot) => void; error?: (error: FirestoreError) => void; complete?: () => void; @@ -745,7 +875,7 @@ export declare function onSnapshotSignature: ```typescript -export declare function onSnapshot(firestore: Firestore, snapshotJson: object, observer: { +export declare function onSnapshotResume(firestore: Firestore, snapshotJson: object, observer: { next: (snapshot: DocumentSnapshot) => void; error?: (error: FirestoreError) => void; complete?: () => void; @@ -776,7 +906,7 @@ export declare function onSnapshotSignature: ```typescript -export declare function onSnapshot(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, observer: { +export declare function onSnapshotResume(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, observer: { next: (snapshot: QuerySnapshot) => void; error?: (error: FirestoreError) => void; complete?: () => void; @@ -808,7 +938,7 @@ export declare function onSnapshotSignature: ```typescript -export declare function onSnapshot(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, observer: { +export declare function onSnapshotResume(firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, observer: { next: (snapshot: DocumentSnapshot) => void; error?: (error: FirestoreError) => void; complete?: () => void; @@ -840,35 +970,6 @@ export declare function onSnapshotSignature: - -```typescript -export declare function onSnapshot(firestore: Firestore, snapshotJson: object, onNext: (snapshot: QuerySnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; -``` - -#### Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| firestore | [Firestore](./firestore_.firestore.md#firestore_class) | The [Firestore](./firestore_.firestore.md#firestore_class) instance to enable persistence for. | -| snapshotJson | object | A JSON object generated by invoking [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). | -| onNext | (snapshot: [QuerySnapshot](./firestore_.querysnapshot.md#querysnapshot_class)<AppModelType, DbModelType>) => void | A callback to be called every time a new QuerySnapshot is available. | -| onError | (error: [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class)) => void | A callback to be called if the listen fails or is cancelled. No further callbacks will occur. | -| onCompletion | () => void | Can be provided, but will not be called since streams are never ending. | -| converter | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface)<DbModelType> | An optional object that converts objects from Firestore before the onNext listener is invoked. | - -Returns: - -[Unsubscribe](./firestore_.unsubscribe.md#unsubscribe_interface) - -An unsubscribe function that can be called to cancel the snapshot listener. - ### onSnapshotsInSync(firestore, observer) {:#onsnapshotsinsync_2f0dfa4} Attaches a listener for a snapshots-in-sync event. The snapshots-in-sync event indicates that all listeners affected by a given change have fired, even if a single server-generated change affects multiple listeners. diff --git a/docs-devsite/firestore_.querysnapshot.md b/docs-devsite/firestore_.querysnapshot.md index da0913d7b6e..78f1ac3f23b 100644 --- a/docs-devsite/firestore_.querysnapshot.md +++ b/docs-devsite/firestore_.querysnapshot.md @@ -34,7 +34,7 @@ export declare class QuerySnapshotQuerySnapshot. | -| [toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) | | | +| [toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) | | Returns a JSON-serializable representation of this QuerySnapshot instance. | ## QuerySnapshot.docs @@ -129,6 +129,8 @@ void ## QuerySnapshot.toJSON() +Returns a JSON-serializable representation of this `QuerySnapshot` instance. + Signature: ```typescript @@ -138,3 +140,5 @@ toJSON(): object; object +a JSON representation of this object. + diff --git a/docs-devsite/firestore_.timestamp.md b/docs-devsite/firestore_.timestamp.md index dd902aeba75..9d7282e5a2a 100644 --- a/docs-devsite/firestore_.timestamp.md +++ b/docs-devsite/firestore_.timestamp.md @@ -40,7 +40,7 @@ export declare class Timestamp | Method | Modifiers | Description | | --- | --- | --- | | [fromDate(date)](./firestore_.timestamp.md#timestampfromdate) | static | Creates a new timestamp from the given date. | -| [fromJSON(json)](./firestore_.timestamp.md#timestampfromjson) | static | Builds a Timestamp instance from a JSON serialized version of Bytes. | +| [fromJSON(json)](./firestore_.timestamp.md#timestampfromjson) | static | Builds a Timestamp instance from a JSON object created by [Timestamp.toJSON()](./firestore_.timestamp.md#timestamptojson). | | [fromMillis(milliseconds)](./firestore_.timestamp.md#timestampfrommillis) | static | Creates a new timestamp from the given number of milliseconds. | | [isEqual(other)](./firestore_.timestamp.md#timestampisequal) | | Returns true if this Timestamp is equal to the provided one. | | [now()](./firestore_.timestamp.md#timestampnow) | static | Creates a new timestamp with the current date, with millisecond precision. | @@ -113,7 +113,7 @@ A new `Timestamp` representing the same point in time as the given date. ## Timestamp.fromJSON() -Builds a `Timestamp` instance from a JSON serialized version of `Bytes`. +Builds a `Timestamp` instance from a JSON object created by [Timestamp.toJSON()](./firestore_.timestamp.md#timestamptojson). Signature: diff --git a/docs-devsite/firestore_.vectorvalue.md b/docs-devsite/firestore_.vectorvalue.md index 265251142ce..1fc4e2b35ab 100644 --- a/docs-devsite/firestore_.vectorvalue.md +++ b/docs-devsite/firestore_.vectorvalue.md @@ -43,7 +43,7 @@ static fromJSON(json: object): VectorValue; | Parameter | Type | Description | | --- | --- | --- | -| json | object | a JSON object represention of a VectorValue instance | +| json | object | a JSON object represention of a VectorValue instance. | Returns: diff --git a/docs-devsite/firestore_lite.documentreference.md b/docs-devsite/firestore_lite.documentreference.md index 4dd970afb7e..2a09e2e5964 100644 --- a/docs-devsite/firestore_lite.documentreference.md +++ b/docs-devsite/firestore_lite.documentreference.md @@ -33,6 +33,7 @@ export declare class DocumentReferencestatic | Builds a DocumentReference instance from a JSON object created by [DocumentReference.toJSON()](./firestore_.documentreference.md#documentreferencetojson). | | [fromJSON(firestore, json, converter)](./firestore_lite.documentreference.md#documentreferencefromjson) | static | Builds a DocumentReference instance from a JSON object created by [DocumentReference.toJSON()](./firestore_.documentreference.md#documentreferencetojson). | | [toJSON()](./firestore_lite.documentreference.md#documentreferencetojson) | | Returns a JSON-serializable representation of this DocumentReference instance. | | [withConverter(converter)](./firestore_lite.documentreference.md#documentreferencewithconverter) | | Applies a custom data converter to this DocumentReference, allowing you to use your own custom model objects with Firestore. When you call [setDoc()](./firestore_lite.md#setdoc_ee215ad), [getDoc()](./firestore_lite.md#getdoc_4569087), etc. with the returned DocumentReference instance, the provided converter will convert between Firestore data of type NewDbModelType and your custom type NewAppModelType. | @@ -105,16 +106,39 @@ Builds a `DocumentReference` instance from a JSON object created by [DocumentRef Signature: ```typescript -static fromJSON(firestore: Firestore, json: object, converter?: FirestoreDataConverter): DocumentReference; +static fromJSON(firestore: Firestore, json: object): DocumentReference; ``` #### Parameters | Parameter | Type | Description | | --- | --- | --- | -| firestore | [Firestore](./firestore_lite.firestore.md#firestore_class) | | +| firestore | [Firestore](./firestore_lite.firestore.md#firestore_class) | The [Firestore](./firestore_.firestore.md#firestore_class) instance the snapshot should be loaded for. | | json | object | a JSON object represention of a DocumentReference instance | -| converter | [FirestoreDataConverter](./firestore_lite.firestoredataconverter.md#firestoredataconverter_interface)<NewAppModelType, NewDbModelType> | | + +Returns: + +[DocumentReference](./firestore_lite.documentreference.md#documentreference_class) + +an instance of [DocumentReference](./firestore_.documentreference.md#documentreference_class) if the JSON object could be parsed. Throws a [FirestoreError](./firestore_.firestoreerror.md#firestoreerror_class) if an error occurs. + +## DocumentReference.fromJSON() + +Builds a `DocumentReference` instance from a JSON object created by [DocumentReference.toJSON()](./firestore_.documentreference.md#documentreferencetojson). + +Signature: + +```typescript +static fromJSON(firestore: Firestore, json: object, converter: FirestoreDataConverter): DocumentReference; +``` + +#### Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| firestore | [Firestore](./firestore_lite.firestore.md#firestore_class) | The [Firestore](./firestore_.firestore.md#firestore_class) instance the snapshot should be loaded for. | +| json | object | a JSON object represention of a DocumentReference instance | +| converter | [FirestoreDataConverter](./firestore_lite.firestoredataconverter.md#firestoredataconverter_interface)<NewAppModelType, NewDbModelType> | Converts objects to and from Firestore. | Returns: diff --git a/docs-devsite/firestore_lite.timestamp.md b/docs-devsite/firestore_lite.timestamp.md index 5dbbbfda0bc..0fb35ada682 100644 --- a/docs-devsite/firestore_lite.timestamp.md +++ b/docs-devsite/firestore_lite.timestamp.md @@ -40,7 +40,7 @@ export declare class Timestamp | Method | Modifiers | Description | | --- | --- | --- | | [fromDate(date)](./firestore_lite.timestamp.md#timestampfromdate) | static | Creates a new timestamp from the given date. | -| [fromJSON(json)](./firestore_lite.timestamp.md#timestampfromjson) | static | Builds a Timestamp instance from a JSON serialized version of Bytes. | +| [fromJSON(json)](./firestore_lite.timestamp.md#timestampfromjson) | static | Builds a Timestamp instance from a JSON object created by [Timestamp.toJSON()](./firestore_.timestamp.md#timestamptojson). | | [fromMillis(milliseconds)](./firestore_lite.timestamp.md#timestampfrommillis) | static | Creates a new timestamp from the given number of milliseconds. | | [isEqual(other)](./firestore_lite.timestamp.md#timestampisequal) | | Returns true if this Timestamp is equal to the provided one. | | [now()](./firestore_lite.timestamp.md#timestampnow) | static | Creates a new timestamp with the current date, with millisecond precision. | @@ -113,7 +113,7 @@ A new `Timestamp` representing the same point in time as the given date. ## Timestamp.fromJSON() -Builds a `Timestamp` instance from a JSON serialized version of `Bytes`. +Builds a `Timestamp` instance from a JSON object created by [Timestamp.toJSON()](./firestore_.timestamp.md#timestamptojson). Signature: diff --git a/docs-devsite/firestore_lite.vectorvalue.md b/docs-devsite/firestore_lite.vectorvalue.md index 99b27bf8553..17c18e4c4ed 100644 --- a/docs-devsite/firestore_lite.vectorvalue.md +++ b/docs-devsite/firestore_lite.vectorvalue.md @@ -43,7 +43,7 @@ static fromJSON(json: object): VectorValue; | Parameter | Type | Description | | --- | --- | --- | -| json | object | a JSON object represention of a VectorValue instance | +| json | object | a JSON object represention of a VectorValue instance. | Returns: diff --git a/packages/firestore/src/api.ts b/packages/firestore/src/api.ts index ea969c6b94c..d05f032a910 100644 --- a/packages/firestore/src/api.ts +++ b/packages/firestore/src/api.ts @@ -89,9 +89,11 @@ export { DocumentChange, DocumentChangeType, DocumentSnapshot, + documentSnapshotFromJSON, FirestoreDataConverter, QueryDocumentSnapshot, QuerySnapshot, + querySnapshotFromJSON, snapshotEqual, SnapshotMetadata, SnapshotOptions @@ -161,6 +163,7 @@ export { getDocsFromServer, onSnapshot, onSnapshotsInSync, + onSnapshotResume, setDoc, updateDoc } from './api/reference_impl'; diff --git a/packages/firestore/src/api/reference_impl.ts b/packages/firestore/src/api/reference_impl.ts index b920c479d0b..832d80c041a 100644 --- a/packages/firestore/src/api/reference_impl.ts +++ b/packages/firestore/src/api/reference_impl.ts @@ -659,6 +659,90 @@ export function onSnapshot( onError?: (error: FirestoreError) => void, onCompletion?: () => void ): Unsubscribe; +export function onSnapshot( + reference: + | Query + | DocumentReference, + ...args: unknown[] +): Unsubscribe { + // onSnapshot for Query or Document. + reference = getModularInstance(reference); + let options: SnapshotListenOptions = { + includeMetadataChanges: false, + source: 'default' + }; + let currArg = 0; + if (typeof args[currArg] === 'object' && !isPartialObserver(args[currArg])) { + options = args[currArg++] as SnapshotListenOptions; + } + + const internalOptions = { + includeMetadataChanges: options.includeMetadataChanges, + source: options.source as ListenerDataSource + }; + + if (isPartialObserver(args[currArg])) { + const userObserver = args[currArg] as PartialObserver< + QuerySnapshot + >; + args[currArg] = userObserver.next?.bind(userObserver); + args[currArg + 1] = userObserver.error?.bind(userObserver); + args[currArg + 2] = userObserver.complete?.bind(userObserver); + } + + let observer: PartialObserver; + let firestore: Firestore; + let internalQuery: InternalQuery; + + if (reference instanceof DocumentReference) { + firestore = cast(reference.firestore, Firestore); + internalQuery = newQueryForPath(reference._key.path); + + observer = { + next: snapshot => { + if (args[currArg]) { + ( + args[currArg] as NextFn> + )( + convertToDocSnapshot( + firestore, + reference as DocumentReference, + snapshot + ) + ); + } + }, + error: args[currArg + 1] as ErrorFn, + complete: args[currArg + 2] as CompleteFn + }; + } else { + const query = cast>(reference, Query); + firestore = cast(query.firestore, Firestore); + internalQuery = query._query; + const userDataWriter = new ExpUserDataWriter(firestore); + observer = { + next: snapshot => { + if (args[currArg]) { + (args[currArg] as NextFn>)( + new QuerySnapshot(firestore, userDataWriter, query, snapshot) + ); + } + }, + error: args[currArg + 1] as ErrorFn, + complete: args[currArg + 2] as CompleteFn + }; + + validateHasExplicitOrderByForLimitToLast(reference._query); + } + + const client = ensureFirestoreConfigured(firestore); + return firestoreClientListen( + client, + internalQuery, + internalOptions, + observer + ); +} /** * Attaches a listener for `QuerySnapshot` events based on data generated by invoking @@ -679,7 +763,10 @@ export function onSnapshot( * listener is invoked. * @returns An unsubscribe function that can be called to cancel the snapshot listener. */ -export function onSnapshot( +export function onSnapshotResume< + AppModelType, + DbModelType extends DocumentData +>( firestore: Firestore, snapshotJson: object, onNext: (snapshot: QuerySnapshot) => void, @@ -689,7 +776,7 @@ export function onSnapshot( ): Unsubscribe; /** * Attaches a listener for `DocumentSnapshot` events based on data generated by invoking - * {@link DocumentSnapshot.toJSON} You may either pass individual `onNext` and `onError` callbacks or + * {@link DocumentSnapshot.toJSON}. You may either pass individual `onNext` and `onError` callbacks or * pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by * calling the function that is returned when `onSnapshot` is called. * @@ -699,7 +786,7 @@ export function onSnapshot( * @param firestore - The {@link Firestore} instance to enable persistence for. * @param snapshotJson - A JSON object generated by invoking {@link DocumentSnapshot.toJSON}. * @param onNext - A callback to be called every time a new `DocumentSnapshot` is available. - * @param onError - A callback to be called if the listen fails or is cancelled. No fruther + * @param onError - A callback to be called if the listen fails or is cancelled. No further * callbacks will occur. * @param onCompletion - Can be provided, but will not be called since streams are * never ending. @@ -707,7 +794,10 @@ export function onSnapshot( * listener is invoked. * @returns An unsubscribe function that can be called to cancel the snapshot listener. */ -export function onSnapshot( +export function onSnapshotResume< + AppModelType, + DbModelType extends DocumentData +>( firestore: Firestore, snapshotJson: object, onNext: (snapshot: DocumentSnapshot) => void, @@ -717,7 +807,7 @@ export function onSnapshot( ): Unsubscribe; /** * Attaches a listener for `QuerySnapshot` events based on data generated by invoking - * {@link QuerySnapshot.toJSON} You may either pass individual `onNext` and `onError` callbacks or + * {@link QuerySnapshot.toJSON}. You may either pass individual `onNext` and `onError` callbacks or * pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by * calling the function that is returned when `onSnapshot` is called. * @@ -735,7 +825,10 @@ export function onSnapshot( * listener is invoked. * @returns An unsubscribe function that can be called to cancel the snapshot listener. */ -export function onSnapshot( +export function onSnapshotResume< + AppModelType, + DbModelType extends DocumentData +>( firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, @@ -746,7 +839,7 @@ export function onSnapshot( ): Unsubscribe; /** * Attaches a listener for `DocumentSnapshot` events based on data generated by invoking - * {@link DocumentSnapshot.toJSON} You may either pass individual `onNext` and `onError` callbacks + * {@link DocumentSnapshot.toJSON}. You may either pass individual `onNext` and `onError` callbacks * or pass a single observer object with `next` and `error` callbacks. The listener can be cancelled * by calling the function that is returned when `onSnapshot` is called. * @@ -765,7 +858,10 @@ export function onSnapshot( * @returns An unsubscribe function that can be called to cancel * the snapshot listener. */ -export function onSnapshot( +export function onSnapshotResume< + AppModelType, + DbModelType extends DocumentData +>( firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, @@ -774,10 +870,9 @@ export function onSnapshot( onCompletion?: () => void, converter?: FirestoreDataConverter ): Unsubscribe; - /** * Attaches a listener for `QuerySnapshot` events based on QuerySnapshot data generated by invoking - * {@link QuerySnapshot.toJSON} You may either pass individual `onNext` and `onError` callbacks or + * {@link QuerySnapshot.toJSON}. You may either pass individual `onNext` and `onError` callbacks or * pass a single observer object with `next` and `error` callbacks. The listener can be cancelled by * calling the function that is returned when `onSnapshot` is called. * @@ -792,7 +887,10 @@ export function onSnapshot( * @returns An unsubscribe function that can be called to cancel * the snapshot listener. */ -export function onSnapshot( +export function onSnapshotResume< + AppModelType, + DbModelType extends DocumentData +>( firestore: Firestore, snapshotJson: object, observer: { @@ -819,7 +917,10 @@ export function onSnapshot( * @returns An unsubscribe function that can be called to cancel * the snapshot listener. */ -export function onSnapshot( +export function onSnapshotResume< + AppModelType, + DbModelType extends DocumentData +>( firestore: Firestore, snapshotJson: object, observer: { @@ -847,7 +948,10 @@ export function onSnapshot( * @returns An unsubscribe function that can be called to cancel * the snapshot listener. */ -export function onSnapshot( +export function onSnapshotResume< + AppModelType, + DbModelType extends DocumentData +>( firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, @@ -875,7 +979,10 @@ export function onSnapshot( * listener is invoked. * @returns An unsubscribe function that can be called to cancel the snapshot listener. */ -export function onSnapshot( +export function onSnapshotResume< + AppModelType, + DbModelType extends DocumentData +>( firestore: Firestore, snapshotJson: object, options: SnapshotListenOptions, @@ -886,94 +993,89 @@ export function onSnapshot( }, converter?: FirestoreDataConverter ): Unsubscribe; -export function onSnapshot( - reference: - | Query - | DocumentReference - | Firestore, - ...args: unknown[] -): Unsubscribe { - if (reference instanceof Firestore) { - return onSnapshotBundle(reference as Firestore, ...args); - } - - // onSnapshot for Query or Document. - reference = getModularInstance(reference); - let options: SnapshotListenOptions = { - includeMetadataChanges: false, - source: 'default' - }; - let currArg = 0; - if (typeof args[currArg] === 'object' && !isPartialObserver(args[currArg])) { - options = args[currArg++] as SnapshotListenOptions; +export function onSnapshotResume< + AppModelType, + DbModelType extends DocumentData +>(reference: Firestore, snapshotJson: object, ...args: unknown[]): Unsubscribe { + const db = getModularInstance(reference); + const json = normalizeSnapshotJsonFields(snapshotJson); + if (json.error) { + throw new FirestoreError(Code.INVALID_ARGUMENT, json.error); } - - const internalOptions = { - includeMetadataChanges: options.includeMetadataChanges, - source: options.source as ListenerDataSource - }; - - if (isPartialObserver(args[currArg])) { - const userObserver = args[currArg] as PartialObserver< - QuerySnapshot - >; - args[currArg] = userObserver.next?.bind(userObserver); - args[currArg + 1] = userObserver.error?.bind(userObserver); - args[currArg + 2] = userObserver.complete?.bind(userObserver); + let curArg = 0; + let options: SnapshotListenOptions | undefined = undefined; + if (typeof args[curArg] === 'object' && !isPartialObserver(args[curArg])) { + options = args[curArg++] as SnapshotListenOptions; } - let observer: PartialObserver; - let firestore: Firestore; - let internalQuery: InternalQuery; - - if (reference instanceof DocumentReference) { - firestore = cast(reference.firestore, Firestore); - internalQuery = newQueryForPath(reference._key.path); - - observer = { - next: snapshot => { - if (args[currArg]) { - ( - args[currArg] as NextFn> - )( - convertToDocSnapshot( - firestore, - reference as DocumentReference, - snapshot - ) - ); - } - }, - error: args[currArg + 1] as ErrorFn, - complete: args[currArg + 2] as CompleteFn - }; + if (json.bundleSource === 'QuerySnapshot') { + let observer: { + next: (snapshot: QuerySnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; + } | null = null; + if (typeof args[curArg] === 'object' && isPartialObserver(args[curArg])) { + const userObserver = args[curArg++] as PartialObserver< + QuerySnapshot + >; + observer = { + next: userObserver.next!, + error: userObserver.error, + complete: userObserver.complete + }; + } else { + observer = { + next: args[curArg++] as ( + snapshot: QuerySnapshot + ) => void, + error: args[curArg++] as (error: FirestoreError) => void, + complete: args[curArg++] as () => void + }; + } + return onSnapshotQuerySnapshotBundle( + db, + json, + options, + observer!, + args[curArg] as FirestoreDataConverter + ); + } else if (json.bundleSource === 'DocumentSnapshot') { + let observer: { + next: (snapshot: DocumentSnapshot) => void; + error?: (error: FirestoreError) => void; + complete?: () => void; + } | null = null; + if (typeof args[curArg] === 'object' && isPartialObserver(args[curArg])) { + const userObserver = args[curArg++] as PartialObserver< + DocumentSnapshot + >; + observer = { + next: userObserver.next!, + error: userObserver.error, + complete: userObserver.complete + }; + } else { + observer = { + next: args[curArg++] as ( + snapshot: DocumentSnapshot + ) => void, + error: args[curArg++] as (error: FirestoreError) => void, + complete: args[curArg++] as () => void + }; + } + return onSnapshotDocumentSnapshotBundle( + db, + json, + options, + observer!, + args[curArg] as FirestoreDataConverter + ); } else { - const query = cast>(reference, Query); - firestore = cast(query.firestore, Firestore); - internalQuery = query._query; - const userDataWriter = new ExpUserDataWriter(firestore); - observer = { - next: snapshot => { - if (args[currArg]) { - (args[currArg] as NextFn>)( - new QuerySnapshot(firestore, userDataWriter, query, snapshot) - ); - } - }, - error: args[currArg + 1] as ErrorFn, - complete: args[currArg + 2] as CompleteFn - }; - - validateHasExplicitOrderByForLimitToLast(reference._query); + throw new FirestoreError( + Code.INVALID_ARGUMENT, + `unsupported bundle source: ${json.bundleSource}` + ); } - - const client = ensureFirestoreConfigured(firestore); - return firestoreClientListen( - client, - internalQuery, - internalOptions, - observer - ); } // TODO(firestorexp): Make sure these overloads are tested via the Firestore @@ -1075,105 +1177,6 @@ function convertToDocSnapshot( ); } -/** - * Handles {@link onSnapshot} for a bundle generated by calling {@link QuerySnapshot.toJSON} or - * {@link DocumentSnapshot.toJSON}. Parse the JSON object containing the bundle to determine the - * `bundleSource` (either form a {@link DocumentSnapshot} or {@link QuerySnapshot}, and marshall the - * other optional parameters before sending the request to either - * {@link onSnapshotDocumentSnapshotBundle} or {@link onSnapshotQuerySnapshotBundle}, respectively. - * - * @param firestore - The {@link Firestore} instance for the {@link onSnapshot} operation request. - * @param args - The variadic arguments passed to {@link onSnapshot}. - * @returns An unsubscribe function that can be called to cancel the snapshot - * listener. - * - * @internal - */ -function onSnapshotBundle( - reference: Firestore, - ...args: unknown[] -): Unsubscribe { - const db = getModularInstance(reference); - let curArg = 0; - const snapshotJson = normalizeSnapshotJsonFields(args[curArg++] as object); - if (snapshotJson.error) { - throw new FirestoreError(Code.INVALID_ARGUMENT, snapshotJson.error); - } - let options: SnapshotListenOptions | undefined = undefined; - if (typeof args[curArg] === 'object' && !isPartialObserver(args[curArg])) { - options = args[curArg++] as SnapshotListenOptions; - } - - if (snapshotJson.bundleSource === 'QuerySnapshot') { - let observer: { - next: (snapshot: QuerySnapshot) => void; - error?: (error: FirestoreError) => void; - complete?: () => void; - } | null = null; - if (typeof args[curArg] === 'object' && isPartialObserver(args[1])) { - const userObserver = args[curArg++] as PartialObserver< - QuerySnapshot - >; - observer = { - next: userObserver.next!, - error: userObserver.error, - complete: userObserver.complete - }; - } else { - observer = { - next: args[curArg++] as ( - snapshot: QuerySnapshot - ) => void, - error: args[curArg++] as (error: FirestoreError) => void, - complete: args[curArg++] as () => void - }; - } - return onSnapshotQuerySnapshotBundle( - db, - snapshotJson, - options, - observer!, - args[curArg] as FirestoreDataConverter - ); - } else if (snapshotJson.bundleSource === 'DocumentSnapshot') { - let observer: { - next: (snapshot: DocumentSnapshot) => void; - error?: (error: FirestoreError) => void; - complete?: () => void; - } | null = null; - if (typeof args[curArg] === 'object' && isPartialObserver(args[1])) { - const userObserver = args[curArg++] as PartialObserver< - DocumentSnapshot - >; - observer = { - next: userObserver.next!, - error: userObserver.error, - complete: userObserver.complete - }; - } else { - observer = { - next: args[curArg++] as ( - snapshot: DocumentSnapshot - ) => void, - error: args[curArg++] as (error: FirestoreError) => void, - complete: args[curArg++] as () => void - }; - } - return onSnapshotDocumentSnapshotBundle( - db, - snapshotJson, - options, - observer!, - args[curArg] as FirestoreDataConverter - ); - } else { - throw new FirestoreError( - Code.INVALID_ARGUMENT, - `unsupported bundle source: ${snapshotJson.bundleSource}` - ); - } -} - /** * Ensures the data required to construct an {@link onSnapshot} listener exist in a `snapshotJson` * object that originates from {@link DocumentSnapshot.toJSON} or {@link Querysnapshot.toJSON}. The diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index e6a66b509d1..b451aabfce2 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +import { BundleLoader } from '../core/bundle_impl'; +import { createBundleReaderSync } from '../core/firestore_client'; import { newQueryComparator } from '../core/query'; import { ChangeType, ViewSnapshot } from '../core/view_snapshot'; import { FieldPath } from '../lite-api/field_path'; @@ -26,6 +28,7 @@ import { SetOptions, WithFieldValue } from '../lite-api/reference'; +import { LiteUserDataWriter } from '../lite-api/reference_impl'; import { DocumentSnapshot as LiteDocumentSnapshot, fieldPathFromArgument, @@ -33,8 +36,14 @@ import { } from '../lite-api/snapshot'; import { UntypedFirestoreDataConverter } from '../lite-api/user_data_reader'; import { AbstractUserDataWriter } from '../lite-api/user_data_writer'; +import { fromBundledQuery } from '../local/local_serializer'; +import { documentKeySet } from '../model/collections'; import { Document } from '../model/document'; import { DocumentKey } from '../model/document_key'; +import { DocumentSet } from '../model/document_set'; +import { ResourcePath } from '../model/path'; +import { newSerializer } from '../platform/serializer'; +import { fromDocument } from '../remote/serializer'; import { debugAssert, fail } from '../util/assert'; import { BundleBuilder, @@ -42,6 +51,9 @@ import { QuerySnapshotBundleData } from '../util/bundle_builder_impl'; import { Code, FirestoreError } from '../util/error'; +// API extractor fails importing 'property' unless we also explicitly import 'Property'. +// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports-ts +import { Property, property, validateJSON } from '../util/json_validation'; import { AutoId } from '../util/misc'; import { Firestore } from './database'; @@ -503,10 +515,24 @@ export class DocumentSnapshot< return undefined; } + static _jsonSchemaVersion: string = 'firestore/documentSnapshot/1.0'; + static _jsonSchema = { + type: property('string', DocumentSnapshot._jsonSchemaVersion), + bundleSource: property('string', 'DocumentSnapshot'), + bundleName: property('string'), + bundle: property('string') + }; + + /** + * Returns a JSON-serializable representation of this `DocumentSnapshot` instance. + * + * @returns a JSON representation of this object. + */ toJSON(): object { const document = this._document; // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: any = {}; + result['type'] = DocumentSnapshot._jsonSchemaVersion; result['bundle'] = ''; result['bundleSource'] = 'DocumentSnapshot'; result['bundleName'] = this._key.toString(); @@ -545,6 +571,92 @@ export class DocumentSnapshot< } } +/** + * Builds a `DocumentSnapshot` instance from a JSON object created by + * {@link DocumentSnapshot.toJSON}. + * + * @param firestore - The {@link Firestore} instance the snapshot should be loaded for. + * @param json - a JSON object represention of a `DocumentSnapshot` instance. + * @returns an instance of {@link DocumentSnapshot} if the JSON object could be + * parsed. Throws a {@link FirestoreError} if an error occurs. + */ +export function documentSnapshotFromJSON( + db: Firestore, + json: object +): DocumentSnapshot; +/** + * Builds a `DocumentSnapshot` instance from a JSON object created by + * {@link DocumentSnapshot.toJSON}. + * + * @param firestore - The {@link Firestore} instance the snapshot should be loaded for. + * @param json - a JSON object represention of a `DocumentSnapshot` instance. + * @param converter - Converts objects to and from Firestore. + * @returns an instance of {@link DocumentSnapshot} if the JSON object could be + * parsed. Throws a {@link FirestoreError} if an error occurs. + */ +export function documentSnapshotFromJSON< + AppModelType, + DbModelType extends DocumentData = DocumentData +>( + db: Firestore, + json: object, + converter: FirestoreDataConverter +): DocumentSnapshot; +export function documentSnapshotFromJSON< + AppModelType, + DbModelType extends DocumentData = DocumentData +>( + db: Firestore, + json: object, + converter?: FirestoreDataConverter +): DocumentSnapshot { + if (validateJSON(json, DocumentSnapshot._jsonSchema)) { + // Parse the bundle data. + const serializer = newSerializer(db._databaseId); + const bundleReader = createBundleReaderSync(json.bundle, serializer); + const elements = bundleReader.getElements(); + const bundleLoader: BundleLoader = new BundleLoader( + bundleReader.getMetadata(), + serializer + ); + for (const element of elements) { + bundleLoader.addSizedElement(element); + } + + // Ensure that we have the correct number of documents in the bundle. + const bundledDocuments = bundleLoader.documents; + if (bundledDocuments.length !== 1) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + `Expected bundle data to contain 1 document, but it contains ${bundledDocuments.length} documents.` + ); + } + + // Build out the internal document data. + const document = fromDocument(serializer, bundledDocuments[0].document!); + const documentKey = new DocumentKey( + ResourcePath.fromString(json.bundleName) + ); + + // Return the external facing DocumentSnapshot. + return new DocumentSnapshot( + db, + new LiteUserDataWriter(db), + documentKey, + document, + new SnapshotMetadata( + /* hasPendingWrites= */ false, + /* fromCache= */ false + ), + converter ? converter : null + ); + } + throw new FirestoreError( + Code.INTERNAL, + 'Unexpected error creating DocumentSnapshot from JSON.' + ); +} + /** * A `QueryDocumentSnapshot` contains data read from a document in your * Firestore database as part of a query. The document is guaranteed to exist @@ -699,9 +811,23 @@ export class QuerySnapshot< return this._cachedChanges; } + static _jsonSchemaVersion: string = 'firestore/querySnapshot/1.0'; + static _jsonSchema = { + type: property('string', QuerySnapshot._jsonSchemaVersion), + bundleSource: property('string', 'QuerySnapshot'), + bundleName: property('string'), + bundle: property('string') + }; + + /** + * Returns a JSON-serializable representation of this `QuerySnapshot` instance. + * + * @returns a JSON representation of this object. + */ toJSON(): object { // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: any = {}; + result['type'] = QuerySnapshot._jsonSchemaVersion; result['bundleSource'] = 'QuerySnapshot'; result['bundleName'] = AutoId.newId(); @@ -749,6 +875,105 @@ export class QuerySnapshot< } } +/** + * Builds a `QuerySnapshot` instance from a JSON object created by + * {@link QuerySnapshot.toJSON}. + * + * @param firestore - The {@link Firestore} instance the snapshot should be loaded for. + * @param json - a JSON object represention of a `QuerySnapshot` instance. + * @returns an instance of {@link QuerySnapshot} if the JSON object could be + * parsed. Throws a {@link FirestoreError} if an error occurs. + */ +export function querySnapshotFromJSON( + db: Firestore, + json: object +): QuerySnapshot; +/** + * Builds a `QuerySnapshot` instance from a JSON object created by + * {@link QuerySnapshot.toJSON}. + * + * @param firestore - The {@link Firestore} instance the snapshot should be loaded for. + * @param json - a JSON object represention of a `QuerySnapshot` instance. + * @param converter - Converts objects to and from Firestore. + * @returns an instance of {@link QuerySnapshot} if the JSON object could be + * parsed. Throws a {@link FirestoreError} if an error occurs. + */ +export function querySnapshotFromJSON< + AppModelType, + DbModelType extends DocumentData = DocumentData +>( + db: Firestore, + json: object, + converter: FirestoreDataConverter +): QuerySnapshot; +export function querySnapshotFromJSON< + AppModelType, + DbModelType extends DocumentData +>( + db: Firestore, + json: object, + converter?: FirestoreDataConverter +): QuerySnapshot { + if (validateJSON(json, QuerySnapshot._jsonSchema)) { + // Parse the bundle data. + const serializer = newSerializer(db._databaseId); + const bundleReader = createBundleReaderSync(json.bundle, serializer); + const elements = bundleReader.getElements(); + const bundleLoader: BundleLoader = new BundleLoader( + bundleReader.getMetadata(), + serializer + ); + for (const element of elements) { + bundleLoader.addSizedElement(element); + } + + if (bundleLoader.queries.length !== 1) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + `Snapshot data expected 1 query but found ${bundleLoader.queries.length} queries.` + ); + } + + // Create an internal Query object from the named query in the budnle. + const query = fromBundledQuery(bundleLoader.queries[0].bundledQuery!); + + // Construct the arrays of document data for the query. + const bundledDocuments = bundleLoader.documents; + let documentSet = new DocumentSet(); + bundledDocuments.map(bundledDocument => { + const document = fromDocument(serializer, bundledDocument.document!); + documentSet = documentSet.add(document); + }); + // Create a view snapshot of the query and documents. + const viewSnapshot = ViewSnapshot.fromInitialDocuments( + query, + documentSet, + documentKeySet() /* Zero mutated keys signifies no pending writes. */, + /* fromCache= */ false, + /* hasCachedResults= */ false + ); + + // Create an external Query object, required to construct the QuerySnapshot. + const externalQuery = new Query( + db, + converter ? converter : null, + query + ); + + // Return a new QuerySnapshot with all of the collected data. + return new QuerySnapshot( + db, + new LiteUserDataWriter(db), + externalQuery, + viewSnapshot + ); + } + throw new FirestoreError( + Code.INTERNAL, + 'Unexpected error creating QuerySnapshot from JSON.' + ); +} + /** Calculates the array of `DocumentChange`s for a given `ViewSnapshot`. */ export function changesFromSnapshot< AppModelType, diff --git a/packages/firestore/src/core/bundle_impl.ts b/packages/firestore/src/core/bundle_impl.ts index 9a42e43261f..b91933f1349 100644 --- a/packages/firestore/src/core/bundle_impl.ts +++ b/packages/firestore/src/core/bundle_impl.ts @@ -82,27 +82,42 @@ export class BundleConverterImpl implements BundleConverter { } /** - * A class to process the elements from a bundle, load them into local + * A class to process the elements from a bundle, and optionally load them into local * storage and provide progress update while loading. */ export class BundleLoader { /** The current progress of loading */ private progress: LoadBundleTaskProgress; /** Batched queries to be saved into storage */ - private queries: ProtoNamedQuery[] = []; + private _queries: ProtoNamedQuery[] = []; /** Batched documents to be saved into storage */ - private documents: BundledDocuments = []; + private _documents: BundledDocuments = []; /** The collection groups affected by this bundle. */ private collectionGroups = new Set(); constructor( private bundleMetadata: ProtoBundleMetadata, - private localStore: LocalStore, private serializer: JsonProtoSerializer ) { this.progress = bundleInitialProgress(bundleMetadata); } + /** + * Returns the named queries that have been parsed from the SizeBundleElements added by + * calling {@link adSizedElement}. + */ + get queries(): ProtoNamedQuery[] { + return this._queries; + } + + /** + * Returns the BundledDocuments that have been parsed from the SizeBundleElements added by + * calling {@link addSizedElement}. + */ + get documents(): BundledDocuments { + return this._documents; + } + /** * Adds an element from the bundle to the loader. * @@ -117,9 +132,9 @@ export class BundleLoader { let documentsLoaded = this.progress.documentsLoaded; if (element.payload.namedQuery) { - this.queries.push(element.payload.namedQuery); + this._queries.push(element.payload.namedQuery); } else if (element.payload.documentMetadata) { - this.documents.push({ metadata: element.payload.documentMetadata }); + this._documents.push({ metadata: element.payload.documentMetadata }); if (!element.payload.documentMetadata.exists) { ++documentsLoaded; } @@ -133,12 +148,12 @@ export class BundleLoader { this.collectionGroups.add(path.get(path.length - 2)); } else if (element.payload.document) { debugAssert( - this.documents.length > 0 && - this.documents[this.documents.length - 1].metadata.name === + this._documents.length > 0 && + this._documents[this._documents.length - 1].metadata.name === element.payload.document.name, 'The document being added does not match the stored metadata.' ); - this.documents[this.documents.length - 1].document = + this._documents[this._documents.length - 1].document = element.payload.document; ++documentsLoaded; } @@ -176,26 +191,28 @@ export class BundleLoader { /** * Update the progress to 'Success' and return the updated progress. */ - async complete(): Promise { + async completeAndStoreAsync( + localStore: LocalStore + ): Promise { debugAssert( - this.documents[this.documents.length - 1]?.metadata.exists !== true || - !!this.documents[this.documents.length - 1].document, + this._documents[this._documents.length - 1]?.metadata.exists !== true || + !!this._documents[this._documents.length - 1].document, 'Bundled documents end with a document metadata element instead of a document.' ); debugAssert(!!this.bundleMetadata.id, 'Bundle ID must be set.'); const changedDocs = await localStoreApplyBundledDocuments( - this.localStore, + localStore, new BundleConverterImpl(this.serializer), - this.documents, + this._documents, this.bundleMetadata.id! ); const queryDocumentMap = this.getQueryDocumentMapping(this.documents); - for (const q of this.queries) { + for (const q of this._queries) { await localStoreSaveNamedQuery( - this.localStore, + localStore, q, queryDocumentMap.get(q.name!) ); diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index e2aa19aaba8..39bb8dd4eba 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -53,8 +53,9 @@ import { JsonProtoSerializer } from '../remote/serializer'; import { debugAssert } from '../util/assert'; import { AsyncObserver } from '../util/async_observer'; import { AsyncQueue, wrapInUserErrorIfRecoverable } from '../util/async_queue'; -import { BundleReader } from '../util/bundle_reader'; +import { BundleReader, BundleReaderSync } from '../util/bundle_reader'; import { newBundleReader } from '../util/bundle_reader_impl'; +import { newBundleReaderSync } from '../util/bundle_reader_sync_impl'; import { Code, FirestoreError } from '../util/error'; import { logDebug, logWarn } from '../util/log'; import { AutoId } from '../util/misc'; @@ -339,7 +340,7 @@ async function ensureOfflineComponents( return client._offlineComponents!; } -async function ensureOnlineComponents( +export async function ensureOnlineComponents( client: FirestoreClient ): Promise { if (!client._onlineComponents) { @@ -821,6 +822,13 @@ function createBundleReader( return newBundleReader(toByteStreamReader(content), serializer); } +export function createBundleReaderSync( + bundleData: string, + serializer: JsonProtoSerializer +): BundleReaderSync { + return newBundleReaderSync(bundleData, serializer); +} + export function firestoreClientSetIndexConfiguration( client: FirestoreClient, indexes: FieldIndex[] diff --git a/packages/firestore/src/core/sync_engine_impl.ts b/packages/firestore/src/core/sync_engine_impl.ts index 404d4663a47..f00acb5a4ee 100644 --- a/packages/firestore/src/core/sync_engine_impl.ts +++ b/packages/firestore/src/core/sync_engine_impl.ts @@ -1697,11 +1697,7 @@ async function loadBundleImpl( task._updateProgress(bundleInitialProgress(metadata)); - const loader = new BundleLoader( - metadata, - syncEngine.localStore, - reader.serializer - ); + const loader = new BundleLoader(metadata, reader.serializer); let element = await reader.nextElement(); while (element) { debugAssert( @@ -1716,7 +1712,7 @@ async function loadBundleImpl( element = await reader.nextElement(); } - const result = await loader.complete(); + const result = await loader.completeAndStoreAsync(syncEngine.localStore); await syncEngineEmitNewSnapsAndNotifyLocalStore( syncEngine, result.changedDocs, diff --git a/packages/firestore/src/lite-api/reference.ts b/packages/firestore/src/lite-api/reference.ts index ed66eae7b70..6b95e28eb69 100644 --- a/packages/firestore/src/lite-api/reference.ts +++ b/packages/firestore/src/lite-api/reference.ts @@ -304,10 +304,30 @@ export class DocumentReference< * Builds a `DocumentReference` instance from a JSON object created by * {@link DocumentReference.toJSON}. * + * @param firestore - The {@link Firestore} instance the snapshot should be loaded for. * @param json a JSON object represention of a `DocumentReference` instance * @returns an instance of {@link DocumentReference} if the JSON object could be parsed. Throws a * {@link FirestoreError} if an error occurs. */ + static fromJSON(firestore: Firestore, json: object): DocumentReference; + /** + * Builds a `DocumentReference` instance from a JSON object created by + * {@link DocumentReference.toJSON}. + * + * @param firestore - The {@link Firestore} instance the snapshot should be loaded for. + * @param json a JSON object represention of a `DocumentReference` instance + * @param converter - Converts objects to and from Firestore. + * @returns an instance of {@link DocumentReference} if the JSON object could be parsed. Throws a + * {@link FirestoreError} if an error occurs. + */ + static fromJSON< + NewAppModelType = DocumentData, + NewDbModelType extends DocumentData = DocumentData + >( + firestore: Firestore, + json: object, + converter: FirestoreDataConverter + ): DocumentReference; static fromJSON< NewAppModelType = DocumentData, NewDbModelType extends DocumentData = DocumentData diff --git a/packages/firestore/src/lite-api/timestamp.ts b/packages/firestore/src/lite-api/timestamp.ts index 5dde51b0e28..dac20ccc94f 100644 --- a/packages/firestore/src/lite-api/timestamp.ts +++ b/packages/firestore/src/lite-api/timestamp.ts @@ -184,7 +184,9 @@ export class Timestamp { nanoseconds: property('number') }; - /** Returns a JSON-serializable representation of this `Timestamp`. */ + /** + * Returns a JSON-serializable representation of this `Timestamp`. + */ toJSON(): { seconds: number; nanoseconds: number; type: string } { return { type: Timestamp._jsonSchemaVersion, @@ -193,7 +195,9 @@ export class Timestamp { }; } - /** Builds a `Timestamp` instance from a JSON serialized version of `Bytes`. */ + /** + * Builds a `Timestamp` instance from a JSON object created by {@link Timestamp.toJSON}. + */ static fromJSON(json: object): Timestamp { if (validateJSON(json, Timestamp._jsonSchema)) { return new Timestamp(json.seconds, json.nanoseconds); diff --git a/packages/firestore/src/lite-api/vector_value.ts b/packages/firestore/src/lite-api/vector_value.ts index 88bc55ef032..311ec351f0c 100644 --- a/packages/firestore/src/lite-api/vector_value.ts +++ b/packages/firestore/src/lite-api/vector_value.ts @@ -74,7 +74,7 @@ export class VectorValue { /** * Builds a `VectorValue` instance from a JSON object created by {@link VectorValue.toJSON}. * - * @param json a JSON object represention of a `VectorValue` instance + * @param json a JSON object represention of a `VectorValue` instance. * @returns an instance of {@link VectorValue} if the JSON object could be parsed. Throws a * {@link FirestoreError} if an error occurs. */ diff --git a/packages/firestore/src/util/bundle_reader.ts b/packages/firestore/src/util/bundle_reader.ts index 6ebfb2d5e8e..cca1c61a538 100644 --- a/packages/firestore/src/util/bundle_reader.ts +++ b/packages/firestore/src/util/bundle_reader.ts @@ -65,3 +65,24 @@ export interface BundleReader { */ nextElement(): Promise; } + +/** + * A class representing a synchronized bundle reader. + * + * Takes a bundle string buffer, parses the data, and provides accessors to the data contained + * within it. + */ +export interface BundleReaderSync { + serializer: JsonProtoSerializer; + + /** + * Returns the metadata of the bundle. + */ + getMetadata(): BundleMetadata; + + /** + * Returns BundleElements parsed from the bundle. Returns an empty array if no bundle elements + * exist. + */ + getElements(): SizedBundleElement[]; +} diff --git a/packages/firestore/src/util/bundle_reader_sync_impl.ts b/packages/firestore/src/util/bundle_reader_sync_impl.ts new file mode 100644 index 00000000000..9379bb5a5a7 --- /dev/null +++ b/packages/firestore/src/util/bundle_reader_sync_impl.ts @@ -0,0 +1,129 @@ +/** + * @license + * Copyright 2025 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 { BundleMetadata } from '../protos/firestore_bundle_proto'; +import { JsonProtoSerializer } from '../remote/serializer'; +import { Code, FirestoreError } from '../util/error'; + +import { BundleReaderSync, SizedBundleElement } from './bundle_reader'; + +/** + * A class that can parse a bundle form the string serialization of a bundle. + */ +export class BundleReaderSyncImpl implements BundleReaderSync { + private metadata: BundleMetadata; + private elements: SizedBundleElement[]; + private cursor: number; + constructor( + private bundleData: string, + readonly serializer: JsonProtoSerializer + ) { + this.cursor = 0; + this.elements = []; + + let element = this.nextElement(); + if (element && element.isBundleMetadata()) { + this.metadata = element as BundleMetadata; + } else { + throw new Error(`The first element of the bundle is not a metadata object, it is + ${JSON.stringify(element?.payload)}`); + } + + do { + element = this.nextElement(); + if (element !== null) { + this.elements.push(element); + } + } while (element !== null); + } + + /* Returns the parsed metadata of the bundle. */ + getMetadata(): BundleMetadata { + return this.metadata; + } + + /* Returns the DocumentSnapshot or NamedQuery elements of the bundle. */ + getElements(): SizedBundleElement[] { + return this.elements; + } + + /** + * Parses the next element of the bundle. + * + * @returns a SizedBundleElement representation of the next element in the bundle, or null if + * no more elements exist. + */ + private nextElement(): SizedBundleElement | null { + if (this.cursor === this.bundleData.length) { + return null; + } + const length: number = this.readLength(); + const jsonString = this.readJsonString(length); + return new SizedBundleElement(JSON.parse(jsonString), length); + } + + /** + * Reads from a specified position from the bundleData string, for a specified + * number of bytes. + * + * @param length how many characters to read. + * @returns a string parsed from the bundle. + */ + private readJsonString(length: number): string { + if (this.cursor + length > this.bundleData.length) { + throw new FirestoreError( + Code.INTERNAL, + 'Reached the end of bundle when more is expected.' + ); + } + const result = this.bundleData.slice(this.cursor, (this.cursor += length)); + return result; + } + + /** + * Reads from the current cursor until the first '{'. + * + * @returns A string to integer represention of the parsed value. + * @throws An {@link Error} if the cursor has reached the end of the stream, since lengths + * prefix bundle objects. + */ + private readLength(): number { + const startIndex = this.cursor; + let curIndex = this.cursor; + while (curIndex < this.bundleData.length) { + if (this.bundleData[curIndex] === '{') { + if (curIndex === startIndex) { + throw new Error('First character is a bracket and not a number'); + } + this.cursor = curIndex; + return Number(this.bundleData.slice(startIndex, curIndex)); + } + curIndex++; + } + throw new Error('Reached the end of bundle when more is expected.'); + } +} + +/** + * Creates an instance of BundleReader without exposing the BundleReaderSyncImpl class type. + */ +export function newBundleReaderSync( + bundleData: string, + serializer: JsonProtoSerializer +): BundleReaderSync { + return new BundleReaderSyncImpl(bundleData, serializer); +} diff --git a/packages/firestore/src/util/json_validation.ts b/packages/firestore/src/util/json_validation.ts index f16e28cacff..1660c8c4e77 100644 --- a/packages/firestore/src/util/json_validation.ts +++ b/packages/firestore/src/util/json_validation.ts @@ -112,7 +112,7 @@ export function validateJSON( schema: S ): json is Json { if (!isPlainObject(json)) { - throw new FirestoreError(Code.INVALID_ARGUMENT, 'json must be an object'); + throw new FirestoreError(Code.INVALID_ARGUMENT, 'JSON must be an object'); } let error: string | undefined = undefined; for (const key in schema) { @@ -121,12 +121,13 @@ export function validateJSON( const value: { value: unknown } | undefined = 'value' in schema[key] ? { value: schema[key].value } : undefined; if (!(key in json)) { - error = `json missing required field: ${key}`; + error = `JSON missing required field: '${key}'`; + break; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const fieldValue = (json as any)[key]; if (typeString && typeof fieldValue !== typeString) { - error = `json field '${key}' must be a ${typeString}.`; + error = `JSON field '${key}' must be a ${typeString}.`; break; } else if (value !== undefined && fieldValue !== value.value) { error = `Expected '${key}' field to equal '${value.value}'`; diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index b6320169582..15e3ab9b84b 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -33,6 +33,7 @@ import { DocumentData, documentId, DocumentSnapshot, + documentSnapshotFromJSON, enableIndexedDbPersistence, enableNetwork, getDoc, @@ -42,6 +43,7 @@ import { initializeFirestore, limit, onSnapshot, + onSnapshotResume, onSnapshotsInSync, orderBy, query, @@ -66,6 +68,7 @@ import { newTestApp, FirestoreError, QuerySnapshot, + querySnapshotFromJSON, vector, getDocsFromServer } from '../util/firebase_export'; @@ -1209,7 +1212,7 @@ apiDescribe('Database', persistence => { async (docRef, db) => { const doc = await getDoc(docRef); const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshot( + const unsubscribe = onSnapshotResume( db, doc.toJSON(), accumulator.storeEvent @@ -1240,7 +1243,7 @@ apiDescribe('Database', persistence => { async (docRef, db) => { const doc = await getDoc(docRef); const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshot( + const unsubscribe = onSnapshotResume( db, doc.toJSON(), accumulator.storeEvent @@ -1271,7 +1274,7 @@ apiDescribe('Database', persistence => { async (docRef, db) => { const doc = await getDoc(docRef); const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshot(db, doc.toJSON(), { + const unsubscribe = onSnapshotResume(db, doc.toJSON(), { next: accumulator.storeEvent }); await accumulator @@ -1299,7 +1302,7 @@ apiDescribe('Database', persistence => { bundleSource: 'DocumentSnapshot' }; const deferred = new Deferred(); - const unsubscribe = onSnapshot( + const unsubscribe = onSnapshotResume( db, json, ds => { @@ -1325,7 +1328,7 @@ apiDescribe('Database', persistence => { bundleSource: 'QuerySnapshot' }; const deferred = new Deferred(); - const unsubscribe = onSnapshot(db, json, { + const unsubscribe = onSnapshotResume(db, json, { next: ds => { expect(ds).to.not.exist; deferred.resolve(); @@ -1341,6 +1344,66 @@ apiDescribe('Database', persistence => { }); }); + it('DocumentSnapshot updated doc events in snapshot created by fromJSON bundle', async () => { + const initialData = { a: 0 }; + const finalData = { a: 1 }; + await withTestDocAndInitialData( + persistence, + initialData, + async (docRef, db) => { + const doc = await getDoc(docRef); + const fromJsonDoc = documentSnapshotFromJSON(db, doc.toJSON()); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshotResume( + db, + fromJsonDoc.toJSON(), + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(initialData); + }) + .then(() => setDoc(docRef, finalData)) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(finalData); + }); + unsubscribe(); + } + ); + }); + + it('DocumentSnapshot updated doc events in snapshot created by fromJSON doc ref', async () => { + const initialData = { a: 0 }; + const finalData = { a: 1 }; + await withTestDocAndInitialData( + persistence, + initialData, + async (docRef, db) => { + const doc = await getDoc(docRef); + const fromJsonDoc = documentSnapshotFromJSON(db, doc.toJSON()); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshot(fromJsonDoc.ref, accumulator.storeEvent); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(initialData); + }) + .then(() => setDoc(docRef, finalData)) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(finalData); + }); + unsubscribe(); + } + ); + }); + it('Querysnapshot events for snapshot created by a bundle', async () => { const testDocs = { a: { foo: 1 }, @@ -1349,7 +1412,7 @@ apiDescribe('Database', persistence => { await withTestCollection(persistence, testDocs, async (coll, db) => { const querySnap = await getDocs(query(coll, orderBy(documentId()))); const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshot( + const unsubscribe = onSnapshotResume( db, querySnap.toJSON(), accumulator.storeEvent @@ -1372,7 +1435,7 @@ apiDescribe('Database', persistence => { await withTestCollection(persistence, testDocs, async (coll, db) => { const querySnap = await getDocs(query(coll, orderBy(documentId()))); const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshot(db, querySnap.toJSON(), { + const unsubscribe = onSnapshotResume(db, querySnap.toJSON(), { next: accumulator.storeEvent }); await accumulator.awaitEvent().then(snap => { @@ -1393,7 +1456,7 @@ apiDescribe('Database', persistence => { bundleSource: 'QuerySnapshot' }; const deferred = new Deferred(); - const unsubscribe = onSnapshot( + const unsubscribe = onSnapshotResume( db, json, qs => { @@ -1419,7 +1482,7 @@ apiDescribe('Database', persistence => { bundleSource: 'QuerySnapshot' }; const deferred = new Deferred(); - const unsubscribe = onSnapshot(db, json, { + const unsubscribe = onSnapshotResume(db, json, { next: qs => { expect(qs).to.not.exist; deferred.resolve(); @@ -1444,7 +1507,7 @@ apiDescribe('Database', persistence => { const querySnap = await getDocs(query(coll, orderBy(documentId()))); const refForDocA = querySnap.docs[0].ref; const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshot( + const unsubscribe = onSnapshotResume( db, querySnap.toJSON(), accumulator.storeEvent @@ -1469,6 +1532,75 @@ apiDescribe('Database', persistence => { }); }); + it('QuerySnapshot updated doc events in snapshot created by fromJSON bundle', async () => { + const testDocs = { + a: { foo: 1 }, + b: { bar: 2 } + }; + await withTestCollection(persistence, testDocs, async (coll, db) => { + const querySnap = await getDocs(query(coll, orderBy(documentId()))); + const querySnapFromJson = querySnapshotFromJSON(db, querySnap.toJSON()); + const refForDocA = querySnapFromJson.docs[0].ref; + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshotResume( + db, + querySnapFromJson.toJSON(), + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal(testDocs.a); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }) + .then(() => setDoc(refForDocA, { foo: 0 })) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal({ foo: 0 }); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }); + unsubscribe(); + }); + }); + + it('QuerySnapshot updated doc events in snapshot created by fromJSON query ref', async () => { + const testDocs = { + a: { foo: 1 }, + b: { bar: 2 } + }; + await withTestCollection(persistence, testDocs, async (coll, db) => { + const querySnap = await getDocs(query(coll, orderBy(documentId()))); + const querySnapFromJson = querySnapshotFromJSON(db, querySnap.toJSON()); + const refForDocA = querySnapFromJson.docs[0].ref; + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshot( + querySnapFromJson.query, + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal(testDocs.a); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }) + .then(() => setDoc(refForDocA, { foo: 0 })) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal({ foo: 0 }); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }); + unsubscribe(); + }); + }); + it('Metadata only changes are not fired when no options provided', () => { return withTestDoc(persistence, docRef => { const secondUpdateFound = new Deferred(); diff --git a/packages/firestore/test/unit/api/database.test.ts b/packages/firestore/test/unit/api/database.test.ts index 308835c8855..9fe02663d6f 100644 --- a/packages/firestore/test/unit/api/database.test.ts +++ b/packages/firestore/test/unit/api/database.test.ts @@ -19,6 +19,10 @@ import { expect } from 'chai'; import { DocumentReference, + DocumentSnapshot, + documentSnapshotFromJSON, + QuerySnapshot, + querySnapshotFromJSON, connectFirestoreEmulator, loadBundle, refEqual, @@ -31,6 +35,7 @@ import { collectionReference, documentReference, documentSnapshot, + firestore, newTestFirestore, query, querySnapshot @@ -88,6 +93,117 @@ describe('DocumentReference', () => { }); }); + it('fromJSON() throws with invalid data', () => { + const db = newTestFirestore(); + expect(() => { + DocumentReference.fromJSON(db, {}); + }).to.throw("JSON missing required field: 'type'"); + }); + + it('fromJSON() throws with missing type data', () => { + const db = newTestFirestore(); + expect(() => { + documentSnapshotFromJSON(db, { + bundleSource: 'DocumentSnapshot', + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw("JSON missing required field: 'type'"); + }); + + it('fromJSON() throws with invalid type data', () => { + const db = newTestFirestore(); + expect(() => { + documentSnapshotFromJSON(db, { + type: 1, + bundleSource: 'DocumentSnapshot', + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw("JSON field 'type' must be a string"); + }); + + it('fromJSON() throws with missing bundleSource', () => { + const db = newTestFirestore(); + expect(() => { + documentSnapshotFromJSON(db, { + type: DocumentSnapshot._jsonSchemaVersion, + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw("JSON missing required field: 'bundleSource'"); + }); + + it('fromJSON() throws with invalid bundleSource type', () => { + const db = newTestFirestore(); + expect(() => { + documentSnapshotFromJSON(db, { + type: DocumentSnapshot._jsonSchemaVersion, + bundleSource: 1, + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw("JSON field 'bundleSource' must be a string"); + }); + + it('fromJSON() throws with invalid bundleSource value', () => { + const db = newTestFirestore(); + expect(() => { + documentSnapshotFromJSON(db, { + type: DocumentSnapshot._jsonSchemaVersion, + bundleSource: 'QuerySnapshot', + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw("Expected 'bundleSource' field to equal 'DocumentSnapshot'"); + }); + + it('fromJSON() throws with missing bundleName', () => { + const db = newTestFirestore(); + expect(() => { + documentSnapshotFromJSON(db, { + type: DocumentSnapshot._jsonSchemaVersion, + bundleSource: 'DocumentSnapshot', + bundle: 'test bundle' + }); + }).to.throw("JSON missing required field: 'bundleName'"); + }); + + it('fromJSON() throws with invalid bundleName', () => { + const db = newTestFirestore(); + expect(() => { + documentSnapshotFromJSON(db, { + type: DocumentSnapshot._jsonSchemaVersion, + bundleSource: 'DocumentSnapshot', + bundleName: 1, + bundle: 'test bundle' + }); + }).to.throw("JSON field 'bundleName' must be a string"); + }); + + it('fromJSON() throws with missing bundle', () => { + const db = newTestFirestore(); + expect(() => { + documentSnapshotFromJSON(db, { + type: DocumentSnapshot._jsonSchemaVersion, + bundleSource: 'DocumentSnapshot', + bundleName: 'test name' + }); + }).to.throw("JSON missing required field: 'bundle'"); + }); + + it('fromJSON() throws with invalid bundle', () => { + const db = newTestFirestore(); + expect(() => { + documentSnapshotFromJSON(db, { + type: DocumentSnapshot._jsonSchemaVersion, + bundleSource: 'DocumentSnapshot', + bundleName: 'test name', + bundle: 1 + }); + }).to.throw("JSON field 'bundle' must be a string"); + }); + it('fromJSON() does not throw', () => { const db = newTestFirestore(); const docRef = documentReference('foo/bar'); @@ -190,6 +306,34 @@ describe('DocumentSnapshot', () => { `Await waitForPendingWrites() before invoking toJSON().` ); }); + + it('fromJSON parses toJSON result', () => { + const docSnap = documentSnapshot('foo/bar', { a: 1 }, /*fromCache=*/ true); + const json = docSnap.toJSON(); + expect(() => { + documentSnapshotFromJSON(docSnap._firestore, json); + }).to.not.throw; + }); + + it('fromJSON produces valid snapshot data.', () => { + const json = documentSnapshot( + 'foo/bar', + { a: 1 }, + /*fromCache=*/ true + ).toJSON(); + const db = firestore(); + const docSnap = documentSnapshotFromJSON(db, json); + expect(docSnap).to.exist; + const data = docSnap.data(); + expect(data).to.not.be.undefined; + expect(data).to.not.be.null; + if (data) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((data as any).a).to.exist; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((data as any).a).to.equal(1); + } + }); }); describe('Query', () => { @@ -318,7 +462,7 @@ describe('QuerySnapshot', () => { 'foo', {}, { a: { a: 1 } }, - keys(), + keys(), // An empty set of mutaded document keys signifies that there are no pending writes. false, false ).toJSON(); @@ -342,15 +486,173 @@ describe('QuerySnapshot', () => { 'foo', {}, { a: { a: 1 } }, - keys('foo/a'), - true, - true + keys('foo/a'), // A non empty set of mutated keys signifies pending writes. + false, + false ).toJSON() ).to.throw( `QuerySnapshot.toJSON() attempted to serialize a document with pending writes. ` + `Await waitForPendingWrites() before invoking toJSON().` ); }); + + it('fromJSON() throws with invalid data', () => { + const db = newTestFirestore(); + expect(() => { + querySnapshotFromJSON(db, {}); + }).to.throw("JSON missing required field: 'type'"); + }); + + it('fromJSON() throws with missing type data', () => { + const db = newTestFirestore(); + expect(() => { + querySnapshotFromJSON(db, { + bundleSource: 'QuerySnapshot', + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw("JSON missing required field: 'type'"); + }); + + it('fromJSON() throws with invalid type data', () => { + const db = newTestFirestore(); + expect(() => { + querySnapshotFromJSON(db, { + type: 1, + bundleSource: 'QuerySnapshot', + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw("JSON field 'type' must be a string"); + }); + + it('fromJSON() throws with missing bundle source data', () => { + const db = newTestFirestore(); + expect(() => { + querySnapshotFromJSON(db, { + type: QuerySnapshot._jsonSchemaVersion, + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw("JSON missing required field: 'bundleSource'"); + }); + + it('fromJSON() throws with invalid bundleSource type', () => { + const db = newTestFirestore(); + expect(() => { + querySnapshotFromJSON(db, { + type: QuerySnapshot._jsonSchemaVersion, + bundleSource: 1, + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw("JSON field 'bundleSource' must be a string"); + }); + + it('fromJSON() throws with invalid bundleSource value', () => { + const db = newTestFirestore(); + expect(() => { + querySnapshotFromJSON(db, { + type: QuerySnapshot._jsonSchemaVersion, + bundleSource: 'DocumentSnapshot', + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw("Expected 'bundleSource' field to equal 'QuerySnapshot'"); + }); + + it('fromJSON() throws with missing bundleName', () => { + const db = newTestFirestore(); + expect(() => { + querySnapshotFromJSON(db, { + type: QuerySnapshot._jsonSchemaVersion, + bundleSource: 'QuerySnapshot', + bundle: 'test bundle' + }); + }).to.throw("JSON missing required field: 'bundleName'"); + }); + + it('fromJSON() throws with invalid bundleName', () => { + const db = newTestFirestore(); + expect(() => { + querySnapshotFromJSON(db, { + type: QuerySnapshot._jsonSchemaVersion, + bundleSource: 'QuerySnapshot', + bundleName: 1, + bundle: 'test bundle' + }); + }).to.throw("JSON field 'bundleName' must be a string"); + }); + + it('fromJSON() throws with missing bundle field', () => { + const db = newTestFirestore(); + expect(() => { + querySnapshotFromJSON(db, { + type: QuerySnapshot._jsonSchemaVersion, + bundleSource: 'QuerySnapshot', + bundleName: 'test name' + }); + }).to.throw("JSON missing required field: 'bundle'"); + }); + + it('fromJSON() throws with invalid bundle field', () => { + const db = newTestFirestore(); + expect(() => { + querySnapshotFromJSON(db, { + type: QuerySnapshot._jsonSchemaVersion, + bundleSource: 'QuerySnapshot', + bundleName: 'test name', + bundle: 1 + }); + }).to.throw("JSON field 'bundle' must be a string"); + }); + + it('fromJSON does not throw', () => { + const json = querySnapshot( + 'foo', + {}, + { a: { a: 1 } }, + keys(), // An empty set of mutaded document keys signifies that there are no pending writes. + false, + false + ).toJSON(); + + const db = firestore(); + expect(() => { + querySnapshotFromJSON(db, json); + }).to.not.throw; + }); + + it('fromJSON parses produces valid snapshot data', () => { + const json = querySnapshot( + 'foo', + {}, + { a: { a: 1 } }, + keys(), // An empty set of mutaded document keys signifies that there are no pending writes. + false, + false + ).toJSON(); + + const db = firestore(); + const querySnap = querySnapshotFromJSON(db, json); + expect(querySnap).to.exist; + if (querySnap !== undefined) { + const docs = querySnap.docs; + expect(docs).to.not.be.undefined; + expect(docs).to.not.be.null; + if (docs) { + expect(docs.length).to.equal(1); + docs.map(document => { + const docData = document.data(); + expect(docData).to.exist; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((docData as any).a).to.exist; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((docData as any).a).to.equal(1); + }); + } + } + }); }); describe('SnapshotMetadata', () => { From 38d8bc5c72377048db526dec1948d0f29077031e Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Mon, 19 May 2025 14:05:19 -0400 Subject: [PATCH 7/7] [FEAT] Restrict Firestore Doc/QuerySnapshot toJSON on client. (#9048) To reduce bundle size on clients, the `toJSON` methods of `DocumentSnapshot` and `QuerySnapshot` will not create bundles. ### Testing Updated the integration and unit tests to test `fromJSON` functionality in node environments only. I did attempt to create pre-packaged bundles so that we can test in browser still, but unforunately the bundle includes the project name, and the project that we test against varies depending on the profile of tests that were running (prod, local, etc). If these don't match then an error is thrown. ### API Changes N/A --- packages/firestore/src/api/snapshot.ts | 124 ++-- .../src/platform/browser/snapshot_to_json.ts | 43 ++ .../platform/browser_lite/snapshot_to_json.ts | 18 + .../src/platform/node/snapshot_to_json.ts | 84 +++ .../platform/node_lite/snapshot_to_json.ts | 18 + .../src/platform/rn/snapshot_to_json.ts | 21 + .../src/platform/rn_lite/snapshot_to_json.ts | 18 + .../src/platform/snapshot_to_json.ts | 62 ++ .../test/integration/api/database.test.ts | 561 +++++++++--------- .../test/integration/api/query.test.ts | 69 +-- .../firestore/test/unit/api/database.test.ts | 259 +++++--- 11 files changed, 823 insertions(+), 454 deletions(-) create mode 100644 packages/firestore/src/platform/browser/snapshot_to_json.ts create mode 100644 packages/firestore/src/platform/browser_lite/snapshot_to_json.ts create mode 100644 packages/firestore/src/platform/node/snapshot_to_json.ts create mode 100644 packages/firestore/src/platform/node_lite/snapshot_to_json.ts create mode 100644 packages/firestore/src/platform/rn/snapshot_to_json.ts create mode 100644 packages/firestore/src/platform/rn_lite/snapshot_to_json.ts create mode 100644 packages/firestore/src/platform/snapshot_to_json.ts diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index b451aabfce2..61e8c89a479 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -43,13 +43,12 @@ import { DocumentKey } from '../model/document_key'; import { DocumentSet } from '../model/document_set'; import { ResourcePath } from '../model/path'; import { newSerializer } from '../platform/serializer'; +import { + buildQuerySnapshotJsonBundle, + buildDocumentSnapshotJsonBundle +} from '../platform/snapshot_to_json'; import { fromDocument } from '../remote/serializer'; import { debugAssert, fail } from '../util/assert'; -import { - BundleBuilder, - DocumentSnapshotBundleData, - QuerySnapshotBundleData -} from '../util/bundle_builder_impl'; import { Code, FirestoreError } from '../util/error'; // API extractor fails importing 'property' unless we also explicitly import 'Property'. // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports-ts @@ -529,6 +528,13 @@ export class DocumentSnapshot< * @returns a JSON representation of this object. */ toJSON(): object { + if (this.metadata.hasPendingWrites) { + throw new FirestoreError( + Code.FAILED_PRECONDITION, + 'DocumentSnapshot.toJSON() attempted to serialize a document with pending writes. ' + + 'Await waitForPendingWrites() before invoking toJSON().' + ); + } const document = this._document; // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: any = {}; @@ -544,29 +550,16 @@ export class DocumentSnapshot< ) { return result; } - const builder: BundleBuilder = new BundleBuilder( - this._firestore, - AutoId.newId() - ); const documentData = this._userDataWriter.convertObjectMap( document.data.value.mapValue.fields, 'previous' ); - if (this.metadata.hasPendingWrites) { - throw new FirestoreError( - Code.FAILED_PRECONDITION, - 'DocumentSnapshot.toJSON() attempted to serialize a document with pending writes. ' + - 'Await waitForPendingWrites() before invoking toJSON().' - ); - } - builder.addBundleDocument( - documentToDocumentSnapshotBundleData( - this.ref.path, - documentData, - document - ) + result['bundle'] = buildDocumentSnapshotJsonBundle( + this._firestore, + document, + documentData, + this.ref.path ); - result['bundle'] = builder.build(); return result; } } @@ -611,6 +604,12 @@ export function documentSnapshotFromJSON< converter?: FirestoreDataConverter ): DocumentSnapshot { if (validateJSON(json, DocumentSnapshot._jsonSchema)) { + if (json.bundle === 'NOT SUPPORTED') { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + 'The provided JSON object was created in a client environment, which is not supported.' + ); + } // Parse the bundle data. const serializer = newSerializer(db._databaseId); const bundleReader = createBundleReaderSync(json.bundle, serializer); @@ -825,52 +824,48 @@ export class QuerySnapshot< * @returns a JSON representation of this object. */ toJSON(): object { + if (this.metadata.hasPendingWrites) { + throw new FirestoreError( + Code.FAILED_PRECONDITION, + 'QuerySnapshot.toJSON() attempted to serialize a document with pending writes. ' + + 'Await waitForPendingWrites() before invoking toJSON().' + ); + } // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: any = {}; result['type'] = QuerySnapshot._jsonSchemaVersion; result['bundleSource'] = 'QuerySnapshot'; result['bundleName'] = AutoId.newId(); - const builder: BundleBuilder = new BundleBuilder( - this._firestore, - result['bundleName'] - ); const databaseId = this._firestore._databaseId.database; const projectId = this._firestore._databaseId.projectId; const parent = `projects/${projectId}/databases/${databaseId}/documents`; - const docBundleDataArray: DocumentSnapshotBundleData[] = []; - const docArray = this.docs; - docArray.forEach(doc => { + const documents: Document[] = []; + const documentData: DocumentData[] = []; + const paths: string[] = []; + + this.docs.forEach(doc => { if (doc._document === null) { return; } - const documentData = this._userDataWriter.convertObjectMap( - doc._document.data.value.mapValue.fields, - 'previous' - ); - if (this.metadata.hasPendingWrites) { - throw new FirestoreError( - Code.FAILED_PRECONDITION, - 'QuerySnapshot.toJSON() attempted to serialize a document with pending writes. ' + - 'Await waitForPendingWrites() before invoking toJSON().' - ); - } - docBundleDataArray.push( - documentToDocumentSnapshotBundleData( - doc.ref.path, - documentData, - doc._document + documents.push(doc._document); + documentData.push( + this._userDataWriter.convertObjectMap( + doc._document.data.value.mapValue.fields, + 'previous' ) ); + paths.push(doc.ref.path); }); - const bundleData: QuerySnapshotBundleData = { - name: result['bundleName'], - query: this.query._query, + result['bundle'] = buildQuerySnapshotJsonBundle( + this._firestore, + this.query._query, + result['bundleName'], parent, - docBundleDataArray - }; - builder.addBundleQuery(bundleData); - result['bundle'] = builder.build(); + paths, + documents, + documentData + ); return result; } } @@ -915,6 +910,12 @@ export function querySnapshotFromJSON< converter?: FirestoreDataConverter ): QuerySnapshot { if (validateJSON(json, QuerySnapshot._jsonSchema)) { + if (json.bundle === 'NOT SUPPORTED') { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + 'The provided JSON object was created in a client environment, which is not supported.' + ); + } // Parse the bundle data. const serializer = newSerializer(db._databaseId); const bundleReader = createBundleReaderSync(json.bundle, serializer); @@ -1111,20 +1112,3 @@ export function snapshotEqual( return false; } - -// Formats Document data for bundling a DocumentSnapshot. -function documentToDocumentSnapshotBundleData( - path: string, - documentData: DocumentData, - document: Document -): DocumentSnapshotBundleData { - return { - documentData, - documentKey: document.mutableCopy().key, - documentPath: path, - documentExists: true, - createdTime: document.createTime.toTimestamp(), - readTime: document.readTime.toTimestamp(), - versionTime: document.version.toTimestamp() - }; -} diff --git a/packages/firestore/src/platform/browser/snapshot_to_json.ts b/packages/firestore/src/platform/browser/snapshot_to_json.ts new file mode 100644 index 00000000000..37c1a0b556d --- /dev/null +++ b/packages/firestore/src/platform/browser/snapshot_to_json.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright 2025 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. + */ + +/** Return the Platform-specific build JSON bundle implementations. */ +import { Firestore } from '../../api/database'; +import { Query } from '../../core/query'; +import { DocumentData } from '../../lite-api/reference'; +import { Document } from '../../model/document'; + +export function buildDocumentSnapshotJsonBundle( + db: Firestore, + document: Document, + docData: DocumentData, + path: string +): string { + return 'NOT SUPPORTED'; +} + +export function buildQuerySnapshotJsonBundle( + db: Firestore, + query: Query, + bundleName: string, + parent: string, + paths: string[], + docs: Document[], + documentData: DocumentData[] +): string { + return 'NOT SUPPORTED'; +} diff --git a/packages/firestore/src/platform/browser_lite/snapshot_to_json.ts b/packages/firestore/src/platform/browser_lite/snapshot_to_json.ts new file mode 100644 index 00000000000..4012dc496b2 --- /dev/null +++ b/packages/firestore/src/platform/browser_lite/snapshot_to_json.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2025 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. + */ + +export * from '../browser/snapshot_to_json'; diff --git a/packages/firestore/src/platform/node/snapshot_to_json.ts b/packages/firestore/src/platform/node/snapshot_to_json.ts new file mode 100644 index 00000000000..61987fbbc3c --- /dev/null +++ b/packages/firestore/src/platform/node/snapshot_to_json.ts @@ -0,0 +1,84 @@ +/** + * @license + * Copyright 2025 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. + */ + +/** Return the Platform-specific build JSON bundle implementations. */ +import { Firestore } from '../../api/database'; +import { Query } from '../../core/query'; +import { DocumentData } from '../../lite-api/reference'; +import { Document } from '../../model/document'; +import { + BundleBuilder, + DocumentSnapshotBundleData, + QuerySnapshotBundleData +} from '../../util/bundle_builder_impl'; +import { AutoId } from '../../util/misc'; + +export function buildDocumentSnapshotJsonBundle( + db: Firestore, + document: Document, + docData: DocumentData, + path: string +): string { + const builder: BundleBuilder = new BundleBuilder(db, AutoId.newId()); + builder.addBundleDocument( + documentToDocumentSnapshotBundleData(path, docData, document) + ); + return builder.build(); +} + +export function buildQuerySnapshotJsonBundle( + db: Firestore, + query: Query, + bundleName: string, + parent: string, + paths: string[], + docs: Document[], + documentData: DocumentData[] +): string { + const docBundleDataArray: DocumentSnapshotBundleData[] = []; + for (let i = 0; i < docs.length; i++) { + docBundleDataArray.push( + documentToDocumentSnapshotBundleData(paths[i], documentData[i], docs[i]) + ); + } + const bundleData: QuerySnapshotBundleData = { + name: bundleName, + query, + parent, + docBundleDataArray + }; + const builder: BundleBuilder = new BundleBuilder(db, bundleName); + builder.addBundleQuery(bundleData); + return builder.build(); +} + +// Formats Document data for bundling a DocumentSnapshot. +function documentToDocumentSnapshotBundleData( + path: string, + documentData: DocumentData, + document: Document +): DocumentSnapshotBundleData { + return { + documentData, + documentKey: document.mutableCopy().key, + documentPath: path, + documentExists: true, + createdTime: document.createTime.toTimestamp(), + readTime: document.readTime.toTimestamp(), + versionTime: document.version.toTimestamp() + }; +} diff --git a/packages/firestore/src/platform/node_lite/snapshot_to_json.ts b/packages/firestore/src/platform/node_lite/snapshot_to_json.ts new file mode 100644 index 00000000000..ba6bbb8424b --- /dev/null +++ b/packages/firestore/src/platform/node_lite/snapshot_to_json.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2025 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. + */ + +export * from '../node/snapshot_to_json'; diff --git a/packages/firestore/src/platform/rn/snapshot_to_json.ts b/packages/firestore/src/platform/rn/snapshot_to_json.ts new file mode 100644 index 00000000000..551f586d20e --- /dev/null +++ b/packages/firestore/src/platform/rn/snapshot_to_json.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2025 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. + */ + +export { + buildDocumentSnapshotJsonBundle, + buildQuerySnapshotJsonBundle +} from '../browser/snapshot_to_json'; diff --git a/packages/firestore/src/platform/rn_lite/snapshot_to_json.ts b/packages/firestore/src/platform/rn_lite/snapshot_to_json.ts new file mode 100644 index 00000000000..709509c8a4e --- /dev/null +++ b/packages/firestore/src/platform/rn_lite/snapshot_to_json.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 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. + */ + +export { toByteStreamReader } from '../browser/byte_stream_reader'; diff --git a/packages/firestore/src/platform/snapshot_to_json.ts b/packages/firestore/src/platform/snapshot_to_json.ts new file mode 100644 index 00000000000..1eae948eb45 --- /dev/null +++ b/packages/firestore/src/platform/snapshot_to_json.ts @@ -0,0 +1,62 @@ +/** + * @license + * Copyright 2025 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 { Firestore } from '../api/database'; +import { Query } from '../core/query'; +import { DocumentData } from '../lite-api/reference'; +import { Document } from '../model/document'; + +// This file is only used under ts-node. +// eslint-disable-next-line @typescript-eslint/no-require-imports +const platform = require(`./${ + process.env.TEST_PLATFORM ?? 'node' +}/snapshot_to_json`); + +/** + * Constructs the bundle data for a DocumentSnapshot used in its toJSON serialization. + */ +export function buildDocumentSnapshotJsonBundle( + db: Firestore, + document: Document, + docData: DocumentData, + path: string +): string { + return platform.buildDocumentSnapshotJsonBundle(db, document, docData, path); +} + +/** + * Constructs the bundle data for a QuerySnapshot used in its toJSON serialization. + */ +export function buildQuerySnapshotJsonBundle( + db: Firestore, + query: Query, + bundleName: string, + parent: string, + paths: string[], + docs: Document[], + documentData: DocumentData[] +): string { + return platform.buildQuerySnapshotJsonBundle( + db, + query, + bundleName, + parent, + paths, + docs, + documentData + ); +} diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index f5212f3ea4b..b63c03a4f62 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -16,7 +16,7 @@ */ import { deleteApp } from '@firebase/app'; -import { Deferred } from '@firebase/util'; +import { Deferred, isNode } from '@firebase/util'; import { expect, use } from 'chai'; import chaiAsPromised from 'chai-as-promised'; @@ -1210,94 +1210,101 @@ apiDescribe('Database', persistence => { }); it('DocumentSnapshot events for snapshot created by a bundle', async () => { - const initialData = { a: 0 }; - const finalData = { a: 1 }; - await withTestDocAndInitialData( - persistence, - initialData, - async (docRef, db) => { - const doc = await getDoc(docRef); - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshotResume( - db, - doc.toJSON(), - accumulator.storeEvent - ); - await accumulator - .awaitEvent() - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(initialData); - }) - .then(() => setDoc(docRef, finalData)) - .then(() => accumulator.awaitEvent()) - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(finalData); - }); - unsubscribe(); - } - ); + if (isNode()) { + const initialData = { a: 1 }; + const finalData = { a: 2 }; + await withTestDocAndInitialData( + persistence, + initialData, + async (docRef, db) => { + const doc = await getDoc(docRef); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshotResume( + db, + doc.toJSON(), + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + console.error('DEDB accumulator event 1'); + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(initialData); + }) + .then(() => setDoc(docRef, finalData)) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(finalData); + }); + unsubscribe(); + } + ); + } }); - it('DocumentSnapshot updated doc events in snapshot created by a bundle', async () => { - const initialData = { a: 0 }; - const finalData = { a: 1 }; - await withTestDocAndInitialData( - persistence, - initialData, - async (docRef, db) => { - const doc = await getDoc(docRef); - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshotResume( - db, - doc.toJSON(), - accumulator.storeEvent - ); - await accumulator - .awaitEvent() - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(initialData); - }) - .then(() => setDoc(docRef, finalData)) - .then(() => accumulator.awaitEvent()) - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(finalData); - }); - unsubscribe(); - } - ); + it('DocumentSnapshot updated doc events in snapshot created by a bundle accumulator', async () => { + if (isNode()) { + const initialData = { a: 1 }; + const finalData = { a: 2 }; + await withTestDocAndInitialData( + persistence, + initialData, + async (docRef, db) => { + const doc = await getDoc(docRef); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshotResume( + db, + doc.toJSON(), + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(initialData); + }) + .then(() => setDoc(docRef, finalData)) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(finalData); + }); + unsubscribe(); + } + ); + } }); it('DocumentSnapshot observer events for snapshot created by a bundle', async () => { - const initialData = { a: 0 }; - const finalData = { a: 1 }; - await withTestDocAndInitialData( - persistence, - initialData, - async (docRef, db) => { - const doc = await getDoc(docRef); - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshotResume(db, doc.toJSON(), { - next: accumulator.storeEvent - }); - await accumulator - .awaitEvent() - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(initialData); - }) - .then(() => setDoc(docRef, finalData)) - .then(() => accumulator.awaitEvent()) - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(finalData); + if (isNode()) { + const initialData = { a: 1 }; + const finalData = { a: 2 }; + await withTestDocAndInitialData( + persistence, + initialData, + async (docRef, db) => { + const doc = await getDoc(docRef); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshotResume(db, doc.toJSON(), { + next: accumulator.storeEvent }); - unsubscribe(); - } - ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(initialData); + }) + .then(() => setDoc(docRef, finalData)) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(finalData); + }); + unsubscribe(); + } + ); + } }); it('DocumentSnapshot error events for snapshot created by a bundle', async () => { @@ -1351,107 +1358,118 @@ apiDescribe('Database', persistence => { }); it('DocumentSnapshot updated doc events in snapshot created by fromJSON bundle', async () => { - const initialData = { a: 0 }; - const finalData = { a: 1 }; - await withTestDocAndInitialData( - persistence, - initialData, - async (docRef, db) => { - const doc = await getDoc(docRef); - const fromJsonDoc = documentSnapshotFromJSON(db, doc.toJSON()); - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshotResume( - db, - fromJsonDoc.toJSON(), - accumulator.storeEvent - ); - await accumulator - .awaitEvent() - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(initialData); - }) - .then(() => setDoc(docRef, finalData)) - .then(() => accumulator.awaitEvent()) - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(finalData); - }); - unsubscribe(); - } - ); + if (isNode()) { + const initialData = { a: 1 }; + const finalData = { a: 2 }; + await withTestDocAndInitialData( + persistence, + initialData, + async (docRef, db) => { + const doc = await getDoc(docRef); + const fromJsonDoc = documentSnapshotFromJSON(db, doc.toJSON()); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshotResume( + db, + fromJsonDoc.toJSON(), + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(initialData); + }) + .then(() => setDoc(docRef, finalData)) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(finalData); + }); + unsubscribe(); + } + ); + } }); it('DocumentSnapshot updated doc events in snapshot created by fromJSON doc ref', async () => { - const initialData = { a: 0 }; - const finalData = { a: 1 }; - await withTestDocAndInitialData( - persistence, - initialData, - async (docRef, db) => { - const doc = await getDoc(docRef); - const fromJsonDoc = documentSnapshotFromJSON(db, doc.toJSON()); - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshot(fromJsonDoc.ref, accumulator.storeEvent); - await accumulator - .awaitEvent() - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(initialData); - }) - .then(() => setDoc(docRef, finalData)) - .then(() => accumulator.awaitEvent()) - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(finalData); - }); - unsubscribe(); - } - ); + if (isNode()) { + const initialData = { a: 1 }; + const finalData = { a: 2 }; + await withTestDocAndInitialData( + persistence, + initialData, + async (docRef, db) => { + const doc = await getDoc(docRef); + const fromJsonDoc = documentSnapshotFromJSON(db, doc.toJSON()); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshot( + fromJsonDoc.ref, + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(initialData); + }) + .then(() => setDoc(docRef, finalData)) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(finalData); + }); + unsubscribe(); + } + ); + } }); it('Querysnapshot events for snapshot created by a bundle', async () => { - const testDocs = { - a: { foo: 1 }, - b: { bar: 2 } - }; - await withTestCollection(persistence, testDocs, async (coll, db) => { - const querySnap = await getDocs(query(coll, orderBy(documentId()))); - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshotResume( - db, - querySnap.toJSON(), - accumulator.storeEvent - ); - await accumulator.awaitEvent().then(snap => { - expect(snap.docs).not.to.be.null; - expect(snap.docs.length).to.equal(2); - expect(snap.docs[0].data()).to.deep.equal(testDocs.a); - expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + if (isNode()) { + const testDocs = { + a: { foo: 1 }, + b: { bar: 2 } + }; + await withTestCollection(persistence, testDocs, async (coll, db) => { + const querySnap = await getDocs(query(coll, orderBy(documentId()))); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshotResume( + db, + querySnap.toJSON(), + accumulator.storeEvent + ); + await accumulator.awaitEvent().then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal(testDocs.a); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }); + unsubscribe(); }); - unsubscribe(); - }); + } }); it('Querysnapshot observer events for snapshot created by a bundle', async () => { - const testDocs = { - a: { foo: 1 }, - b: { bar: 2 } - }; - await withTestCollection(persistence, testDocs, async (coll, db) => { - const querySnap = await getDocs(query(coll, orderBy(documentId()))); - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshotResume(db, querySnap.toJSON(), { - next: accumulator.storeEvent - }); - await accumulator.awaitEvent().then(snap => { - expect(snap.docs).not.to.be.null; - expect(snap.docs.length).to.equal(2); - expect(snap.docs[0].data()).to.deep.equal(testDocs.a); - expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + if (isNode()) { + const testDocs = { + a: { foo: 1 }, + b: { bar: 2 } + }; + await withTestCollection(persistence, testDocs, async (coll, db) => { + const querySnap = await getDocs(query(coll, orderBy(documentId()))); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshotResume(db, querySnap.toJSON(), { + next: accumulator.storeEvent + }); + await accumulator.awaitEvent().then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal(testDocs.a); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }); + unsubscribe(); }); - unsubscribe(); - }); + } }); it('QuerySnapshot error events for snapshot created by a bundle', async () => { @@ -1505,106 +1523,113 @@ apiDescribe('Database', persistence => { }); it('QuerySnapshot updated doc events in snapshot created by a bundle', async () => { - const testDocs = { - a: { foo: 1 }, - b: { bar: 2 } - }; - await withTestCollection(persistence, testDocs, async (coll, db) => { - const querySnap = await getDocs(query(coll, orderBy(documentId()))); - const refForDocA = querySnap.docs[0].ref; - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshotResume( - db, - querySnap.toJSON(), - accumulator.storeEvent - ); - await accumulator - .awaitEvent() - .then(snap => { - expect(snap.docs).not.to.be.null; - expect(snap.docs.length).to.equal(2); - expect(snap.docs[0].data()).to.deep.equal(testDocs.a); - expect(snap.docs[1].data()).to.deep.equal(testDocs.b); - }) - .then(() => setDoc(refForDocA, { foo: 0 })) - .then(() => accumulator.awaitEvent()) - .then(snap => { - expect(snap.docs).not.to.be.null; - expect(snap.docs.length).to.equal(2); - expect(snap.docs[0].data()).to.deep.equal({ foo: 0 }); - expect(snap.docs[1].data()).to.deep.equal(testDocs.b); - }); - unsubscribe(); - }); + if (isNode()) { + const testDocs = { + a: { foo: 1 }, + b: { bar: 2 } + }; + await withTestCollection(persistence, testDocs, async (coll, db) => { + const querySnap = await getDocs(query(coll, orderBy(documentId()))); + const refForDocA = querySnap.docs[0].ref; + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshotResume( + db, + querySnap.toJSON(), + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal(testDocs.a); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }) + .then(() => setDoc(refForDocA, { foo: 0 })) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal({ foo: 0 }); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }); + unsubscribe(); + }); + } }); - it('QuerySnapshot updated doc events in snapshot created by fromJSON bundle', async () => { - const testDocs = { - a: { foo: 1 }, - b: { bar: 2 } - }; - await withTestCollection(persistence, testDocs, async (coll, db) => { - const querySnap = await getDocs(query(coll, orderBy(documentId()))); - const querySnapFromJson = querySnapshotFromJSON(db, querySnap.toJSON()); - const refForDocA = querySnapFromJson.docs[0].ref; - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshotResume( - db, - querySnapFromJson.toJSON(), - accumulator.storeEvent - ); - await accumulator - .awaitEvent() - .then(snap => { - expect(snap.docs).not.to.be.null; - expect(snap.docs.length).to.equal(2); - expect(snap.docs[0].data()).to.deep.equal(testDocs.a); - expect(snap.docs[1].data()).to.deep.equal(testDocs.b); - }) - .then(() => setDoc(refForDocA, { foo: 0 })) - .then(() => accumulator.awaitEvent()) - .then(snap => { - expect(snap.docs).not.to.be.null; - expect(snap.docs.length).to.equal(2); - expect(snap.docs[0].data()).to.deep.equal({ foo: 0 }); - expect(snap.docs[1].data()).to.deep.equal(testDocs.b); - }); - unsubscribe(); - }); + it('QuerySnapshot updated doc events in snapshot created by fromJSON ', async () => { + if (isNode()) { + const testDocs = { + a: { foo: 1 }, + b: { bar: 2 } + }; + await withTestCollection(persistence, testDocs, async (coll, db) => { + const querySnap = await getDocs(query(coll, orderBy(documentId()))); + const querySnapFromJson = querySnapshotFromJSON(db, querySnap.toJSON()); + const refForDocA = querySnapFromJson.docs[0].ref; + const accumulator = new EventsAccumulator(); + + const unsubscribe = onSnapshotResume( + db, + querySnapFromJson.toJSON(), + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal(testDocs.a); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }) + .then(() => setDoc(refForDocA, { foo: 0 })) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal({ foo: 0 }); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }); + unsubscribe(); + }); + } }); it('QuerySnapshot updated doc events in snapshot created by fromJSON query ref', async () => { - const testDocs = { - a: { foo: 1 }, - b: { bar: 2 } - }; - await withTestCollection(persistence, testDocs, async (coll, db) => { - const querySnap = await getDocs(query(coll, orderBy(documentId()))); - const querySnapFromJson = querySnapshotFromJSON(db, querySnap.toJSON()); - const refForDocA = querySnapFromJson.docs[0].ref; - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshot( - querySnapFromJson.query, - accumulator.storeEvent - ); - await accumulator - .awaitEvent() - .then(snap => { - expect(snap.docs).not.to.be.null; - expect(snap.docs.length).to.equal(2); - expect(snap.docs[0].data()).to.deep.equal(testDocs.a); - expect(snap.docs[1].data()).to.deep.equal(testDocs.b); - }) - .then(() => setDoc(refForDocA, { foo: 0 })) - .then(() => accumulator.awaitEvent()) - .then(snap => { - expect(snap.docs).not.to.be.null; - expect(snap.docs.length).to.equal(2); - expect(snap.docs[0].data()).to.deep.equal({ foo: 0 }); - expect(snap.docs[1].data()).to.deep.equal(testDocs.b); - }); - unsubscribe(); - }); + if (isNode()) { + const testDocs = { + a: { foo: 1 }, + b: { bar: 2 } + }; + await withTestCollection(persistence, testDocs, async (coll, db) => { + const querySnap = await getDocs(query(coll, orderBy(documentId()))); + const querySnapFromJson = querySnapshotFromJSON(db, querySnap.toJSON()); + const refForDocA = querySnapFromJson.docs[0].ref; + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshot( + querySnapFromJson.query, + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal(testDocs.a); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }) + .then(() => setDoc(refForDocA, { foo: 0 })) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal({ foo: 0 }); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }); + unsubscribe(); + }); + } }); it('Metadata only changes are not fired when no options provided', () => { diff --git a/packages/firestore/test/integration/api/query.test.ts b/packages/firestore/test/integration/api/query.test.ts index 9ccd942027d..a12c843bf26 100644 --- a/packages/firestore/test/integration/api/query.test.ts +++ b/packages/firestore/test/integration/api/query.test.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { isNode } from '@firebase/util'; import { expect } from 'chai'; import { addEqualityMatcher } from '../../util/equality_matcher'; @@ -77,41 +78,43 @@ apiDescribe('Queries', persistence => { addEqualityMatcher(); it('QuerySnapshot.toJSON bundle getDocFromCache', async () => { - let path: string | null = null; - let jsonBundle: object | null = null; - const testDocs = { - a: { k: 'a' }, - b: { k: 'b' }, - c: { k: 'c' } - }; - // Write an initial document in an isolated Firestore instance so it's not stored in the cache. - await withTestCollection(persistence, testDocs, async collection => { - await getDocs(query(collection)).then(querySnapshot => { - expect(querySnapshot.docs.length).to.equal(3); - // Find the path to a known doc. - querySnapshot.docs.forEach(docSnapshot => { - if (docSnapshot.ref.path.endsWith('a')) { - path = docSnapshot.ref.path; - } + if (isNode()) { + let path: string | null = null; + let jsonBundle: object | null = null; + const testDocs = { + a: { k: 'a' }, + b: { k: 'b' }, + c: { k: 'c' } + }; + // Write an initial document in an isolated Firestore instance so it's not stored in the cache. + await withTestCollection(persistence, testDocs, async collection => { + await getDocs(query(collection)).then(querySnapshot => { + expect(querySnapshot.docs.length).to.equal(3); + // Find the path to a known doc. + querySnapshot.docs.forEach(docSnapshot => { + if (docSnapshot.ref.path.endsWith('a')) { + path = docSnapshot.ref.path; + } + }); + expect(path).to.not.be.null; + jsonBundle = querySnapshot.toJSON(); }); - expect(path).to.not.be.null; - jsonBundle = querySnapshot.toJSON(); - }); - }); - expect(jsonBundle).to.not.be.null; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const json = (jsonBundle as any).bundle; - expect(json).to.exist; - expect(json.length).to.be.greaterThan(0); - - if (path !== null) { - await withTestDb(persistence, async db => { - const docRef = doc(db, path!); - await loadBundle(db, json); - const docSnap = await getDocFromCache(docRef); - expect(docSnap.exists); - expect(docSnap.data()).to.deep.equal(testDocs.a); }); + expect(jsonBundle).to.not.be.null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const json = (jsonBundle as any).bundle; + expect(json).to.exist; + expect(json.length).to.be.greaterThan(0); + + if (path !== null) { + await withTestDb(persistence, async db => { + const docRef = doc(db, path!); + await loadBundle(db, json); + const docSnap = await getDocFromCache(docRef); + expect(docSnap.exists); + expect(docSnap.data()).to.deep.equal(testDocs.a); + }); + } } }); diff --git a/packages/firestore/test/unit/api/database.test.ts b/packages/firestore/test/unit/api/database.test.ts index ad0d5b6ddab..7f8ce10ffd7 100644 --- a/packages/firestore/test/unit/api/database.test.ts +++ b/packages/firestore/test/unit/api/database.test.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { isNode } from '@firebase/util'; import { expect } from 'chai'; import { @@ -270,27 +271,43 @@ describe('DocumentSnapshot', () => { }); it('toJSON returns a bundle', () => { - const json = documentSnapshot( + const snapshotJson = documentSnapshot( 'foo/bar', { a: 1 }, /*fromCache=*/ true ).toJSON(); // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((json as any).bundle).to.exist; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((json as any).bundle.length).to.be.greaterThan(0); + const json = snapshotJson as any; + expect(json.bundle).to.exist; + expect(json.bundle.length).to.be.greaterThan(0); + }); + + it('toJSON returns a bundle containing NOT_SUPPORTED in non-node environments', () => { + if (!isNode()) { + const snapshotJson = documentSnapshot( + 'foo/bar', + { a: 1 }, + /*fromCache=*/ true + ).toJSON(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const json = snapshotJson as any; + expect(json.bundle).to.exist; + expect(json.bundle).to.equal('NOT SUPPORTED'); + } }); it('toJSON returns an empty bundle when there are no documents', () => { - const json = documentSnapshot( - 'foo/bar', - /*data=*/ null, - /*fromCache=*/ true - ).toJSON(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((json as any).bundle).to.exist; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((json as any).bundle.length).to.equal(0); + if (isNode()) { + const snapshotJson = documentSnapshot( + 'foo/bar', + /*data=*/ null, + /*fromCache=*/ true + ).toJSON(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const json = snapshotJson as any; + expect(json.bundle).to.exist; + expect(json.bundle.length).to.equal(0); + } }); it('toJSON throws when there are pending writes', () => { @@ -307,31 +324,51 @@ describe('DocumentSnapshot', () => { ); }); + it('fromJSON throws when parsing client-side toJSON result', () => { + if (!isNode()) { + const docSnap = documentSnapshot( + 'foo/bar', + { a: 1 }, + /*fromCache=*/ true + ); + expect(() => { + documentSnapshotFromJSON(docSnap._firestore, docSnap.toJSON()); + }).to.throw; + } + }); + it('fromJSON parses toJSON result', () => { - const docSnap = documentSnapshot('foo/bar', { a: 1 }, /*fromCache=*/ true); - const json = docSnap.toJSON(); - expect(() => { - documentSnapshotFromJSON(docSnap._firestore, json); - }).to.not.throw; + if (isNode()) { + const docSnap = documentSnapshot( + 'foo/bar', + { a: 1 }, + /*fromCache=*/ true + ); + expect(() => { + documentSnapshotFromJSON(docSnap._firestore, docSnap.toJSON()); + }).to.not.throw; + } }); it('fromJSON produces valid snapshot data.', () => { - const json = documentSnapshot( - 'foo/bar', - { a: 1 }, - /*fromCache=*/ true - ).toJSON(); - const db = firestore(); - const docSnap = documentSnapshotFromJSON(db, json); - expect(docSnap).to.exist; - const data = docSnap.data(); - expect(data).to.not.be.undefined; - expect(data).to.not.be.null; - if (data) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((data as any).a).to.exist; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((data as any).a).to.equal(1); + if (isNode()) { + const docSnap = documentSnapshot( + 'foo/bar', + { a: 1 }, + /*fromCache=*/ true + ); + const db = firestore(); + const docSnapFromJSON = documentSnapshotFromJSON(db, docSnap.toJSON()); + expect(docSnapFromJSON).to.exist; + const data = docSnapFromJSON.data(); + expect(docSnapFromJSON).to.not.be.undefined; + expect(docSnapFromJSON).to.not.be.null; + if (data) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((data as any).a).to.exist; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((data as any).a).to.equal(1); + } } }); }); @@ -458,7 +495,7 @@ describe('QuerySnapshot', () => { }); it('toJSON returns a bundle', () => { - const json = querySnapshot( + const snapshotJson = querySnapshot( 'foo', {}, { a: { a: 1 } }, @@ -467,17 +504,43 @@ describe('QuerySnapshot', () => { false ).toJSON(); // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((json as any).bundle).to.exist; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((json as any).bundle.length).to.be.greaterThan(0); + const json = snapshotJson as any; + expect(json.bundle).to.exist; + expect(json.bundle.length).to.be.greaterThan(0); + }); + + it('toJSON returns a bundle containing NOT_SUPPORTED in non-node environments', () => { + if (!isNode()) { + const snapshotJson = querySnapshot( + 'foo', + {}, + { a: { a: 1 } }, + keys(), // An empty set of mutaded document keys signifies that there are no pending writes. + false, + false + ).toJSON(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const json = snapshotJson as any; + expect(json.bundle).to.exist; + expect(json.bundle).to.equal('NOT SUPPORTED'); + } }); it('toJSON returns a bundle when there are no documents', () => { - const json = querySnapshot('foo', {}, {}, keys(), false, false).toJSON(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((json as any).bundle).to.exist; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((json as any).bundle.length).to.be.greaterThan(0); + if (isNode()) { + const snapshotJson = querySnapshot( + 'foo', + {}, + {}, + keys(), + false, + false + ).toJSON(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const json = snapshotJson as any; + expect(json.bundle).to.exist; + expect(json.bundle.length).to.be.greaterThan(0); + } }); it('toJSON throws when there are pending writes', () => { @@ -608,51 +671,81 @@ describe('QuerySnapshot', () => { }); it('fromJSON does not throw', () => { - const json = querySnapshot( - 'foo', - {}, - { a: { a: 1 } }, - keys(), // An empty set of mutaded document keys signifies that there are no pending writes. - false, - false - ).toJSON(); - - const db = firestore(); - expect(() => { - querySnapshotFromJSON(db, json); - }).to.not.throw; + if (isNode()) { + const snapshot = querySnapshot( + 'foo', + {}, + { + a: { a: 1 }, + b: { bar: 2 } + }, + keys(), // An empty set of mutaded document keys signifies that there are no pending writes. + false, + false + ); + const db = firestore(); + expect(() => { + querySnapshotFromJSON(db, snapshot.toJSON()); + }).to.not.throw; + } }); - it('fromJSON parses produces valid snapshot data', () => { - const json = querySnapshot( - 'foo', - {}, - { a: { a: 1 } }, - keys(), // An empty set of mutaded document keys signifies that there are no pending writes. - false, - false - ).toJSON(); - - const db = firestore(); - const querySnap = querySnapshotFromJSON(db, json); - expect(querySnap).to.exist; - if (querySnap !== undefined) { - const docs = querySnap.docs; - expect(docs).to.not.be.undefined; - expect(docs).to.not.be.null; - if (docs) { - expect(docs.length).to.equal(1); - docs.map(document => { - const docData = document.data(); - expect(docData).to.exist; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((docData as any).a).to.exist; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((docData as any).a).to.equal(1); - }); + it('fromJSON produces valid snapshot data', () => { + if (isNode()) { + const snapshot = querySnapshot( + 'foo', + {}, + { + a: { a: 1 }, + b: { bar: 2 } + }, + keys(), // An empty set of mutaded document keys signifies that there are no pending writes. + false, + false + ); + const db = firestore(); + const querySnap = querySnapshotFromJSON(db, snapshot.toJSON()); + expect(querySnap).to.exist; + if (querySnap !== undefined) { + const docs = querySnap.docs; + expect(docs).to.not.be.undefined; + expect(docs).to.not.be.null; + if (docs) { + expect(docs.length).to.equal(2); + if (docs.length === 2) { + let docData = docs[0].data(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let data = docData as any; + expect(data.a).to.exist; + expect(data.a).to.equal(1); + + docData = docs[1].data(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data = docData as any; + expect(data.bar).to.exist; + expect(data.bar).to.equal(2); + } + } } } }); + + it('fromJSON throws when parsing client-side toJSON result', () => { + if (!isNode()) { + const querySnap = querySnapshot( + 'foo', + {}, + { a: { a: 1 } }, + keys(), // An empty set of mutaded document keys signifies that there are no pending writes. + false, + false + ); + const json = querySnap.toJSON(); + expect(() => { + querySnapshotFromJSON(querySnap._firestore, json); + }).to.throw; + } + }); }); describe('SnapshotMetadata', () => {