From fdaa93788d9ac8b349a17195e9be8f79e772139e Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Fri, 18 Apr 2025 14:47:19 -0400 Subject: [PATCH 01/32] Draft of synchronized bundle reader --- .../firestore/src/core/firestore_client.ts | 14 ++- packages/firestore/src/util/bundle_reader.ts | 6 +- .../firestore/src/util/bundle_reader_impl.ts | 85 +++++++++++++++++++ 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index e2aa19aaba8..01472a74ef9 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -339,7 +339,7 @@ async function ensureOfflineComponents( return client._offlineComponents!; } -async function ensureOnlineComponents( +export async function ensureOnlineComponents( client: FirestoreClient ): Promise { if (!client._onlineComponents) { @@ -799,6 +799,16 @@ export function firestoreClientLoadBundle( }); } +export function firestoreClientLoadDodcumentSnapshotBundle( + client: FirestoreClient, + databaseId: DatabaseId, + data: string +): object { + const reader = createBundleReader(data, newSerializer(databaseId)); + const encodedContent: Uint8Array = newTextEncoder().encode(data); + return {}; +} + export function firestoreClientGetNamedQuery( client: FirestoreClient, queryName: string @@ -808,7 +818,7 @@ export function firestoreClientGetNamedQuery( ); } -function createBundleReader( +export function createBundleReader( data: ReadableStream | ArrayBuffer | string, serializer: JsonProtoSerializer ): BundleReader { diff --git a/packages/firestore/src/util/bundle_reader.ts b/packages/firestore/src/util/bundle_reader.ts index 6ebfb2d5e8e..adf1b7a4bb2 100644 --- a/packages/firestore/src/util/bundle_reader.ts +++ b/packages/firestore/src/util/bundle_reader.ts @@ -30,7 +30,11 @@ export class SizedBundleElement { public readonly payload: BundleElement, // How many bytes this element takes to store in the bundle. public readonly byteLength: number - ) {} + ) { + console.log('DEDB new sizedBundle element.'); + console.log('payload: ', payload); + console.log('byteLength: ', byteLength); + } isBundleMetadata(): boolean { return 'metadata' in this.payload; diff --git a/packages/firestore/src/util/bundle_reader_impl.ts b/packages/firestore/src/util/bundle_reader_impl.ts index 1e8890bf5a9..df6d6c7dec9 100644 --- a/packages/firestore/src/util/bundle_reader_impl.ts +++ b/packages/firestore/src/util/bundle_reader_impl.ts @@ -23,6 +23,91 @@ import { debugAssert } from './assert'; import { BundleReader, SizedBundleElement } from './bundle_reader'; import { Deferred } from './promise'; +class SyncBundleReaderImpl { + private metadata: BundleMetadata | null; + private elements: Array; + private cursor: number; + constructor( + /** The reader to read from underlying binary bundle data source. */ + private bundleData: string, + readonly serializer: JsonProtoSerializer + ) { + this.metadata = null; + this.cursor = 0; + this.elements = new Array(); + } + + parse(): void { + 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, it is + ${JSON.stringify(element?.payload)}`); + } + } + + getMetadata(): BundleMetadata | null { + return this.metadata; + } + + private nextElement(): SizedBundleElement | null { + if (this.cursor === this.bundleData.length) { + return null; + } + + const length = this.readLength(); + const jsonString = this.readJsonString(length); + + return new SizedBundleElement(JSON.parse(jsonString), jsonString.length); + } + + /** + * Reads from a specified position from the bundleData string, for a specified + * number of bytes. + * + * Returns a string parsed from the bundle. + */ + private readJsonString(length: number): string { + if (this.cursor + length > this.bundleData.length) { + throw new Error('Reached the end of bundle when more is expected.'); + } + const result = this.bundleData.slice(this.cursor, length); + this.cursor += length; + return result; + } + + /** First index of '{' from the bundle starting at the optionally provided offset. */ + private indexOfOpenBracket(offset?: number): number { + let buffer: string = this.bundleData; + if (offset) { + buffer = this.bundleData.substring(offset); + } + return buffer.indexOf('{'); + } + + /** + * Reads from the current cursor until the first '{', returns number value + * + * If reached end of the stream, or the value isn't a number, then throws. + */ + 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.'); + } +} + /** * A class representing a bundle. * From a8886679bd2d93e0f58f55dbc31fb597930ce1be Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Sun, 20 Apr 2025 19:07:05 -0400 Subject: [PATCH 02/32] Sync bundle reader stands alone --- packages/firestore/src/util/bundle_reader.ts | 28 +++- .../firestore/src/util/bundle_reader_impl.ts | 85 ------------ .../src/util/bundle_reader_sync_impl.ts | 126 ++++++++++++++++++ 3 files changed, 151 insertions(+), 88 deletions(-) create mode 100644 packages/firestore/src/util/bundle_reader_sync_impl.ts diff --git a/packages/firestore/src/util/bundle_reader.ts b/packages/firestore/src/util/bundle_reader.ts index adf1b7a4bb2..f5bd80d0f4b 100644 --- a/packages/firestore/src/util/bundle_reader.ts +++ b/packages/firestore/src/util/bundle_reader.ts @@ -31,9 +31,9 @@ export class SizedBundleElement { // How many bytes this element takes to store in the bundle. public readonly byteLength: number ) { - console.log('DEDB new sizedBundle element.'); - console.log('payload: ', payload); - console.log('byteLength: ', byteLength); + console.log("DEDB new sizedBundle element."); + console.log("payload: ", payload); + console.log("byteLength: ", byteLength); } isBundleMetadata(): boolean { @@ -69,3 +69,25 @@ export interface BundleReader { */ nextElement(): Promise; } + +/** + * A class representing a bundle. + * + * Takes a bundle stream or buffer, and presents abstractions to read bundled + * elements out of the underlying content. + */ +export interface BundleReaderSync { + serializer: JsonProtoSerializer; + + /** + * Returns the metadata of the bundle. + */ + getMetadata(): BundleMetadata; + + /** + * Returns the next BundleElement (together with its byte size in the bundle) + * that has not been read from underlying ReadableStream. Returns null if we + * have reached the end of the stream. + */ + getElements(): Array; +} diff --git a/packages/firestore/src/util/bundle_reader_impl.ts b/packages/firestore/src/util/bundle_reader_impl.ts index df6d6c7dec9..1e8890bf5a9 100644 --- a/packages/firestore/src/util/bundle_reader_impl.ts +++ b/packages/firestore/src/util/bundle_reader_impl.ts @@ -23,91 +23,6 @@ import { debugAssert } from './assert'; import { BundleReader, SizedBundleElement } from './bundle_reader'; import { Deferred } from './promise'; -class SyncBundleReaderImpl { - private metadata: BundleMetadata | null; - private elements: Array; - private cursor: number; - constructor( - /** The reader to read from underlying binary bundle data source. */ - private bundleData: string, - readonly serializer: JsonProtoSerializer - ) { - this.metadata = null; - this.cursor = 0; - this.elements = new Array(); - } - - parse(): void { - 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, it is - ${JSON.stringify(element?.payload)}`); - } - } - - getMetadata(): BundleMetadata | null { - return this.metadata; - } - - private nextElement(): SizedBundleElement | null { - if (this.cursor === this.bundleData.length) { - return null; - } - - const length = this.readLength(); - const jsonString = this.readJsonString(length); - - return new SizedBundleElement(JSON.parse(jsonString), jsonString.length); - } - - /** - * Reads from a specified position from the bundleData string, for a specified - * number of bytes. - * - * Returns a string parsed from the bundle. - */ - private readJsonString(length: number): string { - if (this.cursor + length > this.bundleData.length) { - throw new Error('Reached the end of bundle when more is expected.'); - } - const result = this.bundleData.slice(this.cursor, length); - this.cursor += length; - return result; - } - - /** First index of '{' from the bundle starting at the optionally provided offset. */ - private indexOfOpenBracket(offset?: number): number { - let buffer: string = this.bundleData; - if (offset) { - buffer = this.bundleData.substring(offset); - } - return buffer.indexOf('{'); - } - - /** - * Reads from the current cursor until the first '{', returns number value - * - * If reached end of the stream, or the value isn't a number, then throws. - */ - 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.'); - } -} - /** * A class representing a bundle. * 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..9368ba76211 --- /dev/null +++ b/packages/firestore/src/util/bundle_reader_sync_impl.ts @@ -0,0 +1,126 @@ +/** + * @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 { BundleReaderSync, SizedBundleElement } from './bundle_reader'; + +/** + * A class that can parse a bundle form the string serialization of a bundle. + */ +class BundleReaderSyncImpl implements BundleReaderSync { + private metadata: BundleMetadata; + private elements: Array; + private cursor: number; + constructor( + private bundleData: string, + readonly serializer: JsonProtoSerializer + ) { + this.cursor = 0; + this.elements = new Array(); + + 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, it is + ${JSON.stringify(element?.payload)}`); + } + + element = this.nextElement(); + while(element !== null) { + this.elements.push(element); + } + } + + /* Returns the parsed metadata of the bundle. */ + getMetadata() : BundleMetadata { + return this.metadata; + } + + /* Returns the DocumentSnapshot or NamedQuery elements of the bundle. */ + getElements() : Array { + 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 = 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 Error('Reached the end of bundle when more is expected.'); + } + const result = this.bundleData.slice(this.cursor, length); + 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.'); + } +} + +export function newBundleReaderSync( + bundleData: string, + serializer: JsonProtoSerializer +): BundleReaderSync { + return new BundleReaderSyncImpl(bundleData, serializer); +} From ab904987828e1b3e65fe4185213fb0dcdf3206e7 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Tue, 22 Apr 2025 15:51:49 -0400 Subject: [PATCH 03/32] docsnapshot.FromJSON prototype --- common/api-review/firestore.api.md | 2 + packages/firestore/src/api/snapshot.ts | 86 +++++++++++++++++++ packages/firestore/src/core/bundle_impl.ts | 18 ++++ .../firestore/src/core/firestore_client.ts | 22 ++--- packages/firestore/src/util/bundle_reader.ts | 15 ++-- .../src/util/bundle_reader_sync_impl.ts | 56 ++++++------ 6 files changed, 150 insertions(+), 49 deletions(-) diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index ebd620e43ca..dfedb15641f 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -174,6 +174,8 @@ export class DocumentSnapshot; + // (undocumented) + static fromJSON(db: Firestore, json: object): object; get(fieldPath: string | FieldPath, options?: SnapshotOptions): any; get id(): string; readonly metadata: SnapshotMetadata; diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index e6a66b509d1..dcae8e13236 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -15,6 +15,8 @@ * limitations under the License. */ +import { BundleConverterImpl } 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'; @@ -33,8 +35,10 @@ import { } from '../lite-api/snapshot'; import { UntypedFirestoreDataConverter } from '../lite-api/user_data_reader'; import { AbstractUserDataWriter } from '../lite-api/user_data_writer'; +import { LiteUserDataWriter } from '../lite-api/reference_impl'; import { Document } from '../model/document'; import { DocumentKey } from '../model/document_key'; +import { newSerializer } from '../platform/serializer'; import { debugAssert, fail } from '../util/assert'; import { BundleBuilder, @@ -543,6 +547,88 @@ export class DocumentSnapshot< result['bundle'] = builder.build(); return result; } + + static fromJSON(db: Firestore, json: object): object { + const requiredFields = ['bundle', 'bundleName', 'bundleSource']; + let error: string | undefined = undefined; + let bundleString: string = ''; + for (const key of requiredFields) { + if (!(key in json)) { + error = `json missing required field: ${key}`; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const value = (json as any)[key]; + if (key === 'bundleSource') { + if (typeof value !== 'string') { + error = `json field 'bundleSource' must be a string.`; + break; + } else if (value !== 'DocumentSnapshot') { + error = "Expected 'bundleSource' field to equal 'DocumentSnapshot'"; + break; + } + } else if (key === 'bundle') { + if (typeof value !== 'string') { + error = `json field 'bundle' must be a string.`; + break; + } + bundleString = value; + } + } + if(error) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + error + ); + } + const serializer = newSerializer(db._databaseId); + const reader = createBundleReaderSync(bundleString, serializer); + const elements = reader.getElements(); + if (elements.length === 0) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + 'No snapshat data was found in the bundle.' + ); + } + if ( + elements.length !== 2 || + !elements[0].payload.documentMetadata || + !elements[1].payload.document + ) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + 'DocumentSnapshot bundle data must contain one document metadata and then one document' + ); + } + const docMetadata = elements[0].payload!.documentMetadata!; + const docData = elements[1].payload.document!; + if (docMetadata.name! !== docData.name) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + 'The document data is not related to the document metadata in the bundle' + ); + } + const bundleConverter = new BundleConverterImpl(serializer); + const bundledDoc = { + metadata: docMetadata, + document: docData + }; + const documentSnapshotData = bundleConverter.toDocumentSnapshotData( + docMetadata, + bundledDoc + ); + const liteUserDataWriter = new LiteUserDataWriter(db); + return new DocumentSnapshot( + db, + liteUserDataWriter, + documentSnapshotData.documentKey, + documentSnapshotData.mutableDoc, + new SnapshotMetadata( + /* hasPendingWrites= */ false, + /* fromCache= */ false + ), + null + ); + } } /** diff --git a/packages/firestore/src/core/bundle_impl.ts b/packages/firestore/src/core/bundle_impl.ts index 9a42e43261f..8472dedc0d8 100644 --- a/packages/firestore/src/core/bundle_impl.ts +++ b/packages/firestore/src/core/bundle_impl.ts @@ -27,6 +27,7 @@ import { MutableDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; import { ResourcePath } from '../model/path'; import { + BundledDocumentMetadata, BundleMetadata as ProtoBundleMetadata, NamedQuery as ProtoNamedQuery } from '../protos/firestore_bundle_proto'; @@ -76,6 +77,23 @@ export class BundleConverterImpl implements BundleConverter { } } + toDocumentSnapshotData(docMetadata: BundledDocumentMetadata, bundledDoc: BundledDocument) : { + documentKey: DocumentKey, + mutableDoc: MutableDocument, + } { + const bundleConverter = new BundleConverterImpl(this.serializer); + const documentKey = bundleConverter.toDocumentKey(docMetadata.name!); + const mutableDoc = bundleConverter.toMutableDocument(bundledDoc); + mutableDoc.setReadTime( + bundleConverter.toSnapshotVersion(docMetadata.readTime!) + ); + + return { + documentKey, + mutableDoc + }; + } + toSnapshotVersion(time: ApiTimestamp): SnapshotVersion { return fromVersion(time); } diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index 01472a74ef9..ebf25d85572 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'; @@ -97,6 +98,8 @@ import { TransactionRunner } from './transaction_runner'; import { View } from './view'; import { ViewSnapshot } from './view_snapshot'; +import { ExpUserDataWriter } from '../api/reference_impl'; + const LOG_TAG = 'FirestoreClient'; export const MAX_CONCURRENT_LIMBO_RESOLUTIONS = 100; @@ -799,16 +802,6 @@ export function firestoreClientLoadBundle( }); } -export function firestoreClientLoadDodcumentSnapshotBundle( - client: FirestoreClient, - databaseId: DatabaseId, - data: string -): object { - const reader = createBundleReader(data, newSerializer(databaseId)); - const encodedContent: Uint8Array = newTextEncoder().encode(data); - return {}; -} - export function firestoreClientGetNamedQuery( client: FirestoreClient, queryName: string @@ -831,6 +824,13 @@ export 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/util/bundle_reader.ts b/packages/firestore/src/util/bundle_reader.ts index f5bd80d0f4b..06e56a35baf 100644 --- a/packages/firestore/src/util/bundle_reader.ts +++ b/packages/firestore/src/util/bundle_reader.ts @@ -30,11 +30,7 @@ export class SizedBundleElement { public readonly payload: BundleElement, // How many bytes this element takes to store in the bundle. public readonly byteLength: number - ) { - console.log("DEDB new sizedBundle element."); - console.log("payload: ", payload); - console.log("byteLength: ", byteLength); - } + ) {} isBundleMetadata(): boolean { return 'metadata' in this.payload; @@ -73,8 +69,8 @@ export interface BundleReader { /** * A class representing a bundle. * - * Takes a bundle stream or buffer, and presents abstractions to read bundled - * elements out of the underlying content. + * Takes a bundle string buffer, parses the data, and provides accessors to the data contained + * within it. */ export interface BundleReaderSync { serializer: JsonProtoSerializer; @@ -85,9 +81,8 @@ export interface BundleReaderSync { getMetadata(): BundleMetadata; /** - * Returns the next BundleElement (together with its byte size in the bundle) - * that has not been read from underlying ReadableStream. Returns null if we - * have reached the end of the stream. + * Returns BundleElements parsed from the bundle. Returns an empty array if no bundle elements + * exist. */ getElements(): Array; } diff --git a/packages/firestore/src/util/bundle_reader_sync_impl.ts b/packages/firestore/src/util/bundle_reader_sync_impl.ts index 9368ba76211..7ffc1bfec5c 100644 --- a/packages/firestore/src/util/bundle_reader_sync_impl.ts +++ b/packages/firestore/src/util/bundle_reader_sync_impl.ts @@ -23,7 +23,7 @@ import { BundleReaderSync, SizedBundleElement } from './bundle_reader'; /** * A class that can parse a bundle form the string serialization of a bundle. */ -class BundleReaderSyncImpl implements BundleReaderSync { +export class BundleReaderSyncImpl implements BundleReaderSync { private metadata: BundleMetadata; private elements: Array; private cursor: number; @@ -35,6 +35,7 @@ class BundleReaderSyncImpl implements BundleReaderSync { this.elements = new Array(); let element = this.nextElement(); + console.error('DEEDB Element (metadata): ', element); if (element && element.isBundleMetadata()) { this.metadata = element as BundleMetadata; } else { @@ -42,71 +43,70 @@ class BundleReaderSyncImpl implements BundleReaderSync { ${JSON.stringify(element?.payload)}`); } - element = this.nextElement(); - while(element !== null) { - this.elements.push(element); - } + do { + element = this.nextElement(); + if (element !== null) { + console.error('DEDB parsed element: ', element); + this.elements.push(element); + } else { + console.error('DEDB no more elements.'); + } + } while (element !== null); } - + /* Returns the parsed metadata of the bundle. */ - getMetadata() : BundleMetadata { + getMetadata(): BundleMetadata { return this.metadata; } /* Returns the DocumentSnapshot or NamedQuery elements of the bundle. */ - getElements() : Array { + getElements(): Array { return this.elements; } /** * Parses the next element of the bundle. * - * @returns a SizedBundleElement representation of the next element in the bundle, or null if + * @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) { + private nextElement(): SizedBundleElement | null { + if (this.cursor === this.bundleData.length) { return null; } - - const length = this.readLength(); + const length: number = this.readLength(); const jsonString = this.readJsonString(length); - - return new SizedBundleElement( - JSON.parse(jsonString), - 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) { + if (this.cursor + length > this.bundleData.length) { throw new Error('Reached the end of bundle when more is expected.'); } - const result = this.bundleData.slice(this.cursor, length); - this.cursor += length; + const result = this.bundleData.slice(this.cursor, (this.cursor += length)); return result; } /** - * Reads from the current cursor until the first '{'. - * + * 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 { + private readLength(): number { const startIndex = this.cursor; let curIndex = this.cursor; - while(curIndex < this.bundleData.length) { - if(this.bundleData[curIndex] === '{') { - if(curIndex === startIndex) { + 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; From 5df33ec512ae34e91c7d23671a7585f6f9dd14a5 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Tue, 22 Apr 2025 16:12:00 -0400 Subject: [PATCH 04/32] Lint fixes. --- packages/firestore/src/api/snapshot.ts | 9 +++------ packages/firestore/src/core/bundle_impl.ts | 9 ++++++--- packages/firestore/src/core/firestore_client.ts | 2 -- packages/firestore/src/util/bundle_reader.ts | 2 +- .../firestore/src/util/bundle_reader_sync_impl.ts | 12 ++++-------- 5 files changed, 14 insertions(+), 20 deletions(-) diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index dcae8e13236..f891bb51cb4 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -28,6 +28,7 @@ import { SetOptions, WithFieldValue } from '../lite-api/reference'; +import { LiteUserDataWriter } from '../lite-api/reference_impl'; import { DocumentSnapshot as LiteDocumentSnapshot, fieldPathFromArgument, @@ -35,7 +36,6 @@ import { } from '../lite-api/snapshot'; import { UntypedFirestoreDataConverter } from '../lite-api/user_data_reader'; import { AbstractUserDataWriter } from '../lite-api/user_data_writer'; -import { LiteUserDataWriter } from '../lite-api/reference_impl'; import { Document } from '../model/document'; import { DocumentKey } from '../model/document_key'; import { newSerializer } from '../platform/serializer'; @@ -574,11 +574,8 @@ export class DocumentSnapshot< bundleString = value; } } - if(error) { - throw new FirestoreError( - Code.INVALID_ARGUMENT, - error - ); + if (error) { + throw new FirestoreError(Code.INVALID_ARGUMENT, error); } const serializer = newSerializer(db._databaseId); const reader = createBundleReaderSync(bundleString, serializer); diff --git a/packages/firestore/src/core/bundle_impl.ts b/packages/firestore/src/core/bundle_impl.ts index 8472dedc0d8..860c7bbf361 100644 --- a/packages/firestore/src/core/bundle_impl.ts +++ b/packages/firestore/src/core/bundle_impl.ts @@ -77,9 +77,12 @@ export class BundleConverterImpl implements BundleConverter { } } - toDocumentSnapshotData(docMetadata: BundledDocumentMetadata, bundledDoc: BundledDocument) : { - documentKey: DocumentKey, - mutableDoc: MutableDocument, + toDocumentSnapshotData( + docMetadata: BundledDocumentMetadata, + bundledDoc: BundledDocument + ): { + documentKey: DocumentKey; + mutableDoc: MutableDocument; } { const bundleConverter = new BundleConverterImpl(this.serializer); const documentKey = bundleConverter.toDocumentKey(docMetadata.name!); diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index ebf25d85572..a111af58282 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -98,8 +98,6 @@ import { TransactionRunner } from './transaction_runner'; import { View } from './view'; import { ViewSnapshot } from './view_snapshot'; -import { ExpUserDataWriter } from '../api/reference_impl'; - const LOG_TAG = 'FirestoreClient'; export const MAX_CONCURRENT_LIMBO_RESOLUTIONS = 100; diff --git a/packages/firestore/src/util/bundle_reader.ts b/packages/firestore/src/util/bundle_reader.ts index 06e56a35baf..2cefe0f7b63 100644 --- a/packages/firestore/src/util/bundle_reader.ts +++ b/packages/firestore/src/util/bundle_reader.ts @@ -84,5 +84,5 @@ export interface BundleReaderSync { * Returns BundleElements parsed from the bundle. Returns an empty array if no bundle elements * exist. */ - getElements(): Array; + getElements(): SizedBundleElement[]; } diff --git a/packages/firestore/src/util/bundle_reader_sync_impl.ts b/packages/firestore/src/util/bundle_reader_sync_impl.ts index 7ffc1bfec5c..522b0252fe6 100644 --- a/packages/firestore/src/util/bundle_reader_sync_impl.ts +++ b/packages/firestore/src/util/bundle_reader_sync_impl.ts @@ -25,17 +25,16 @@ import { BundleReaderSync, SizedBundleElement } from './bundle_reader'; */ export class BundleReaderSyncImpl implements BundleReaderSync { private metadata: BundleMetadata; - private elements: Array; + private elements: SizedBundleElement[]; private cursor: number; constructor( private bundleData: string, readonly serializer: JsonProtoSerializer ) { this.cursor = 0; - this.elements = new Array(); + this.elements = []; let element = this.nextElement(); - console.error('DEEDB Element (metadata): ', element); if (element && element.isBundleMetadata()) { this.metadata = element as BundleMetadata; } else { @@ -46,11 +45,8 @@ export class BundleReaderSyncImpl implements BundleReaderSync { do { element = this.nextElement(); if (element !== null) { - console.error('DEDB parsed element: ', element); this.elements.push(element); - } else { - console.error('DEDB no more elements.'); - } + } } while (element !== null); } @@ -60,7 +56,7 @@ export class BundleReaderSyncImpl implements BundleReaderSync { } /* Returns the DocumentSnapshot or NamedQuery elements of the bundle. */ - getElements(): Array { + getElements(): SizedBundleElement[] { return this.elements; } From 7c8ef871a54f9ea960987a6b025ce4fa2390d633 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Wed, 23 Apr 2025 09:59:02 -0400 Subject: [PATCH 05/32] Put more logic into bundle_impl. --- packages/firestore/src/api/snapshot.ts | 35 +++++-------------- packages/firestore/src/core/bundle_impl.ts | 40 +++++++++++++--------- 2 files changed, 33 insertions(+), 42 deletions(-) diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index f891bb51cb4..8a66d01ea2b 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -577,42 +577,25 @@ export class DocumentSnapshot< if (error) { throw new FirestoreError(Code.INVALID_ARGUMENT, error); } + // Parse the bundle data. const serializer = newSerializer(db._databaseId); - const reader = createBundleReaderSync(bundleString, serializer); - const elements = reader.getElements(); + const elements = createBundleReaderSync(bundleString, serializer).getElements(); if (elements.length === 0) { - throw new FirestoreError( - Code.INVALID_ARGUMENT, - 'No snapshat data was found in the bundle.' - ); + error = 'No snapshat data was found in the bundle.'; } if ( elements.length !== 2 || !elements[0].payload.documentMetadata || !elements[1].payload.document ) { - throw new FirestoreError( - Code.INVALID_ARGUMENT, - 'DocumentSnapshot bundle data must contain one document metadata and then one document' - ); + error = 'DocumentSnapshot bundle data must contain one document metadata and then one document.'; } - const docMetadata = elements[0].payload!.documentMetadata!; - const docData = elements[1].payload.document!; - if (docMetadata.name! !== docData.name) { - throw new FirestoreError( - Code.INVALID_ARGUMENT, - 'The document data is not related to the document metadata in the bundle' - ); + if (error) { + throw new FirestoreError(Code.INVALID_ARGUMENT, error); } + // convert bundle data into the types that the DocumentSnapshot constructore requires. const bundleConverter = new BundleConverterImpl(serializer); - const bundledDoc = { - metadata: docMetadata, - document: docData - }; - const documentSnapshotData = bundleConverter.toDocumentSnapshotData( - docMetadata, - bundledDoc - ); + const documentSnapshotData = bundleConverter.toDocumentSnapshotData(elements); const liteUserDataWriter = new LiteUserDataWriter(db); return new DocumentSnapshot( db, @@ -623,7 +606,7 @@ export class DocumentSnapshot< /* hasPendingWrites= */ false, /* fromCache= */ false ), - null + /* converter= */ null ); } } diff --git a/packages/firestore/src/core/bundle_impl.ts b/packages/firestore/src/core/bundle_impl.ts index 860c7bbf361..57c5adb6ff3 100644 --- a/packages/firestore/src/core/bundle_impl.ts +++ b/packages/firestore/src/core/bundle_impl.ts @@ -40,6 +40,7 @@ import { } from '../remote/serializer'; import { debugAssert } from '../util/assert'; import { SizedBundleElement } from '../util/bundle_reader'; +import { Code, FirestoreError } from '../util/error'; import { BundleConverter, @@ -53,7 +54,7 @@ import { SnapshotVersion } from './snapshot_version'; * Helper to convert objects from bundles to model objects in the SDK. */ export class BundleConverterImpl implements BundleConverter { - constructor(private readonly serializer: JsonProtoSerializer) {} + constructor(private readonly serializer: JsonProtoSerializer) { } toDocumentKey(name: string): DocumentKey { return fromName(this.serializer, name); @@ -78,19 +79,26 @@ export class BundleConverterImpl implements BundleConverter { } toDocumentSnapshotData( - docMetadata: BundledDocumentMetadata, - bundledDoc: BundledDocument - ): { - documentKey: DocumentKey; - mutableDoc: MutableDocument; - } { + bundleElements: SizedBundleElement[]): { + documentKey: DocumentKey; + mutableDoc: MutableDocument; + } { + const metadata = bundleElements[0]?.payload?.documentMetadata!; + const document = bundleElements[1]?.payload?.document!; + let error : string | undefined = undefined; + if( !metadata || !document) { + error = 'DocumentSnapshot bundle data requires both document metadata and document data'; + } else if( metadata.name !== document.name ) { + error = 'DocumentSnapshot metadata is not related to the document in the bundle.'; + } + if ( error ) { + throw new FirestoreError(Code.INVALID_ARGUMENT, error); + } const bundleConverter = new BundleConverterImpl(this.serializer); - const documentKey = bundleConverter.toDocumentKey(docMetadata.name!); - const mutableDoc = bundleConverter.toMutableDocument(bundledDoc); - mutableDoc.setReadTime( - bundleConverter.toSnapshotVersion(docMetadata.readTime!) - ); - + const documentKey = bundleConverter.toDocumentKey(metadata.name!); + const mutableDoc = bundleConverter.toMutableDocument({ + metadata, document + }); return { documentKey, mutableDoc @@ -155,8 +163,8 @@ export class BundleLoader { } else if (element.payload.document) { debugAssert( this.documents.length > 0 && - this.documents[this.documents.length - 1].metadata.name === - element.payload.document.name, + 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 = @@ -200,7 +208,7 @@ export class BundleLoader { async complete(): Promise { debugAssert( this.documents[this.documents.length - 1]?.metadata.exists !== true || - !!this.documents[this.documents.length - 1].document, + !!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.'); From f0d3304fad677bfcd4efdead8247ae6a7149f0e0 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Wed, 23 Apr 2025 11:27:28 -0400 Subject: [PATCH 06/32] Templated converter DocSnapshot.fromJSON --- common/api-review/firestore.api.md | 2 +- packages/firestore/src/api/snapshot.ts | 22 ++++++++--- packages/firestore/src/core/bundle_impl.ts | 37 ++++++++++--------- .../src/util/bundle_reader_sync_impl.ts | 2 +- 4 files changed, 38 insertions(+), 25 deletions(-) diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index dfedb15641f..cc19be5191b 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -175,7 +175,7 @@ export class DocumentSnapshot; // (undocumented) - static fromJSON(db: Firestore, json: object): object; + static fromJSON(db: Firestore, json: object, converter: FirestoreDataConverter): DocumentSnapshot; get(fieldPath: string | FieldPath, options?: SnapshotOptions): any; get id(): string; readonly metadata: SnapshotMetadata; diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index 8a66d01ea2b..7859fbdf6b6 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -548,7 +548,14 @@ export class DocumentSnapshot< return result; } - static fromJSON(db: Firestore, json: object): object { + static fromJSON< + AppModelType, + DbModelType extends DocumentData = DocumentData + >( + db: Firestore, + json: object, + converter: FirestoreDataConverter + ): DocumentSnapshot { const requiredFields = ['bundle', 'bundleName', 'bundleSource']; let error: string | undefined = undefined; let bundleString: string = ''; @@ -579,7 +586,10 @@ export class DocumentSnapshot< } // Parse the bundle data. const serializer = newSerializer(db._databaseId); - const elements = createBundleReaderSync(bundleString, serializer).getElements(); + const elements = createBundleReaderSync( + bundleString, + serializer + ).getElements(); if (elements.length === 0) { error = 'No snapshat data was found in the bundle.'; } @@ -588,14 +598,16 @@ export class DocumentSnapshot< !elements[0].payload.documentMetadata || !elements[1].payload.document ) { - error = 'DocumentSnapshot bundle data must contain one document metadata and then one document.'; + error = + 'DocumentSnapshot bundle data must contain one metadata and then one document.'; } if (error) { throw new FirestoreError(Code.INVALID_ARGUMENT, error); } // convert bundle data into the types that the DocumentSnapshot constructore requires. const bundleConverter = new BundleConverterImpl(serializer); - const documentSnapshotData = bundleConverter.toDocumentSnapshotData(elements); + const documentSnapshotData = + bundleConverter.toDocumentSnapshotData(elements); const liteUserDataWriter = new LiteUserDataWriter(db); return new DocumentSnapshot( db, @@ -606,7 +618,7 @@ export class DocumentSnapshot< /* hasPendingWrites= */ false, /* fromCache= */ false ), - /* converter= */ null + converter ); } } diff --git a/packages/firestore/src/core/bundle_impl.ts b/packages/firestore/src/core/bundle_impl.ts index 57c5adb6ff3..3db5dcc0fdf 100644 --- a/packages/firestore/src/core/bundle_impl.ts +++ b/packages/firestore/src/core/bundle_impl.ts @@ -27,7 +27,6 @@ import { MutableDocument } from '../model/document'; import { DocumentKey } from '../model/document_key'; import { ResourcePath } from '../model/path'; import { - BundledDocumentMetadata, BundleMetadata as ProtoBundleMetadata, NamedQuery as ProtoNamedQuery } from '../protos/firestore_bundle_proto'; @@ -54,7 +53,7 @@ import { SnapshotVersion } from './snapshot_version'; * Helper to convert objects from bundles to model objects in the SDK. */ export class BundleConverterImpl implements BundleConverter { - constructor(private readonly serializer: JsonProtoSerializer) { } + constructor(private readonly serializer: JsonProtoSerializer) {} toDocumentKey(name: string): DocumentKey { return fromName(this.serializer, name); @@ -78,26 +77,28 @@ export class BundleConverterImpl implements BundleConverter { } } - toDocumentSnapshotData( - bundleElements: SizedBundleElement[]): { - documentKey: DocumentKey; - mutableDoc: MutableDocument; - } { + toDocumentSnapshotData(bundleElements: SizedBundleElement[]): { + documentKey: DocumentKey; + mutableDoc: MutableDocument; + } { const metadata = bundleElements[0]?.payload?.documentMetadata!; const document = bundleElements[1]?.payload?.document!; - let error : string | undefined = undefined; - if( !metadata || !document) { - error = 'DocumentSnapshot bundle data requires both document metadata and document data'; - } else if( metadata.name !== document.name ) { - error = 'DocumentSnapshot metadata is not related to the document in the bundle.'; + let error: string | undefined = undefined; + if (!metadata || !document) { + error = + 'DocumentSnapshot bundle data requires both document metadata and document data'; + } else if (metadata.name !== document.name) { + error = + 'DocumentSnapshot metadata is not related to the document in the bundle.'; } - if ( error ) { + if (error) { throw new FirestoreError(Code.INVALID_ARGUMENT, error); - } + } const bundleConverter = new BundleConverterImpl(this.serializer); const documentKey = bundleConverter.toDocumentKey(metadata.name!); const mutableDoc = bundleConverter.toMutableDocument({ - metadata, document + metadata, + document }); return { documentKey, @@ -163,8 +164,8 @@ export class BundleLoader { } else if (element.payload.document) { debugAssert( this.documents.length > 0 && - this.documents[this.documents.length - 1].metadata.name === - element.payload.document.name, + 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 = @@ -208,7 +209,7 @@ export class BundleLoader { async complete(): Promise { debugAssert( this.documents[this.documents.length - 1]?.metadata.exists !== true || - !!this.documents[this.documents.length - 1].document, + !!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.'); diff --git a/packages/firestore/src/util/bundle_reader_sync_impl.ts b/packages/firestore/src/util/bundle_reader_sync_impl.ts index 522b0252fe6..8c1cd1142c1 100644 --- a/packages/firestore/src/util/bundle_reader_sync_impl.ts +++ b/packages/firestore/src/util/bundle_reader_sync_impl.ts @@ -46,7 +46,7 @@ export class BundleReaderSyncImpl implements BundleReaderSync { element = this.nextElement(); if (element !== null) { this.elements.push(element); - } + } } while (element !== null); } From f80c49dd218ad88cf00ed3b9ba27d7b356865156 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Wed, 30 Apr 2025 09:10:10 -0400 Subject: [PATCH 07/32] Update bundleReader to be multi use utility --- packages/firestore/src/core/bundle_impl.ts | 51 ++++++++++++++-------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/packages/firestore/src/core/bundle_impl.ts b/packages/firestore/src/core/bundle_impl.ts index 3db5dcc0fdf..30a6e5569fd 100644 --- a/packages/firestore/src/core/bundle_impl.ts +++ b/packages/firestore/src/core/bundle_impl.ts @@ -17,6 +17,7 @@ import { LoadBundleTaskProgress } from '@firebase/firestore-types'; +import { fromBundledQuery } from '../local/local_serializer'; import { LocalStore } from '../local/local_store'; import { localStoreApplyBundledDocuments, @@ -105,34 +106,48 @@ export class BundleConverterImpl implements BundleConverter { mutableDoc }; } - toSnapshotVersion(time: ApiTimestamp): SnapshotVersion { return fromVersion(time); } } /** - * 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. * @@ -147,9 +162,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; } @@ -163,12 +178,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; } @@ -206,26 +221,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!) ); From be5aee836f071d008e714c6864669639d5be5c9d Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Wed, 30 Apr 2025 09:16:49 -0400 Subject: [PATCH 08/32] Update sync engine use of bundle loader. --- packages/firestore/src/core/sync_engine_impl.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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, From ad2f0ace3d813037a7279db4ef8c0fff01745db3 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Wed, 30 Apr 2025 09:17:04 -0400 Subject: [PATCH 09/32] typo fix --- packages/firestore/src/api/reference_impl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/firestore/src/api/reference_impl.ts b/packages/firestore/src/api/reference_impl.ts index b920c479d0b..8f6b5a60b6d 100644 --- a/packages/firestore/src/api/reference_impl.ts +++ b/packages/firestore/src/api/reference_impl.ts @@ -699,7 +699,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. From 5ea4ea5e42877705c30b3250c6d0a7aa9127217b Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Wed, 30 Apr 2025 09:55:23 -0400 Subject: [PATCH 10/32] initial QuerySnapshot fromJSON impl --- common/api-review/firestore.api.md | 2 + packages/firestore/src/api/snapshot.ts | 132 ++++++++++++++++++++- packages/firestore/src/core/bundle_impl.ts | 1 - 3 files changed, 133 insertions(+), 2 deletions(-) diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index cc19be5191b..31871d1a308 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -651,6 +651,8 @@ export class QuerySnapshot>; get empty(): boolean; forEach(callback: (result: QueryDocumentSnapshot) => void, thisArg?: unknown): void; + // (undocumented) + static fromJSON(db: Firestore, json: object): QuerySnapshot | null; readonly metadata: SnapshotMetadata; readonly query: Query; get size(): number; diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index 7859fbdf6b6..4158f4a25a1 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { BundleConverterImpl } from '../core/bundle_impl'; +import { BundleConverterImpl, BundleLoader } from '../core/bundle_impl'; import { createBundleReaderSync } from '../core/firestore_client'; import { newQueryComparator } from '../core/query'; import { ChangeType, ViewSnapshot } from '../core/view_snapshot'; @@ -36,9 +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, @@ -507,6 +512,11 @@ export class DocumentSnapshot< return undefined; } + /** + * 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 @@ -548,6 +558,16 @@ export class DocumentSnapshot< return result; } + /** + * 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. + */ static fromJSON< AppModelType, DbModelType extends DocumentData = DocumentData @@ -777,6 +797,11 @@ export class QuerySnapshot< return this._cachedChanges; } + /** + * 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 = {}; @@ -825,6 +850,111 @@ export class QuerySnapshot< result['bundle'] = builder.build(); return result; } + + /** + * 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. + */ + static fromJSON< + AppModelType, + DbModelType extends DocumentData = DocumentData + >( + db: Firestore, + json: object + ): QuerySnapshot | null { + const requiredFields = ['bundle', 'bundleName', 'bundleSource']; + let error: string | undefined = undefined; + let bundleString: string = ''; + for (const key of requiredFields) { + if (!(key in json)) { + error = `json missing required field: ${key}`; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const value = (json as any)[key]; + if (key === 'bundleSource') { + if (typeof value !== 'string') { + error = `json field 'bundleSource' must be a string.`; + break; + } else if (value !== 'QuerySnapshot') { + error = "Expected 'bundleSource' field to equal 'QuerySnapshot'"; + break; + } + } else if (key === 'bundle') { + if (typeof value !== 'string') { + error = `json field 'bundle' must be a string.`; + break; + } + bundleString = value; + } + } + if (error) { + throw new FirestoreError(Code.INVALID_ARGUMENT, error); + } + // Parse the bundle data. + const serializer = newSerializer(db._databaseId); + const bundleReader = createBundleReaderSync(bundleString, serializer); + const bundleMetadata = bundleReader.getMetadata(); + const elements = bundleReader.getElements(); + if (elements.length === 0) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + 'No snapshat data was found in the bundle.' + ); + } + + const bundleLoader: BundleLoader = new BundleLoader( + bundleMetadata, + serializer + ); + for (const element of elements) { + bundleLoader.addSizedElement(element); + } + const parsedNamedQueries = bundleLoader.queries; + if (parsedNamedQueries.length !== 1) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + 'Snapshot data contained more than one named query.' + ); + } + + const query = fromBundledQuery(parsedNamedQueries[0].bundledQuery!); + // convert bundle data into the types that the DocumentSnapshot constructore requires. + const liteUserDataWriter = new LiteUserDataWriter(db); + + const bundledDocuments = bundleLoader.documents; + const documentSet = new DocumentSet(); + const documentKeys = documentKeySet(); + for (const bundledDocumet of bundledDocuments) { + const document = fromDocument(serializer, bundledDocumet.document!); + documentSet.add(document); + const documentPath = ResourcePath.fromString( + bundledDocumet.metadata.name! + ); + documentKeys.add(new DocumentKey(documentPath)); + } + + const viewSnapshot = ViewSnapshot.fromInitialDocuments( + query, + documentSet, + documentKeys, + false, // fromCache + false // hasCachedResults + ); + + const externalQuery = new Query(db, null, query); + + return new QuerySnapshot( + db, + liteUserDataWriter, + externalQuery, + viewSnapshot + ); + } } /** Calculates the array of `DocumentChange`s for a given `ViewSnapshot`. */ diff --git a/packages/firestore/src/core/bundle_impl.ts b/packages/firestore/src/core/bundle_impl.ts index 30a6e5569fd..0ce74ec381c 100644 --- a/packages/firestore/src/core/bundle_impl.ts +++ b/packages/firestore/src/core/bundle_impl.ts @@ -17,7 +17,6 @@ import { LoadBundleTaskProgress } from '@firebase/firestore-types'; -import { fromBundledQuery } from '../local/local_serializer'; import { LocalStore } from '../local/local_store'; import { localStoreApplyBundledDocuments, From 95b69f693f353f48ba2770c23c57d597fa3dbba0 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Wed, 30 Apr 2025 10:13:08 -0400 Subject: [PATCH 11/32] docgen --- common/api-review/firestore.api.md | 4 --- docs-devsite/firestore_.documentsnapshot.md | 31 ++++++++++++++++++++- docs-devsite/firestore_.md | 2 +- docs-devsite/firestore_.querysnapshot.md | 30 +++++++++++++++++++- packages/firestore/src/core/bundle_impl.ts | 1 + 5 files changed, 61 insertions(+), 7 deletions(-) diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index 31871d1a308..fae0a8b13b7 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -174,13 +174,11 @@ export class DocumentSnapshot; - // (undocumented) static fromJSON(db: Firestore, json: object, converter: FirestoreDataConverter): DocumentSnapshot; get(fieldPath: string | FieldPath, options?: SnapshotOptions): any; get id(): string; readonly metadata: SnapshotMetadata; get ref(): DocumentReference; - // (undocumented) toJSON(): object; } @@ -651,12 +649,10 @@ export class QuerySnapshot>; get empty(): boolean; forEach(callback: (result: QueryDocumentSnapshot) => void, thisArg?: unknown): void; - // (undocumented) static fromJSON(db: Firestore, json: object): QuerySnapshot | null; readonly metadata: SnapshotMetadata; readonly query: Query; get size(): number; - // (undocumented) toJSON(): object; } diff --git a/docs-devsite/firestore_.documentsnapshot.md b/docs-devsite/firestore_.documentsnapshot.md index 8c4825593dc..1c80fe9f2eb 100644 --- a/docs-devsite/firestore_.documentsnapshot.md +++ b/docs-devsite/firestore_.documentsnapshot.md @@ -40,8 +40,9 @@ 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. | +| [fromJSON(db, json, converter)](./firestore_.documentsnapshot.md#documentsnapshotfromjson) | static | Builds a DocumentSnapshot instance from a JSON object created by [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). | | [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) @@ -120,6 +121,30 @@ exists(): this is QueryDocumentSnapshot; this is [QueryDocumentSnapshot](./firestore_.querydocumentsnapshot.md#querydocumentsnapshot_class)<AppModelType, DbModelType> +## DocumentSnapshot.fromJSON() + +Builds a `DocumentSnapshot` instance from a JSON object created by [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). + +Signature: + +```typescript +static fromJSON(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. + ## DocumentSnapshot.get() Retrieves the field specified by `fieldPath`. Returns `undefined` if the document or field doesn't exist. @@ -147,6 +172,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 +183,5 @@ toJSON(): object; object +a JSON representation of this object. + diff --git a/docs-devsite/firestore_.md b/docs-devsite/firestore_.md index 685958d364c..70a0604af6c 100644 --- a/docs-devsite/firestore_.md +++ b/docs-devsite/firestore_.md @@ -644,7 +644,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. | diff --git a/docs-devsite/firestore_.querysnapshot.md b/docs-devsite/firestore_.querysnapshot.md index da0913d7b6e..ba52572cacc 100644 --- a/docs-devsite/firestore_.querysnapshot.md +++ b/docs-devsite/firestore_.querysnapshot.md @@ -34,7 +34,8 @@ export declare class QuerySnapshotQuerySnapshot. | -| [toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) | | | +| [fromJSON(db, json)](./firestore_.querysnapshot.md#querysnapshotfromjson) | static | Builds a QuerySnapshot instance from a JSON object created by [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). | +| [toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) | | Returns a JSON-serializable representation of this QuerySnapshot instance. | ## QuerySnapshot.docs @@ -127,8 +128,33 @@ forEach(callback: (result: QueryDocumentSnapshot) => void +## QuerySnapshot.fromJSON() + +Builds a `QuerySnapshot` instance from a JSON object created by [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). + +Signature: + +```typescript +static fromJSON(db: Firestore, json: object): QuerySnapshot | null; +``` + +#### 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)<AppModelType, DbModelType> \| null + +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. + ## QuerySnapshot.toJSON() +Returns a JSON-serializable representation of this `QuerySnapshot` instance. + Signature: ```typescript @@ -138,3 +164,5 @@ toJSON(): object; object +a JSON representation of this object. + diff --git a/packages/firestore/src/core/bundle_impl.ts b/packages/firestore/src/core/bundle_impl.ts index 0ce74ec381c..e30d776f855 100644 --- a/packages/firestore/src/core/bundle_impl.ts +++ b/packages/firestore/src/core/bundle_impl.ts @@ -105,6 +105,7 @@ export class BundleConverterImpl implements BundleConverter { mutableDoc }; } + toSnapshotVersion(time: ApiTimestamp): SnapshotVersion { return fromVersion(time); } From fccd633a3496896219c9eadc7855afb4e8beb8ff Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Wed, 30 Apr 2025 11:17:51 -0400 Subject: [PATCH 12/32] QuerySnapshot jsonSchema --- packages/firestore/src/api/snapshot.ts | 158 +++++++++++-------------- 1 file changed, 72 insertions(+), 86 deletions(-) diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index 4158f4a25a1..eea3bd42dfb 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -51,6 +51,7 @@ import { QuerySnapshotBundleData } from '../util/bundle_builder_impl'; import { Code, FirestoreError } from '../util/error'; +import { Property, property, validateJSON } from '../util/json_validation'; import { AutoId } from '../util/misc'; import { Firestore } from './database'; @@ -544,7 +545,7 @@ export class DocumentSnapshot< throw new FirestoreError( Code.FAILED_PRECONDITION, 'DocumentSnapshot.toJSON() attempted to serialize a document with pending writes. ' + - 'Await waitForPendingWrites() before invoking toJSON().' + 'Await waitForPendingWrites() before invoking toJSON().' ); } builder.addBundleDocument( @@ -612,8 +613,7 @@ export class DocumentSnapshot< ).getElements(); if (elements.length === 0) { error = 'No snapshat data was found in the bundle.'; - } - if ( + } else if ( elements.length !== 2 || !elements[0].payload.documentMetadata || !elements[1].payload.document @@ -782,7 +782,7 @@ export class QuerySnapshot< throw new FirestoreError( Code.INVALID_ARGUMENT, 'To include metadata changes with your document changes, you must ' + - 'also pass { includeMetadataChanges:true } to onSnapshot().' + 'also pass { includeMetadataChanges:true } to onSnapshot().' ); } @@ -797,6 +797,14 @@ export class QuerySnapshot< return this._cachedChanges; } + static _jsonSchemaVersion: string = 'firestore/querySnapshot/1.0'; + static _jsonSchema = { + type: property('string', QuerySnapshot._jsonSchemaVersion), + bundleSource: property('string'), + bundleName: property('string'), + bundle: property('string') + }; + /** * Returns a JSON-serializable representation of this `QuerySnapshot` instance. * @@ -805,6 +813,7 @@ export class QuerySnapshot< 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(); @@ -829,7 +838,7 @@ export class QuerySnapshot< throw new FirestoreError( Code.FAILED_PRECONDITION, 'QuerySnapshot.toJSON() attempted to serialize a document with pending writes. ' + - 'Await waitForPendingWrites() before invoking toJSON().' + 'Await waitForPendingWrites() before invoking toJSON().' ); } docBundleDataArray.push( @@ -867,92 +876,69 @@ export class QuerySnapshot< db: Firestore, json: object ): QuerySnapshot | null { - const requiredFields = ['bundle', 'bundleName', 'bundleSource']; - let error: string | undefined = undefined; - let bundleString: string = ''; - for (const key of requiredFields) { - if (!(key in json)) { - error = `json missing required field: ${key}`; + if (validateJSON(json, QuerySnapshot._jsonSchema)) { + if (json.bundleSource !== 'QuerySnapshot') { + throw new FirestoreError(Code.INVALID_ARGUMENT, "Expected 'bundleSource' field to equal 'QuerySnapshot'"); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const value = (json as any)[key]; - if (key === 'bundleSource') { - if (typeof value !== 'string') { - error = `json field 'bundleSource' must be a string.`; - break; - } else if (value !== 'QuerySnapshot') { - error = "Expected 'bundleSource' field to equal 'QuerySnapshot'"; - break; - } - } else if (key === 'bundle') { - if (typeof value !== 'string') { - error = `json field 'bundle' must be a string.`; - break; - } - bundleString = value; - } - } - if (error) { - throw new FirestoreError(Code.INVALID_ARGUMENT, error); - } - // Parse the bundle data. - const serializer = newSerializer(db._databaseId); - const bundleReader = createBundleReaderSync(bundleString, serializer); - const bundleMetadata = bundleReader.getMetadata(); - const elements = bundleReader.getElements(); - if (elements.length === 0) { - throw new FirestoreError( - Code.INVALID_ARGUMENT, - 'No snapshat data was found in the bundle.' + // Parse the bundle data. + const serializer = newSerializer(db._databaseId); + const bundleReader = createBundleReaderSync(json.bundle, serializer); + const bundleLoader: BundleLoader = new BundleLoader( + bundleReader.getMetadata(), + serializer ); - } + const elements = bundleReader.getElements(); + for (const element of elements) { + bundleLoader.addSizedElement(element); + } + const parsedNamedQueries = bundleLoader.queries; + if (parsedNamedQueries.length !== 1) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + `Snapshot data expected 1 query but found ${parsedNamedQueries.length} queries.` + ); + } - const bundleLoader: BundleLoader = new BundleLoader( - bundleMetadata, - serializer - ); - for (const element of elements) { - bundleLoader.addSizedElement(element); - } - const parsedNamedQueries = bundleLoader.queries; - if (parsedNamedQueries.length !== 1) { - throw new FirestoreError( - Code.INVALID_ARGUMENT, - 'Snapshot data contained more than one named query.' - ); - } + // Create an internal Query object from the named query in the budnle. + const query = fromBundledQuery(parsedNamedQueries[0].bundledQuery!); - const query = fromBundledQuery(parsedNamedQueries[0].bundledQuery!); - // convert bundle data into the types that the DocumentSnapshot constructore requires. - const liteUserDataWriter = new LiteUserDataWriter(db); + // Construct the arrays of document data for the query. + const bundledDocuments = bundleLoader.documents; + const documentSet = new DocumentSet(); + const documentKeys = documentKeySet(); + for (const bundledDocumet of bundledDocuments) { + const document = fromDocument(serializer, bundledDocumet.document!); + documentSet.add(document); + const documentPath = ResourcePath.fromString( + bundledDocumet.metadata.name! + ); + documentKeys.add(new DocumentKey(documentPath)); + } - const bundledDocuments = bundleLoader.documents; - const documentSet = new DocumentSet(); - const documentKeys = documentKeySet(); - for (const bundledDocumet of bundledDocuments) { - const document = fromDocument(serializer, bundledDocumet.document!); - documentSet.add(document); - const documentPath = ResourcePath.fromString( - bundledDocumet.metadata.name! + // Create a view snapshot of the query and documents. + const viewSnapshot = ViewSnapshot.fromInitialDocuments( + query, + documentSet, + documentKeys, + /* fromCache= */ false, + /* hasCachedResults= */ false ); - documentKeys.add(new DocumentKey(documentPath)); - } - const viewSnapshot = ViewSnapshot.fromInitialDocuments( - query, - documentSet, - documentKeys, - false, // fromCache - false // hasCachedResults - ); + // Create an external Query object, required to construct the QuerySnapshot. + const externalQuery = new Query(db, null, query); - const externalQuery = new Query(db, null, query); + // Return a new QuerySnapshot with all of the collected data. + return new QuerySnapshot( + db, + new LiteUserDataWriter(db), + externalQuery, + viewSnapshot + ); + } - return new QuerySnapshot( - db, - liteUserDataWriter, - externalQuery, - viewSnapshot + throw new FirestoreError( + Code.INTERNAL, + 'Unexpected error creating QuerySnapshot from JSON.' ); } } @@ -977,10 +963,10 @@ export function changesFromSnapshot< ); debugAssert( !lastDoc || - newQueryComparator(querySnapshot._snapshot.query)( - lastDoc, - change.doc - ) < 0, + newQueryComparator(querySnapshot._snapshot.query)( + lastDoc, + change.doc + ) < 0, 'Got added events in wrong order' ); const doc = new QueryDocumentSnapshot( From 51bc4bc6f127e0f0099524bfe7489f2b80d781b4 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Wed, 30 Apr 2025 14:30:27 -0400 Subject: [PATCH 13/32] Use jsonSchemas --- packages/firestore/src/api/snapshot.ts | 112 +++++++++++-------------- 1 file changed, 48 insertions(+), 64 deletions(-) diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index eea3bd42dfb..df53370f49b 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -513,6 +513,14 @@ 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. * @@ -522,6 +530,7 @@ export class DocumentSnapshot< 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(); @@ -577,68 +586,47 @@ export class DocumentSnapshot< json: object, converter: FirestoreDataConverter ): DocumentSnapshot { - const requiredFields = ['bundle', 'bundleName', 'bundleSource']; - let error: string | undefined = undefined; - let bundleString: string = ''; - for (const key of requiredFields) { - if (!(key in json)) { - error = `json missing required field: ${key}`; + if (validateJSON(json, DocumentSnapshot._jsonSchema)) { + let error : string | undefined = undefined; + // Parse the bundle data. + const serializer = newSerializer(db._databaseId); + const elements = createBundleReaderSync( + json.bundle, + serializer + ).getElements(); + if (elements.length === 0) { + error = 'No snapshat data was found in the bundle.'; + } else if ( + elements.length !== 2 || + !elements[0].payload.documentMetadata || + !elements[1].payload.document + ) { + error = + 'DocumentSnapshot bundle data must contain one metadata and then one document.'; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const value = (json as any)[key]; - if (key === 'bundleSource') { - if (typeof value !== 'string') { - error = `json field 'bundleSource' must be a string.`; - break; - } else if (value !== 'DocumentSnapshot') { - error = "Expected 'bundleSource' field to equal 'DocumentSnapshot'"; - break; - } - } else if (key === 'bundle') { - if (typeof value !== 'string') { - error = `json field 'bundle' must be a string.`; - break; - } - bundleString = value; + if (error) { + throw new FirestoreError(Code.INVALID_ARGUMENT, error); } + // convert bundle data into the types that the DocumentSnapshot constructore requires. + const bundleConverter = new BundleConverterImpl(serializer); + const documentSnapshotData = + bundleConverter.toDocumentSnapshotData(elements); + const liteUserDataWriter = new LiteUserDataWriter(db); + return new DocumentSnapshot( + db, + liteUserDataWriter, + documentSnapshotData.documentKey, + documentSnapshotData.mutableDoc, + new SnapshotMetadata( + /* hasPendingWrites= */ false, + /* fromCache= */ false + ), + converter + ); } - if (error) { - throw new FirestoreError(Code.INVALID_ARGUMENT, error); - } - // Parse the bundle data. - const serializer = newSerializer(db._databaseId); - const elements = createBundleReaderSync( - bundleString, - serializer - ).getElements(); - if (elements.length === 0) { - error = 'No snapshat data was found in the bundle.'; - } else if ( - elements.length !== 2 || - !elements[0].payload.documentMetadata || - !elements[1].payload.document - ) { - error = - 'DocumentSnapshot bundle data must contain one metadata and then one document.'; - } - if (error) { - throw new FirestoreError(Code.INVALID_ARGUMENT, error); - } - // convert bundle data into the types that the DocumentSnapshot constructore requires. - const bundleConverter = new BundleConverterImpl(serializer); - const documentSnapshotData = - bundleConverter.toDocumentSnapshotData(elements); - const liteUserDataWriter = new LiteUserDataWriter(db); - return new DocumentSnapshot( - db, - liteUserDataWriter, - documentSnapshotData.documentKey, - documentSnapshotData.mutableDoc, - new SnapshotMetadata( - /* hasPendingWrites= */ false, - /* fromCache= */ false - ), - converter + throw new FirestoreError( + Code.INTERNAL, + 'Unexpected error creating DocumentSnapshot from JSON.' ); } } @@ -800,7 +788,7 @@ export class QuerySnapshot< static _jsonSchemaVersion: string = 'firestore/querySnapshot/1.0'; static _jsonSchema = { type: property('string', QuerySnapshot._jsonSchemaVersion), - bundleSource: property('string'), + bundleSource: property('string', 'QuerySnapshot'), bundleName: property('string'), bundle: property('string') }; @@ -877,9 +865,6 @@ export class QuerySnapshot< json: object ): QuerySnapshot | null { if (validateJSON(json, QuerySnapshot._jsonSchema)) { - if (json.bundleSource !== 'QuerySnapshot') { - throw new FirestoreError(Code.INVALID_ARGUMENT, "Expected 'bundleSource' field to equal 'QuerySnapshot'"); - } // Parse the bundle data. const serializer = newSerializer(db._databaseId); const bundleReader = createBundleReaderSync(json.bundle, serializer); @@ -935,7 +920,6 @@ export class QuerySnapshot< viewSnapshot ); } - throw new FirestoreError( Code.INTERNAL, 'Unexpected error creating QuerySnapshot from JSON.' From 019aa4d6203b56281638520fabfe9dae3003b0ad Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Wed, 30 Apr 2025 16:04:50 -0400 Subject: [PATCH 14/32] Remove need for toDocumentSnapshotData impl --- packages/firestore/src/api/snapshot.ts | 85 ++++++++++++---------- packages/firestore/src/core/bundle_impl.ts | 30 -------- 2 files changed, 47 insertions(+), 68 deletions(-) diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index df53370f49b..f7376d59fdd 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { BundleConverterImpl, BundleLoader } from '../core/bundle_impl'; +import { BundleLoader } from '../core/bundle_impl'; import { createBundleReaderSync } from '../core/firestore_client'; import { newQueryComparator } from '../core/query'; import { ChangeType, ViewSnapshot } from '../core/view_snapshot'; @@ -51,6 +51,8 @@ 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'; @@ -554,7 +556,7 @@ export class DocumentSnapshot< throw new FirestoreError( Code.FAILED_PRECONDITION, 'DocumentSnapshot.toJSON() attempted to serialize a document with pending writes. ' + - 'Await waitForPendingWrites() before invoking toJSON().' + 'Await waitForPendingWrites() before invoking toJSON().' ); } builder.addBundleDocument( @@ -587,36 +589,39 @@ export class DocumentSnapshot< converter: FirestoreDataConverter ): DocumentSnapshot { if (validateJSON(json, DocumentSnapshot._jsonSchema)) { - let error : string | undefined = undefined; // Parse the bundle data. const serializer = newSerializer(db._databaseId); - const elements = createBundleReaderSync( - json.bundle, + const bundleReader = createBundleReaderSync(json.bundle, serializer); + const elements = bundleReader.getElements(); + const bundleLoader: BundleLoader = new BundleLoader( + bundleReader.getMetadata(), serializer - ).getElements(); - if (elements.length === 0) { - error = 'No snapshat data was found in the bundle.'; - } else if ( - elements.length !== 2 || - !elements[0].payload.documentMetadata || - !elements[1].payload.document - ) { - error = - 'DocumentSnapshot bundle data must contain one metadata and then one document.'; + ); + for (const element of elements) { + bundleLoader.addSizedElement(element); } - if (error) { - throw new FirestoreError(Code.INVALID_ARGUMENT, error); + + // 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.` + ); } - // convert bundle data into the types that the DocumentSnapshot constructore requires. - const bundleConverter = new BundleConverterImpl(serializer); - const documentSnapshotData = - bundleConverter.toDocumentSnapshotData(elements); - const liteUserDataWriter = new LiteUserDataWriter(db); + + // 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, - liteUserDataWriter, - documentSnapshotData.documentKey, - documentSnapshotData.mutableDoc, + new LiteUserDataWriter(db), + documentKey, + document, new SnapshotMetadata( /* hasPendingWrites= */ false, /* fromCache= */ false @@ -770,7 +775,7 @@ export class QuerySnapshot< throw new FirestoreError( Code.INVALID_ARGUMENT, 'To include metadata changes with your document changes, you must ' + - 'also pass { includeMetadataChanges:true } to onSnapshot().' + 'also pass { includeMetadataChanges:true } to onSnapshot().' ); } @@ -826,7 +831,7 @@ export class QuerySnapshot< throw new FirestoreError( Code.FAILED_PRECONDITION, 'QuerySnapshot.toJSON() attempted to serialize a document with pending writes. ' + - 'Await waitForPendingWrites() before invoking toJSON().' + 'Await waitForPendingWrites() before invoking toJSON().' ); } docBundleDataArray.push( @@ -868,24 +873,24 @@ export class QuerySnapshot< // 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 ); - const elements = bundleReader.getElements(); for (const element of elements) { bundleLoader.addSizedElement(element); } - const parsedNamedQueries = bundleLoader.queries; - if (parsedNamedQueries.length !== 1) { + + if (bundleLoader.queries.length !== 1) { throw new FirestoreError( Code.INVALID_ARGUMENT, - `Snapshot data expected 1 query but found ${parsedNamedQueries.length} queries.` + `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(parsedNamedQueries[0].bundledQuery!); + const query = fromBundledQuery(bundleLoader.queries[0].bundledQuery!); // Construct the arrays of document data for the query. const bundledDocuments = bundleLoader.documents; @@ -905,12 +910,16 @@ export class QuerySnapshot< query, documentSet, documentKeys, - /* fromCache= */ false, + /* fromCache= */ false, /* hasCachedResults= */ false ); // Create an external Query object, required to construct the QuerySnapshot. - const externalQuery = new Query(db, null, query); + const externalQuery = new Query( + db, + null, + query + ); // Return a new QuerySnapshot with all of the collected data. return new QuerySnapshot( @@ -947,10 +956,10 @@ export function changesFromSnapshot< ); debugAssert( !lastDoc || - newQueryComparator(querySnapshot._snapshot.query)( - lastDoc, - change.doc - ) < 0, + newQueryComparator(querySnapshot._snapshot.query)( + lastDoc, + change.doc + ) < 0, 'Got added events in wrong order' ); const doc = new QueryDocumentSnapshot( diff --git a/packages/firestore/src/core/bundle_impl.ts b/packages/firestore/src/core/bundle_impl.ts index e30d776f855..b91933f1349 100644 --- a/packages/firestore/src/core/bundle_impl.ts +++ b/packages/firestore/src/core/bundle_impl.ts @@ -39,7 +39,6 @@ import { } from '../remote/serializer'; import { debugAssert } from '../util/assert'; import { SizedBundleElement } from '../util/bundle_reader'; -import { Code, FirestoreError } from '../util/error'; import { BundleConverter, @@ -77,35 +76,6 @@ export class BundleConverterImpl implements BundleConverter { } } - toDocumentSnapshotData(bundleElements: SizedBundleElement[]): { - documentKey: DocumentKey; - mutableDoc: MutableDocument; - } { - const metadata = bundleElements[0]?.payload?.documentMetadata!; - const document = bundleElements[1]?.payload?.document!; - let error: string | undefined = undefined; - if (!metadata || !document) { - error = - 'DocumentSnapshot bundle data requires both document metadata and document data'; - } else if (metadata.name !== document.name) { - error = - 'DocumentSnapshot metadata is not related to the document in the bundle.'; - } - if (error) { - throw new FirestoreError(Code.INVALID_ARGUMENT, error); - } - const bundleConverter = new BundleConverterImpl(this.serializer); - const documentKey = bundleConverter.toDocumentKey(metadata.name!); - const mutableDoc = bundleConverter.toMutableDocument({ - metadata, - document - }); - return { - documentKey, - mutableDoc - }; - } - toSnapshotVersion(time: ApiTimestamp): SnapshotVersion { return fromVersion(time); } From 52bc2177d175f2ae1818bd91e6285b0b0c061af4 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Thu, 1 May 2025 11:38:38 -0400 Subject: [PATCH 15/32] Revised comments. --- common/api-review/firestore.api.md | 4 ++-- docs-devsite/firestore_.documentsnapshot.md | 4 ++-- docs-devsite/firestore_.querysnapshot.md | 4 ++-- docs-devsite/firestore_.timestamp.md | 4 ++-- docs-devsite/firestore_.vectorvalue.md | 2 +- docs-devsite/firestore_lite.timestamp.md | 4 ++-- docs-devsite/firestore_lite.vectorvalue.md | 2 +- packages/firestore/src/api/snapshot.ts | 7 ++----- packages/firestore/src/core/firestore_client.ts | 2 +- packages/firestore/src/lite-api/timestamp.ts | 8 ++++++-- packages/firestore/src/lite-api/vector_value.ts | 2 +- packages/firestore/src/util/bundle_reader.ts | 2 +- packages/firestore/src/util/bundle_reader_sync_impl.ts | 5 ++++- 13 files changed, 27 insertions(+), 23 deletions(-) diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index ce7b46d8a5a..7ae9545aacb 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -178,7 +178,7 @@ export class DocumentSnapshot; - static fromJSON(db: Firestore, json: object, converter: FirestoreDataConverter): DocumentSnapshot; + static fromJSON(db: Firestore, json: object, converter?: FirestoreDataConverter | null): DocumentSnapshot; get(fieldPath: string | FieldPath, options?: SnapshotOptions): any; get id(): string; readonly metadata: SnapshotMetadata; @@ -655,7 +655,7 @@ export class QuerySnapshot>; get empty(): boolean; forEach(callback: (result: QueryDocumentSnapshot) => void, thisArg?: unknown): void; - static fromJSON(db: Firestore, json: object): QuerySnapshot | null; + static fromJSON(db: Firestore, json: object): QuerySnapshot; readonly metadata: SnapshotMetadata; readonly query: Query; get size(): number; diff --git a/docs-devsite/firestore_.documentsnapshot.md b/docs-devsite/firestore_.documentsnapshot.md index 1c80fe9f2eb..67887c7e93c 100644 --- a/docs-devsite/firestore_.documentsnapshot.md +++ b/docs-devsite/firestore_.documentsnapshot.md @@ -128,7 +128,7 @@ Builds a `DocumentSnapshot` instance from a JSON object created by [DocumentSnap Signature: ```typescript -static fromJSON(db: Firestore, json: object, converter: FirestoreDataConverter): DocumentSnapshot; +static fromJSON(db: Firestore, json: object, converter?: FirestoreDataConverter | null): DocumentSnapshot; ``` #### Parameters @@ -137,7 +137,7 @@ static fromJSON(d | --- | --- | --- | | 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. | +| converter | [FirestoreDataConverter](./firestore_.firestoredataconverter.md#firestoredataconverter_interface)<AppModelType, DbModelType> \| null | Converts objects to and from Firestore. | Returns: diff --git a/docs-devsite/firestore_.querysnapshot.md b/docs-devsite/firestore_.querysnapshot.md index ba52572cacc..66afca13db1 100644 --- a/docs-devsite/firestore_.querysnapshot.md +++ b/docs-devsite/firestore_.querysnapshot.md @@ -135,7 +135,7 @@ Builds a `QuerySnapshot` instance from a JSON object created by [QuerySnapshot.t Signature: ```typescript -static fromJSON(db: Firestore, json: object): QuerySnapshot | null; +static fromJSON(db: Firestore, json: object): QuerySnapshot; ``` #### Parameters @@ -147,7 +147,7 @@ static fromJSON(d Returns: -[QuerySnapshot](./firestore_.querysnapshot.md#querysnapshot_class)<AppModelType, DbModelType> \| null +[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. 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.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/snapshot.ts b/packages/firestore/src/api/snapshot.ts index f7376d59fdd..4f228ae4934 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -586,7 +586,7 @@ export class DocumentSnapshot< >( db: Firestore, json: object, - converter: FirestoreDataConverter + converter: FirestoreDataConverter | null = null ): DocumentSnapshot { if (validateJSON(json, DocumentSnapshot._jsonSchema)) { // Parse the bundle data. @@ -865,10 +865,7 @@ export class QuerySnapshot< static fromJSON< AppModelType, DbModelType extends DocumentData = DocumentData - >( - db: Firestore, - json: object - ): QuerySnapshot | null { + >(db: Firestore, json: object): QuerySnapshot { if (validateJSON(json, QuerySnapshot._jsonSchema)) { // Parse the bundle data. const serializer = newSerializer(db._databaseId); diff --git a/packages/firestore/src/core/firestore_client.ts b/packages/firestore/src/core/firestore_client.ts index a111af58282..39bb8dd4eba 100644 --- a/packages/firestore/src/core/firestore_client.ts +++ b/packages/firestore/src/core/firestore_client.ts @@ -809,7 +809,7 @@ export function firestoreClientGetNamedQuery( ); } -export function createBundleReader( +function createBundleReader( data: ReadableStream | ArrayBuffer | string, serializer: JsonProtoSerializer ): BundleReader { 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 2cefe0f7b63..cca1c61a538 100644 --- a/packages/firestore/src/util/bundle_reader.ts +++ b/packages/firestore/src/util/bundle_reader.ts @@ -67,7 +67,7 @@ export interface BundleReader { } /** - * A class representing a bundle. + * A class representing a synchronized bundle reader. * * Takes a bundle string buffer, parses the data, and provides accessors to the data contained * within it. diff --git a/packages/firestore/src/util/bundle_reader_sync_impl.ts b/packages/firestore/src/util/bundle_reader_sync_impl.ts index 8c1cd1142c1..29f9272c846 100644 --- a/packages/firestore/src/util/bundle_reader_sync_impl.ts +++ b/packages/firestore/src/util/bundle_reader_sync_impl.ts @@ -38,7 +38,7 @@ export class BundleReaderSyncImpl implements BundleReaderSync { if (element && element.isBundleMetadata()) { this.metadata = element as BundleMetadata; } else { - throw new Error(`The first element of the bundle is not a metadata, it is + throw new Error(`The first element of the bundle is not a metadata object, it is ${JSON.stringify(element?.payload)}`); } @@ -114,6 +114,9 @@ export class BundleReaderSyncImpl implements BundleReaderSync { } } +/** + * Creates an instance of BundleReader without exposing the BundleReaderSyncImpl class type. + */ export function newBundleReaderSync( bundleData: string, serializer: JsonProtoSerializer From 90ade111550ce8643012c03518d93d91557a08dd Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Fri, 2 May 2025 10:57:17 -0400 Subject: [PATCH 16/32] Add QuerySnapshot.fromJSON converter variant. --- common/api-review/firestore.api.md | 3 +- docs-devsite/firestore_.querysnapshot.md | 27 +++++++++++++++- packages/firestore/src/api/snapshot.ts | 40 ++++++++++++++++++++++-- 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index 7ae9545aacb..1479124dcfb 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -655,7 +655,8 @@ export class QuerySnapshot>; get empty(): boolean; forEach(callback: (result: QueryDocumentSnapshot) => void, thisArg?: unknown): void; - static fromJSON(db: Firestore, json: object): QuerySnapshot; + static fromJSON(db: Firestore, json: object): QuerySnapshot; + static fromJSONUsingConverter(db: Firestore, json: object, converter: FirestoreDataConverter): QuerySnapshot; readonly metadata: SnapshotMetadata; readonly query: Query; get size(): number; diff --git a/docs-devsite/firestore_.querysnapshot.md b/docs-devsite/firestore_.querysnapshot.md index 66afca13db1..9968d3d0450 100644 --- a/docs-devsite/firestore_.querysnapshot.md +++ b/docs-devsite/firestore_.querysnapshot.md @@ -35,6 +35,7 @@ export declare class QuerySnapshotQuerySnapshot. | | [fromJSON(db, json)](./firestore_.querysnapshot.md#querysnapshotfromjson) | static | Builds a QuerySnapshot instance from a JSON object created by [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). | +| [fromJSONUsingConverter(db, json, converter)](./firestore_.querysnapshot.md#querysnapshotfromjsonusingconverter) | static | Builds a QuerySnapshot instance from a JSON object created by [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). | | [toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) | | Returns a JSON-serializable representation of this QuerySnapshot instance. | ## QuerySnapshot.docs @@ -135,7 +136,7 @@ Builds a `QuerySnapshot` instance from a JSON object created by [QuerySnapshot.t Signature: ```typescript -static fromJSON(db: Firestore, json: object): QuerySnapshot; +static fromJSON(db: Firestore, json: object): QuerySnapshot; ``` #### Parameters @@ -147,6 +148,30 @@ static fromJSON(d 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. + +## QuerySnapshot.fromJSONUsingConverter() + +Builds a `QuerySnapshot` instance from a JSON object created by [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). + +Signature: + +```typescript +static fromJSONUsingConverter(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. diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index 4f228ae4934..a9743f2b65b 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -862,10 +862,44 @@ export class QuerySnapshot< * @returns an instance of {@link QuerySnapshot} if the JSON object could be * parsed. Throws a {@link FirestoreError} if an error occurs. */ - static fromJSON< + static fromJSON(db: Firestore, json: object): QuerySnapshot { + return QuerySnapshot.fromJSONInternal(db, json, /* converter = */ null); + } + + /** + * 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. + */ + static fromJSONUsingConverter< AppModelType, DbModelType extends DocumentData = DocumentData - >(db: Firestore, json: object): QuerySnapshot { + >( + db: Firestore, + json: object, + converter: FirestoreDataConverter + ): QuerySnapshot { + return QuerySnapshot.fromJSONInternal(db, json, converter); + } + + /** + * Internal implementation for 'fromJSON' and 'fromJSONUsingCoverter'. + * @internal + * @private + */ + private static fromJSONInternal< + AppModelType, + DbModelType extends DocumentData = DocumentData + >( + db: Firestore, + json: object, + converter: FirestoreDataConverter | null + ): QuerySnapshot { if (validateJSON(json, QuerySnapshot._jsonSchema)) { // Parse the bundle data. const serializer = newSerializer(db._databaseId); @@ -914,7 +948,7 @@ export class QuerySnapshot< // Create an external Query object, required to construct the QuerySnapshot. const externalQuery = new Query( db, - null, + converter, query ); From 7ec7a0c1be283bae8d3f74acd02710b42724d0ce Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Mon, 5 May 2025 21:12:30 -0400 Subject: [PATCH 17/32] DocumentSet fix. --- packages/firestore/src/api/snapshot.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index a9743f2b65b..5a3cbc48fd8 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -925,22 +925,16 @@ export class QuerySnapshot< // Construct the arrays of document data for the query. const bundledDocuments = bundleLoader.documents; - const documentSet = new DocumentSet(); - const documentKeys = documentKeySet(); - for (const bundledDocumet of bundledDocuments) { - const document = fromDocument(serializer, bundledDocumet.document!); - documentSet.add(document); - const documentPath = ResourcePath.fromString( - bundledDocumet.metadata.name! - ); - documentKeys.add(new DocumentKey(documentPath)); - } - + 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, - documentKeys, + documentKeySet() /* Zero mutated keys signifies no pending writes. */, /* fromCache= */ false, /* hasCachedResults= */ false ); From d97009e37262b936ae78a19476d796535d06ae9d Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Mon, 5 May 2025 21:22:04 -0400 Subject: [PATCH 18/32] Unit tests. --- .../firestore/test/unit/api/database.test.ts | 308 +++++++++++++++++- 1 file changed, 304 insertions(+), 4 deletions(-) diff --git a/packages/firestore/test/unit/api/database.test.ts b/packages/firestore/test/unit/api/database.test.ts index 308835c8855..9e049ee23de 100644 --- a/packages/firestore/test/unit/api/database.test.ts +++ b/packages/firestore/test/unit/api/database.test.ts @@ -19,6 +19,8 @@ import { expect } from 'chai'; import { DocumentReference, + DocumentSnapshot, + QuerySnapshot, connectFirestoreEmulator, loadBundle, refEqual, @@ -31,6 +33,7 @@ import { collectionReference, documentReference, documentSnapshot, + firestore, newTestFirestore, query, querySnapshot @@ -88,6 +91,117 @@ describe('DocumentReference', () => { }); }); + it('fromJSON() throws with invalid data', () => { + const db = newTestFirestore(); + expect(() => { + DocumentReference.fromJSON(db, {}); + }).to.throw; + }); + + it('fromJSON() throws with missing type data', () => { + const db = newTestFirestore(); + expect(() => { + DocumentSnapshot.fromJSON(db, { + bundleSource: 'DocumentSnapshot', + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw; + }); + + it('fromJSON() throws with invalid type data', () => { + const db = newTestFirestore(); + expect(() => { + DocumentSnapshot.fromJSON(db, { + type: 1, + bundleSource: 'DocumentSnapshot', + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw; + }); + + it('fromJSON() throws with missing bundleSource', () => { + const db = newTestFirestore(); + expect(() => { + DocumentSnapshot.fromJSON(db, { + type: DocumentSnapshot._jsonSchemaVersion, + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw; + }); + + it('fromJSON() throws with invalid bundleSource type', () => { + const db = newTestFirestore(); + expect(() => { + DocumentSnapshot.fromJSON(db, { + type: DocumentSnapshot._jsonSchemaVersion, + bundleSource: 1, + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw; + }); + + it('fromJSON() throws with invalid bundleSource value', () => { + const db = newTestFirestore(); + expect(() => { + DocumentSnapshot.fromJSON(db, { + type: DocumentSnapshot._jsonSchemaVersion, + bundleSource: 'QuerySnapshot', + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw; + }); + + it('fromJSON() throws with missing bundleName', () => { + const db = newTestFirestore(); + expect(() => { + DocumentSnapshot.fromJSON(db, { + type: DocumentSnapshot._jsonSchemaVersion, + bundleSource: 'DocumentSnapshot', + bundle: 'test bundle' + }); + }).to.throw; + }); + + it('fromJSON() throws with invalid bundleName', () => { + const db = newTestFirestore(); + expect(() => { + DocumentSnapshot.fromJSON(db, { + type: DocumentSnapshot._jsonSchemaVersion, + bundleSource: 'DocumentSnapshot', + bundleName: 1, + bundle: 'test bundle' + }); + }).to.throw; + }); + + it('fromJSON() throws with missing bundle', () => { + const db = newTestFirestore(); + expect(() => { + DocumentSnapshot.fromJSON(db, { + type: DocumentSnapshot._jsonSchemaVersion, + bundleSource: 'DocumentSnapshot', + bundleName: 'test name' + }); + }).to.throw; + }); + + it('fromJSON() throws with invalid bundle', () => { + const db = newTestFirestore(); + expect(() => { + DocumentSnapshot.fromJSON(db, { + type: DocumentSnapshot._jsonSchemaVersion, + bundleSource: 'DocumentSnapshot', + bundleName: 'test name', + bundle: 1 + }); + }).to.throw; + }); + it('fromJSON() does not throw', () => { const db = newTestFirestore(); const docRef = documentReference('foo/bar'); @@ -190,6 +304,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(() => { + DocumentSnapshot.fromJSON(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 = DocumentSnapshot.fromJSON(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 +460,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 +484,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(() => { + QuerySnapshot.fromJSON(db, {}); + }).to.throw; + }); + + it('fromJSON() throws with missing type data', () => { + const db = newTestFirestore(); + expect(() => { + QuerySnapshot.fromJSON(db, { + bundleSource: 'QuerySnapshot', + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw; + }); + + it('fromJSON() throws with invalid type data', () => { + const db = newTestFirestore(); + expect(() => { + QuerySnapshot.fromJSON(db, { + type: 1, + bundleSource: 'QuerySnapshot', + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw; + }); + + it('fromJSON() throws with invalid type data', () => { + const db = newTestFirestore(); + expect(() => { + QuerySnapshot.fromJSON(db, { + type: QuerySnapshot._jsonSchemaVersion, + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw; + }); + + it('fromJSON() throws with invalid bundleSource type', () => { + const db = newTestFirestore(); + expect(() => { + QuerySnapshot.fromJSON(db, { + type: QuerySnapshot._jsonSchemaVersion, + bundleSource: 1, + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw; + }); + + it('fromJSON() throws with invalid bundleSource value', () => { + const db = newTestFirestore(); + expect(() => { + QuerySnapshot.fromJSON(db, { + type: QuerySnapshot._jsonSchemaVersion, + bundleSource: 'DocumentSnapshot', + bundleName: 'test name', + bundle: 'test bundle' + }); + }).to.throw; + }); + + it('fromJSON() throws with missing bundleName', () => { + const db = newTestFirestore(); + expect(() => { + QuerySnapshot.fromJSON(db, { + type: QuerySnapshot._jsonSchemaVersion, + bundleSource: 'QuerySnapshot', + bundle: 'test bundle' + }); + }).to.throw; + }); + + it('fromJSON() throws with invalid bundleName', () => { + const db = newTestFirestore(); + expect(() => { + QuerySnapshot.fromJSON(db, { + type: QuerySnapshot._jsonSchemaVersion, + bundleSource: 'QuerySnapshot', + bundleName: 1, + bundle: 'test bundle' + }); + }).to.throw; + }); + + it('fromJSON() throws with missing bundle data', () => { + const db = newTestFirestore(); + expect(() => { + QuerySnapshot.fromJSON(db, { + type: QuerySnapshot._jsonSchemaVersion, + bundleSource: 'QuerySnapshot', + bundleName: 'test name' + }); + }).to.throw; + }); + + it('fromJSON() throws with invalid bundle data', () => { + const db = newTestFirestore(); + expect(() => { + QuerySnapshot.fromJSON(db, { + type: QuerySnapshot._jsonSchemaVersion, + bundleSource: 'QuerySnapshot', + bundleName: 'test name', + bundle: 1 + }); + }).to.throw; + }); + + 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(() => { + QuerySnapshot.fromJSON(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 = QuerySnapshot.fromJSON(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 8a942f1989af09a25cd1123741535073d0f08043 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Tue, 6 May 2025 11:16:24 -0400 Subject: [PATCH 19/32] Non templated DocumentSnapshot.fromJSON --- common/api-review/firestore.api.md | 3 +- docs-devsite/firestore_.documentsnapshot.md | 30 ++++++++++++++++++-- packages/firestore/src/api/snapshot.ts | 31 ++++++++++++++++++++- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index 1479124dcfb..500a340f1b4 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -178,7 +178,8 @@ export class DocumentSnapshot; - static fromJSON(db: Firestore, json: object, converter?: FirestoreDataConverter | null): DocumentSnapshot; + static fromJSON(db: Firestore, json: object): DocumentSnapshot; + static fromJSONUsingConverter(db: Firestore, json: object, converter: FirestoreDataConverter): DocumentSnapshot; get(fieldPath: string | FieldPath, options?: SnapshotOptions): any; get id(): string; readonly metadata: SnapshotMetadata; diff --git a/docs-devsite/firestore_.documentsnapshot.md b/docs-devsite/firestore_.documentsnapshot.md index 67887c7e93c..b49ac11db47 100644 --- a/docs-devsite/firestore_.documentsnapshot.md +++ b/docs-devsite/firestore_.documentsnapshot.md @@ -40,7 +40,8 @@ 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. | -| [fromJSON(db, json, converter)](./firestore_.documentsnapshot.md#documentsnapshotfromjson) | static | Builds a DocumentSnapshot instance from a JSON object created by [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). | +| [fromJSON(db, json)](./firestore_.documentsnapshot.md#documentsnapshotfromjson) | static | Builds a DocumentSnapshot instance from a JSON object created by [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). | +| [fromJSONUsingConverter(db, json, converter)](./firestore_.documentsnapshot.md#documentsnapshotfromjsonusingconverter) | static | Builds a DocumentSnapshot instance from a JSON object created by [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). | | [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) | | Returns a JSON-serializable representation of this DocumentSnapshot instance. | @@ -128,7 +129,7 @@ Builds a `DocumentSnapshot` instance from a JSON object created by [DocumentSnap Signature: ```typescript -static fromJSON(db: Firestore, json: object, converter?: FirestoreDataConverter | null): DocumentSnapshot; +static fromJSON(db: Firestore, json: object): DocumentSnapshot; ``` #### Parameters @@ -137,7 +138,30 @@ static fromJSON(d | --- | --- | --- | | 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> \| null | Converts objects to and from Firestore. | + +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. + +## DocumentSnapshot.fromJSONUsingConverter() + +Builds a `DocumentSnapshot` instance from a JSON object created by [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). + +Signature: + +```typescript +static fromJSONUsingConverter(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: diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index 5a3cbc48fd8..6b6554f2464 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -570,6 +570,19 @@ export class DocumentSnapshot< return result; } + /** + * 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. + */ + static fromJSON(db: Firestore, json: object): DocumentSnapshot { + return DocumentSnapshot.fromJSONInternal(db, json, /* converter = */ null); + } + /** * Builds a `DocumentSnapshot` instance from a JSON object created by * {@link DocumentSnapshot.toJSON}. @@ -580,7 +593,23 @@ export class DocumentSnapshot< * @returns an instance of {@link DocumentSnapshot} if the JSON object could be * parsed. Throws a {@link FirestoreError} if an error occurs. */ - static fromJSON< + static fromJSONUsingConverter< + AppModelType, + DbModelType extends DocumentData = DocumentData + >( + db: Firestore, + json: object, + converter: FirestoreDataConverter + ): DocumentSnapshot { + return DocumentSnapshot.fromJSONInternal(db, json, converter); + } + + /** + * Internal implementation for 'fromJSON' and 'fromJSONUsingCoverter'. + * @internal + * @private + */ + private static fromJSONInternal< AppModelType, DbModelType extends DocumentData = DocumentData >( From 78626ef707013946d47685d73b312937311c364f Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Tue, 6 May 2025 11:16:37 -0400 Subject: [PATCH 20/32] fromJSON integration tests --- .../test/integration/api/database.test.ts | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index b6320169582..c03fae3b1d0 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -1341,6 +1341,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 = DocumentSnapshot.fromJSON(db, doc.toJSON()); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshot( + 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 = DocumentSnapshot.fromJSON(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 }, @@ -1469,6 +1529,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 = QuerySnapshot.fromJSON(db, querySnap.toJSON()); + const refForDocA = querySnapFromJson.docs[0].ref; + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshot( + 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 = QuerySnapshot.fromJSON(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(); From 5bf576739fbe4446481280e388879d941ca3c23b Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Tue, 6 May 2025 16:11:16 -0400 Subject: [PATCH 21/32] remove fromJSONUsingConverter, user override. --- common/api-review/firestore.api.md | 4 +- docs-devsite/firestore_.documentsnapshot.md | 6 +- docs-devsite/firestore_.querysnapshot.md | 6 +- packages/firestore/src/api/snapshot.ts | 62 ++++++++++----------- 4 files changed, 36 insertions(+), 42 deletions(-) diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index 500a340f1b4..e7ef2eed7c1 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -179,7 +179,7 @@ export class DocumentSnapshot; static fromJSON(db: Firestore, json: object): DocumentSnapshot; - static fromJSONUsingConverter(db: Firestore, json: object, converter: FirestoreDataConverter): DocumentSnapshot; + static fromJSON(db: Firestore, json: object, converter: FirestoreDataConverter): DocumentSnapshot; get(fieldPath: string | FieldPath, options?: SnapshotOptions): any; get id(): string; readonly metadata: SnapshotMetadata; @@ -657,7 +657,7 @@ export class QuerySnapshot) => void, thisArg?: unknown): void; static fromJSON(db: Firestore, json: object): QuerySnapshot; - static fromJSONUsingConverter(db: Firestore, json: object, converter: FirestoreDataConverter): QuerySnapshot; + static fromJSON(db: Firestore, json: object, converter: FirestoreDataConverter): QuerySnapshot; readonly metadata: SnapshotMetadata; readonly query: Query; get size(): number; diff --git a/docs-devsite/firestore_.documentsnapshot.md b/docs-devsite/firestore_.documentsnapshot.md index b49ac11db47..486321e415d 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. | | [fromJSON(db, json)](./firestore_.documentsnapshot.md#documentsnapshotfromjson) | static | Builds a DocumentSnapshot instance from a JSON object created by [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). | -| [fromJSONUsingConverter(db, json, converter)](./firestore_.documentsnapshot.md#documentsnapshotfromjsonusingconverter) | static | Builds a DocumentSnapshot instance from a JSON object created by [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). | +| [fromJSON(db, json, converter)](./firestore_.documentsnapshot.md#documentsnapshotfromjson) | static | Builds a DocumentSnapshot instance from a JSON object created by [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). | | [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) | | Returns a JSON-serializable representation of this DocumentSnapshot instance. | @@ -145,14 +145,14 @@ static fromJSON(db: Firestore, json: object): DocumentSnapshot; 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. -## DocumentSnapshot.fromJSONUsingConverter() +## DocumentSnapshot.fromJSON() Builds a `DocumentSnapshot` instance from a JSON object created by [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). Signature: ```typescript -static fromJSONUsingConverter(db: Firestore, json: object, converter: FirestoreDataConverter): DocumentSnapshot; +static fromJSON(db: Firestore, json: object, converter: FirestoreDataConverter): DocumentSnapshot; ``` #### Parameters diff --git a/docs-devsite/firestore_.querysnapshot.md b/docs-devsite/firestore_.querysnapshot.md index 9968d3d0450..4ca79275f49 100644 --- a/docs-devsite/firestore_.querysnapshot.md +++ b/docs-devsite/firestore_.querysnapshot.md @@ -35,7 +35,7 @@ export declare class QuerySnapshotQuerySnapshot. | | [fromJSON(db, json)](./firestore_.querysnapshot.md#querysnapshotfromjson) | static | Builds a QuerySnapshot instance from a JSON object created by [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). | -| [fromJSONUsingConverter(db, json, converter)](./firestore_.querysnapshot.md#querysnapshotfromjsonusingconverter) | static | Builds a QuerySnapshot instance from a JSON object created by [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). | +| [fromJSON(db, json, converter)](./firestore_.querysnapshot.md#querysnapshotfromjson) | static | Builds a QuerySnapshot instance from a JSON object created by [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). | | [toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) | | Returns a JSON-serializable representation of this QuerySnapshot instance. | ## QuerySnapshot.docs @@ -152,14 +152,14 @@ static fromJSON(db: Firestore, json: object): QuerySnapshot; 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. -## QuerySnapshot.fromJSONUsingConverter() +## QuerySnapshot.fromJSON() Builds a `QuerySnapshot` instance from a JSON object created by [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). Signature: ```typescript -static fromJSONUsingConverter(db: Firestore, json: object, converter: FirestoreDataConverter): QuerySnapshot; +static fromJSON(db: Firestore, json: object, converter: FirestoreDataConverter): QuerySnapshot; ``` #### Parameters diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index 6b6554f2464..75bbe836fed 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -579,10 +579,7 @@ export class DocumentSnapshot< * @returns an instance of {@link DocumentSnapshot} if the JSON object could be * parsed. Throws a {@link FirestoreError} if an error occurs. */ - static fromJSON(db: Firestore, json: object): DocumentSnapshot { - return DocumentSnapshot.fromJSONInternal(db, json, /* converter = */ null); - } - + static fromJSON(db: Firestore, json: object): DocumentSnapshot; /** * Builds a `DocumentSnapshot` instance from a JSON object created by * {@link DocumentSnapshot.toJSON}. @@ -593,29 +590,21 @@ export class DocumentSnapshot< * @returns an instance of {@link DocumentSnapshot} if the JSON object could be * parsed. Throws a {@link FirestoreError} if an error occurs. */ - static fromJSONUsingConverter< + static fromJSON< AppModelType, DbModelType extends DocumentData = DocumentData >( db: Firestore, json: object, converter: FirestoreDataConverter - ): DocumentSnapshot { - return DocumentSnapshot.fromJSONInternal(db, json, converter); - } - - /** - * Internal implementation for 'fromJSON' and 'fromJSONUsingCoverter'. - * @internal - * @private - */ - private static fromJSONInternal< + ): DocumentSnapshot; + static fromJSON< AppModelType, DbModelType extends DocumentData = DocumentData >( db: Firestore, json: object, - converter: FirestoreDataConverter | null = null + ...args: unknown[] ): DocumentSnapshot { if (validateJSON(json, DocumentSnapshot._jsonSchema)) { // Parse the bundle data. @@ -645,6 +634,15 @@ export class DocumentSnapshot< ResourcePath.fromString(json.bundleName) ); + let converter: FirestoreDataConverter | null = + null; + if (args[0]) { + converter = args[0] as FirestoreDataConverter< + AppModelType, + DbModelType + >; + } + // Return the external facing DocumentSnapshot. return new DocumentSnapshot( db, @@ -891,9 +889,7 @@ export class QuerySnapshot< * @returns an instance of {@link QuerySnapshot} if the JSON object could be * parsed. Throws a {@link FirestoreError} if an error occurs. */ - static fromJSON(db: Firestore, json: object): QuerySnapshot { - return QuerySnapshot.fromJSONInternal(db, json, /* converter = */ null); - } + static fromJSON(db: Firestore, json: object): QuerySnapshot; /** * Builds a `QuerySnapshot` instance from a JSON object created by @@ -905,29 +901,18 @@ export class QuerySnapshot< * @returns an instance of {@link QuerySnapshot} if the JSON object could be * parsed. Throws a {@link FirestoreError} if an error occurs. */ - static fromJSONUsingConverter< + static fromJSON< AppModelType, DbModelType extends DocumentData = DocumentData >( db: Firestore, json: object, converter: FirestoreDataConverter - ): QuerySnapshot { - return QuerySnapshot.fromJSONInternal(db, json, converter); - } - - /** - * Internal implementation for 'fromJSON' and 'fromJSONUsingCoverter'. - * @internal - * @private - */ - private static fromJSONInternal< - AppModelType, - DbModelType extends DocumentData = DocumentData - >( + ): QuerySnapshot; + static fromJSON( db: Firestore, json: object, - converter: FirestoreDataConverter | null + ...args: unknown[] ): QuerySnapshot { if (validateJSON(json, QuerySnapshot._jsonSchema)) { // Parse the bundle data. @@ -968,6 +953,15 @@ export class QuerySnapshot< /* hasCachedResults= */ false ); + let converter: FirestoreDataConverter | null = + null; + if (args[0]) { + converter = args[0] as FirestoreDataConverter< + AppModelType, + DbModelType + >; + } + // Create an external Query object, required to construct the QuerySnapshot. const externalQuery = new Query( db, From 4f27dc4293ba77f718529dc5f2c531ce319d57be Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Wed, 7 May 2025 10:34:47 -0400 Subject: [PATCH 22/32] DocumentReference fromJSON overload --- common/api-review/firestore-lite.api.md | 3 +- common/api-review/firestore.api.md | 3 +- docs-devsite/firestore_.documentreference.md | 30 ++++++++++++++-- .../firestore_lite.documentreference.md | 30 ++++++++++++++-- packages/firestore/src/lite-api/reference.ts | 34 +++++++++++++++++-- 5 files changed, 90 insertions(+), 10 deletions(-) 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 e7ef2eed7c1..ce9dfa6b2c4 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; 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_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/packages/firestore/src/lite-api/reference.ts b/packages/firestore/src/lite-api/reference.ts index ed66eae7b70..c42a3a7e993 100644 --- a/packages/firestore/src/lite-api/reference.ts +++ b/packages/firestore/src/lite-api/reference.ts @@ -304,22 +304,52 @@ 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 >( firestore: Firestore, json: object, - converter?: FirestoreDataConverter + ...args: unknown[] ): DocumentReference { if (validateJSON(json, DocumentReference._jsonSchema)) { + let converter: FirestoreDataConverter< + NewAppModelType, + NewDbModelType + > | null = null; + if (args[0]) { + converter = args[0] as FirestoreDataConverter< + NewAppModelType, + NewDbModelType + >; + } return new DocumentReference( firestore, - converter ? converter : null, + converter, new DocumentKey(ResourcePath.fromString(json.referencePath)) ); } From 2840bb3ca6b00c9a3cfe024c831885449b05f726 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Mon, 12 May 2025 14:03:59 -0400 Subject: [PATCH 23/32] Created onSnapshotResume variant. --- common/api-review/firestore.api.md | 16 +- docs-devsite/firestore_.md | 110 ++--- packages/firestore/src/api.ts | 1 + packages/firestore/src/api/reference_impl.ts | 396 +++++++++--------- .../test/integration/api/database.test.ts | 25 +- 5 files changed, 278 insertions(+), 270 deletions(-) diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index ce9dfa6b2c4..3425fb9279b 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -470,40 +470,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; diff --git a/docs-devsite/firestore_.md b/docs-devsite/firestore_.md index 70a0604af6c..34ebe4d264e 100644 --- a/docs-devsite/firestore_.md +++ b/docs-devsite/firestore_.md @@ -32,14 +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. | +| [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. | @@ -625,16 +625,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 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: 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 onSnapshotResume(firestore: Firestore, snapshotJson: object, onNext: (snapshot: DocumentSnapshot) => void, onError?: (error: FirestoreError) => void, onCompletion?: () => void, converter?: FirestoreDataConverter): Unsubscribe; ``` #### Parameters @@ -654,16 +683,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 +713,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 +743,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 +774,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 +805,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 +837,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 +869,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/packages/firestore/src/api.ts b/packages/firestore/src/api.ts index ea969c6b94c..c04a1027f74 100644 --- a/packages/firestore/src/api.ts +++ b/packages/firestore/src/api.ts @@ -161,6 +161,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 8f6b5a60b6d..03c61be6586 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. * @@ -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,92 @@ 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])) { + console.error('DEDB arg 0 is SnapsotLsitenOptions'); + options = args[curArg++] as SnapshotListenOptions; + } else { + console.error('DEDB arg 0 is NOT SnapsotLsitenOptions'); } - 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 +1180,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/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index c03fae3b1d0..bc383cdc18f 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -42,6 +42,7 @@ import { initializeFirestore, limit, onSnapshot, + onSnapshotResume, onSnapshotsInSync, orderBy, query, @@ -1209,7 +1210,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 +1241,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 +1272,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 +1300,7 @@ apiDescribe('Database', persistence => { bundleSource: 'DocumentSnapshot' }; const deferred = new Deferred(); - const unsubscribe = onSnapshot( + const unsubscribe = onSnapshotResume( db, json, ds => { @@ -1325,7 +1326,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(); @@ -1351,7 +1352,7 @@ apiDescribe('Database', persistence => { const doc = await getDoc(docRef); const fromJsonDoc = DocumentSnapshot.fromJSON(db, doc.toJSON()); const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshot( + const unsubscribe = onSnapshotResume( db, fromJsonDoc.toJSON(), accumulator.storeEvent @@ -1409,7 +1410,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 @@ -1432,7 +1433,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 => { @@ -1453,7 +1454,7 @@ apiDescribe('Database', persistence => { bundleSource: 'QuerySnapshot' }; const deferred = new Deferred(); - const unsubscribe = onSnapshot( + const unsubscribe = onSnapshotResume( db, json, qs => { @@ -1479,7 +1480,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(); @@ -1504,7 +1505,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 @@ -1539,7 +1540,7 @@ apiDescribe('Database', persistence => { const querySnapFromJson = QuerySnapshot.fromJSON(db, querySnap.toJSON()); const refForDocA = querySnapFromJson.docs[0].ref; const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshot( + const unsubscribe = onSnapshotResume( db, querySnapFromJson.toJSON(), accumulator.storeEvent From 96a49f7e8316dd1cf61b4d56fcfdb7a9ec650c73 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Mon, 12 May 2025 14:49:55 -0400 Subject: [PATCH 24/32] Move doc / querysnapshot fromJSON to func impl. --- common/api-review/firestore.api.md | 16 +- docs-devsite/firestore_.documentsnapshot.md | 49 --- docs-devsite/firestore_.md | 101 +++++ docs-devsite/firestore_.querysnapshot.md | 49 --- packages/firestore/src/api.ts | 2 + packages/firestore/src/api/snapshot.ts | 356 +++++++++--------- .../test/integration/api/database.test.ts | 10 +- .../firestore/test/unit/api/database.test.ts | 48 +-- 8 files changed, 325 insertions(+), 306 deletions(-) diff --git a/common/api-review/firestore.api.md b/common/api-review/firestore.api.md index 3425fb9279b..292d81d7a75 100644 --- a/common/api-review/firestore.api.md +++ b/common/api-review/firestore.api.md @@ -179,8 +179,6 @@ export class DocumentSnapshot; - static fromJSON(db: Firestore, json: object): DocumentSnapshot; - static fromJSON(db: Firestore, json: object, converter: FirestoreDataConverter): DocumentSnapshot; get(fieldPath: string | FieldPath, options?: SnapshotOptions): any; get id(): string; readonly metadata: SnapshotMetadata; @@ -188,6 +186,12 @@ export class DocumentSnapshot(db: Firestore, json: object, converter: FirestoreDataConverter): DocumentSnapshot; + export { EmulatorMockTokenOptions } // @public @deprecated @@ -657,14 +661,18 @@ export class QuerySnapshot>; get empty(): boolean; forEach(callback: (result: QueryDocumentSnapshot) => void, thisArg?: unknown): void; - static fromJSON(db: Firestore, json: object): QuerySnapshot; - static fromJSON(db: Firestore, json: object, converter: FirestoreDataConverter): QuerySnapshot; readonly metadata: SnapshotMetadata; readonly query: Query; get size(): number; 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_.documentsnapshot.md b/docs-devsite/firestore_.documentsnapshot.md index 486321e415d..3281873c525 100644 --- a/docs-devsite/firestore_.documentsnapshot.md +++ b/docs-devsite/firestore_.documentsnapshot.md @@ -40,8 +40,6 @@ 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. | -| [fromJSON(db, json)](./firestore_.documentsnapshot.md#documentsnapshotfromjson) | static | Builds a DocumentSnapshot instance from a JSON object created by [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). | -| [fromJSON(db, json, converter)](./firestore_.documentsnapshot.md#documentsnapshotfromjson) | static | Builds a DocumentSnapshot instance from a JSON object created by [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). | | [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) | | Returns a JSON-serializable representation of this DocumentSnapshot instance. | @@ -122,53 +120,6 @@ exists(): this is QueryDocumentSnapshot; this is [QueryDocumentSnapshot](./firestore_.querydocumentsnapshot.md#querydocumentsnapshot_class)<AppModelType, DbModelType> -## DocumentSnapshot.fromJSON() - -Builds a `DocumentSnapshot` instance from a JSON object created by [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). - -Signature: - -```typescript -static fromJSON(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. - -## DocumentSnapshot.fromJSON() - -Builds a `DocumentSnapshot` instance from a JSON object created by [DocumentSnapshot.toJSON()](./firestore_.documentsnapshot.md#documentsnapshottojson). - -Signature: - -```typescript -static fromJSON(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. - ## DocumentSnapshot.get() Retrieves the field specified by `fieldPath`. Returns `undefined` if the document or field doesn't exist. diff --git a/docs-devsite/firestore_.md b/docs-devsite/firestore_.md index 34ebe4d264e..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. | @@ -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} diff --git a/docs-devsite/firestore_.querysnapshot.md b/docs-devsite/firestore_.querysnapshot.md index 4ca79275f49..78f1ac3f23b 100644 --- a/docs-devsite/firestore_.querysnapshot.md +++ b/docs-devsite/firestore_.querysnapshot.md @@ -34,8 +34,6 @@ export declare class QuerySnapshotQuerySnapshot. | -| [fromJSON(db, json)](./firestore_.querysnapshot.md#querysnapshotfromjson) | static | Builds a QuerySnapshot instance from a JSON object created by [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). | -| [fromJSON(db, json, converter)](./firestore_.querysnapshot.md#querysnapshotfromjson) | static | Builds a QuerySnapshot instance from a JSON object created by [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). | | [toJSON()](./firestore_.querysnapshot.md#querysnapshottojson) | | Returns a JSON-serializable representation of this QuerySnapshot instance. | ## QuerySnapshot.docs @@ -129,53 +127,6 @@ forEach(callback: (result: QueryDocumentSnapshot) => void -## QuerySnapshot.fromJSON() - -Builds a `QuerySnapshot` instance from a JSON object created by [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). - -Signature: - -```typescript -static fromJSON(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. - -## QuerySnapshot.fromJSON() - -Builds a `QuerySnapshot` instance from a JSON object created by [QuerySnapshot.toJSON()](./firestore_.querysnapshot.md#querysnapshottojson). - -Signature: - -```typescript -static fromJSON(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. - ## QuerySnapshot.toJSON() Returns a JSON-serializable representation of this `QuerySnapshot` instance. diff --git a/packages/firestore/src/api.ts b/packages/firestore/src/api.ts index c04a1027f74..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 diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index 75bbe836fed..10e8158d7e5 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -569,98 +569,98 @@ export class DocumentSnapshot< result['bundle'] = builder.build(); return result; } +} - /** - * 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. - */ - static fromJSON(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. - */ - static fromJSON< - AppModelType, - DbModelType extends DocumentData = DocumentData - >( - db: Firestore, - json: object, - converter: FirestoreDataConverter - ): DocumentSnapshot; - static fromJSON< - AppModelType, - DbModelType extends DocumentData = DocumentData - >( - db: Firestore, - json: object, - ...args: unknown[] - ): 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.` - ); - } +/** + * 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, + ...args: unknown[] +): 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); + } - // Build out the internal document data. - const document = fromDocument(serializer, bundledDocuments[0].document!); - const documentKey = new DocumentKey( - ResourcePath.fromString(json.bundleName) + // 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.` ); + } - let converter: FirestoreDataConverter | null = - null; - if (args[0]) { - converter = args[0] as FirestoreDataConverter< - AppModelType, - DbModelType - >; - } + // 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 - ); + let converter: FirestoreDataConverter | null = + null; + if (args[0]) { + converter = args[0] as FirestoreDataConverter; } - throw new FirestoreError( - Code.INTERNAL, - 'Unexpected error creating DocumentSnapshot from JSON.' + + // Return the external facing DocumentSnapshot. + return new DocumentSnapshot( + db, + new LiteUserDataWriter(db), + documentKey, + document, + new SnapshotMetadata( + /* hasPendingWrites= */ false, + /* fromCache= */ false + ), + converter ); } + throw new FirestoreError( + Code.INTERNAL, + 'Unexpected error creating DocumentSnapshot from JSON.' + ); } /** @@ -879,109 +879,111 @@ export class QuerySnapshot< result['bundle'] = builder.build(); return result; } +} - /** - * 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. - */ - static fromJSON(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. + * @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, + ...args: unknown[] +): 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); + } - /** - * 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. - */ - static fromJSON< - AppModelType, - DbModelType extends DocumentData = DocumentData - >( - db: Firestore, - json: object, - converter: FirestoreDataConverter - ): QuerySnapshot; - static fromJSON( - db: Firestore, - json: object, - ...args: unknown[] - ): 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 + if (bundleLoader.queries.length !== 1) { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + `Snapshot data expected 1 query but found ${bundleLoader.queries.length} queries.` ); - 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!); + // 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 - ); + // 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 + ); - let converter: FirestoreDataConverter | null = - null; - if (args[0]) { - converter = args[0] as FirestoreDataConverter< - AppModelType, - DbModelType - >; - } + let converter: FirestoreDataConverter | null = + null; + if (args[0]) { + converter = args[0] as FirestoreDataConverter; + } - // Create an external Query object, required to construct the QuerySnapshot. - const externalQuery = new Query( - db, - converter, - query - ); + // Create an external Query object, required to construct the QuerySnapshot. + const externalQuery = new Query( + db, + converter, + 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.' + // 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`. */ diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index bc383cdc18f..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, @@ -67,6 +68,7 @@ import { newTestApp, FirestoreError, QuerySnapshot, + querySnapshotFromJSON, vector, getDocsFromServer } from '../util/firebase_export'; @@ -1350,7 +1352,7 @@ apiDescribe('Database', persistence => { initialData, async (docRef, db) => { const doc = await getDoc(docRef); - const fromJsonDoc = DocumentSnapshot.fromJSON(db, doc.toJSON()); + const fromJsonDoc = documentSnapshotFromJSON(db, doc.toJSON()); const accumulator = new EventsAccumulator(); const unsubscribe = onSnapshotResume( db, @@ -1382,7 +1384,7 @@ apiDescribe('Database', persistence => { initialData, async (docRef, db) => { const doc = await getDoc(docRef); - const fromJsonDoc = DocumentSnapshot.fromJSON(db, doc.toJSON()); + const fromJsonDoc = documentSnapshotFromJSON(db, doc.toJSON()); const accumulator = new EventsAccumulator(); const unsubscribe = onSnapshot(fromJsonDoc.ref, accumulator.storeEvent); await accumulator @@ -1537,7 +1539,7 @@ apiDescribe('Database', persistence => { }; await withTestCollection(persistence, testDocs, async (coll, db) => { const querySnap = await getDocs(query(coll, orderBy(documentId()))); - const querySnapFromJson = QuerySnapshot.fromJSON(db, querySnap.toJSON()); + const querySnapFromJson = querySnapshotFromJSON(db, querySnap.toJSON()); const refForDocA = querySnapFromJson.docs[0].ref; const accumulator = new EventsAccumulator(); const unsubscribe = onSnapshotResume( @@ -1572,7 +1574,7 @@ apiDescribe('Database', persistence => { }; await withTestCollection(persistence, testDocs, async (coll, db) => { const querySnap = await getDocs(query(coll, orderBy(documentId()))); - const querySnapFromJson = QuerySnapshot.fromJSON(db, querySnap.toJSON()); + const querySnapFromJson = querySnapshotFromJSON(db, querySnap.toJSON()); const refForDocA = querySnapFromJson.docs[0].ref; const accumulator = new EventsAccumulator(); const unsubscribe = onSnapshot( diff --git a/packages/firestore/test/unit/api/database.test.ts b/packages/firestore/test/unit/api/database.test.ts index 9e049ee23de..cf64ede5b02 100644 --- a/packages/firestore/test/unit/api/database.test.ts +++ b/packages/firestore/test/unit/api/database.test.ts @@ -20,7 +20,9 @@ import { expect } from 'chai'; import { DocumentReference, DocumentSnapshot, + documentSnapshotFromJSON, QuerySnapshot, + querySnapshotFromJSON, connectFirestoreEmulator, loadBundle, refEqual, @@ -101,7 +103,7 @@ describe('DocumentReference', () => { it('fromJSON() throws with missing type data', () => { const db = newTestFirestore(); expect(() => { - DocumentSnapshot.fromJSON(db, { + documentSnapshotFromJSON(db, { bundleSource: 'DocumentSnapshot', bundleName: 'test name', bundle: 'test bundle' @@ -112,7 +114,7 @@ describe('DocumentReference', () => { it('fromJSON() throws with invalid type data', () => { const db = newTestFirestore(); expect(() => { - DocumentSnapshot.fromJSON(db, { + documentSnapshotFromJSON(db, { type: 1, bundleSource: 'DocumentSnapshot', bundleName: 'test name', @@ -124,7 +126,7 @@ describe('DocumentReference', () => { it('fromJSON() throws with missing bundleSource', () => { const db = newTestFirestore(); expect(() => { - DocumentSnapshot.fromJSON(db, { + documentSnapshotFromJSON(db, { type: DocumentSnapshot._jsonSchemaVersion, bundleName: 'test name', bundle: 'test bundle' @@ -135,7 +137,7 @@ describe('DocumentReference', () => { it('fromJSON() throws with invalid bundleSource type', () => { const db = newTestFirestore(); expect(() => { - DocumentSnapshot.fromJSON(db, { + documentSnapshotFromJSON(db, { type: DocumentSnapshot._jsonSchemaVersion, bundleSource: 1, bundleName: 'test name', @@ -147,7 +149,7 @@ describe('DocumentReference', () => { it('fromJSON() throws with invalid bundleSource value', () => { const db = newTestFirestore(); expect(() => { - DocumentSnapshot.fromJSON(db, { + documentSnapshotFromJSON(db, { type: DocumentSnapshot._jsonSchemaVersion, bundleSource: 'QuerySnapshot', bundleName: 'test name', @@ -159,7 +161,7 @@ describe('DocumentReference', () => { it('fromJSON() throws with missing bundleName', () => { const db = newTestFirestore(); expect(() => { - DocumentSnapshot.fromJSON(db, { + documentSnapshotFromJSON(db, { type: DocumentSnapshot._jsonSchemaVersion, bundleSource: 'DocumentSnapshot', bundle: 'test bundle' @@ -170,7 +172,7 @@ describe('DocumentReference', () => { it('fromJSON() throws with invalid bundleName', () => { const db = newTestFirestore(); expect(() => { - DocumentSnapshot.fromJSON(db, { + documentSnapshotFromJSON(db, { type: DocumentSnapshot._jsonSchemaVersion, bundleSource: 'DocumentSnapshot', bundleName: 1, @@ -182,7 +184,7 @@ describe('DocumentReference', () => { it('fromJSON() throws with missing bundle', () => { const db = newTestFirestore(); expect(() => { - DocumentSnapshot.fromJSON(db, { + documentSnapshotFromJSON(db, { type: DocumentSnapshot._jsonSchemaVersion, bundleSource: 'DocumentSnapshot', bundleName: 'test name' @@ -193,7 +195,7 @@ describe('DocumentReference', () => { it('fromJSON() throws with invalid bundle', () => { const db = newTestFirestore(); expect(() => { - DocumentSnapshot.fromJSON(db, { + documentSnapshotFromJSON(db, { type: DocumentSnapshot._jsonSchemaVersion, bundleSource: 'DocumentSnapshot', bundleName: 'test name', @@ -309,7 +311,7 @@ describe('DocumentSnapshot', () => { const docSnap = documentSnapshot('foo/bar', { a: 1 }, /*fromCache=*/ true); const json = docSnap.toJSON(); expect(() => { - DocumentSnapshot.fromJSON(docSnap._firestore, json); + documentSnapshotFromJSON(docSnap._firestore, json); }).to.not.throw; }); @@ -320,7 +322,7 @@ describe('DocumentSnapshot', () => { /*fromCache=*/ true ).toJSON(); const db = firestore(); - const docSnap = DocumentSnapshot.fromJSON(db, json); + const docSnap = documentSnapshotFromJSON(db, json); expect(docSnap).to.exist; const data = docSnap.data(); expect(data).to.not.be.undefined; @@ -497,14 +499,14 @@ describe('QuerySnapshot', () => { it('fromJSON() throws with invalid data', () => { const db = newTestFirestore(); expect(() => { - QuerySnapshot.fromJSON(db, {}); + querySnapshotFromJSON(db, {}); }).to.throw; }); it('fromJSON() throws with missing type data', () => { const db = newTestFirestore(); expect(() => { - QuerySnapshot.fromJSON(db, { + querySnapshotFromJSON(db, { bundleSource: 'QuerySnapshot', bundleName: 'test name', bundle: 'test bundle' @@ -515,7 +517,7 @@ describe('QuerySnapshot', () => { it('fromJSON() throws with invalid type data', () => { const db = newTestFirestore(); expect(() => { - QuerySnapshot.fromJSON(db, { + querySnapshotFromJSON(db, { type: 1, bundleSource: 'QuerySnapshot', bundleName: 'test name', @@ -527,7 +529,7 @@ describe('QuerySnapshot', () => { it('fromJSON() throws with invalid type data', () => { const db = newTestFirestore(); expect(() => { - QuerySnapshot.fromJSON(db, { + querySnapshotFromJSON(db, { type: QuerySnapshot._jsonSchemaVersion, bundleName: 'test name', bundle: 'test bundle' @@ -538,7 +540,7 @@ describe('QuerySnapshot', () => { it('fromJSON() throws with invalid bundleSource type', () => { const db = newTestFirestore(); expect(() => { - QuerySnapshot.fromJSON(db, { + querySnapshotFromJSON(db, { type: QuerySnapshot._jsonSchemaVersion, bundleSource: 1, bundleName: 'test name', @@ -550,7 +552,7 @@ describe('QuerySnapshot', () => { it('fromJSON() throws with invalid bundleSource value', () => { const db = newTestFirestore(); expect(() => { - QuerySnapshot.fromJSON(db, { + querySnapshotFromJSON(db, { type: QuerySnapshot._jsonSchemaVersion, bundleSource: 'DocumentSnapshot', bundleName: 'test name', @@ -562,7 +564,7 @@ describe('QuerySnapshot', () => { it('fromJSON() throws with missing bundleName', () => { const db = newTestFirestore(); expect(() => { - QuerySnapshot.fromJSON(db, { + querySnapshotFromJSON(db, { type: QuerySnapshot._jsonSchemaVersion, bundleSource: 'QuerySnapshot', bundle: 'test bundle' @@ -573,7 +575,7 @@ describe('QuerySnapshot', () => { it('fromJSON() throws with invalid bundleName', () => { const db = newTestFirestore(); expect(() => { - QuerySnapshot.fromJSON(db, { + querySnapshotFromJSON(db, { type: QuerySnapshot._jsonSchemaVersion, bundleSource: 'QuerySnapshot', bundleName: 1, @@ -585,7 +587,7 @@ describe('QuerySnapshot', () => { it('fromJSON() throws with missing bundle data', () => { const db = newTestFirestore(); expect(() => { - QuerySnapshot.fromJSON(db, { + querySnapshotFromJSON(db, { type: QuerySnapshot._jsonSchemaVersion, bundleSource: 'QuerySnapshot', bundleName: 'test name' @@ -596,7 +598,7 @@ describe('QuerySnapshot', () => { it('fromJSON() throws with invalid bundle data', () => { const db = newTestFirestore(); expect(() => { - QuerySnapshot.fromJSON(db, { + querySnapshotFromJSON(db, { type: QuerySnapshot._jsonSchemaVersion, bundleSource: 'QuerySnapshot', bundleName: 'test name', @@ -617,7 +619,7 @@ describe('QuerySnapshot', () => { const db = firestore(); expect(() => { - QuerySnapshot.fromJSON(db, json); + querySnapshotFromJSON(db, json); }).to.not.throw; }); @@ -632,7 +634,7 @@ describe('QuerySnapshot', () => { ).toJSON(); const db = firestore(); - const querySnap = QuerySnapshot.fromJSON(db, json); + const querySnap = querySnapshotFromJSON(db, json); expect(querySnap).to.exist; if (querySnap !== undefined) { const docs = querySnap.docs; From df70734f0791afdada8808d808e3521bab63d5d1 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Thu, 15 May 2025 08:21:19 -0400 Subject: [PATCH 25/32] Implementation. Test fails client side. Can't easily test bundles client side. They can't be generated on the client. I tried pre-generating some but the bundle must align with the Firebase project name, and the Firebase project name varies across test targets (prod, local, etc). --- packages/firestore/src/api/reference_impl.ts | 7 +- packages/firestore/src/api/snapshot.ts | 125 ++++------ .../src/platform/browser/snapshot_to_json.ts | 44 ++++ .../platform/browser_lite/snapshot_to_json.ts | 18 ++ .../src/platform/node/snapshot_to_json.ts | 96 +++++++ .../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 | 51 ++++ .../test/integration/api/database.test.ts | 235 +++++++++++------- .../firestore/test/unit/api/database.test.ts | 201 +++++++++++---- packages/firestore/test/util/helpers.ts | 58 +++++ 12 files changed, 673 insertions(+), 219 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/reference_impl.ts b/packages/firestore/src/api/reference_impl.ts index 03c61be6586..5ca0278c386 100644 --- a/packages/firestore/src/api/reference_impl.ts +++ b/packages/firestore/src/api/reference_impl.ts @@ -1005,10 +1005,7 @@ export function onSnapshotResume< let curArg = 0; let options: SnapshotListenOptions | undefined = undefined; if (typeof args[curArg] === 'object' && !isPartialObserver(args[curArg])) { - console.error('DEDB arg 0 is SnapsotLsitenOptions'); options = args[curArg++] as SnapshotListenOptions; - } else { - console.error('DEDB arg 0 is NOT SnapsotLsitenOptions'); } if (json.bundleSource === 'QuerySnapshot') { @@ -1275,6 +1272,10 @@ function onSnapshotDocumentSnapshotBundle< converter ? converter : null, DocumentKey.fromPath(json.bundleName) ); + console.error( + 'DEDB onSnapshotDocumentSnapshotBundle callong onSnapshot with docRef: ', + docReference.path.toString() + ); internalUnsubscribe = onSnapshot( docReference as DocumentReference, options ? options : {}, diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index 10e8158d7e5..50e622676ba 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -43,13 +43,13 @@ 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 +529,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 +551,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 +605,12 @@ export function documentSnapshotFromJSON< ...args: unknown[] ): 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); @@ -831,52 +831,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; } } @@ -921,6 +917,12 @@ export function querySnapshotFromJSON< ...args: unknown[] ): 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); @@ -1123,20 +1125,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..537b39f8c02 --- /dev/null +++ b/packages/firestore/src/platform/browser/snapshot_to_json.ts @@ -0,0 +1,44 @@ +/** + * @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 { Query } from '../../core/query'; +import { DocumentData } from '../../lite-api/reference'; +import { Document } from '../../model/document'; +import { Firestore } from '../../api/database'; + + +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..c078e29fa25 --- /dev/null +++ b/packages/firestore/src/platform/node/snapshot_to_json.ts @@ -0,0 +1,96 @@ +/** + * @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..9f5a90f6c12 --- /dev/null +++ b/packages/firestore/src/platform/snapshot_to_json.ts @@ -0,0 +1,51 @@ +/** + * @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 { Query } from '../core/query'; +import { DocumentData } from '../lite-api/reference'; +import { Document } from '../model/document'; +import { Firestore } from '../api/database'; + +// 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); +} \ No newline at end of file diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 15e3ab9b84b..403935f31a3 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -87,6 +87,13 @@ import { } from '../util/helpers'; import { DEFAULT_SETTINGS, DEFAULT_PROJECT_ID } from '../util/settings'; +import { + DOCUMENT_SNAPSHOT_BUNDLE_TEST_PROJECT, + QUERY_SNAPSHOT_BUNDLE_TEST_PROJECT +} from '../../util/helpers'; + +import { isNode } from '@firebase/util'; + use(chaiAsPromised); apiDescribe('Database', persistence => { @@ -1204,22 +1211,25 @@ apiDescribe('Database', persistence => { }); it('DocumentSnapshot events for snapshot created by a bundle', async () => { - const initialData = { a: 0 }; - const finalData = { a: 1 }; + 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 - ); + let json: object = DOCUMENT_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled Doc: { a: 1 }. + if (isNode()) { + // Test programmatically generated JSON if the environment supports bundle generation. + json = doc.toJSON(); + } + + const unsubscribe = onSnapshotResume(db, json, 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); }) @@ -1234,29 +1244,35 @@ apiDescribe('Database', persistence => { ); }); - it('DocumentSnapshot updated doc events in snapshot created by a bundle', async () => { - const initialData = { a: 0 }; - const finalData = { a: 1 }; + it('DocumentSnapshot updated doc events in snapshot created by a bundle accumulator', async () => { + 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 - ); + let json: object = DOCUMENT_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled Doc: { a: 1 }. + if (isNode()) { + // Test programmatically generated JSON if the environment supports bundle generation. + json = doc.toJSON(); + } + const unsubscribe = onSnapshotResume(db, json, accumulator.storeEvent); await accumulator .awaitEvent() .then(snap => { + console.error('accumulator DDB 1!'); expect(snap.exists()).to.be.true; expect(snap.data()).to.deep.equal(initialData); }) - .then(() => setDoc(docRef, finalData)) + .then(() => { + console.error('accumulator DDB 2!'); + setDoc(docRef, finalData); + }) .then(() => accumulator.awaitEvent()) .then(snap => { + console.error('accumulator DDB 3!'); expect(snap.exists()).to.be.true; expect(snap.data()).to.deep.equal(finalData); }); @@ -1266,15 +1282,20 @@ apiDescribe('Database', persistence => { }); it('DocumentSnapshot observer events for snapshot created by a bundle', async () => { - const initialData = { a: 0 }; - const finalData = { a: 1 }; + 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(), { + let json: object = DOCUMENT_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled Doc: { a: 1 }. + if (isNode()) { + // Test programmatically generated JSON if the environment supports bundle generation. + json = doc.toJSON(); + } + const unsubscribe = onSnapshotResume(db, json, { next: accumulator.storeEvent }); await accumulator @@ -1345,46 +1366,53 @@ 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 }; + 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()); + let json: object = DOCUMENT_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled Doc: { a: 1 }, same as initialData. + if (isNode()) { + // Test generated JSON if we're in an enviorment that supports bundle generation. + json = doc.toJSON(); + } + const fromJsonDoc = documentSnapshotFromJSON(db, json); const accumulator = new EventsAccumulator(); const unsubscribe = onSnapshot(fromJsonDoc.ref, accumulator.storeEvent); await accumulator @@ -1412,11 +1440,12 @@ apiDescribe('Database', persistence => { 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 - ); + let json: object = QUERY_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled docs: { a: { foo: 1 }, b: { bar: 2 } }. + if (isNode()) { + // Test programmatically generated JSON if the environment supports bundle generation. + json = querySnap.toJSON(); + } + const unsubscribe = onSnapshotResume(db, json, accumulator.storeEvent); await accumulator.awaitEvent().then(snap => { expect(snap.docs).not.to.be.null; expect(snap.docs.length).to.equal(2); @@ -1434,8 +1463,13 @@ apiDescribe('Database', persistence => { }; await withTestCollection(persistence, testDocs, async (coll, db) => { const querySnap = await getDocs(query(coll, orderBy(documentId()))); + let json: object = QUERY_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled docs: { a: { foo: 1 }, b: { bar: 2 } }. + if (isNode()) { + // Test programmatically generated JSON if the environment supports bundle generation. + json = querySnap.toJSON(); + } const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshotResume(db, querySnap.toJSON(), { + const unsubscribe = onSnapshotResume(db, json, { next: accumulator.storeEvent }); await accumulator.awaitEvent().then(snap => { @@ -1507,11 +1541,12 @@ apiDescribe('Database', persistence => { 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 - ); + let json: object = QUERY_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled docs: { a: { foo: 1 }, b: { bar: 2 } }. + if (isNode()) { + // Test programmatically generated JSON if the environment supports bundle generation. + json = querySnap.toJSON(); + } + const unsubscribe = onSnapshotResume(db, json, accumulator.storeEvent); await accumulator .awaitEvent() .then(snap => { @@ -1532,39 +1567,42 @@ 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 ', 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 () => { @@ -1574,7 +1612,12 @@ apiDescribe('Database', persistence => { }; await withTestCollection(persistence, testDocs, async (coll, db) => { const querySnap = await getDocs(query(coll, orderBy(documentId()))); - const querySnapFromJson = querySnapshotFromJSON(db, querySnap.toJSON()); + let json: object = QUERY_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled docs: { a: { foo: 1 }, b: { bar: 2 } }. + if (isNode()) { + // Test programmatically generated JSON if the environment supports bundle generation. + json = querySnap.toJSON(); + } + const querySnapFromJson = querySnapshotFromJSON(db, json); const refForDocA = querySnapFromJson.docs[0].ref; const accumulator = new EventsAccumulator(); const unsubscribe = onSnapshot( diff --git a/packages/firestore/test/unit/api/database.test.ts b/packages/firestore/test/unit/api/database.test.ts index cf64ede5b02..4e60a47bb0f 100644 --- a/packages/firestore/test/unit/api/database.test.ts +++ b/packages/firestore/test/unit/api/database.test.ts @@ -40,7 +40,13 @@ import { query, querySnapshot } from '../../util/api_helpers'; -import { keys } from '../../util/helpers'; +import { + DOCUMENT_SNAPSHOT_BUNDLE_TEST_PROJECT, + QUERY_SNAPSHOT_BUNDLE_TEST_PROJECT, + keys +} from '../../util/helpers'; + +import { isNode } from '@firebase/util'; describe('Bundle', () => { it('loadBundle does not throw with an empty bundle string)', async () => { @@ -270,27 +276,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', + /*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).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,26 +329,45 @@ 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(); + let json: object = DOCUMENT_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled Doc: { a: 1 }. + if (isNode()) { + 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 docSnap = documentSnapshot('foo/bar', { a: 1 }, /*fromCache=*/ true); + let json: object = DOCUMENT_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled Doc: { a: 1 }. + //if (isNode()) { + if (true) { + console.error('DEDB docSnap.toJSON: ', docSnap.toJSON()); + json = docSnap.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; + const docSnapFromJSON = documentSnapshotFromJSON(db, json); + 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; @@ -458,7 +499,7 @@ describe('QuerySnapshot', () => { }); it('toJSON returns a bundle', () => { - const json = querySnapshot( + const snapshotJson = querySnapshot( 'foo', {}, { a: { a: 1 } }, @@ -467,17 +508,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,31 +675,43 @@ describe('QuerySnapshot', () => { }); it('fromJSON does not throw', () => { - const json = querySnapshot( + const snapshot = querySnapshot( 'foo', {}, - { a: { a: 1 } }, + { + a: { a: 1 }, + b: { bar: 2 } + }, keys(), // An empty set of mutaded document keys signifies that there are no pending writes. false, false - ).toJSON(); - + ); + let json: object = QUERY_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled docs: { a: { foo: 1 }, b: { bar: 2 } }. + if (isNode()) { + json = snapshot.toJSON(); + } const db = firestore(); expect(() => { querySnapshotFromJSON(db, json); }).to.not.throw; }); - it('fromJSON parses produces valid snapshot data', () => { - const json = querySnapshot( + it('fromJSON produces valid snapshot data', () => { + const snapshot = querySnapshot( 'foo', {}, - { a: { a: 1 } }, + { + a: { a: 1 }, + b: { bar: 2 } + }, keys(), // An empty set of mutaded document keys signifies that there are no pending writes. false, false - ).toJSON(); - + ); + let json: object = QUERY_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled docs: { a: { foo: 1 }, b: { bar: 2 } }. + if (isNode()) { + json = snapshot.toJSON(); + } const db = firestore(); const querySnap = querySnapshotFromJSON(db, json); expect(querySnap).to.exist; @@ -641,18 +720,40 @@ describe('QuerySnapshot', () => { 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; + expect(docs.length).to.equal(2); + if (docs.length === 2) { + let docData = docs[0].data(); // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((docData as any).a).to.exist; + 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 - expect((docData as any).a).to.equal(1); - }); + 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', () => { diff --git a/packages/firestore/test/util/helpers.ts b/packages/firestore/test/util/helpers.ts index 1ddaf174b19..e9107315d59 100644 --- a/packages/firestore/test/util/helpers.ts +++ b/packages/firestore/test/util/helpers.ts @@ -131,6 +131,64 @@ import { FIRESTORE } from './api_helpers'; export type TestSnapshotVersion = number; +// Bundle creation is disabled in client enviornments, but we still would like to test bundle +// parsing, so test against some pre-generated bundles: + +// DocSnapshot for a document: { a: 1 } +export const DOCUMENT_SNAPSHOT_BUNDLE_TEST_PROJECT = { + type: 'firestore/documentSnapshot/1.0', + bundle: + '136{"metadata":{"id":"EOwNtTspbiBhdlUwGG35","createTime":"1970-01-01T00:00:00.000001000Z","version":1,"totalDocuments":1,"totalBytes":366}}149{"documentMetadata":{"name":"projects/test-project/databases/(default)/documents/foo/bar","readTime":"1970-01-01T00:00:00.000001000Z","exists":true}}211{"document":{"name":"projects/test-project/databases/(default)/documents/foo/bar","fields":{"a":{"integerValue":"1"}},"updateTime":"1970-01-01T00:00:00.000001000Z","createTime":"1970-01-01T00:00:00.000000000Z"}}', + bundleSource: 'DocumentSnapshot', + bundleName: 'foo/bar' +}; + +export const DOCUMENT_SNAPSHOT_BUNDLE = { + type: 'firestore/documentSnapshot/1.0', + bundle: + '136{"metadata":{"id":"Bgp6Hfz4727aGh0tnRg2","createTime":"2025-05-14T14:44:12.73326700' + + '0","version":1,"totalDocuments":1,"totalBytes":440}}186{"documentMetadata":{"name":"projects/' + + 'jscore-sandbox-141b5/databases/(default)/documents/test-collection/aUuEtA9KU1ur7bGiolOy","rea' + + 'dTime":"2025-05-14T14:44:12.733267000Z","exists":true}}248{"document":{"name":"projects/jscor' + + 'e-sandbox-141b5/databases/(default)/documents/test-collection/aUuEtA9KU1ur7bGiolOy","fields":' + + '{"a":{"integerValue":"1"}},"updateTime":"2025-05-14T14:44:12.733267000Z","createTime":"2025-0' + + '5-14T14:44:12.733267000Z"}}', + bundleSource: 'DocumentSnapshot', + bundleName: 'test-collection/aUuEtA9KU1ur7bGiolOy' +}; + +// QuerySnapshot for documents: { a: { foo: 1 }, b: { bar: 2 } } +/* +export const QUERY_SNAPSHOT_BUNDLE = { + type: 'firestore/querySnapshot/1.0', + bundleSource: 'QuerySnapshot', + bundleName: 'VQKk4gLMygspliy1FOoJ', + bundle: '137{"metadata":{"id":"VQKk4gLMygspliy1FOoJ","createTime":"2025-05-14T15:10:14.87945200' + + '0Z","version":1,"totalDocuments":2,"totalBytes":1219}}318{"namedQuery":{"name":"VQKk4gLMygsp' + + 'liy1FOoJ","bundledQuery":{"parent":"projects/jscore-sandbox-141b5/databases/(default)/docume' + + 'nts","structuredQuery":{"from":[{"collectionId":"aasegAu0fT6StwPkDCME"}],"orderBy":[{"field"' + + ':{"fieldPath":"__name__"},"direction":"ASCENDING"}]}},"readTime":"2025-05-14T15:10:14.879452' + + '000Z"}}207{"documentMetadata":{"name":"projects/jscore-sandbox-141b5/databases/(default)/doc' + + 'uments/aasegAu0fT6StwPkDCME/a","readTime":"2025-05-14T15:10:14.879452000Z","exists":true,"qu' + + 'eries":["VQKk4gLMygspliy1FOoJ"]}}236{"document":{"name":"projects/jscore-sandbox-141b5/datab' + + 'ases/(default)/documents/aasegAu0fT6StwPkDCME/a","fields":{"foo":{"integerValue":"1"}},"upda' + + 'teTime":"2025-05-14T15:10:14.683496000Z","createTime":"2025-05-14T15:10:14.683496000Z"}}207{' + + '"documentMetadata":{"name":"projects/jscore-sandbox-141b5/databases/(default)/documents/aase' + + 'gAu0fT6StwPkDCME/b","readTime":"2025-05-14T15:10:14.879452000Z","exists":true,"queries":["VQ' + + 'Kk4gLMygspliy1FOoJ"]}}236{"document":{"name":"projects/jscore-sandbox-141b5/databases/(defau' + + 'lt)/documents/aasegAu0fT6StwPkDCME/b","fields":{"bar":{"integerValue":"2"}},"updateTime":"20' + + '25-05-14T15:10:14.683496000Z","createTime":"2025-05-14T15:10:14.683496000Z"}}' +}; +*/ + +export const QUERY_SNAPSHOT_BUNDLE_TEST_PROJECT = { + type: 'firestore/querySnapshot/1.0', + bundleSource: 'QuerySnapshot', + bundleName: 'wwGYOhjXqZ0rRAVvSixP', + bundle: + '137{"metadata":{"id":"wwGYOhjXqZ0rRAVvSixP","createTime":"1970-01-01T00:00:00.000001000Z","version":1,"totalDocuments":2,"totalBytes":1092}}293{"namedQuery":{"name":"wwGYOhjXqZ0rRAVvSixP","bundledQuery":{"parent":"projects/test-project/databases/(default)/documents","structuredQuery":{"from":[{"collectionId":"foo"}],"orderBy":[{"field":{"fieldPath":"__name__"},"direction":"ASCENDING"}]}},"readTime":"1970-01-01T00:00:00.000001000Z"}}182{"documentMetadata":{"name":"projects/test-project/databases/(default)/documents/foo/a","readTime":"1970-01-01T00:00:00.000001000Z","exists":true,"queries":["wwGYOhjXqZ0rRAVvSixP"]}}209{"document":{"name":"projects/test-project/databases/(default)/documents/foo/a","fields":{"a":{"integerValue":"1"}},"updateTime":"1970-01-01T00:00:00.000001000Z","createTime":"1970-01-01T00:00:00.000000000Z"}}182{"documentMetadata":{"name":"projects/test-project/databases/(default)/documents/foo/b","readTime":"1970-01-01T00:00:00.000001000Z","exists":true,"queries":["wwGYOhjXqZ0rRAVvSixP"]}}211{"document":{"name":"projects/test-project/databases/(default)/documents/foo/b","fields":{"bar":{"integerValue":"2"}},"updateTime":"1970-01-01T00:00:00.000001000Z","createTime":"1970-01-01T00:00:00.000000000Z"}}' +}; + export function testUserDataReader(useProto3Json?: boolean): UserDataReader { return new UserDataReader( TEST_DATABASE_ID, From 8e1e51a143dcf31be0c53de71154a90e5d4169ee Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Thu, 15 May 2025 11:48:59 -0400 Subject: [PATCH 26/32] Remove tests on client side. Lint / format. --- packages/firestore/src/api/snapshot.ts | 1 - .../src/platform/browser/snapshot_to_json.ts | 9 +- .../src/platform/node/snapshot_to_json.ts | 24 +- .../src/platform/snapshot_to_json.ts | 27 +- .../test/integration/api/database.test.ts | 462 +++++++++--------- .../test/integration/api/query.test.ts | 71 +-- .../firestore/test/unit/api/database.test.ts | 161 +++--- packages/firestore/test/util/helpers.ts | 58 --- 8 files changed, 366 insertions(+), 447 deletions(-) diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index 50e622676ba..b109c7e1cda 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -49,7 +49,6 @@ import { } from '../platform/snapshot_to_json'; import { fromDocument } from '../remote/serializer'; import { debugAssert, fail } from '../util/assert'; - 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 diff --git a/packages/firestore/src/platform/browser/snapshot_to_json.ts b/packages/firestore/src/platform/browser/snapshot_to_json.ts index 537b39f8c02..37c1a0b556d 100644 --- a/packages/firestore/src/platform/browser/snapshot_to_json.ts +++ b/packages/firestore/src/platform/browser/snapshot_to_json.ts @@ -16,11 +16,10 @@ */ /** 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 { Firestore } from '../../api/database'; - export function buildDocumentSnapshotJsonBundle( db: Firestore, @@ -28,7 +27,7 @@ export function buildDocumentSnapshotJsonBundle( docData: DocumentData, path: string ): string { - return "NOT SUPPORTED"; + return 'NOT SUPPORTED'; } export function buildQuerySnapshotJsonBundle( @@ -39,6 +38,6 @@ export function buildQuerySnapshotJsonBundle( paths: string[], docs: Document[], documentData: DocumentData[] -) : string { - return "NOT SUPPORTED"; +): string { + return 'NOT SUPPORTED'; } diff --git a/packages/firestore/src/platform/node/snapshot_to_json.ts b/packages/firestore/src/platform/node/snapshot_to_json.ts index c078e29fa25..61987fbbc3c 100644 --- a/packages/firestore/src/platform/node/snapshot_to_json.ts +++ b/packages/firestore/src/platform/node/snapshot_to_json.ts @@ -18,9 +18,7 @@ /** 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 { DocumentData } from '../../lite-api/reference'; import { Document } from '../../model/document'; import { BundleBuilder, @@ -29,22 +27,16 @@ import { } 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() + const builder: BundleBuilder = new BundleBuilder(db, AutoId.newId()); + builder.addBundleDocument( + documentToDocumentSnapshotBundleData(path, docData, document) ); - builder.addBundleDocument(documentToDocumentSnapshotBundleData( - path, - docData, - document - )); return builder.build(); } @@ -56,15 +48,11 @@ export function buildQuerySnapshotJsonBundle( paths: string[], docs: Document[], documentData: DocumentData[] -) : string { +): string { const docBundleDataArray: DocumentSnapshotBundleData[] = []; for (let i = 0; i < docs.length; i++) { docBundleDataArray.push( - documentToDocumentSnapshotBundleData( - paths[i], - documentData[i], - docs[i] - ) + documentToDocumentSnapshotBundleData(paths[i], documentData[i], docs[i]) ); } const bundleData: QuerySnapshotBundleData = { diff --git a/packages/firestore/src/platform/snapshot_to_json.ts b/packages/firestore/src/platform/snapshot_to_json.ts index 9f5a90f6c12..1eae948eb45 100644 --- a/packages/firestore/src/platform/snapshot_to_json.ts +++ b/packages/firestore/src/platform/snapshot_to_json.ts @@ -15,16 +15,18 @@ * 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'; -import { Firestore } from '../api/database'; // 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`); +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( @@ -36,16 +38,25 @@ export function buildDocumentSnapshotJsonBundle( 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, + 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); -} \ No newline at end of file +): 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 403935f31a3..4cf22e6086e 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'; @@ -87,13 +87,6 @@ import { } from '../util/helpers'; import { DEFAULT_SETTINGS, DEFAULT_PROJECT_ID } from '../util/settings'; -import { - DOCUMENT_SNAPSHOT_BUNDLE_TEST_PROJECT, - QUERY_SNAPSHOT_BUNDLE_TEST_PROJECT -} from '../../util/helpers'; - -import { isNode } from '@firebase/util'; - use(chaiAsPromised); apiDescribe('Database', persistence => { @@ -1211,108 +1204,101 @@ apiDescribe('Database', persistence => { }); it('DocumentSnapshot events for snapshot created by a bundle', async () => { - const initialData = { a: 1 }; - const finalData = { a: 2 }; - await withTestDocAndInitialData( - persistence, - initialData, - async (docRef, db) => { - const doc = await getDoc(docRef); - const accumulator = new EventsAccumulator(); - let json: object = DOCUMENT_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled Doc: { a: 1 }. - if (isNode()) { - // Test programmatically generated JSON if the environment supports bundle generation. - json = doc.toJSON(); + 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(); } - - const unsubscribe = onSnapshotResume(db, json, 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 accumulator', async () => { - const initialData = { a: 1 }; - const finalData = { a: 2 }; - await withTestDocAndInitialData( - persistence, - initialData, - async (docRef, db) => { - const doc = await getDoc(docRef); - const accumulator = new EventsAccumulator(); - let json: object = DOCUMENT_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled Doc: { a: 1 }. - if (isNode()) { - // Test programmatically generated JSON if the environment supports bundle generation. - json = doc.toJSON(); + 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(); } - const unsubscribe = onSnapshotResume(db, json, accumulator.storeEvent); - await accumulator - .awaitEvent() - .then(snap => { - console.error('accumulator DDB 1!'); - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(initialData); - }) - .then(() => { - console.error('accumulator DDB 2!'); - setDoc(docRef, finalData); - }) - .then(() => accumulator.awaitEvent()) - .then(snap => { - console.error('accumulator DDB 3!'); - 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: 1 }; - const finalData = { a: 2 }; - await withTestDocAndInitialData( - persistence, - initialData, - async (docRef, db) => { - const doc = await getDoc(docRef); - const accumulator = new EventsAccumulator(); - let json: object = DOCUMENT_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled Doc: { a: 1 }. - if (isNode()) { - // Test programmatically generated JSON if the environment supports bundle generation. - json = doc.toJSON(); - } - const unsubscribe = onSnapshotResume(db, json, { - 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 () => { @@ -1400,86 +1386,84 @@ apiDescribe('Database', persistence => { }); it('DocumentSnapshot updated doc events in snapshot created by fromJSON doc ref', async () => { - const initialData = { a: 1 }; - const finalData = { a: 2 }; - await withTestDocAndInitialData( - persistence, - initialData, - async (docRef, db) => { - const doc = await getDoc(docRef); - let json: object = DOCUMENT_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled Doc: { a: 1 }, same as initialData. - if (isNode()) { - // Test generated JSON if we're in an enviorment that supports bundle generation. - json = doc.toJSON(); + 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(); } - const fromJsonDoc = documentSnapshotFromJSON(db, json); - 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(); - let json: object = QUERY_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled docs: { a: { foo: 1 }, b: { bar: 2 } }. - if (isNode()) { - // Test programmatically generated JSON if the environment supports bundle generation. - json = querySnap.toJSON(); - } - const unsubscribe = onSnapshotResume(db, json, 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()))); - let json: object = QUERY_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled docs: { a: { foo: 1 }, b: { bar: 2 } }. - if (isNode()) { - // Test programmatically generated JSON if the environment supports bundle generation. - json = querySnap.toJSON(); - } - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshotResume(db, json, { - 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 () => { @@ -1533,38 +1517,39 @@ 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(); - let json: object = QUERY_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled docs: { a: { foo: 1 }, b: { bar: 2 } }. - if (isNode()) { - // Test programmatically generated JSON if the environment supports bundle generation. - json = querySnap.toJSON(); - } - const unsubscribe = onSnapshotResume(db, json, 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 ', async () => { @@ -1606,42 +1591,39 @@ apiDescribe('Database', persistence => { }); 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()))); - let json: object = QUERY_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled docs: { a: { foo: 1 }, b: { bar: 2 } }. - if (isNode()) { - // Test programmatically generated JSON if the environment supports bundle generation. - json = querySnap.toJSON(); - } - const querySnapFromJson = querySnapshotFromJSON(db, json); - 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 ca0db28eb4d..776d0399963 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'; @@ -73,45 +74,49 @@ import { import { USE_EMULATOR } from '../util/settings'; 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; - } + 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!); + console.error('DEDB loading bundle with json: ', json); + 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 4e60a47bb0f..d20efcd4bc7 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 { @@ -40,13 +41,8 @@ import { query, querySnapshot } from '../../util/api_helpers'; -import { - DOCUMENT_SNAPSHOT_BUNDLE_TEST_PROJECT, - QUERY_SNAPSHOT_BUNDLE_TEST_PROJECT, - keys -} from '../../util/helpers'; +import { keys } from '../../util/helpers'; -import { isNode } from '@firebase/util'; describe('Bundle', () => { it('loadBundle does not throw with an empty bundle string)', async () => { @@ -291,7 +287,7 @@ describe('DocumentSnapshot', () => { if (!isNode()) { const snapshotJson = documentSnapshot( 'foo/bar', - /*data=*/ null, + { a: 1 }, /*fromCache=*/ true ).toJSON(); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -343,36 +339,37 @@ describe('DocumentSnapshot', () => { }); it('fromJSON parses toJSON result', () => { - const docSnap = documentSnapshot('foo/bar', { a: 1 }, /*fromCache=*/ true); - let json: object = DOCUMENT_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled Doc: { a: 1 }. if (isNode()) { - json = docSnap.toJSON(); + const docSnap = documentSnapshot( + 'foo/bar', + { a: 1 }, + /*fromCache=*/ true + ); + expect(() => { + documentSnapshotFromJSON(docSnap._firestore, docSnap.toJSON()); + }).to.not.throw; } - - expect(() => { - documentSnapshotFromJSON(docSnap._firestore, json); - }).to.not.throw; }); it('fromJSON produces valid snapshot data.', () => { - const docSnap = documentSnapshot('foo/bar', { a: 1 }, /*fromCache=*/ true); - let json: object = DOCUMENT_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled Doc: { a: 1 }. - //if (isNode()) { - if (true) { - console.error('DEDB docSnap.toJSON: ', docSnap.toJSON()); - json = docSnap.toJSON(); - } - const db = firestore(); - const docSnapFromJSON = documentSnapshotFromJSON(db, json); - 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); + 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); + } } }); }); @@ -675,64 +672,60 @@ describe('QuerySnapshot', () => { }); it('fromJSON does not throw', () => { - 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 - ); - let json: object = QUERY_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled docs: { a: { foo: 1 }, b: { bar: 2 } }. if (isNode()) { - json = snapshot.toJSON(); + 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; } - const db = firestore(); - expect(() => { - querySnapshotFromJSON(db, json); - }).to.not.throw; }); it('fromJSON produces valid snapshot data', () => { - 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 - ); - let json: object = QUERY_SNAPSHOT_BUNDLE_TEST_PROJECT; // Bundled docs: { a: { foo: 1 }, b: { bar: 2 } }. if (isNode()) { - json = snapshot.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(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); + 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); + } } } } diff --git a/packages/firestore/test/util/helpers.ts b/packages/firestore/test/util/helpers.ts index e9107315d59..1ddaf174b19 100644 --- a/packages/firestore/test/util/helpers.ts +++ b/packages/firestore/test/util/helpers.ts @@ -131,64 +131,6 @@ import { FIRESTORE } from './api_helpers'; export type TestSnapshotVersion = number; -// Bundle creation is disabled in client enviornments, but we still would like to test bundle -// parsing, so test against some pre-generated bundles: - -// DocSnapshot for a document: { a: 1 } -export const DOCUMENT_SNAPSHOT_BUNDLE_TEST_PROJECT = { - type: 'firestore/documentSnapshot/1.0', - bundle: - '136{"metadata":{"id":"EOwNtTspbiBhdlUwGG35","createTime":"1970-01-01T00:00:00.000001000Z","version":1,"totalDocuments":1,"totalBytes":366}}149{"documentMetadata":{"name":"projects/test-project/databases/(default)/documents/foo/bar","readTime":"1970-01-01T00:00:00.000001000Z","exists":true}}211{"document":{"name":"projects/test-project/databases/(default)/documents/foo/bar","fields":{"a":{"integerValue":"1"}},"updateTime":"1970-01-01T00:00:00.000001000Z","createTime":"1970-01-01T00:00:00.000000000Z"}}', - bundleSource: 'DocumentSnapshot', - bundleName: 'foo/bar' -}; - -export const DOCUMENT_SNAPSHOT_BUNDLE = { - type: 'firestore/documentSnapshot/1.0', - bundle: - '136{"metadata":{"id":"Bgp6Hfz4727aGh0tnRg2","createTime":"2025-05-14T14:44:12.73326700' + - '0","version":1,"totalDocuments":1,"totalBytes":440}}186{"documentMetadata":{"name":"projects/' + - 'jscore-sandbox-141b5/databases/(default)/documents/test-collection/aUuEtA9KU1ur7bGiolOy","rea' + - 'dTime":"2025-05-14T14:44:12.733267000Z","exists":true}}248{"document":{"name":"projects/jscor' + - 'e-sandbox-141b5/databases/(default)/documents/test-collection/aUuEtA9KU1ur7bGiolOy","fields":' + - '{"a":{"integerValue":"1"}},"updateTime":"2025-05-14T14:44:12.733267000Z","createTime":"2025-0' + - '5-14T14:44:12.733267000Z"}}', - bundleSource: 'DocumentSnapshot', - bundleName: 'test-collection/aUuEtA9KU1ur7bGiolOy' -}; - -// QuerySnapshot for documents: { a: { foo: 1 }, b: { bar: 2 } } -/* -export const QUERY_SNAPSHOT_BUNDLE = { - type: 'firestore/querySnapshot/1.0', - bundleSource: 'QuerySnapshot', - bundleName: 'VQKk4gLMygspliy1FOoJ', - bundle: '137{"metadata":{"id":"VQKk4gLMygspliy1FOoJ","createTime":"2025-05-14T15:10:14.87945200' + - '0Z","version":1,"totalDocuments":2,"totalBytes":1219}}318{"namedQuery":{"name":"VQKk4gLMygsp' + - 'liy1FOoJ","bundledQuery":{"parent":"projects/jscore-sandbox-141b5/databases/(default)/docume' + - 'nts","structuredQuery":{"from":[{"collectionId":"aasegAu0fT6StwPkDCME"}],"orderBy":[{"field"' + - ':{"fieldPath":"__name__"},"direction":"ASCENDING"}]}},"readTime":"2025-05-14T15:10:14.879452' + - '000Z"}}207{"documentMetadata":{"name":"projects/jscore-sandbox-141b5/databases/(default)/doc' + - 'uments/aasegAu0fT6StwPkDCME/a","readTime":"2025-05-14T15:10:14.879452000Z","exists":true,"qu' + - 'eries":["VQKk4gLMygspliy1FOoJ"]}}236{"document":{"name":"projects/jscore-sandbox-141b5/datab' + - 'ases/(default)/documents/aasegAu0fT6StwPkDCME/a","fields":{"foo":{"integerValue":"1"}},"upda' + - 'teTime":"2025-05-14T15:10:14.683496000Z","createTime":"2025-05-14T15:10:14.683496000Z"}}207{' + - '"documentMetadata":{"name":"projects/jscore-sandbox-141b5/databases/(default)/documents/aase' + - 'gAu0fT6StwPkDCME/b","readTime":"2025-05-14T15:10:14.879452000Z","exists":true,"queries":["VQ' + - 'Kk4gLMygspliy1FOoJ"]}}236{"document":{"name":"projects/jscore-sandbox-141b5/databases/(defau' + - 'lt)/documents/aasegAu0fT6StwPkDCME/b","fields":{"bar":{"integerValue":"2"}},"updateTime":"20' + - '25-05-14T15:10:14.683496000Z","createTime":"2025-05-14T15:10:14.683496000Z"}}' -}; -*/ - -export const QUERY_SNAPSHOT_BUNDLE_TEST_PROJECT = { - type: 'firestore/querySnapshot/1.0', - bundleSource: 'QuerySnapshot', - bundleName: 'wwGYOhjXqZ0rRAVvSixP', - bundle: - '137{"metadata":{"id":"wwGYOhjXqZ0rRAVvSixP","createTime":"1970-01-01T00:00:00.000001000Z","version":1,"totalDocuments":2,"totalBytes":1092}}293{"namedQuery":{"name":"wwGYOhjXqZ0rRAVvSixP","bundledQuery":{"parent":"projects/test-project/databases/(default)/documents","structuredQuery":{"from":[{"collectionId":"foo"}],"orderBy":[{"field":{"fieldPath":"__name__"},"direction":"ASCENDING"}]}},"readTime":"1970-01-01T00:00:00.000001000Z"}}182{"documentMetadata":{"name":"projects/test-project/databases/(default)/documents/foo/a","readTime":"1970-01-01T00:00:00.000001000Z","exists":true,"queries":["wwGYOhjXqZ0rRAVvSixP"]}}209{"document":{"name":"projects/test-project/databases/(default)/documents/foo/a","fields":{"a":{"integerValue":"1"}},"updateTime":"1970-01-01T00:00:00.000001000Z","createTime":"1970-01-01T00:00:00.000000000Z"}}182{"documentMetadata":{"name":"projects/test-project/databases/(default)/documents/foo/b","readTime":"1970-01-01T00:00:00.000001000Z","exists":true,"queries":["wwGYOhjXqZ0rRAVvSixP"]}}211{"document":{"name":"projects/test-project/databases/(default)/documents/foo/b","fields":{"bar":{"integerValue":"2"}},"updateTime":"1970-01-01T00:00:00.000001000Z","createTime":"1970-01-01T00:00:00.000000000Z"}}' -}; - export function testUserDataReader(useProto3Json?: boolean): UserDataReader { return new UserDataReader( TEST_DATABASE_ID, From 8359b95812460c4d73e7a3371bd078a84b361801 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Thu, 15 May 2025 13:35:41 -0400 Subject: [PATCH 27/32] converter feedback --- packages/firestore/src/api/reference_impl.ts | 3 --- packages/firestore/src/api/snapshot.ts | 20 ++++--------------- packages/firestore/src/lite-api/reference.ts | 14 ++----------- .../src/util/bundle_reader_sync_impl.ts | 5 ++++- 4 files changed, 10 insertions(+), 32 deletions(-) diff --git a/packages/firestore/src/api/reference_impl.ts b/packages/firestore/src/api/reference_impl.ts index 03c61be6586..832d80c041a 100644 --- a/packages/firestore/src/api/reference_impl.ts +++ b/packages/firestore/src/api/reference_impl.ts @@ -1005,10 +1005,7 @@ export function onSnapshotResume< let curArg = 0; let options: SnapshotListenOptions | undefined = undefined; if (typeof args[curArg] === 'object' && !isPartialObserver(args[curArg])) { - console.error('DEDB arg 0 is SnapsotLsitenOptions'); options = args[curArg++] as SnapshotListenOptions; - } else { - console.error('DEDB arg 0 is NOT SnapsotLsitenOptions'); } if (json.bundleSource === 'QuerySnapshot') { diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index 10e8158d7e5..b451aabfce2 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -608,7 +608,7 @@ export function documentSnapshotFromJSON< >( db: Firestore, json: object, - ...args: unknown[] + converter?: FirestoreDataConverter ): DocumentSnapshot { if (validateJSON(json, DocumentSnapshot._jsonSchema)) { // Parse the bundle data. @@ -638,12 +638,6 @@ export function documentSnapshotFromJSON< ResourcePath.fromString(json.bundleName) ); - let converter: FirestoreDataConverter | null = - null; - if (args[0]) { - converter = args[0] as FirestoreDataConverter; - } - // Return the external facing DocumentSnapshot. return new DocumentSnapshot( db, @@ -654,7 +648,7 @@ export function documentSnapshotFromJSON< /* hasPendingWrites= */ false, /* fromCache= */ false ), - converter + converter ? converter : null ); } throw new FirestoreError( @@ -918,7 +912,7 @@ export function querySnapshotFromJSON< >( db: Firestore, json: object, - ...args: unknown[] + converter?: FirestoreDataConverter ): QuerySnapshot { if (validateJSON(json, QuerySnapshot._jsonSchema)) { // Parse the bundle data. @@ -959,16 +953,10 @@ export function querySnapshotFromJSON< /* hasCachedResults= */ false ); - let converter: FirestoreDataConverter | null = - null; - if (args[0]) { - converter = args[0] as FirestoreDataConverter; - } - // Create an external Query object, required to construct the QuerySnapshot. const externalQuery = new Query( db, - converter, + converter ? converter : null, query ); diff --git a/packages/firestore/src/lite-api/reference.ts b/packages/firestore/src/lite-api/reference.ts index c42a3a7e993..6b95e28eb69 100644 --- a/packages/firestore/src/lite-api/reference.ts +++ b/packages/firestore/src/lite-api/reference.ts @@ -334,22 +334,12 @@ export class DocumentReference< >( firestore: Firestore, json: object, - ...args: unknown[] + converter?: FirestoreDataConverter ): DocumentReference { if (validateJSON(json, DocumentReference._jsonSchema)) { - let converter: FirestoreDataConverter< - NewAppModelType, - NewDbModelType - > | null = null; - if (args[0]) { - converter = args[0] as FirestoreDataConverter< - NewAppModelType, - NewDbModelType - >; - } return new DocumentReference( firestore, - converter, + converter ? converter : null, new DocumentKey(ResourcePath.fromString(json.referencePath)) ); } diff --git a/packages/firestore/src/util/bundle_reader_sync_impl.ts b/packages/firestore/src/util/bundle_reader_sync_impl.ts index 29f9272c846..09d9fa0bfab 100644 --- a/packages/firestore/src/util/bundle_reader_sync_impl.ts +++ b/packages/firestore/src/util/bundle_reader_sync_impl.ts @@ -17,6 +17,7 @@ import { BundleMetadata } from '../protos/firestore_bundle_proto'; import { JsonProtoSerializer } from '../remote/serializer'; +import { Code, FirestoreError } from '../util/error' import { BundleReaderSync, SizedBundleElement } from './bundle_reader'; @@ -84,7 +85,9 @@ export class BundleReaderSyncImpl implements BundleReaderSync { */ private readJsonString(length: number): string { if (this.cursor + length > this.bundleData.length) { - throw new Error('Reached the end of bundle when more is expected.'); + 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; From 2fb5df3be69be1a611b7fb11aa76326b13527b5a Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Thu, 15 May 2025 16:29:42 -0400 Subject: [PATCH 28/32] Test for specific exception text. --- .../firestore/src/util/json_validation.ts | 7 +-- .../firestore/test/unit/api/database.test.ts | 46 +++++++++---------- 2 files changed, 27 insertions(+), 26 deletions(-) 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/unit/api/database.test.ts b/packages/firestore/test/unit/api/database.test.ts index cf64ede5b02..9fe02663d6f 100644 --- a/packages/firestore/test/unit/api/database.test.ts +++ b/packages/firestore/test/unit/api/database.test.ts @@ -97,7 +97,7 @@ describe('DocumentReference', () => { const db = newTestFirestore(); expect(() => { DocumentReference.fromJSON(db, {}); - }).to.throw; + }).to.throw("JSON missing required field: 'type'"); }); it('fromJSON() throws with missing type data', () => { @@ -108,7 +108,7 @@ describe('DocumentReference', () => { bundleName: 'test name', bundle: 'test bundle' }); - }).to.throw; + }).to.throw("JSON missing required field: 'type'"); }); it('fromJSON() throws with invalid type data', () => { @@ -120,7 +120,7 @@ describe('DocumentReference', () => { bundleName: 'test name', bundle: 'test bundle' }); - }).to.throw; + }).to.throw("JSON field 'type' must be a string"); }); it('fromJSON() throws with missing bundleSource', () => { @@ -131,7 +131,7 @@ describe('DocumentReference', () => { bundleName: 'test name', bundle: 'test bundle' }); - }).to.throw; + }).to.throw("JSON missing required field: 'bundleSource'"); }); it('fromJSON() throws with invalid bundleSource type', () => { @@ -143,7 +143,7 @@ describe('DocumentReference', () => { bundleName: 'test name', bundle: 'test bundle' }); - }).to.throw; + }).to.throw("JSON field 'bundleSource' must be a string"); }); it('fromJSON() throws with invalid bundleSource value', () => { @@ -155,7 +155,7 @@ describe('DocumentReference', () => { bundleName: 'test name', bundle: 'test bundle' }); - }).to.throw; + }).to.throw("Expected 'bundleSource' field to equal 'DocumentSnapshot'"); }); it('fromJSON() throws with missing bundleName', () => { @@ -166,7 +166,7 @@ describe('DocumentReference', () => { bundleSource: 'DocumentSnapshot', bundle: 'test bundle' }); - }).to.throw; + }).to.throw("JSON missing required field: 'bundleName'"); }); it('fromJSON() throws with invalid bundleName', () => { @@ -178,7 +178,7 @@ describe('DocumentReference', () => { bundleName: 1, bundle: 'test bundle' }); - }).to.throw; + }).to.throw("JSON field 'bundleName' must be a string"); }); it('fromJSON() throws with missing bundle', () => { @@ -189,7 +189,7 @@ describe('DocumentReference', () => { bundleSource: 'DocumentSnapshot', bundleName: 'test name' }); - }).to.throw; + }).to.throw("JSON missing required field: 'bundle'"); }); it('fromJSON() throws with invalid bundle', () => { @@ -201,7 +201,7 @@ describe('DocumentReference', () => { bundleName: 'test name', bundle: 1 }); - }).to.throw; + }).to.throw("JSON field 'bundle' must be a string"); }); it('fromJSON() does not throw', () => { @@ -500,7 +500,7 @@ describe('QuerySnapshot', () => { const db = newTestFirestore(); expect(() => { querySnapshotFromJSON(db, {}); - }).to.throw; + }).to.throw("JSON missing required field: 'type'"); }); it('fromJSON() throws with missing type data', () => { @@ -511,7 +511,7 @@ describe('QuerySnapshot', () => { bundleName: 'test name', bundle: 'test bundle' }); - }).to.throw; + }).to.throw("JSON missing required field: 'type'"); }); it('fromJSON() throws with invalid type data', () => { @@ -523,10 +523,10 @@ describe('QuerySnapshot', () => { bundleName: 'test name', bundle: 'test bundle' }); - }).to.throw; + }).to.throw("JSON field 'type' must be a string"); }); - it('fromJSON() throws with invalid type data', () => { + it('fromJSON() throws with missing bundle source data', () => { const db = newTestFirestore(); expect(() => { querySnapshotFromJSON(db, { @@ -534,7 +534,7 @@ describe('QuerySnapshot', () => { bundleName: 'test name', bundle: 'test bundle' }); - }).to.throw; + }).to.throw("JSON missing required field: 'bundleSource'"); }); it('fromJSON() throws with invalid bundleSource type', () => { @@ -546,7 +546,7 @@ describe('QuerySnapshot', () => { bundleName: 'test name', bundle: 'test bundle' }); - }).to.throw; + }).to.throw("JSON field 'bundleSource' must be a string"); }); it('fromJSON() throws with invalid bundleSource value', () => { @@ -558,7 +558,7 @@ describe('QuerySnapshot', () => { bundleName: 'test name', bundle: 'test bundle' }); - }).to.throw; + }).to.throw("Expected 'bundleSource' field to equal 'QuerySnapshot'"); }); it('fromJSON() throws with missing bundleName', () => { @@ -569,7 +569,7 @@ describe('QuerySnapshot', () => { bundleSource: 'QuerySnapshot', bundle: 'test bundle' }); - }).to.throw; + }).to.throw("JSON missing required field: 'bundleName'"); }); it('fromJSON() throws with invalid bundleName', () => { @@ -581,10 +581,10 @@ describe('QuerySnapshot', () => { bundleName: 1, bundle: 'test bundle' }); - }).to.throw; + }).to.throw("JSON field 'bundleName' must be a string"); }); - it('fromJSON() throws with missing bundle data', () => { + it('fromJSON() throws with missing bundle field', () => { const db = newTestFirestore(); expect(() => { querySnapshotFromJSON(db, { @@ -592,10 +592,10 @@ describe('QuerySnapshot', () => { bundleSource: 'QuerySnapshot', bundleName: 'test name' }); - }).to.throw; + }).to.throw("JSON missing required field: 'bundle'"); }); - it('fromJSON() throws with invalid bundle data', () => { + it('fromJSON() throws with invalid bundle field', () => { const db = newTestFirestore(); expect(() => { querySnapshotFromJSON(db, { @@ -604,7 +604,7 @@ describe('QuerySnapshot', () => { bundleName: 'test name', bundle: 1 }); - }).to.throw; + }).to.throw("JSON field 'bundle' must be a string"); }); it('fromJSON does not throw', () => { From 04620bc8b0b8877791f91526e984486a6a8ba879 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Thu, 15 May 2025 16:29:50 -0400 Subject: [PATCH 29/32] format --- packages/firestore/src/util/bundle_reader_sync_impl.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/firestore/src/util/bundle_reader_sync_impl.ts b/packages/firestore/src/util/bundle_reader_sync_impl.ts index 09d9fa0bfab..9379bb5a5a7 100644 --- a/packages/firestore/src/util/bundle_reader_sync_impl.ts +++ b/packages/firestore/src/util/bundle_reader_sync_impl.ts @@ -17,7 +17,7 @@ import { BundleMetadata } from '../protos/firestore_bundle_proto'; import { JsonProtoSerializer } from '../remote/serializer'; -import { Code, FirestoreError } from '../util/error' +import { Code, FirestoreError } from '../util/error'; import { BundleReaderSync, SizedBundleElement } from './bundle_reader'; @@ -87,7 +87,8 @@ export class BundleReaderSyncImpl implements BundleReaderSync { if (this.cursor + length > this.bundleData.length) { throw new FirestoreError( Code.INTERNAL, - 'Reached the end of bundle when more is expected.'); + 'Reached the end of bundle when more is expected.' + ); } const result = this.bundleData.slice(this.cursor, (this.cursor += length)); return result; From 3c91ad407388db013cffa90eb680daea294a418a Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Thu, 15 May 2025 16:51:57 -0400 Subject: [PATCH 30/32] format --- packages/firestore/test/integration/api/database.test.ts | 2 +- packages/firestore/test/integration/api/query.test.ts | 1 - packages/firestore/test/unit/api/database.test.ts | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index 4cf22e6086e..22ff0c1b993 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 , isNode } from '@firebase/util'; +import { Deferred, isNode } from '@firebase/util'; import { expect, use } from 'chai'; import chaiAsPromised from 'chai-as-promised'; diff --git a/packages/firestore/test/integration/api/query.test.ts b/packages/firestore/test/integration/api/query.test.ts index 776d0399963..6c0919e0b10 100644 --- a/packages/firestore/test/integration/api/query.test.ts +++ b/packages/firestore/test/integration/api/query.test.ts @@ -74,7 +74,6 @@ import { import { USE_EMULATOR } from '../util/settings'; import { captureExistenceFilterMismatches } from '../util/testing_hooks_util'; - apiDescribe('Queries', persistence => { addEqualityMatcher(); diff --git a/packages/firestore/test/unit/api/database.test.ts b/packages/firestore/test/unit/api/database.test.ts index 196641d03a5..783519832cc 100644 --- a/packages/firestore/test/unit/api/database.test.ts +++ b/packages/firestore/test/unit/api/database.test.ts @@ -43,7 +43,6 @@ 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(); From 618e3c26afbac801bd1a97cb67c7a5388849af24 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Fri, 16 May 2025 13:28:19 -0400 Subject: [PATCH 31/32] format --- .../test/integration/api/database.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index a78bbbe0c1d..b63c03a4f62 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -1205,7 +1205,7 @@ apiDescribe('Database', persistence => { onSnapshot(docA, () => deferred2.resolve()); }); }); - return Promise.all([deferred1.promise, deferred2.promise]).then(() => { }); + return Promise.all([deferred1.promise, deferred2.promise]).then(() => {}); }); }); @@ -1669,7 +1669,7 @@ apiDescribe('Database', persistence => { const queryForRejection = collection(db, 'a/__badpath__/b'); onSnapshot( queryForRejection, - () => { }, + () => {}, (err: Error) => { expect(err.name).to.exist; expect(err.message).to.exist; @@ -1686,13 +1686,13 @@ apiDescribe('Database', persistence => { const queryForRejection = collection(db, 'a/__badpath__/b'); onSnapshot( queryForRejection, - () => { }, + () => {}, (err: Error) => { expect(err.name).to.exist; expect(err.message).to.exist; onSnapshot( queryForRejection, - () => { }, + () => {}, (err2: Error) => { expect(err2.name).to.exist; expect(err2.message).to.exist; @@ -2107,7 +2107,7 @@ apiDescribe('Database', persistence => { it('can query after firestore restart', async () => { return withTestDoc(persistence, async (docRef, firestore) => { const deferred: Deferred = new Deferred(); - const unsubscribe = onSnapshot(docRef, snapshot => { }, deferred.resolve); + const unsubscribe = onSnapshot(docRef, snapshot => {}, deferred.resolve); await firestore._restart(); @@ -2127,7 +2127,7 @@ apiDescribe('Database', persistence => { it('query listener throws error on termination', async () => { return withTestDoc(persistence, async (docRef, firestore) => { const deferred: Deferred = new Deferred(); - const unsubscribe = onSnapshot(docRef, snapshot => { }, deferred.resolve); + const unsubscribe = onSnapshot(docRef, snapshot => {}, deferred.resolve); await terminate(firestore); @@ -2174,7 +2174,7 @@ apiDescribe('Database', persistence => { readonly title: string, readonly author: string, readonly ref: DocumentReference | null = null - ) { } + ) {} byline(): string { return this.title + ', by ' + this.author; } @@ -2304,8 +2304,8 @@ apiDescribe('Database', persistence => { batch.set(ref, { title: 'olive' }, { merge: true }) ).to.throw( 'Function WriteBatch.set() called with invalid ' + - 'data (via `toFirestore()`). Unsupported field value: undefined ' + - '(found in field author in document posts/some-post)' + 'data (via `toFirestore()`). Unsupported field value: undefined ' + + '(found in field author in document posts/some-post)' ); }); }); From 158b491c1bd13be76bb933fb97a0654b33d019e1 Mon Sep 17 00:00:00 2001 From: DellaBitta Date: Fri, 16 May 2025 13:33:03 -0400 Subject: [PATCH 32/32] merged console logging --- packages/firestore/src/api/reference_impl.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/firestore/src/api/reference_impl.ts b/packages/firestore/src/api/reference_impl.ts index 5ca0278c386..832d80c041a 100644 --- a/packages/firestore/src/api/reference_impl.ts +++ b/packages/firestore/src/api/reference_impl.ts @@ -1272,10 +1272,6 @@ function onSnapshotDocumentSnapshotBundle< converter ? converter : null, DocumentKey.fromPath(json.bundleName) ); - console.error( - 'DEDB onSnapshotDocumentSnapshotBundle callong onSnapshot with docRef: ', - docReference.path.toString() - ); internalUnsubscribe = onSnapshot( docReference as DocumentReference, options ? options : {},