Skip to content

Commit a1e6a0b

Browse files
committed
Bundle DocumentSnaps with toJSON
Tested the results in loadBundle with a document consisting of all data types, and it succeeded.
1 parent c361963 commit a1e6a0b

File tree

3 files changed

+87
-131
lines changed

3 files changed

+87
-131
lines changed

common/api-review/firestore.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ export class DocumentSnapshot<AppModelType = DocumentData, DbModelType extends D
182182
// (undocumented)
183183
get readTime(): Timestamp;
184184
get ref(): DocumentReference<AppModelType, DbModelType>;
185+
// (undocumented)
186+
toJSON(): object;
185187
}
186188

187189
export { EmulatorMockTokenOptions }

packages/firestore/src/api/snapshot.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { AbstractUserDataWriter } from '../lite-api/user_data_writer';
3636
import { Document } from '../model/document';
3737
import { DocumentKey } from '../model/document_key';
3838
import { debugAssert, fail } from '../util/assert';
39+
import { BundleBuilder, DocumentBundleData } from '../util/bundle_builder_impl';
3940
import { Code, FirestoreError } from '../util/error';
4041

4142
import { Firestore } from './database';
@@ -496,6 +497,27 @@ export class DocumentSnapshot<
496497
}
497498
return undefined;
498499
}
500+
501+
toJSON(): object {
502+
if (
503+
!this._document ||
504+
!this._document.isValidDocument() ||
505+
!this._document.isFoundDocument()
506+
) {
507+
return { bundle: '' };
508+
}
509+
510+
const builder: BundleBuilder = new BundleBuilder(this._firestore, 'abc123');
511+
const documentData = this._userDataWriter.convertObjectMap(
512+
this._document.data.value.mapValue.fields,
513+
'previous'
514+
);
515+
516+
builder.addBundleDocument(DocumentToDocumentBundleData(this._firestore, this.ref.path, documentData, this._document));
517+
return {
518+
bundle: builder.build()
519+
};
520+
}
499521
}
500522

501523
/**
@@ -637,7 +659,7 @@ export class QuerySnapshot<
637659
throw new FirestoreError(
638660
Code.INVALID_ARGUMENT,
639661
'To include metadata changes with your document changes, you must ' +
640-
'also pass { includeMetadataChanges:true } to onSnapshot().'
662+
'also pass { includeMetadataChanges:true } to onSnapshot().'
641663
);
642664
}
643665

@@ -673,10 +695,10 @@ export function changesFromSnapshot<
673695
);
674696
debugAssert(
675697
!lastDoc ||
676-
newQueryComparator(querySnapshot._snapshot.query)(
677-
lastDoc,
678-
change.doc
679-
) < 0,
698+
newQueryComparator(querySnapshot._snapshot.query)(
699+
lastDoc,
700+
change.doc
701+
) < 0,
680702
'Got added events in wrong order'
681703
);
682704
const doc = new QueryDocumentSnapshot<AppModelType, DbModelType>(
@@ -790,3 +812,15 @@ export function snapshotEqual<AppModelType, DbModelType extends DocumentData>(
790812

791813
return false;
792814
}
815+
816+
function DocumentToDocumentBundleData(firestore: Firestore, path: string, documentData: DocumentData, document: Document): DocumentBundleData {
817+
return {
818+
documentData,
819+
documentKey: document.mutableCopy().key,
820+
documentPath: path,
821+
documentExists: true,
822+
createdTime: document.createTime.toTimestamp(),
823+
readTime: document.readTime.toTimestamp(),
824+
versionTime: document.version.toTimestamp()
825+
};
826+
}

packages/firestore/src/util/bundle_builder_impl.ts

Lines changed: 46 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,22 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { queryToTarget } from '../../src/core/query';
1918
import {
2019
JsonProtoSerializer,
2120
toName,
22-
toQueryTarget,
2321
toTimestamp
2422
} from '../../src/remote/serializer';
2523
import { encoder } from '../../test/unit/util/bundle_data';
2624
import { Firestore } from '../api/database';
27-
import { ExpUserDataWriter } from '../api/reference_impl';
2825
import { DatabaseId } from '../core/database_info';
29-
import { DocumentSnapshot, QuerySnapshot } from '../lite-api/snapshot';
26+
import { DocumentData } from '../lite-api/reference';
3027
import { Timestamp } from '../lite-api/timestamp';
3128
import {
3229
parseObject,
3330
UserDataReader,
3431
UserDataSource
3532
} from '../lite-api/user_data_reader';
36-
import { AbstractUserDataWriter } from '../lite-api/user_data_writer';
37-
import { MutableDocument } from '../model/document';
33+
import { DocumentKey } from '../model/document_key';
3834
import {
3935
BundledDocumentMetadata as ProtoBundledDocumentMetadata,
4036
BundleElement as ProtoBundleElement,
@@ -66,7 +62,6 @@ export class BundleBuilder {
6662

6763
private readonly serializer: JsonProtoSerializer;
6864
private readonly userDataReader: UserDataReader;
69-
private readonly userDataWriter: AbstractUserDataWriter;
7065

7166
constructor(private firestore: Firestore, readonly bundleId: string) {
7267
this.databaseId = firestore._databaseId;
@@ -78,133 +73,71 @@ export class BundleBuilder {
7873
/*useProto3Json=*/ true
7974
);
8075

81-
this.userDataWriter = new ExpUserDataWriter(firestore);
8276
this.userDataReader = new UserDataReader(
8377
this.databaseId,
8478
true,
8579
this.serializer
8680
);
8781
}
8882

89-
/**
90-
* Adds a Firestore document snapshot or query snapshot to the bundle.
91-
* Both the documents data and the query read time will be included in the bundle.
92-
*
93-
* @param {DocumentSnapshot | string} documentOrName A document snapshot to add or a name of a query.
94-
* @param {Query=} querySnapshot A query snapshot to add to the bundle, if provided.
95-
* @returns {BundleBuilder} This instance.
96-
*
97-
* @example
98-
* ```
99-
* const bundle = firestore.bundle('data-bundle');
100-
* const docSnapshot = await firestore.doc('abc/123').get();
101-
* const querySnapshot = await firestore.collection('coll').get();
102-
*
103-
* const bundleBuffer = bundle.add(docSnapshot) // Add a document
104-
* .add('coll-query', querySnapshot) // Add a named query.
105-
* .build()
106-
* // Save `bundleBuffer` to CDN or stream it to clients.
107-
* ```
108-
*/
109-
add(
110-
documentOrName: DocumentSnapshot | string,
111-
querySnapshot?: QuerySnapshot
112-
): BundleBuilder {
113-
if (arguments.length < 1 || arguments.length > 2) {
114-
throw new Error(
115-
'Function BundleBuilder.add() requires 1 or 2 arguments.'
116-
);
117-
}
118-
if (arguments.length === 1) {
119-
validateDocumentSnapshot('documentOrName', documentOrName);
120-
this.addBundledDocument(documentOrName as DocumentSnapshot);
121-
} else {
122-
validateString('documentOrName', documentOrName);
123-
validateQuerySnapshot('querySnapshot', querySnapshot);
124-
this.addNamedQuery(documentOrName as string, querySnapshot!);
125-
}
126-
return this;
127-
}
128-
129-
toBundleDocument(document: MutableDocument): ProtoDocument {
83+
toBundleDocument(docBundleData: DocumentBundleData): ProtoDocument {
13084
// TODO handle documents that have mutations
13185
debugAssert(
132-
!document.hasLocalMutations,
86+
!docBundleData.documentData.hasLocalMutations,
13387
"Can't serialize documents with mutations."
13488
);
13589

136-
// Convert document fields proto to DocumentData and then back
137-
// to Proto3 JSON objects. This is the same approach used in
138-
// bundling in the nodejs-firestore SDK. It may not be the most
139-
// performant approach.
140-
const documentData = this.userDataWriter.convertObjectMap(
141-
document.data.value.mapValue.fields,
142-
'previous'
143-
);
14490
// a parse context is typically used for validating and parsing user data, but in this
14591
// case we are using it internally to convert DocumentData to Proto3 JSON
14692
const context = this.userDataReader.createContext(
14793
UserDataSource.ArrayArgument,
14894
'internal toBundledDocument'
14995
);
150-
const proto3Fields = parseObject(documentData, context);
96+
const proto3Fields = parseObject(docBundleData.documentData, context);
15197

15298
return {
153-
name: toName(this.serializer, document.key),
99+
name: toName(this.serializer, docBundleData.documentKey),
154100
fields: proto3Fields.mapValue.fields,
155-
updateTime: toTimestamp(this.serializer, document.version.toTimestamp()),
156-
createTime: toTimestamp(
157-
this.serializer,
158-
document.createTime.toTimestamp()
159-
)
101+
updateTime: toTimestamp(this.serializer, docBundleData.versionTime),
102+
createTime: toTimestamp(this.serializer, docBundleData.createdTime)
160103
};
161104
}
162105

163-
private addBundledDocument(snap: DocumentSnapshot, queryName?: string): void {
164-
if (
165-
!snap._document ||
166-
!snap._document.isValidDocument() ||
167-
!snap._document.isFoundDocument()
168-
) {
169-
return;
170-
}
171-
const originalDocument = this.documents.get(snap.ref.path);
106+
addBundleDocument(docBundleData: DocumentBundleData): void {
107+
const originalDocument = this.documents.get(docBundleData.documentPath);
172108
const originalQueries = originalDocument?.metadata.queries;
173-
const mutableCopy = snap._document.mutableCopy();
174109

110+
const readTime = docBundleData.readTime;
175111
// Update with document built from `snap` because it is newer.
176-
const snapReadTime = snap.readTime;
177112
if (
178113
!originalDocument ||
179-
(!snapReadTime && !originalDocument.metadata.readTime) ||
180-
(snapReadTime && originalDocument.metadata.readTime! < snapReadTime)
114+
(!readTime && !originalDocument.metadata.readTime) ||
115+
(readTime && originalDocument.metadata.readTime! < readTime)
181116
) {
182-
this.documents.set(snap.ref.path, {
183-
document: this.toBundleDocument(mutableCopy),
117+
this.documents.set(docBundleData.documentPath, {
118+
document: this.toBundleDocument(docBundleData),
184119
metadata: {
185-
name: toName(this.serializer, mutableCopy.key),
186-
readTime: !!snapReadTime
187-
? toTimestamp(this.serializer, snapReadTime)
120+
name: toName(this.serializer, docBundleData.documentKey),
121+
readTime: !!readTime
122+
? toTimestamp(this.serializer, readTime)
188123
: undefined,
189-
exists: snap.exists()
124+
exists: docBundleData.documentExists
190125
}
191126
});
192127
}
193128

194129
// Update `queries` to include both original and `queryName`.
195-
const newDocument = this.documents.get(snap.ref.path)!;
130+
const newDocument = this.documents.get(docBundleData.documentPath)!;
196131
newDocument.metadata.queries = originalQueries || [];
197-
if (queryName) {
198-
newDocument.metadata.queries!.push(queryName);
132+
if (docBundleData.queryName) {
133+
newDocument.metadata.queries!.push(docBundleData.queryName);
199134
}
200-
201-
if (snapReadTime && snapReadTime > this.latestReadTime) {
202-
this.latestReadTime = snapReadTime;
135+
if (readTime && readTime > this.latestReadTime) {
136+
this.latestReadTime = readTime;
203137
}
204138
}
205139

206-
// TODO: remove this since we're not planning to serialize named queries.
207-
private addNamedQuery(name: string, querySnap: QuerySnapshot): void {
140+
/*private addNamedQuery(name: string, querySnap: QuerySnapshot): void {
208141
if (this.namedQueries.has(name)) {
209142
throw new Error(`Query name conflict: ${name} has already been added.`);
210143
}
@@ -233,7 +166,7 @@ export class BundleBuilder {
233166
bundledQuery,
234167
readTime: toTimestamp(this.serializer, latestReadTime)
235168
});
236-
}
169+
} */
237170

238171
/**
239172
* Converts a IBundleElement to a Buffer whose content is the length prefixed JSON representation
@@ -252,7 +185,7 @@ export class BundleBuilder {
252185
return `${l}${str}`;
253186
}
254187

255-
build(): Uint8Array {
188+
build(): string {
256189
let bundleString = '';
257190

258191
for (const namedQuery of this.namedQueries.values()) {
@@ -282,51 +215,38 @@ export class BundleBuilder {
282215
// Prepends the metadata element to the bundleBuffer: `bundleBuffer` is the second argument to `Buffer.concat`.
283216
bundleString = this.lengthPrefixedString({ metadata }) + bundleString;
284217

285-
// TODO: it's not ideal to have to re-encode all of these strings multiple times
286-
// the implementation in nodejs-firestore concatenates Buffers instead of
287-
// concatenating strings.
288-
return encoder.encode(bundleString);
218+
return bundleString;
289219
}
290220
}
291221

292222
/**
293-
* Convenient class to hold both the metadata and the actual content of a document to be bundled.
294-
* @private
295-
* @internal
296-
*/
297-
class BundledDocument {
298-
constructor(
299-
readonly metadata: ProtoBundledDocumentMetadata,
300-
readonly document?: Document
301-
) {}
302-
}
303-
304-
/**
305-
* Validates that 'value' is DocumentSnapshot.
223+
* Interface for an object that contains data required to bundle a DocumentSnapshot.
224+
* Accessing the methods of DocumentSnapshot directly to retreivew this data in this
225+
* implementation would create a circular dependency.
306226
*
307-
* @private
308227
* @internal
309-
* @param arg The argument name or argument index (for varargs methods).
310-
* @param value The input to validate.
311228
*/
312-
function validateDocumentSnapshot(arg: string | number, value: unknown): void {
313-
if (!(value instanceof DocumentSnapshot)) {
314-
throw new Error(invalidArgumentMessage(arg, 'DocumentSnapshot'));
315-
}
229+
export interface DocumentBundleData {
230+
readonly documentData: DocumentData;
231+
readonly documentKey: DocumentKey;
232+
readonly documentPath: string;
233+
readonly documentExists: boolean;
234+
readonly createdTime: Timestamp;
235+
readonly readTime?: Timestamp;
236+
readonly versionTime: Timestamp;
237+
readonly queryName?: string;
316238
}
317239

318240
/**
319-
* Validates that 'value' is QuerySnapshot.
320-
*
241+
* Convenient class to hold both the metadata and the actual content of a document to be bundled.
321242
* @private
322243
* @internal
323-
* @param arg The argument name or argument index (for varargs methods).
324-
* @param value The input to validate.
325244
*/
326-
function validateQuerySnapshot(arg: string | number, value: unknown): void {
327-
if (!(value instanceof QuerySnapshot)) {
328-
throw new Error(invalidArgumentMessage(arg, 'QuerySnapshot'));
329-
}
245+
class BundledDocument {
246+
constructor(
247+
readonly metadata: ProtoBundledDocumentMetadata,
248+
readonly document?: Document
249+
) {}
330250
}
331251

332252
/**

0 commit comments

Comments
 (0)