Skip to content

Commit 5ea4ea5

Browse files
committed
initial QuerySnapshot fromJSON impl
1 parent ad2f0ac commit 5ea4ea5

File tree

3 files changed

+133
-2
lines changed

3 files changed

+133
-2
lines changed

common/api-review/firestore.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,8 @@ export class QuerySnapshot<AppModelType = DocumentData, DbModelType extends Docu
651651
get docs(): Array<QueryDocumentSnapshot<AppModelType, DbModelType>>;
652652
get empty(): boolean;
653653
forEach(callback: (result: QueryDocumentSnapshot<AppModelType, DbModelType>) => void, thisArg?: unknown): void;
654+
// (undocumented)
655+
static fromJSON<AppModelType, DbModelType extends DocumentData = DocumentData>(db: Firestore, json: object): QuerySnapshot<AppModelType, DbModelType> | null;
654656
readonly metadata: SnapshotMetadata;
655657
readonly query: Query<AppModelType, DbModelType>;
656658
get size(): number;

packages/firestore/src/api/snapshot.ts

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { BundleConverterImpl } from '../core/bundle_impl';
18+
import { BundleConverterImpl, BundleLoader } from '../core/bundle_impl';
1919
import { createBundleReaderSync } from '../core/firestore_client';
2020
import { newQueryComparator } from '../core/query';
2121
import { ChangeType, ViewSnapshot } from '../core/view_snapshot';
@@ -36,9 +36,14 @@ import {
3636
} from '../lite-api/snapshot';
3737
import { UntypedFirestoreDataConverter } from '../lite-api/user_data_reader';
3838
import { AbstractUserDataWriter } from '../lite-api/user_data_writer';
39+
import { fromBundledQuery } from '../local/local_serializer';
40+
import { documentKeySet } from '../model/collections';
3941
import { Document } from '../model/document';
4042
import { DocumentKey } from '../model/document_key';
43+
import { DocumentSet } from '../model/document_set';
44+
import { ResourcePath } from '../model/path';
4145
import { newSerializer } from '../platform/serializer';
46+
import { fromDocument } from '../remote/serializer';
4247
import { debugAssert, fail } from '../util/assert';
4348
import {
4449
BundleBuilder,
@@ -507,6 +512,11 @@ export class DocumentSnapshot<
507512
return undefined;
508513
}
509514

515+
/**
516+
* Returns a JSON-serializable representation of this `DocumentSnapshot` instance.
517+
*
518+
* @returns a JSON representation of this object.
519+
*/
510520
toJSON(): object {
511521
const document = this._document;
512522
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -548,6 +558,16 @@ export class DocumentSnapshot<
548558
return result;
549559
}
550560

561+
/**
562+
* Builds a `DocumentSnapshot` instance from a JSON object created by
563+
* {@link DocumentSnapshot.toJSON}.
564+
*
565+
* @param firestore - The {@link Firestore} instance the snapshot should be loaded for.
566+
* @param json - a JSON object represention of a `DocumentSnapshot` instance.
567+
* @param converter - Converts objects to and from Firestore.
568+
* @returns an instance of {@link DocumentSnapshot} if the JSON object could be
569+
* parsed. Throws a {@link FirestoreError} if an error occurs.
570+
*/
551571
static fromJSON<
552572
AppModelType,
553573
DbModelType extends DocumentData = DocumentData
@@ -777,6 +797,11 @@ export class QuerySnapshot<
777797
return this._cachedChanges;
778798
}
779799

800+
/**
801+
* Returns a JSON-serializable representation of this `QuerySnapshot` instance.
802+
*
803+
* @returns a JSON representation of this object.
804+
*/
780805
toJSON(): object {
781806
// eslint-disable-next-line @typescript-eslint/no-explicit-any
782807
const result: any = {};
@@ -825,6 +850,111 @@ export class QuerySnapshot<
825850
result['bundle'] = builder.build();
826851
return result;
827852
}
853+
854+
/**
855+
* Builds a `QuerySnapshot` instance from a JSON object created by
856+
* {@link QuerySnapshot.toJSON}.
857+
*
858+
* @param firestore - The {@link Firestore} instance the snapshot should be loaded for.
859+
* @param json - a JSON object represention of a `QuerySnapshot` instance.
860+
* @returns an instance of {@link QuerySnapshot} if the JSON object could be
861+
* parsed. Throws a {@link FirestoreError} if an error occurs.
862+
*/
863+
static fromJSON<
864+
AppModelType,
865+
DbModelType extends DocumentData = DocumentData
866+
>(
867+
db: Firestore,
868+
json: object
869+
): QuerySnapshot<AppModelType, DbModelType> | null {
870+
const requiredFields = ['bundle', 'bundleName', 'bundleSource'];
871+
let error: string | undefined = undefined;
872+
let bundleString: string = '';
873+
for (const key of requiredFields) {
874+
if (!(key in json)) {
875+
error = `json missing required field: ${key}`;
876+
}
877+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
878+
const value = (json as any)[key];
879+
if (key === 'bundleSource') {
880+
if (typeof value !== 'string') {
881+
error = `json field 'bundleSource' must be a string.`;
882+
break;
883+
} else if (value !== 'QuerySnapshot') {
884+
error = "Expected 'bundleSource' field to equal 'QuerySnapshot'";
885+
break;
886+
}
887+
} else if (key === 'bundle') {
888+
if (typeof value !== 'string') {
889+
error = `json field 'bundle' must be a string.`;
890+
break;
891+
}
892+
bundleString = value;
893+
}
894+
}
895+
if (error) {
896+
throw new FirestoreError(Code.INVALID_ARGUMENT, error);
897+
}
898+
// Parse the bundle data.
899+
const serializer = newSerializer(db._databaseId);
900+
const bundleReader = createBundleReaderSync(bundleString, serializer);
901+
const bundleMetadata = bundleReader.getMetadata();
902+
const elements = bundleReader.getElements();
903+
if (elements.length === 0) {
904+
throw new FirestoreError(
905+
Code.INVALID_ARGUMENT,
906+
'No snapshat data was found in the bundle.'
907+
);
908+
}
909+
910+
const bundleLoader: BundleLoader = new BundleLoader(
911+
bundleMetadata,
912+
serializer
913+
);
914+
for (const element of elements) {
915+
bundleLoader.addSizedElement(element);
916+
}
917+
const parsedNamedQueries = bundleLoader.queries;
918+
if (parsedNamedQueries.length !== 1) {
919+
throw new FirestoreError(
920+
Code.INVALID_ARGUMENT,
921+
'Snapshot data contained more than one named query.'
922+
);
923+
}
924+
925+
const query = fromBundledQuery(parsedNamedQueries[0].bundledQuery!);
926+
// convert bundle data into the types that the DocumentSnapshot constructore requires.
927+
const liteUserDataWriter = new LiteUserDataWriter(db);
928+
929+
const bundledDocuments = bundleLoader.documents;
930+
const documentSet = new DocumentSet();
931+
const documentKeys = documentKeySet();
932+
for (const bundledDocumet of bundledDocuments) {
933+
const document = fromDocument(serializer, bundledDocumet.document!);
934+
documentSet.add(document);
935+
const documentPath = ResourcePath.fromString(
936+
bundledDocumet.metadata.name!
937+
);
938+
documentKeys.add(new DocumentKey(documentPath));
939+
}
940+
941+
const viewSnapshot = ViewSnapshot.fromInitialDocuments(
942+
query,
943+
documentSet,
944+
documentKeys,
945+
false, // fromCache
946+
false // hasCachedResults
947+
);
948+
949+
const externalQuery = new Query<AppModelType, DbModelType>(db, null, query);
950+
951+
return new QuerySnapshot<AppModelType, DbModelType>(
952+
db,
953+
liteUserDataWriter,
954+
externalQuery,
955+
viewSnapshot
956+
);
957+
}
828958
}
829959

830960
/** Calculates the array of `DocumentChange`s for a given `ViewSnapshot`. */

packages/firestore/src/core/bundle_impl.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import { LoadBundleTaskProgress } from '@firebase/firestore-types';
1919

20-
import { fromBundledQuery } from '../local/local_serializer';
2120
import { LocalStore } from '../local/local_store';
2221
import {
2322
localStoreApplyBundledDocuments,

0 commit comments

Comments
 (0)