diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index b451aabfce2..61e8c89a479 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -43,13 +43,12 @@ import { DocumentKey } from '../model/document_key'; import { DocumentSet } from '../model/document_set'; import { ResourcePath } from '../model/path'; import { newSerializer } from '../platform/serializer'; +import { + buildQuerySnapshotJsonBundle, + buildDocumentSnapshotJsonBundle +} from '../platform/snapshot_to_json'; import { fromDocument } from '../remote/serializer'; import { debugAssert, fail } from '../util/assert'; -import { - BundleBuilder, - DocumentSnapshotBundleData, - QuerySnapshotBundleData -} from '../util/bundle_builder_impl'; import { Code, FirestoreError } from '../util/error'; // API extractor fails importing 'property' unless we also explicitly import 'Property'. // eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports-ts @@ -529,6 +528,13 @@ export class DocumentSnapshot< * @returns a JSON representation of this object. */ toJSON(): object { + if (this.metadata.hasPendingWrites) { + throw new FirestoreError( + Code.FAILED_PRECONDITION, + 'DocumentSnapshot.toJSON() attempted to serialize a document with pending writes. ' + + 'Await waitForPendingWrites() before invoking toJSON().' + ); + } const document = this._document; // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: any = {}; @@ -544,29 +550,16 @@ export class DocumentSnapshot< ) { return result; } - const builder: BundleBuilder = new BundleBuilder( - this._firestore, - AutoId.newId() - ); const documentData = this._userDataWriter.convertObjectMap( document.data.value.mapValue.fields, 'previous' ); - if (this.metadata.hasPendingWrites) { - throw new FirestoreError( - Code.FAILED_PRECONDITION, - 'DocumentSnapshot.toJSON() attempted to serialize a document with pending writes. ' + - 'Await waitForPendingWrites() before invoking toJSON().' - ); - } - builder.addBundleDocument( - documentToDocumentSnapshotBundleData( - this.ref.path, - documentData, - document - ) + result['bundle'] = buildDocumentSnapshotJsonBundle( + this._firestore, + document, + documentData, + this.ref.path ); - result['bundle'] = builder.build(); return result; } } @@ -611,6 +604,12 @@ export function documentSnapshotFromJSON< converter?: FirestoreDataConverter ): DocumentSnapshot { if (validateJSON(json, DocumentSnapshot._jsonSchema)) { + if (json.bundle === 'NOT SUPPORTED') { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + 'The provided JSON object was created in a client environment, which is not supported.' + ); + } // Parse the bundle data. const serializer = newSerializer(db._databaseId); const bundleReader = createBundleReaderSync(json.bundle, serializer); @@ -825,52 +824,48 @@ export class QuerySnapshot< * @returns a JSON representation of this object. */ toJSON(): object { + if (this.metadata.hasPendingWrites) { + throw new FirestoreError( + Code.FAILED_PRECONDITION, + 'QuerySnapshot.toJSON() attempted to serialize a document with pending writes. ' + + 'Await waitForPendingWrites() before invoking toJSON().' + ); + } // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: any = {}; result['type'] = QuerySnapshot._jsonSchemaVersion; result['bundleSource'] = 'QuerySnapshot'; result['bundleName'] = AutoId.newId(); - const builder: BundleBuilder = new BundleBuilder( - this._firestore, - result['bundleName'] - ); const databaseId = this._firestore._databaseId.database; const projectId = this._firestore._databaseId.projectId; const parent = `projects/${projectId}/databases/${databaseId}/documents`; - const docBundleDataArray: DocumentSnapshotBundleData[] = []; - const docArray = this.docs; - docArray.forEach(doc => { + const documents: Document[] = []; + const documentData: DocumentData[] = []; + const paths: string[] = []; + + this.docs.forEach(doc => { if (doc._document === null) { return; } - const documentData = this._userDataWriter.convertObjectMap( - doc._document.data.value.mapValue.fields, - 'previous' - ); - if (this.metadata.hasPendingWrites) { - throw new FirestoreError( - Code.FAILED_PRECONDITION, - 'QuerySnapshot.toJSON() attempted to serialize a document with pending writes. ' + - 'Await waitForPendingWrites() before invoking toJSON().' - ); - } - docBundleDataArray.push( - documentToDocumentSnapshotBundleData( - doc.ref.path, - documentData, - doc._document + documents.push(doc._document); + documentData.push( + this._userDataWriter.convertObjectMap( + doc._document.data.value.mapValue.fields, + 'previous' ) ); + paths.push(doc.ref.path); }); - const bundleData: QuerySnapshotBundleData = { - name: result['bundleName'], - query: this.query._query, + result['bundle'] = buildQuerySnapshotJsonBundle( + this._firestore, + this.query._query, + result['bundleName'], parent, - docBundleDataArray - }; - builder.addBundleQuery(bundleData); - result['bundle'] = builder.build(); + paths, + documents, + documentData + ); return result; } } @@ -915,6 +910,12 @@ export function querySnapshotFromJSON< converter?: FirestoreDataConverter ): QuerySnapshot { if (validateJSON(json, QuerySnapshot._jsonSchema)) { + if (json.bundle === 'NOT SUPPORTED') { + throw new FirestoreError( + Code.INVALID_ARGUMENT, + 'The provided JSON object was created in a client environment, which is not supported.' + ); + } // Parse the bundle data. const serializer = newSerializer(db._databaseId); const bundleReader = createBundleReaderSync(json.bundle, serializer); @@ -1111,20 +1112,3 @@ export function snapshotEqual( return false; } - -// Formats Document data for bundling a DocumentSnapshot. -function documentToDocumentSnapshotBundleData( - path: string, - documentData: DocumentData, - document: Document -): DocumentSnapshotBundleData { - return { - documentData, - documentKey: document.mutableCopy().key, - documentPath: path, - documentExists: true, - createdTime: document.createTime.toTimestamp(), - readTime: document.readTime.toTimestamp(), - versionTime: document.version.toTimestamp() - }; -} diff --git a/packages/firestore/src/platform/browser/snapshot_to_json.ts b/packages/firestore/src/platform/browser/snapshot_to_json.ts new file mode 100644 index 00000000000..37c1a0b556d --- /dev/null +++ b/packages/firestore/src/platform/browser/snapshot_to_json.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** Return the Platform-specific build JSON bundle implementations. */ +import { Firestore } from '../../api/database'; +import { Query } from '../../core/query'; +import { DocumentData } from '../../lite-api/reference'; +import { Document } from '../../model/document'; + +export function buildDocumentSnapshotJsonBundle( + db: Firestore, + document: Document, + docData: DocumentData, + path: string +): string { + return 'NOT SUPPORTED'; +} + +export function buildQuerySnapshotJsonBundle( + db: Firestore, + query: Query, + bundleName: string, + parent: string, + paths: string[], + docs: Document[], + documentData: DocumentData[] +): string { + return 'NOT SUPPORTED'; +} diff --git a/packages/firestore/src/platform/browser_lite/snapshot_to_json.ts b/packages/firestore/src/platform/browser_lite/snapshot_to_json.ts new file mode 100644 index 00000000000..4012dc496b2 --- /dev/null +++ b/packages/firestore/src/platform/browser_lite/snapshot_to_json.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from '../browser/snapshot_to_json'; diff --git a/packages/firestore/src/platform/node/snapshot_to_json.ts b/packages/firestore/src/platform/node/snapshot_to_json.ts new file mode 100644 index 00000000000..61987fbbc3c --- /dev/null +++ b/packages/firestore/src/platform/node/snapshot_to_json.ts @@ -0,0 +1,84 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** Return the Platform-specific build JSON bundle implementations. */ +import { Firestore } from '../../api/database'; +import { Query } from '../../core/query'; +import { DocumentData } from '../../lite-api/reference'; +import { Document } from '../../model/document'; +import { + BundleBuilder, + DocumentSnapshotBundleData, + QuerySnapshotBundleData +} from '../../util/bundle_builder_impl'; +import { AutoId } from '../../util/misc'; + +export function buildDocumentSnapshotJsonBundle( + db: Firestore, + document: Document, + docData: DocumentData, + path: string +): string { + const builder: BundleBuilder = new BundleBuilder(db, AutoId.newId()); + builder.addBundleDocument( + documentToDocumentSnapshotBundleData(path, docData, document) + ); + return builder.build(); +} + +export function buildQuerySnapshotJsonBundle( + db: Firestore, + query: Query, + bundleName: string, + parent: string, + paths: string[], + docs: Document[], + documentData: DocumentData[] +): string { + const docBundleDataArray: DocumentSnapshotBundleData[] = []; + for (let i = 0; i < docs.length; i++) { + docBundleDataArray.push( + documentToDocumentSnapshotBundleData(paths[i], documentData[i], docs[i]) + ); + } + const bundleData: QuerySnapshotBundleData = { + name: bundleName, + query, + parent, + docBundleDataArray + }; + const builder: BundleBuilder = new BundleBuilder(db, bundleName); + builder.addBundleQuery(bundleData); + return builder.build(); +} + +// Formats Document data for bundling a DocumentSnapshot. +function documentToDocumentSnapshotBundleData( + path: string, + documentData: DocumentData, + document: Document +): DocumentSnapshotBundleData { + return { + documentData, + documentKey: document.mutableCopy().key, + documentPath: path, + documentExists: true, + createdTime: document.createTime.toTimestamp(), + readTime: document.readTime.toTimestamp(), + versionTime: document.version.toTimestamp() + }; +} diff --git a/packages/firestore/src/platform/node_lite/snapshot_to_json.ts b/packages/firestore/src/platform/node_lite/snapshot_to_json.ts new file mode 100644 index 00000000000..ba6bbb8424b --- /dev/null +++ b/packages/firestore/src/platform/node_lite/snapshot_to_json.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from '../node/snapshot_to_json'; diff --git a/packages/firestore/src/platform/rn/snapshot_to_json.ts b/packages/firestore/src/platform/rn/snapshot_to_json.ts new file mode 100644 index 00000000000..551f586d20e --- /dev/null +++ b/packages/firestore/src/platform/rn/snapshot_to_json.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + buildDocumentSnapshotJsonBundle, + buildQuerySnapshotJsonBundle +} from '../browser/snapshot_to_json'; diff --git a/packages/firestore/src/platform/rn_lite/snapshot_to_json.ts b/packages/firestore/src/platform/rn_lite/snapshot_to_json.ts new file mode 100644 index 00000000000..709509c8a4e --- /dev/null +++ b/packages/firestore/src/platform/rn_lite/snapshot_to_json.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { toByteStreamReader } from '../browser/byte_stream_reader'; diff --git a/packages/firestore/src/platform/snapshot_to_json.ts b/packages/firestore/src/platform/snapshot_to_json.ts new file mode 100644 index 00000000000..1eae948eb45 --- /dev/null +++ b/packages/firestore/src/platform/snapshot_to_json.ts @@ -0,0 +1,62 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Firestore } from '../api/database'; +import { Query } from '../core/query'; +import { DocumentData } from '../lite-api/reference'; +import { Document } from '../model/document'; + +// This file is only used under ts-node. +// eslint-disable-next-line @typescript-eslint/no-require-imports +const platform = require(`./${ + process.env.TEST_PLATFORM ?? 'node' +}/snapshot_to_json`); + +/** + * Constructs the bundle data for a DocumentSnapshot used in its toJSON serialization. + */ +export function buildDocumentSnapshotJsonBundle( + db: Firestore, + document: Document, + docData: DocumentData, + path: string +): string { + return platform.buildDocumentSnapshotJsonBundle(db, document, docData, path); +} + +/** + * Constructs the bundle data for a QuerySnapshot used in its toJSON serialization. + */ +export function buildQuerySnapshotJsonBundle( + db: Firestore, + query: Query, + bundleName: string, + parent: string, + paths: string[], + docs: Document[], + documentData: DocumentData[] +): string { + return platform.buildQuerySnapshotJsonBundle( + db, + query, + bundleName, + parent, + paths, + docs, + documentData + ); +} diff --git a/packages/firestore/test/integration/api/database.test.ts b/packages/firestore/test/integration/api/database.test.ts index f5212f3ea4b..b63c03a4f62 100644 --- a/packages/firestore/test/integration/api/database.test.ts +++ b/packages/firestore/test/integration/api/database.test.ts @@ -16,7 +16,7 @@ */ import { deleteApp } from '@firebase/app'; -import { Deferred } from '@firebase/util'; +import { Deferred, isNode } from '@firebase/util'; import { expect, use } from 'chai'; import chaiAsPromised from 'chai-as-promised'; @@ -1210,94 +1210,101 @@ apiDescribe('Database', persistence => { }); it('DocumentSnapshot events for snapshot created by a bundle', async () => { - const initialData = { a: 0 }; - const finalData = { a: 1 }; - await withTestDocAndInitialData( - persistence, - initialData, - async (docRef, db) => { - const doc = await getDoc(docRef); - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshotResume( - db, - doc.toJSON(), - accumulator.storeEvent - ); - await accumulator - .awaitEvent() - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(initialData); - }) - .then(() => setDoc(docRef, finalData)) - .then(() => accumulator.awaitEvent()) - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(finalData); - }); - unsubscribe(); - } - ); + if (isNode()) { + const initialData = { a: 1 }; + const finalData = { a: 2 }; + await withTestDocAndInitialData( + persistence, + initialData, + async (docRef, db) => { + const doc = await getDoc(docRef); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshotResume( + db, + doc.toJSON(), + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + console.error('DEDB accumulator event 1'); + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(initialData); + }) + .then(() => setDoc(docRef, finalData)) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(finalData); + }); + unsubscribe(); + } + ); + } }); - it('DocumentSnapshot updated doc events in snapshot created by a bundle', async () => { - const initialData = { a: 0 }; - const finalData = { a: 1 }; - await withTestDocAndInitialData( - persistence, - initialData, - async (docRef, db) => { - const doc = await getDoc(docRef); - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshotResume( - db, - doc.toJSON(), - accumulator.storeEvent - ); - await accumulator - .awaitEvent() - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(initialData); - }) - .then(() => setDoc(docRef, finalData)) - .then(() => accumulator.awaitEvent()) - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(finalData); - }); - unsubscribe(); - } - ); + it('DocumentSnapshot updated doc events in snapshot created by a bundle accumulator', async () => { + if (isNode()) { + const initialData = { a: 1 }; + const finalData = { a: 2 }; + await withTestDocAndInitialData( + persistence, + initialData, + async (docRef, db) => { + const doc = await getDoc(docRef); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshotResume( + db, + doc.toJSON(), + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(initialData); + }) + .then(() => setDoc(docRef, finalData)) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(finalData); + }); + unsubscribe(); + } + ); + } }); it('DocumentSnapshot observer events for snapshot created by a bundle', async () => { - const initialData = { a: 0 }; - const finalData = { a: 1 }; - await withTestDocAndInitialData( - persistence, - initialData, - async (docRef, db) => { - const doc = await getDoc(docRef); - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshotResume(db, doc.toJSON(), { - next: accumulator.storeEvent - }); - await accumulator - .awaitEvent() - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(initialData); - }) - .then(() => setDoc(docRef, finalData)) - .then(() => accumulator.awaitEvent()) - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(finalData); + if (isNode()) { + const initialData = { a: 1 }; + const finalData = { a: 2 }; + await withTestDocAndInitialData( + persistence, + initialData, + async (docRef, db) => { + const doc = await getDoc(docRef); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshotResume(db, doc.toJSON(), { + next: accumulator.storeEvent }); - unsubscribe(); - } - ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(initialData); + }) + .then(() => setDoc(docRef, finalData)) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(finalData); + }); + unsubscribe(); + } + ); + } }); it('DocumentSnapshot error events for snapshot created by a bundle', async () => { @@ -1351,107 +1358,118 @@ apiDescribe('Database', persistence => { }); it('DocumentSnapshot updated doc events in snapshot created by fromJSON bundle', async () => { - const initialData = { a: 0 }; - const finalData = { a: 1 }; - await withTestDocAndInitialData( - persistence, - initialData, - async (docRef, db) => { - const doc = await getDoc(docRef); - const fromJsonDoc = documentSnapshotFromJSON(db, doc.toJSON()); - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshotResume( - db, - fromJsonDoc.toJSON(), - accumulator.storeEvent - ); - await accumulator - .awaitEvent() - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(initialData); - }) - .then(() => setDoc(docRef, finalData)) - .then(() => accumulator.awaitEvent()) - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(finalData); - }); - unsubscribe(); - } - ); + if (isNode()) { + const initialData = { a: 1 }; + const finalData = { a: 2 }; + await withTestDocAndInitialData( + persistence, + initialData, + async (docRef, db) => { + const doc = await getDoc(docRef); + const fromJsonDoc = documentSnapshotFromJSON(db, doc.toJSON()); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshotResume( + db, + fromJsonDoc.toJSON(), + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(initialData); + }) + .then(() => setDoc(docRef, finalData)) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(finalData); + }); + unsubscribe(); + } + ); + } }); it('DocumentSnapshot updated doc events in snapshot created by fromJSON doc ref', async () => { - const initialData = { a: 0 }; - const finalData = { a: 1 }; - await withTestDocAndInitialData( - persistence, - initialData, - async (docRef, db) => { - const doc = await getDoc(docRef); - const fromJsonDoc = documentSnapshotFromJSON(db, doc.toJSON()); - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshot(fromJsonDoc.ref, accumulator.storeEvent); - await accumulator - .awaitEvent() - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(initialData); - }) - .then(() => setDoc(docRef, finalData)) - .then(() => accumulator.awaitEvent()) - .then(snap => { - expect(snap.exists()).to.be.true; - expect(snap.data()).to.deep.equal(finalData); - }); - unsubscribe(); - } - ); + if (isNode()) { + const initialData = { a: 1 }; + const finalData = { a: 2 }; + await withTestDocAndInitialData( + persistence, + initialData, + async (docRef, db) => { + const doc = await getDoc(docRef); + const fromJsonDoc = documentSnapshotFromJSON(db, doc.toJSON()); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshot( + fromJsonDoc.ref, + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(initialData); + }) + .then(() => setDoc(docRef, finalData)) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.exists()).to.be.true; + expect(snap.data()).to.deep.equal(finalData); + }); + unsubscribe(); + } + ); + } }); it('Querysnapshot events for snapshot created by a bundle', async () => { - const testDocs = { - a: { foo: 1 }, - b: { bar: 2 } - }; - await withTestCollection(persistence, testDocs, async (coll, db) => { - const querySnap = await getDocs(query(coll, orderBy(documentId()))); - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshotResume( - db, - querySnap.toJSON(), - accumulator.storeEvent - ); - await accumulator.awaitEvent().then(snap => { - expect(snap.docs).not.to.be.null; - expect(snap.docs.length).to.equal(2); - expect(snap.docs[0].data()).to.deep.equal(testDocs.a); - expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + if (isNode()) { + const testDocs = { + a: { foo: 1 }, + b: { bar: 2 } + }; + await withTestCollection(persistence, testDocs, async (coll, db) => { + const querySnap = await getDocs(query(coll, orderBy(documentId()))); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshotResume( + db, + querySnap.toJSON(), + accumulator.storeEvent + ); + await accumulator.awaitEvent().then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal(testDocs.a); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }); + unsubscribe(); }); - unsubscribe(); - }); + } }); it('Querysnapshot observer events for snapshot created by a bundle', async () => { - const testDocs = { - a: { foo: 1 }, - b: { bar: 2 } - }; - await withTestCollection(persistence, testDocs, async (coll, db) => { - const querySnap = await getDocs(query(coll, orderBy(documentId()))); - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshotResume(db, querySnap.toJSON(), { - next: accumulator.storeEvent - }); - await accumulator.awaitEvent().then(snap => { - expect(snap.docs).not.to.be.null; - expect(snap.docs.length).to.equal(2); - expect(snap.docs[0].data()).to.deep.equal(testDocs.a); - expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + if (isNode()) { + const testDocs = { + a: { foo: 1 }, + b: { bar: 2 } + }; + await withTestCollection(persistence, testDocs, async (coll, db) => { + const querySnap = await getDocs(query(coll, orderBy(documentId()))); + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshotResume(db, querySnap.toJSON(), { + next: accumulator.storeEvent + }); + await accumulator.awaitEvent().then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal(testDocs.a); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }); + unsubscribe(); }); - unsubscribe(); - }); + } }); it('QuerySnapshot error events for snapshot created by a bundle', async () => { @@ -1505,106 +1523,113 @@ apiDescribe('Database', persistence => { }); it('QuerySnapshot updated doc events in snapshot created by a bundle', async () => { - const testDocs = { - a: { foo: 1 }, - b: { bar: 2 } - }; - await withTestCollection(persistence, testDocs, async (coll, db) => { - const querySnap = await getDocs(query(coll, orderBy(documentId()))); - const refForDocA = querySnap.docs[0].ref; - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshotResume( - db, - querySnap.toJSON(), - accumulator.storeEvent - ); - await accumulator - .awaitEvent() - .then(snap => { - expect(snap.docs).not.to.be.null; - expect(snap.docs.length).to.equal(2); - expect(snap.docs[0].data()).to.deep.equal(testDocs.a); - expect(snap.docs[1].data()).to.deep.equal(testDocs.b); - }) - .then(() => setDoc(refForDocA, { foo: 0 })) - .then(() => accumulator.awaitEvent()) - .then(snap => { - expect(snap.docs).not.to.be.null; - expect(snap.docs.length).to.equal(2); - expect(snap.docs[0].data()).to.deep.equal({ foo: 0 }); - expect(snap.docs[1].data()).to.deep.equal(testDocs.b); - }); - unsubscribe(); - }); + if (isNode()) { + const testDocs = { + a: { foo: 1 }, + b: { bar: 2 } + }; + await withTestCollection(persistence, testDocs, async (coll, db) => { + const querySnap = await getDocs(query(coll, orderBy(documentId()))); + const refForDocA = querySnap.docs[0].ref; + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshotResume( + db, + querySnap.toJSON(), + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal(testDocs.a); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }) + .then(() => setDoc(refForDocA, { foo: 0 })) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal({ foo: 0 }); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }); + unsubscribe(); + }); + } }); - it('QuerySnapshot updated doc events in snapshot created by fromJSON bundle', async () => { - const testDocs = { - a: { foo: 1 }, - b: { bar: 2 } - }; - await withTestCollection(persistence, testDocs, async (coll, db) => { - const querySnap = await getDocs(query(coll, orderBy(documentId()))); - const querySnapFromJson = querySnapshotFromJSON(db, querySnap.toJSON()); - const refForDocA = querySnapFromJson.docs[0].ref; - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshotResume( - db, - querySnapFromJson.toJSON(), - accumulator.storeEvent - ); - await accumulator - .awaitEvent() - .then(snap => { - expect(snap.docs).not.to.be.null; - expect(snap.docs.length).to.equal(2); - expect(snap.docs[0].data()).to.deep.equal(testDocs.a); - expect(snap.docs[1].data()).to.deep.equal(testDocs.b); - }) - .then(() => setDoc(refForDocA, { foo: 0 })) - .then(() => accumulator.awaitEvent()) - .then(snap => { - expect(snap.docs).not.to.be.null; - expect(snap.docs.length).to.equal(2); - expect(snap.docs[0].data()).to.deep.equal({ foo: 0 }); - expect(snap.docs[1].data()).to.deep.equal(testDocs.b); - }); - unsubscribe(); - }); + it('QuerySnapshot updated doc events in snapshot created by fromJSON ', async () => { + if (isNode()) { + const testDocs = { + a: { foo: 1 }, + b: { bar: 2 } + }; + await withTestCollection(persistence, testDocs, async (coll, db) => { + const querySnap = await getDocs(query(coll, orderBy(documentId()))); + const querySnapFromJson = querySnapshotFromJSON(db, querySnap.toJSON()); + const refForDocA = querySnapFromJson.docs[0].ref; + const accumulator = new EventsAccumulator(); + + const unsubscribe = onSnapshotResume( + db, + querySnapFromJson.toJSON(), + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal(testDocs.a); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }) + .then(() => setDoc(refForDocA, { foo: 0 })) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal({ foo: 0 }); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }); + unsubscribe(); + }); + } }); it('QuerySnapshot updated doc events in snapshot created by fromJSON query ref', async () => { - const testDocs = { - a: { foo: 1 }, - b: { bar: 2 } - }; - await withTestCollection(persistence, testDocs, async (coll, db) => { - const querySnap = await getDocs(query(coll, orderBy(documentId()))); - const querySnapFromJson = querySnapshotFromJSON(db, querySnap.toJSON()); - const refForDocA = querySnapFromJson.docs[0].ref; - const accumulator = new EventsAccumulator(); - const unsubscribe = onSnapshot( - querySnapFromJson.query, - accumulator.storeEvent - ); - await accumulator - .awaitEvent() - .then(snap => { - expect(snap.docs).not.to.be.null; - expect(snap.docs.length).to.equal(2); - expect(snap.docs[0].data()).to.deep.equal(testDocs.a); - expect(snap.docs[1].data()).to.deep.equal(testDocs.b); - }) - .then(() => setDoc(refForDocA, { foo: 0 })) - .then(() => accumulator.awaitEvent()) - .then(snap => { - expect(snap.docs).not.to.be.null; - expect(snap.docs.length).to.equal(2); - expect(snap.docs[0].data()).to.deep.equal({ foo: 0 }); - expect(snap.docs[1].data()).to.deep.equal(testDocs.b); - }); - unsubscribe(); - }); + if (isNode()) { + const testDocs = { + a: { foo: 1 }, + b: { bar: 2 } + }; + await withTestCollection(persistence, testDocs, async (coll, db) => { + const querySnap = await getDocs(query(coll, orderBy(documentId()))); + const querySnapFromJson = querySnapshotFromJSON(db, querySnap.toJSON()); + const refForDocA = querySnapFromJson.docs[0].ref; + const accumulator = new EventsAccumulator(); + const unsubscribe = onSnapshot( + querySnapFromJson.query, + accumulator.storeEvent + ); + await accumulator + .awaitEvent() + .then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal(testDocs.a); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }) + .then(() => setDoc(refForDocA, { foo: 0 })) + .then(() => accumulator.awaitEvent()) + .then(snap => { + expect(snap.docs).not.to.be.null; + expect(snap.docs.length).to.equal(2); + expect(snap.docs[0].data()).to.deep.equal({ foo: 0 }); + expect(snap.docs[1].data()).to.deep.equal(testDocs.b); + }); + unsubscribe(); + }); + } }); it('Metadata only changes are not fired when no options provided', () => { diff --git a/packages/firestore/test/integration/api/query.test.ts b/packages/firestore/test/integration/api/query.test.ts index 9ccd942027d..a12c843bf26 100644 --- a/packages/firestore/test/integration/api/query.test.ts +++ b/packages/firestore/test/integration/api/query.test.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { isNode } from '@firebase/util'; import { expect } from 'chai'; import { addEqualityMatcher } from '../../util/equality_matcher'; @@ -77,41 +78,43 @@ apiDescribe('Queries', persistence => { addEqualityMatcher(); it('QuerySnapshot.toJSON bundle getDocFromCache', async () => { - let path: string | null = null; - let jsonBundle: object | null = null; - const testDocs = { - a: { k: 'a' }, - b: { k: 'b' }, - c: { k: 'c' } - }; - // Write an initial document in an isolated Firestore instance so it's not stored in the cache. - await withTestCollection(persistence, testDocs, async collection => { - await getDocs(query(collection)).then(querySnapshot => { - expect(querySnapshot.docs.length).to.equal(3); - // Find the path to a known doc. - querySnapshot.docs.forEach(docSnapshot => { - if (docSnapshot.ref.path.endsWith('a')) { - path = docSnapshot.ref.path; - } + if (isNode()) { + let path: string | null = null; + let jsonBundle: object | null = null; + const testDocs = { + a: { k: 'a' }, + b: { k: 'b' }, + c: { k: 'c' } + }; + // Write an initial document in an isolated Firestore instance so it's not stored in the cache. + await withTestCollection(persistence, testDocs, async collection => { + await getDocs(query(collection)).then(querySnapshot => { + expect(querySnapshot.docs.length).to.equal(3); + // Find the path to a known doc. + querySnapshot.docs.forEach(docSnapshot => { + if (docSnapshot.ref.path.endsWith('a')) { + path = docSnapshot.ref.path; + } + }); + expect(path).to.not.be.null; + jsonBundle = querySnapshot.toJSON(); }); - expect(path).to.not.be.null; - jsonBundle = querySnapshot.toJSON(); - }); - }); - expect(jsonBundle).to.not.be.null; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const json = (jsonBundle as any).bundle; - expect(json).to.exist; - expect(json.length).to.be.greaterThan(0); - - if (path !== null) { - await withTestDb(persistence, async db => { - const docRef = doc(db, path!); - await loadBundle(db, json); - const docSnap = await getDocFromCache(docRef); - expect(docSnap.exists); - expect(docSnap.data()).to.deep.equal(testDocs.a); }); + expect(jsonBundle).to.not.be.null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const json = (jsonBundle as any).bundle; + expect(json).to.exist; + expect(json.length).to.be.greaterThan(0); + + if (path !== null) { + await withTestDb(persistence, async db => { + const docRef = doc(db, path!); + await loadBundle(db, json); + const docSnap = await getDocFromCache(docRef); + expect(docSnap.exists); + expect(docSnap.data()).to.deep.equal(testDocs.a); + }); + } } }); diff --git a/packages/firestore/test/unit/api/database.test.ts b/packages/firestore/test/unit/api/database.test.ts index ad0d5b6ddab..7f8ce10ffd7 100644 --- a/packages/firestore/test/unit/api/database.test.ts +++ b/packages/firestore/test/unit/api/database.test.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { isNode } from '@firebase/util'; import { expect } from 'chai'; import { @@ -270,27 +271,43 @@ describe('DocumentSnapshot', () => { }); it('toJSON returns a bundle', () => { - const json = documentSnapshot( + const snapshotJson = documentSnapshot( 'foo/bar', { a: 1 }, /*fromCache=*/ true ).toJSON(); // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((json as any).bundle).to.exist; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((json as any).bundle.length).to.be.greaterThan(0); + const json = snapshotJson as any; + expect(json.bundle).to.exist; + expect(json.bundle.length).to.be.greaterThan(0); + }); + + it('toJSON returns a bundle containing NOT_SUPPORTED in non-node environments', () => { + if (!isNode()) { + const snapshotJson = documentSnapshot( + 'foo/bar', + { a: 1 }, + /*fromCache=*/ true + ).toJSON(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const json = snapshotJson as any; + expect(json.bundle).to.exist; + expect(json.bundle).to.equal('NOT SUPPORTED'); + } }); it('toJSON returns an empty bundle when there are no documents', () => { - const json = documentSnapshot( - 'foo/bar', - /*data=*/ null, - /*fromCache=*/ true - ).toJSON(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((json as any).bundle).to.exist; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((json as any).bundle.length).to.equal(0); + if (isNode()) { + const snapshotJson = documentSnapshot( + 'foo/bar', + /*data=*/ null, + /*fromCache=*/ true + ).toJSON(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const json = snapshotJson as any; + expect(json.bundle).to.exist; + expect(json.bundle.length).to.equal(0); + } }); it('toJSON throws when there are pending writes', () => { @@ -307,31 +324,51 @@ describe('DocumentSnapshot', () => { ); }); + it('fromJSON throws when parsing client-side toJSON result', () => { + if (!isNode()) { + const docSnap = documentSnapshot( + 'foo/bar', + { a: 1 }, + /*fromCache=*/ true + ); + expect(() => { + documentSnapshotFromJSON(docSnap._firestore, docSnap.toJSON()); + }).to.throw; + } + }); + it('fromJSON parses toJSON result', () => { - const docSnap = documentSnapshot('foo/bar', { a: 1 }, /*fromCache=*/ true); - const json = docSnap.toJSON(); - expect(() => { - documentSnapshotFromJSON(docSnap._firestore, json); - }).to.not.throw; + if (isNode()) { + const docSnap = documentSnapshot( + 'foo/bar', + { a: 1 }, + /*fromCache=*/ true + ); + expect(() => { + documentSnapshotFromJSON(docSnap._firestore, docSnap.toJSON()); + }).to.not.throw; + } }); it('fromJSON produces valid snapshot data.', () => { - const json = documentSnapshot( - 'foo/bar', - { a: 1 }, - /*fromCache=*/ true - ).toJSON(); - const db = firestore(); - const docSnap = documentSnapshotFromJSON(db, json); - expect(docSnap).to.exist; - const data = docSnap.data(); - expect(data).to.not.be.undefined; - expect(data).to.not.be.null; - if (data) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((data as any).a).to.exist; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((data as any).a).to.equal(1); + if (isNode()) { + const docSnap = documentSnapshot( + 'foo/bar', + { a: 1 }, + /*fromCache=*/ true + ); + const db = firestore(); + const docSnapFromJSON = documentSnapshotFromJSON(db, docSnap.toJSON()); + expect(docSnapFromJSON).to.exist; + const data = docSnapFromJSON.data(); + expect(docSnapFromJSON).to.not.be.undefined; + expect(docSnapFromJSON).to.not.be.null; + if (data) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((data as any).a).to.exist; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((data as any).a).to.equal(1); + } } }); }); @@ -458,7 +495,7 @@ describe('QuerySnapshot', () => { }); it('toJSON returns a bundle', () => { - const json = querySnapshot( + const snapshotJson = querySnapshot( 'foo', {}, { a: { a: 1 } }, @@ -467,17 +504,43 @@ describe('QuerySnapshot', () => { false ).toJSON(); // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((json as any).bundle).to.exist; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((json as any).bundle.length).to.be.greaterThan(0); + const json = snapshotJson as any; + expect(json.bundle).to.exist; + expect(json.bundle.length).to.be.greaterThan(0); + }); + + it('toJSON returns a bundle containing NOT_SUPPORTED in non-node environments', () => { + if (!isNode()) { + const snapshotJson = querySnapshot( + 'foo', + {}, + { a: { a: 1 } }, + keys(), // An empty set of mutaded document keys signifies that there are no pending writes. + false, + false + ).toJSON(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const json = snapshotJson as any; + expect(json.bundle).to.exist; + expect(json.bundle).to.equal('NOT SUPPORTED'); + } }); it('toJSON returns a bundle when there are no documents', () => { - const json = querySnapshot('foo', {}, {}, keys(), false, false).toJSON(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((json as any).bundle).to.exist; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((json as any).bundle.length).to.be.greaterThan(0); + if (isNode()) { + const snapshotJson = querySnapshot( + 'foo', + {}, + {}, + keys(), + false, + false + ).toJSON(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const json = snapshotJson as any; + expect(json.bundle).to.exist; + expect(json.bundle.length).to.be.greaterThan(0); + } }); it('toJSON throws when there are pending writes', () => { @@ -608,51 +671,81 @@ describe('QuerySnapshot', () => { }); it('fromJSON does not throw', () => { - const json = querySnapshot( - 'foo', - {}, - { a: { a: 1 } }, - keys(), // An empty set of mutaded document keys signifies that there are no pending writes. - false, - false - ).toJSON(); - - const db = firestore(); - expect(() => { - querySnapshotFromJSON(db, json); - }).to.not.throw; + if (isNode()) { + const snapshot = querySnapshot( + 'foo', + {}, + { + a: { a: 1 }, + b: { bar: 2 } + }, + keys(), // An empty set of mutaded document keys signifies that there are no pending writes. + false, + false + ); + const db = firestore(); + expect(() => { + querySnapshotFromJSON(db, snapshot.toJSON()); + }).to.not.throw; + } }); - it('fromJSON parses produces valid snapshot data', () => { - const json = querySnapshot( - 'foo', - {}, - { a: { a: 1 } }, - keys(), // An empty set of mutaded document keys signifies that there are no pending writes. - false, - false - ).toJSON(); - - const db = firestore(); - const querySnap = querySnapshotFromJSON(db, json); - expect(querySnap).to.exist; - if (querySnap !== undefined) { - const docs = querySnap.docs; - expect(docs).to.not.be.undefined; - expect(docs).to.not.be.null; - if (docs) { - expect(docs.length).to.equal(1); - docs.map(document => { - const docData = document.data(); - expect(docData).to.exist; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((docData as any).a).to.exist; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((docData as any).a).to.equal(1); - }); + it('fromJSON produces valid snapshot data', () => { + if (isNode()) { + const snapshot = querySnapshot( + 'foo', + {}, + { + a: { a: 1 }, + b: { bar: 2 } + }, + keys(), // An empty set of mutaded document keys signifies that there are no pending writes. + false, + false + ); + const db = firestore(); + const querySnap = querySnapshotFromJSON(db, snapshot.toJSON()); + expect(querySnap).to.exist; + if (querySnap !== undefined) { + const docs = querySnap.docs; + expect(docs).to.not.be.undefined; + expect(docs).to.not.be.null; + if (docs) { + expect(docs.length).to.equal(2); + if (docs.length === 2) { + let docData = docs[0].data(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let data = docData as any; + expect(data.a).to.exist; + expect(data.a).to.equal(1); + + docData = docs[1].data(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data = docData as any; + expect(data.bar).to.exist; + expect(data.bar).to.equal(2); + } + } } } }); + + it('fromJSON throws when parsing client-side toJSON result', () => { + if (!isNode()) { + const querySnap = querySnapshot( + 'foo', + {}, + { a: { a: 1 } }, + keys(), // An empty set of mutaded document keys signifies that there are no pending writes. + false, + false + ); + const json = querySnap.toJSON(); + expect(() => { + querySnapshotFromJSON(querySnap._firestore, json); + }).to.throw; + } + }); }); describe('SnapshotMetadata', () => {