Skip to content

Commit 38d8bc5

Browse files
authored
[FEAT] Restrict Firestore Doc/QuerySnapshot toJSON on client. (#9048)
To reduce bundle size on clients, the `toJSON` methods of `DocumentSnapshot` and `QuerySnapshot` will not create bundles. ### Testing Updated the integration and unit tests to test `fromJSON` functionality in node environments only. I did attempt to create pre-packaged bundles so that we can test in browser still, but unforunately the bundle includes the project name, and the project that we test against varies depending on the profile of tests that were running (prod, local, etc). If these don't match then an error is thrown. ### API Changes N/A
1 parent a49774f commit 38d8bc5

File tree

11 files changed

+823
-454
lines changed

11 files changed

+823
-454
lines changed

packages/firestore/src/api/snapshot.ts

Lines changed: 54 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,12 @@ import { DocumentKey } from '../model/document_key';
4343
import { DocumentSet } from '../model/document_set';
4444
import { ResourcePath } from '../model/path';
4545
import { newSerializer } from '../platform/serializer';
46+
import {
47+
buildQuerySnapshotJsonBundle,
48+
buildDocumentSnapshotJsonBundle
49+
} from '../platform/snapshot_to_json';
4650
import { fromDocument } from '../remote/serializer';
4751
import { debugAssert, fail } from '../util/assert';
48-
import {
49-
BundleBuilder,
50-
DocumentSnapshotBundleData,
51-
QuerySnapshotBundleData
52-
} from '../util/bundle_builder_impl';
5352
import { Code, FirestoreError } from '../util/error';
5453
// API extractor fails importing 'property' unless we also explicitly import 'Property'.
5554
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports-ts
@@ -529,6 +528,13 @@ export class DocumentSnapshot<
529528
* @returns a JSON representation of this object.
530529
*/
531530
toJSON(): object {
531+
if (this.metadata.hasPendingWrites) {
532+
throw new FirestoreError(
533+
Code.FAILED_PRECONDITION,
534+
'DocumentSnapshot.toJSON() attempted to serialize a document with pending writes. ' +
535+
'Await waitForPendingWrites() before invoking toJSON().'
536+
);
537+
}
532538
const document = this._document;
533539
// eslint-disable-next-line @typescript-eslint/no-explicit-any
534540
const result: any = {};
@@ -544,29 +550,16 @@ export class DocumentSnapshot<
544550
) {
545551
return result;
546552
}
547-
const builder: BundleBuilder = new BundleBuilder(
548-
this._firestore,
549-
AutoId.newId()
550-
);
551553
const documentData = this._userDataWriter.convertObjectMap(
552554
document.data.value.mapValue.fields,
553555
'previous'
554556
);
555-
if (this.metadata.hasPendingWrites) {
556-
throw new FirestoreError(
557-
Code.FAILED_PRECONDITION,
558-
'DocumentSnapshot.toJSON() attempted to serialize a document with pending writes. ' +
559-
'Await waitForPendingWrites() before invoking toJSON().'
560-
);
561-
}
562-
builder.addBundleDocument(
563-
documentToDocumentSnapshotBundleData(
564-
this.ref.path,
565-
documentData,
566-
document
567-
)
557+
result['bundle'] = buildDocumentSnapshotJsonBundle(
558+
this._firestore,
559+
document,
560+
documentData,
561+
this.ref.path
568562
);
569-
result['bundle'] = builder.build();
570563
return result;
571564
}
572565
}
@@ -611,6 +604,12 @@ export function documentSnapshotFromJSON<
611604
converter?: FirestoreDataConverter<AppModelType, DbModelType>
612605
): DocumentSnapshot<AppModelType, DbModelType> {
613606
if (validateJSON(json, DocumentSnapshot._jsonSchema)) {
607+
if (json.bundle === 'NOT SUPPORTED') {
608+
throw new FirestoreError(
609+
Code.INVALID_ARGUMENT,
610+
'The provided JSON object was created in a client environment, which is not supported.'
611+
);
612+
}
614613
// Parse the bundle data.
615614
const serializer = newSerializer(db._databaseId);
616615
const bundleReader = createBundleReaderSync(json.bundle, serializer);
@@ -825,52 +824,48 @@ export class QuerySnapshot<
825824
* @returns a JSON representation of this object.
826825
*/
827826
toJSON(): object {
827+
if (this.metadata.hasPendingWrites) {
828+
throw new FirestoreError(
829+
Code.FAILED_PRECONDITION,
830+
'QuerySnapshot.toJSON() attempted to serialize a document with pending writes. ' +
831+
'Await waitForPendingWrites() before invoking toJSON().'
832+
);
833+
}
828834
// eslint-disable-next-line @typescript-eslint/no-explicit-any
829835
const result: any = {};
830836
result['type'] = QuerySnapshot._jsonSchemaVersion;
831837
result['bundleSource'] = 'QuerySnapshot';
832838
result['bundleName'] = AutoId.newId();
833839

834-
const builder: BundleBuilder = new BundleBuilder(
835-
this._firestore,
836-
result['bundleName']
837-
);
838840
const databaseId = this._firestore._databaseId.database;
839841
const projectId = this._firestore._databaseId.projectId;
840842
const parent = `projects/${projectId}/databases/${databaseId}/documents`;
841-
const docBundleDataArray: DocumentSnapshotBundleData[] = [];
842-
const docArray = this.docs;
843-
docArray.forEach(doc => {
843+
const documents: Document[] = [];
844+
const documentData: DocumentData[] = [];
845+
const paths: string[] = [];
846+
847+
this.docs.forEach(doc => {
844848
if (doc._document === null) {
845849
return;
846850
}
847-
const documentData = this._userDataWriter.convertObjectMap(
848-
doc._document.data.value.mapValue.fields,
849-
'previous'
850-
);
851-
if (this.metadata.hasPendingWrites) {
852-
throw new FirestoreError(
853-
Code.FAILED_PRECONDITION,
854-
'QuerySnapshot.toJSON() attempted to serialize a document with pending writes. ' +
855-
'Await waitForPendingWrites() before invoking toJSON().'
856-
);
857-
}
858-
docBundleDataArray.push(
859-
documentToDocumentSnapshotBundleData(
860-
doc.ref.path,
861-
documentData,
862-
doc._document
851+
documents.push(doc._document);
852+
documentData.push(
853+
this._userDataWriter.convertObjectMap(
854+
doc._document.data.value.mapValue.fields,
855+
'previous'
863856
)
864857
);
858+
paths.push(doc.ref.path);
865859
});
866-
const bundleData: QuerySnapshotBundleData = {
867-
name: result['bundleName'],
868-
query: this.query._query,
860+
result['bundle'] = buildQuerySnapshotJsonBundle(
861+
this._firestore,
862+
this.query._query,
863+
result['bundleName'],
869864
parent,
870-
docBundleDataArray
871-
};
872-
builder.addBundleQuery(bundleData);
873-
result['bundle'] = builder.build();
865+
paths,
866+
documents,
867+
documentData
868+
);
874869
return result;
875870
}
876871
}
@@ -915,6 +910,12 @@ export function querySnapshotFromJSON<
915910
converter?: FirestoreDataConverter<AppModelType, DbModelType>
916911
): QuerySnapshot<AppModelType, DbModelType> {
917912
if (validateJSON(json, QuerySnapshot._jsonSchema)) {
913+
if (json.bundle === 'NOT SUPPORTED') {
914+
throw new FirestoreError(
915+
Code.INVALID_ARGUMENT,
916+
'The provided JSON object was created in a client environment, which is not supported.'
917+
);
918+
}
918919
// Parse the bundle data.
919920
const serializer = newSerializer(db._databaseId);
920921
const bundleReader = createBundleReaderSync(json.bundle, serializer);
@@ -1111,20 +1112,3 @@ export function snapshotEqual<AppModelType, DbModelType extends DocumentData>(
11111112

11121113
return false;
11131114
}
1114-
1115-
// Formats Document data for bundling a DocumentSnapshot.
1116-
function documentToDocumentSnapshotBundleData(
1117-
path: string,
1118-
documentData: DocumentData,
1119-
document: Document
1120-
): DocumentSnapshotBundleData {
1121-
return {
1122-
documentData,
1123-
documentKey: document.mutableCopy().key,
1124-
documentPath: path,
1125-
documentExists: true,
1126-
createdTime: document.createTime.toTimestamp(),
1127-
readTime: document.readTime.toTimestamp(),
1128-
versionTime: document.version.toTimestamp()
1129-
};
1130-
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/** Return the Platform-specific build JSON bundle implementations. */
19+
import { Firestore } from '../../api/database';
20+
import { Query } from '../../core/query';
21+
import { DocumentData } from '../../lite-api/reference';
22+
import { Document } from '../../model/document';
23+
24+
export function buildDocumentSnapshotJsonBundle(
25+
db: Firestore,
26+
document: Document,
27+
docData: DocumentData,
28+
path: string
29+
): string {
30+
return 'NOT SUPPORTED';
31+
}
32+
33+
export function buildQuerySnapshotJsonBundle(
34+
db: Firestore,
35+
query: Query,
36+
bundleName: string,
37+
parent: string,
38+
paths: string[],
39+
docs: Document[],
40+
documentData: DocumentData[]
41+
): string {
42+
return 'NOT SUPPORTED';
43+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
export * from '../browser/snapshot_to_json';
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/** Return the Platform-specific build JSON bundle implementations. */
19+
import { Firestore } from '../../api/database';
20+
import { Query } from '../../core/query';
21+
import { DocumentData } from '../../lite-api/reference';
22+
import { Document } from '../../model/document';
23+
import {
24+
BundleBuilder,
25+
DocumentSnapshotBundleData,
26+
QuerySnapshotBundleData
27+
} from '../../util/bundle_builder_impl';
28+
import { AutoId } from '../../util/misc';
29+
30+
export function buildDocumentSnapshotJsonBundle(
31+
db: Firestore,
32+
document: Document,
33+
docData: DocumentData,
34+
path: string
35+
): string {
36+
const builder: BundleBuilder = new BundleBuilder(db, AutoId.newId());
37+
builder.addBundleDocument(
38+
documentToDocumentSnapshotBundleData(path, docData, document)
39+
);
40+
return builder.build();
41+
}
42+
43+
export function buildQuerySnapshotJsonBundle(
44+
db: Firestore,
45+
query: Query,
46+
bundleName: string,
47+
parent: string,
48+
paths: string[],
49+
docs: Document[],
50+
documentData: DocumentData[]
51+
): string {
52+
const docBundleDataArray: DocumentSnapshotBundleData[] = [];
53+
for (let i = 0; i < docs.length; i++) {
54+
docBundleDataArray.push(
55+
documentToDocumentSnapshotBundleData(paths[i], documentData[i], docs[i])
56+
);
57+
}
58+
const bundleData: QuerySnapshotBundleData = {
59+
name: bundleName,
60+
query,
61+
parent,
62+
docBundleDataArray
63+
};
64+
const builder: BundleBuilder = new BundleBuilder(db, bundleName);
65+
builder.addBundleQuery(bundleData);
66+
return builder.build();
67+
}
68+
69+
// Formats Document data for bundling a DocumentSnapshot.
70+
function documentToDocumentSnapshotBundleData(
71+
path: string,
72+
documentData: DocumentData,
73+
document: Document
74+
): DocumentSnapshotBundleData {
75+
return {
76+
documentData,
77+
documentKey: document.mutableCopy().key,
78+
documentPath: path,
79+
documentExists: true,
80+
createdTime: document.createTime.toTimestamp(),
81+
readTime: document.readTime.toTimestamp(),
82+
versionTime: document.version.toTimestamp()
83+
};
84+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
export * from '../node/snapshot_to_json';
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
export {
19+
buildDocumentSnapshotJsonBundle,
20+
buildQuerySnapshotJsonBundle
21+
} from '../browser/snapshot_to_json';

0 commit comments

Comments
 (0)