Skip to content

Commit ab90498

Browse files
committed
docsnapshot.FromJSON prototype
1 parent a888667 commit ab90498

File tree

6 files changed

+150
-49
lines changed

6 files changed

+150
-49
lines changed

common/api-review/firestore.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ export class DocumentSnapshot<AppModelType = DocumentData, DbModelType extends D
174174
protected constructor();
175175
data(options?: SnapshotOptions): AppModelType | undefined;
176176
exists(): this is QueryDocumentSnapshot<AppModelType, DbModelType>;
177+
// (undocumented)
178+
static fromJSON(db: Firestore, json: object): object;
177179
get(fieldPath: string | FieldPath, options?: SnapshotOptions): any;
178180
get id(): string;
179181
readonly metadata: SnapshotMetadata;

packages/firestore/src/api/snapshot.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
* limitations under the License.
1616
*/
1717

18+
import { BundleConverterImpl } from '../core/bundle_impl';
19+
import { createBundleReaderSync } from '../core/firestore_client';
1820
import { newQueryComparator } from '../core/query';
1921
import { ChangeType, ViewSnapshot } from '../core/view_snapshot';
2022
import { FieldPath } from '../lite-api/field_path';
@@ -33,8 +35,10 @@ import {
3335
} from '../lite-api/snapshot';
3436
import { UntypedFirestoreDataConverter } from '../lite-api/user_data_reader';
3537
import { AbstractUserDataWriter } from '../lite-api/user_data_writer';
38+
import { LiteUserDataWriter } from '../lite-api/reference_impl';
3639
import { Document } from '../model/document';
3740
import { DocumentKey } from '../model/document_key';
41+
import { newSerializer } from '../platform/serializer';
3842
import { debugAssert, fail } from '../util/assert';
3943
import {
4044
BundleBuilder,
@@ -543,6 +547,88 @@ export class DocumentSnapshot<
543547
result['bundle'] = builder.build();
544548
return result;
545549
}
550+
551+
static fromJSON(db: Firestore, json: object): object {
552+
const requiredFields = ['bundle', 'bundleName', 'bundleSource'];
553+
let error: string | undefined = undefined;
554+
let bundleString: string = '';
555+
for (const key of requiredFields) {
556+
if (!(key in json)) {
557+
error = `json missing required field: ${key}`;
558+
}
559+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
560+
const value = (json as any)[key];
561+
if (key === 'bundleSource') {
562+
if (typeof value !== 'string') {
563+
error = `json field 'bundleSource' must be a string.`;
564+
break;
565+
} else if (value !== 'DocumentSnapshot') {
566+
error = "Expected 'bundleSource' field to equal 'DocumentSnapshot'";
567+
break;
568+
}
569+
} else if (key === 'bundle') {
570+
if (typeof value !== 'string') {
571+
error = `json field 'bundle' must be a string.`;
572+
break;
573+
}
574+
bundleString = value;
575+
}
576+
}
577+
if(error) {
578+
throw new FirestoreError(
579+
Code.INVALID_ARGUMENT,
580+
error
581+
);
582+
}
583+
const serializer = newSerializer(db._databaseId);
584+
const reader = createBundleReaderSync(bundleString, serializer);
585+
const elements = reader.getElements();
586+
if (elements.length === 0) {
587+
throw new FirestoreError(
588+
Code.INVALID_ARGUMENT,
589+
'No snapshat data was found in the bundle.'
590+
);
591+
}
592+
if (
593+
elements.length !== 2 ||
594+
!elements[0].payload.documentMetadata ||
595+
!elements[1].payload.document
596+
) {
597+
throw new FirestoreError(
598+
Code.INVALID_ARGUMENT,
599+
'DocumentSnapshot bundle data must contain one document metadata and then one document'
600+
);
601+
}
602+
const docMetadata = elements[0].payload!.documentMetadata!;
603+
const docData = elements[1].payload.document!;
604+
if (docMetadata.name! !== docData.name) {
605+
throw new FirestoreError(
606+
Code.INVALID_ARGUMENT,
607+
'The document data is not related to the document metadata in the bundle'
608+
);
609+
}
610+
const bundleConverter = new BundleConverterImpl(serializer);
611+
const bundledDoc = {
612+
metadata: docMetadata,
613+
document: docData
614+
};
615+
const documentSnapshotData = bundleConverter.toDocumentSnapshotData(
616+
docMetadata,
617+
bundledDoc
618+
);
619+
const liteUserDataWriter = new LiteUserDataWriter(db);
620+
return new DocumentSnapshot(
621+
db,
622+
liteUserDataWriter,
623+
documentSnapshotData.documentKey,
624+
documentSnapshotData.mutableDoc,
625+
new SnapshotMetadata(
626+
/* hasPendingWrites= */ false,
627+
/* fromCache= */ false
628+
),
629+
null
630+
);
631+
}
546632
}
547633

548634
/**

packages/firestore/src/core/bundle_impl.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { MutableDocument } from '../model/document';
2727
import { DocumentKey } from '../model/document_key';
2828
import { ResourcePath } from '../model/path';
2929
import {
30+
BundledDocumentMetadata,
3031
BundleMetadata as ProtoBundleMetadata,
3132
NamedQuery as ProtoNamedQuery
3233
} from '../protos/firestore_bundle_proto';
@@ -76,6 +77,23 @@ export class BundleConverterImpl implements BundleConverter {
7677
}
7778
}
7879

80+
toDocumentSnapshotData(docMetadata: BundledDocumentMetadata, bundledDoc: BundledDocument) : {
81+
documentKey: DocumentKey,
82+
mutableDoc: MutableDocument,
83+
} {
84+
const bundleConverter = new BundleConverterImpl(this.serializer);
85+
const documentKey = bundleConverter.toDocumentKey(docMetadata.name!);
86+
const mutableDoc = bundleConverter.toMutableDocument(bundledDoc);
87+
mutableDoc.setReadTime(
88+
bundleConverter.toSnapshotVersion(docMetadata.readTime!)
89+
);
90+
91+
return {
92+
documentKey,
93+
mutableDoc
94+
};
95+
}
96+
7997
toSnapshotVersion(time: ApiTimestamp): SnapshotVersion {
8098
return fromVersion(time);
8199
}

packages/firestore/src/core/firestore_client.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,9 @@ import { JsonProtoSerializer } from '../remote/serializer';
5353
import { debugAssert } from '../util/assert';
5454
import { AsyncObserver } from '../util/async_observer';
5555
import { AsyncQueue, wrapInUserErrorIfRecoverable } from '../util/async_queue';
56-
import { BundleReader } from '../util/bundle_reader';
56+
import { BundleReader, BundleReaderSync } from '../util/bundle_reader';
5757
import { newBundleReader } from '../util/bundle_reader_impl';
58+
import { newBundleReaderSync } from '../util/bundle_reader_sync_impl';
5859
import { Code, FirestoreError } from '../util/error';
5960
import { logDebug, logWarn } from '../util/log';
6061
import { AutoId } from '../util/misc';
@@ -97,6 +98,8 @@ import { TransactionRunner } from './transaction_runner';
9798
import { View } from './view';
9899
import { ViewSnapshot } from './view_snapshot';
99100

101+
import { ExpUserDataWriter } from '../api/reference_impl';
102+
100103
const LOG_TAG = 'FirestoreClient';
101104
export const MAX_CONCURRENT_LIMBO_RESOLUTIONS = 100;
102105

@@ -799,16 +802,6 @@ export function firestoreClientLoadBundle(
799802
});
800803
}
801804

802-
export function firestoreClientLoadDodcumentSnapshotBundle(
803-
client: FirestoreClient,
804-
databaseId: DatabaseId,
805-
data: string
806-
): object {
807-
const reader = createBundleReader(data, newSerializer(databaseId));
808-
const encodedContent: Uint8Array = newTextEncoder().encode(data);
809-
return {};
810-
}
811-
812805
export function firestoreClientGetNamedQuery(
813806
client: FirestoreClient,
814807
queryName: string
@@ -831,6 +824,13 @@ export function createBundleReader(
831824
return newBundleReader(toByteStreamReader(content), serializer);
832825
}
833826

827+
export function createBundleReaderSync(
828+
bundleData: string,
829+
serializer: JsonProtoSerializer
830+
): BundleReaderSync {
831+
return newBundleReaderSync(bundleData, serializer);
832+
}
833+
834834
export function firestoreClientSetIndexConfiguration(
835835
client: FirestoreClient,
836836
indexes: FieldIndex[]

packages/firestore/src/util/bundle_reader.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,7 @@ export class SizedBundleElement {
3030
public readonly payload: BundleElement,
3131
// How many bytes this element takes to store in the bundle.
3232
public readonly byteLength: number
33-
) {
34-
console.log("DEDB new sizedBundle element.");
35-
console.log("payload: ", payload);
36-
console.log("byteLength: ", byteLength);
37-
}
33+
) {}
3834

3935
isBundleMetadata(): boolean {
4036
return 'metadata' in this.payload;
@@ -73,8 +69,8 @@ export interface BundleReader {
7369
/**
7470
* A class representing a bundle.
7571
*
76-
* Takes a bundle stream or buffer, and presents abstractions to read bundled
77-
* elements out of the underlying content.
72+
* Takes a bundle string buffer, parses the data, and provides accessors to the data contained
73+
* within it.
7874
*/
7975
export interface BundleReaderSync {
8076
serializer: JsonProtoSerializer;
@@ -85,9 +81,8 @@ export interface BundleReaderSync {
8581
getMetadata(): BundleMetadata;
8682

8783
/**
88-
* Returns the next BundleElement (together with its byte size in the bundle)
89-
* that has not been read from underlying ReadableStream. Returns null if we
90-
* have reached the end of the stream.
84+
* Returns BundleElements parsed from the bundle. Returns an empty array if no bundle elements
85+
* exist.
9186
*/
9287
getElements(): Array<SizedBundleElement>;
9388
}

packages/firestore/src/util/bundle_reader_sync_impl.ts

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { BundleReaderSync, SizedBundleElement } from './bundle_reader';
2323
/**
2424
* A class that can parse a bundle form the string serialization of a bundle.
2525
*/
26-
class BundleReaderSyncImpl implements BundleReaderSync {
26+
export class BundleReaderSyncImpl implements BundleReaderSync {
2727
private metadata: BundleMetadata;
2828
private elements: Array<SizedBundleElement>;
2929
private cursor: number;
@@ -35,78 +35,78 @@ class BundleReaderSyncImpl implements BundleReaderSync {
3535
this.elements = new Array<SizedBundleElement>();
3636

3737
let element = this.nextElement();
38+
console.error('DEEDB Element (metadata): ', element);
3839
if (element && element.isBundleMetadata()) {
3940
this.metadata = element as BundleMetadata;
4041
} else {
4142
throw new Error(`The first element of the bundle is not a metadata, it is
4243
${JSON.stringify(element?.payload)}`);
4344
}
4445

45-
element = this.nextElement();
46-
while(element !== null) {
47-
this.elements.push(element);
48-
}
46+
do {
47+
element = this.nextElement();
48+
if (element !== null) {
49+
console.error('DEDB parsed element: ', element);
50+
this.elements.push(element);
51+
} else {
52+
console.error('DEDB no more elements.');
53+
}
54+
} while (element !== null);
4955
}
50-
56+
5157
/* Returns the parsed metadata of the bundle. */
52-
getMetadata() : BundleMetadata {
58+
getMetadata(): BundleMetadata {
5359
return this.metadata;
5460
}
5561

5662
/* Returns the DocumentSnapshot or NamedQuery elements of the bundle. */
57-
getElements() : Array<SizedBundleElement> {
63+
getElements(): Array<SizedBundleElement> {
5864
return this.elements;
5965
}
6066

6167
/**
6268
* Parses the next element of the bundle.
6369
*
64-
* @returns a SizedBundleElement representation of the next element in the bundle, or null if
70+
* @returns a SizedBundleElement representation of the next element in the bundle, or null if
6571
* no more elements exist.
6672
*/
67-
private nextElement() : SizedBundleElement | null {
68-
if(this.cursor === this.bundleData.length) {
73+
private nextElement(): SizedBundleElement | null {
74+
if (this.cursor === this.bundleData.length) {
6975
return null;
7076
}
71-
72-
const length = this.readLength();
77+
const length: number = this.readLength();
7378
const jsonString = this.readJsonString(length);
74-
75-
return new SizedBundleElement(
76-
JSON.parse(jsonString),
77-
length
78-
);
79+
return new SizedBundleElement(JSON.parse(jsonString), length);
7980
}
8081

8182
/**
8283
* Reads from a specified position from the bundleData string, for a specified
8384
* number of bytes.
84-
*
85+
*
8586
* @param length how many characters to read.
8687
* @returns a string parsed from the bundle.
8788
*/
8889
private readJsonString(length: number): string {
89-
if(this.cursor + length > this.bundleData.length) {
90+
if (this.cursor + length > this.bundleData.length) {
9091
throw new Error('Reached the end of bundle when more is expected.');
9192
}
92-
const result = this.bundleData.slice(this.cursor, length);
93-
this.cursor += length;
93+
const result = this.bundleData.slice(this.cursor, (this.cursor += length));
9494
return result;
9595
}
9696

9797
/**
98-
* Reads from the current cursor until the first '{'.
99-
*
98+
* Reads from the current cursor until the first '{'.
99+
*
100100
* @returns A string to integer represention of the parsed value.
101101
* @throws An {@link Error} if the cursor has reached the end of the stream, since lengths
102102
* prefix bundle objects.
103103
*/
104-
private readLength(): number {
104+
private readLength(): number {
105105
const startIndex = this.cursor;
106106
let curIndex = this.cursor;
107-
while(curIndex < this.bundleData.length) {
108-
if(this.bundleData[curIndex] === '{') {
109-
if(curIndex === startIndex) {
107+
while (curIndex < this.bundleData.length) {
108+
if (this.bundleData[curIndex] === '{') {
109+
if (curIndex === startIndex) {
110110
throw new Error('First character is a bracket and not a number');
111111
}
112112
this.cursor = curIndex;

0 commit comments

Comments
 (0)