15
15
* limitations under the License.
16
16
*/
17
17
18
- import { queryToTarget } from '../../src/core/query' ;
18
+ import { queryToTarget } from '../../src/core/query' ;
19
19
import {
20
20
JsonProtoSerializer ,
21
21
toDocument ,
22
22
toName ,
23
23
toQueryTarget ,
24
+ toTimestamp ,
24
25
} from '../../src/remote/serializer' ;
25
- import { Firestore } from '../api/database' ;
26
- import { DatabaseId } from '../core/database_info' ;
27
- import { DocumentSnapshot , QuerySnapshot } from '../lite-api/snapshot' ;
28
- import { Timestamp } from '../lite-api/timestamp' ;
26
+ import { Firestore } from '../api/database' ;
27
+ import { DatabaseId } from '../core/database_info' ;
28
+ import { DocumentSnapshot , QuerySnapshot } from '../lite-api/snapshot' ;
29
+ import { Timestamp } from '../lite-api/timestamp' ;
29
30
import {
31
+ BundledDocumentMetadata as ProtoBundledDocumentMetadata ,
30
32
BundleElement as ProtoBundleElement ,
31
33
BundleMetadata as ProtoBundleMetadata ,
32
- BundledDocumentMetadata as ProtoBundledDocumentMetadata ,
33
34
NamedQuery as ProtoNamedQuery ,
34
35
} from '../protos/firestore_bundle_proto' ;
35
- import { Document } from '../protos/firestore_proto_api' ;
36
+ import {
37
+ Document as ProtoDocument ,
38
+ Document
39
+ } from '../protos/firestore_proto_api' ;
36
40
37
41
import {
38
42
invalidArgumentMessage ,
39
43
validateString ,
40
44
} from './bundle_builder_validation_utils' ;
45
+ import { encoder } from "../../test/unit/util/bundle_data" ;
46
+ import {
47
+ parseData , parseObject ,
48
+ UserDataReader ,
49
+ UserDataSource
50
+ } from "../lite-api/user_data_reader" ;
51
+ import { AbstractUserDataWriter } from "../lite-api/user_data_writer" ;
52
+ import { ExpUserDataWriter } from "../api/reference_impl" ;
53
+ import { MutableDocument } from "../model/document" ;
54
+ import { debugAssert } from "./assert" ;
41
55
42
56
const BUNDLE_VERSION = 1 ;
43
57
44
58
/**
45
59
* Builds a Firestore data bundle with results from the given document and query snapshots.
46
60
*/
47
61
export class BundleBuilder {
48
-
62
+
49
63
// Resulting documents for the bundle, keyed by full document path.
50
64
private documents : Map < string , BundledDocument > = new Map ( ) ;
51
65
// Named queries saved in the bundle, keyed by query name.
@@ -56,10 +70,21 @@ export class BundleBuilder {
56
70
57
71
private databaseId : DatabaseId ;
58
72
73
+ private readonly serializer : JsonProtoSerializer ;
74
+ private readonly userDataReader : UserDataReader ;
75
+ private readonly userDataWriter : AbstractUserDataWriter ;
76
+
59
77
constructor ( private firestore : Firestore , readonly bundleId : string ) {
60
78
this . databaseId = firestore . _databaseId ;
79
+
80
+ // useProto3Json is true because the objects will be serialized to JSON string
81
+ // before being written to the bundle buffer.
82
+ this . serializer = new JsonProtoSerializer ( this . databaseId , /*useProto3Json=*/ true ) ;
83
+
84
+ this . userDataWriter = new ExpUserDataWriter ( firestore ) ;
85
+ this . userDataReader = new UserDataReader ( this . databaseId , true , this . serializer ) ;
61
86
}
62
-
87
+
63
88
/**
64
89
* Adds a Firestore document snapshot or query snapshot to the bundle.
65
90
* Both the documents data and the query read time will be included in the bundle.
@@ -97,7 +122,34 @@ export class BundleBuilder {
97
122
}
98
123
return this ;
99
124
}
100
-
125
+
126
+ toBundleDocument (
127
+ document : MutableDocument
128
+ ) : ProtoDocument {
129
+ // TODO handle documents that have mutations
130
+ debugAssert (
131
+ ! document . hasLocalMutations ,
132
+ "Can't serialize documents with mutations."
133
+ ) ;
134
+
135
+ // Convert document fields proto to DocumentData and then back
136
+ // to Proto3 JSON objects. This is the same approach used in
137
+ // bundling in the nodejs-firestore SDK. It may not be the most
138
+ // performant approach.
139
+ const documentData = this . userDataWriter . convertObjectMap ( document . data . value . mapValue . fields , 'previous' ) ;
140
+ // a parse context is typically used for validating and parsing user data, but in this
141
+ // case we are using it internally to convert DocumentData to Proto3 JSON
142
+ const context = this . userDataReader . createContext ( UserDataSource . ArrayArgument , 'internal toBundledDocument' ) ;
143
+ const proto3Fields = parseObject ( documentData , context ) ;
144
+
145
+ return {
146
+ name : toName ( this . serializer , document . key ) ,
147
+ fields : proto3Fields . mapValue . fields ,
148
+ updateTime : toTimestamp ( this . serializer , document . version . toTimestamp ( ) ) ,
149
+ createTime : toTimestamp ( this . serializer , document . createTime . toTimestamp ( ) )
150
+ } ;
151
+ }
152
+
101
153
private addBundledDocument ( snap : DocumentSnapshot , queryName ?: string ) : void {
102
154
// TODO: is this a valid shortcircuit?
103
155
if ( ! snap . _document || ! snap . _document . isValidDocument ( ) ) {
@@ -114,15 +166,11 @@ export class BundleBuilder {
114
166
( snapReadTime && originalDocument . metadata . readTime ! < snapReadTime )
115
167
) {
116
168
117
- // TODO: Should I create on serializer for the bundler instance, or just created one adhoc
118
- // like this?
119
- const serializer = new JsonProtoSerializer ( this . databaseId , /*useProto3Json=*/ false ) ;
120
-
121
169
this . documents . set ( snap . ref . path , {
122
- document : snap . _document . isFoundDocument ( ) ? toDocument ( serializer , mutableCopy ) : undefined ,
170
+ document : snap . _document . isFoundDocument ( ) ? this . toBundleDocument ( mutableCopy ) : undefined ,
123
171
metadata : {
124
- name : toName ( serializer , mutableCopy . key ) ,
125
- readTime : snapReadTime ,
172
+ name : toName ( this . serializer , mutableCopy . key ) ,
173
+ readTime : ! ! snapReadTime ? toTimestamp ( this . serializer , snapReadTime ) : undefined ,
126
174
exists : snap . exists ( ) ,
127
175
} ,
128
176
} ) ;
@@ -145,10 +193,8 @@ export class BundleBuilder {
145
193
if ( this . namedQueries . has ( name ) ) {
146
194
throw new Error ( `Query name conflict: ${ name } has already been added.` ) ;
147
195
}
196
+ const queryTarget = toQueryTarget ( this . serializer , queryToTarget ( querySnap . query . _query ) ) ;
148
197
149
- const serializer = new JsonProtoSerializer ( this . databaseId , /*useProto3Json=*/ false ) ;
150
- const queryTarget = toQueryTarget ( serializer , queryToTarget ( querySnap . query . _query ) ) ;
151
-
152
198
// TODO: if we can't resolve the query's readTime then can we set it to the latest
153
199
// of the document collection?
154
200
let latestReadTime = new Timestamp ( 0 , 0 ) ;
@@ -169,7 +215,7 @@ export class BundleBuilder {
169
215
this . namedQueries . set ( name , {
170
216
name,
171
217
bundledQuery,
172
- readTime : latestReadTime
218
+ readTime : toTimestamp ( this . serializer , latestReadTime )
173
219
} ) ;
174
220
}
175
221
@@ -178,65 +224,54 @@ export class BundleBuilder {
178
224
* of the element.
179
225
* @private
180
226
* @internal
227
+ * @param bundleElement A ProtoBundleElement that is expected to be Proto3 JSON compatible.
181
228
*/
182
- private elementToLengthPrefixedBuffer (
229
+ private lengthPrefixedString (
183
230
bundleElement : ProtoBundleElement
184
- ) : Buffer {
185
- // Convert to a valid proto message object then take its JSON representation.
186
- // This take cares of stuff like converting internal byte array fields
187
- // to Base64 encodings.
188
-
189
- // TODO: This fails. BundleElement doesn't have a toJSON method.
190
- const message = require ( '../protos/firestore_v1_proto_api' )
191
- . firestore . BundleElement . fromObject ( bundleElement )
192
- . toJSON ( ) ;
193
- const buffer = Buffer . from ( JSON . stringify ( message ) , 'utf-8' ) ;
194
- const lengthBuffer = Buffer . from ( buffer . length . toString ( ) ) ;
195
- return Buffer . concat ( [ lengthBuffer , buffer ] ) ;
231
+ ) : string {
232
+ const str = JSON . stringify ( bundleElement ) ;
233
+ // TODO: it's not ideal to have to re-encode all of these strings multiple times
234
+ // It may be more performant to return a UInt8Array that is concatenated to other
235
+ // UInt8Arrays instead of returning and concatenating strings and then
236
+ // converting the full string to UInt8Array.
237
+ const l = encoder . encode ( str ) . byteLength ;
238
+ return `${ l } ${ str } ` ;
196
239
}
197
240
198
- build ( ) : Buffer {
199
-
200
- let bundleBuffer = Buffer . alloc ( 0 ) ;
241
+ build ( ) : Uint8Array {
242
+ let bundleString = '' ;
201
243
202
244
for ( const namedQuery of this . namedQueries . values ( ) ) {
203
- bundleBuffer = Buffer . concat ( [
204
- bundleBuffer ,
205
- this . elementToLengthPrefixedBuffer ( { namedQuery} ) ,
206
- ] ) ;
245
+ bundleString += this . lengthPrefixedString ( { namedQuery} ) ;
207
246
}
208
247
209
248
for ( const bundledDocument of this . documents . values ( ) ) {
210
249
const documentMetadata : ProtoBundledDocumentMetadata =
211
250
bundledDocument . metadata ;
212
251
213
- bundleBuffer = Buffer . concat ( [
214
- bundleBuffer ,
215
- this . elementToLengthPrefixedBuffer ( { documentMetadata} ) ,
216
- ] ) ;
252
+ bundleString += this . lengthPrefixedString ( { documentMetadata} ) ;
217
253
// Write to the bundle if document exists.
218
254
const document = bundledDocument . document ;
219
255
if ( document ) {
220
- bundleBuffer = Buffer . concat ( [
221
- bundleBuffer ,
222
- this . elementToLengthPrefixedBuffer ( { document} ) ,
223
- ] ) ;
256
+ bundleString += this . lengthPrefixedString ( { document} ) ;
224
257
}
225
258
}
226
259
227
260
const metadata : ProtoBundleMetadata = {
228
261
id : this . bundleId ,
229
- createTime : this . latestReadTime ,
262
+ createTime : toTimestamp ( this . serializer , this . latestReadTime ) ,
230
263
version : BUNDLE_VERSION ,
231
264
totalDocuments : this . documents . size ,
232
- totalBytes : bundleBuffer . length ,
265
+ // TODO: it's not ideal to have to re-encode all of these strings multiple times
266
+ totalBytes : encoder . encode ( bundleString ) . length ,
233
267
} ;
234
268
// Prepends the metadata element to the bundleBuffer: `bundleBuffer` is the second argument to `Buffer.concat`.
235
- bundleBuffer = Buffer . concat ( [
236
- this . elementToLengthPrefixedBuffer ( { metadata} ) ,
237
- bundleBuffer ,
238
- ] ) ;
239
- return bundleBuffer ;
269
+ bundleString = this . lengthPrefixedString ( { metadata} ) + bundleString ;
270
+
271
+ // TODO: it's not ideal to have to re-encode all of these strings multiple times
272
+ // the implementation in nodejs-firestore concatenates Buffers instead of
273
+ // concatenating strings.
274
+ return encoder . encode ( bundleString ) ;
240
275
}
241
276
}
242
277
@@ -278,4 +313,4 @@ function validateQuerySnapshot(arg: string | number, value: unknown): void {
278
313
if ( ! ( value instanceof QuerySnapshot ) ) {
279
314
throw new Error ( invalidArgumentMessage ( arg , 'QuerySnapshot' ) ) ;
280
315
}
281
- }
316
+ }
0 commit comments