Skip to content

Commit 6dfb8b8

Browse files
Merge branch 'master' into mrschmidt/rewritefieldvalue
2 parents 806f0a1 + b739763 commit 6dfb8b8

File tree

14 files changed

+305
-150
lines changed

14 files changed

+305
-150
lines changed

README.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,12 @@ $ yarn build
8888
A production project is required to test the Firebase JS SDK. You can create a
8989
new project by visiting the [Firebase Console](https://console.firebase.google.com/).
9090

91-
#### Firestore Support
91+
#### Firestore Database Setup
9292

93-
Visit the database section of the console and enable the Cloud Firestore Beta.
94-
You can select either of the default permissions settings as we will overwrite
95-
them below.
93+
Visit the "Database" section of the console and create a Cloud Firestore
94+
database. When prompted to select the set of initial security rules, select
95+
any option (e.g. "Start in Production Mode") since these permission settings
96+
will be overwritten below.
9697

9798
#### Authentication Support
9899

@@ -101,17 +102,25 @@ sign-in provider to complete your project config.
101102

102103
#### Automated Setup
103104

104-
The remainder of the test setup requires choosing a test project. You can
105-
choose the project manually or specify the project directly at the root of
106-
the package.
105+
The tests need to be configured to use the Firebase production project that you
106+
created in the "Test Setup" section above. To do this, run the `yarn test:setup`
107+
command, as follows:
108+
107109

108110
```bash
109-
# Select a project manually when running setup
111+
# Select the Firebase project via the text-based UI.
110112
$ yarn test:setup
111113

112-
# Specify the specific project for setup
113-
$ yarn test:setup --projectId=<your-test-project>
114+
# Specify the Firebase project via the command-line arguments.
115+
$ yarn test:setup --projectId=<test_firebase_project_id>
116+
```
117+
118+
If you see an error like
119+
```
120+
HTTP Error: 404, Project '<test_firebase_project_id>' does not exist.
114121
```
122+
then make sure that you have created the database as specified in the "Firestore
123+
Database Setup" section above.
115124

116125
### Running the tests
117126

packages/firestore/src/api/user_data_converter.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ export class UserDataConverter {
404404
validatePlainObject('Data must be an object, but it was:', context, input);
405405

406406
let fieldMaskPaths = new SortedSet<FieldPath>(FieldPath.comparator);
407-
let updateData = ObjectValue.EMPTY;
407+
const updateData = ObjectValue.newBuilder();
408408
forEach(input as Dict<unknown>, (key, value) => {
409409
const path = fieldPathFromDotSeparatedString(methodName, key);
410410

@@ -417,13 +417,17 @@ export class UserDataConverter {
417417
const parsedValue = this.parseData(value, childContext);
418418
if (parsedValue != null) {
419419
fieldMaskPaths = fieldMaskPaths.add(path);
420-
updateData = updateData.set(path, parsedValue);
420+
updateData.set(path, parsedValue);
421421
}
422422
}
423423
});
424424

425425
const mask = FieldMask.fromSet(fieldMaskPaths);
426-
return new ParsedUpdateData(updateData, mask, context.fieldTransforms);
426+
return new ParsedUpdateData(
427+
updateData.build(),
428+
mask,
429+
context.fieldTransforms
430+
);
427431
}
428432

429433
/** Parse update data from a list of field/value arguments. */
@@ -460,7 +464,7 @@ export class UserDataConverter {
460464
}
461465

462466
let fieldMaskPaths = new SortedSet<FieldPath>(FieldPath.comparator);
463-
let updateData = ObjectValue.EMPTY;
467+
const updateData = ObjectValue.newBuilder();
464468

465469
for (let i = 0; i < keys.length; ++i) {
466470
const path = keys[i];
@@ -473,13 +477,17 @@ export class UserDataConverter {
473477
const parsedValue = this.parseData(value, childContext);
474478
if (parsedValue != null) {
475479
fieldMaskPaths = fieldMaskPaths.add(path);
476-
updateData = updateData.set(path, parsedValue);
480+
updateData.set(path, parsedValue);
477481
}
478482
}
479483
}
480484

481485
const mask = FieldMask.fromSet(fieldMaskPaths);
482-
return new ParsedUpdateData(updateData, mask, context.fieldTransforms);
486+
return new ParsedUpdateData(
487+
updateData.build(),
488+
mask,
489+
context.fieldTransforms
490+
);
483491
}
484492

485493
/**

packages/firestore/src/model/document.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,11 @@ export class Document extends MaybeDocument {
119119

120120
data(): ObjectValue {
121121
if (!this.objectValue) {
122-
let result = ObjectValue.EMPTY;
122+
const result = ObjectValue.newBuilder();
123123
obj.forEach(this.proto!.fields || {}, (key: string, value: api.Value) => {
124-
result = result.set(new FieldPath([key]), this.converter!(value));
124+
result.set(new FieldPath([key]), this.converter!(value));
125125
});
126-
this.objectValue = result;
126+
this.objectValue = result.build();
127127

128128
// Once objectValue is computed, values inside the fieldValueCache are no
129129
// longer accessed.

packages/firestore/src/model/field_value.ts

Lines changed: 86 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ export class BooleanValue extends FieldValue {
203203

204204
compareTo(other: FieldValue): number {
205205
if (other instanceof BooleanValue) {
206-
return primitiveComparator(this, other);
206+
return primitiveComparator(this.internalValue, other.internalValue);
207207
}
208208
return this.defaultCompareTo(other);
209209
}
@@ -558,6 +558,11 @@ export class ObjectValue extends FieldValue {
558558
super();
559559
}
560560

561+
/** Returns a new ObjectValueBuilder instance that is based on an empty object. */
562+
static newBuilder(): ObjectValueBuilder {
563+
return new ObjectValueBuilder(ObjectValue.EMPTY.internalValue);
564+
}
565+
561566
value(options?: FieldValueOptions): JsonObject<FieldType> {
562567
const result: JsonObject<FieldType> = {};
563568
this.internalValue.inorderTraversal((key, val) => {
@@ -610,42 +615,6 @@ export class ObjectValue extends FieldValue {
610615
}
611616
}
612617

613-
set(path: FieldPath, to: FieldValue): ObjectValue {
614-
assert(!path.isEmpty(), 'Cannot set field for empty path on ObjectValue');
615-
if (path.length === 1) {
616-
return this.setChild(path.firstSegment(), to);
617-
} else {
618-
let child = this.child(path.firstSegment());
619-
if (!(child instanceof ObjectValue)) {
620-
child = ObjectValue.EMPTY;
621-
}
622-
const newChild = (child as ObjectValue).set(path.popFirst(), to);
623-
return this.setChild(path.firstSegment(), newChild);
624-
}
625-
}
626-
627-
delete(path: FieldPath): ObjectValue {
628-
assert(
629-
!path.isEmpty(),
630-
'Cannot delete field for empty path on ObjectValue'
631-
);
632-
if (path.length === 1) {
633-
return new ObjectValue(this.internalValue.remove(path.firstSegment()));
634-
} else {
635-
// nested field
636-
const child = this.child(path.firstSegment());
637-
if (child instanceof ObjectValue) {
638-
const newChild = child.delete(path.popFirst());
639-
return new ObjectValue(
640-
this.internalValue.insert(path.firstSegment(), newChild)
641-
);
642-
} else {
643-
// Don't actually change a primitive value to an object for a delete
644-
return this;
645-
}
646-
}
647-
}
648-
649618
contains(path: FieldPath): boolean {
650619
return this.field(path) !== null;
651620
}
@@ -703,17 +672,90 @@ export class ObjectValue extends FieldValue {
703672
return this.internalValue.toString();
704673
}
705674

706-
private child(childName: string): FieldValue | undefined {
707-
return this.internalValue.get(childName) || undefined;
675+
static EMPTY = new ObjectValue(
676+
new SortedMap<string, FieldValue>(primitiveComparator)
677+
);
678+
679+
/** Creates a ObjectValueBuilder instance that is based on the current value. */
680+
toBuilder(): ObjectValueBuilder {
681+
return new ObjectValueBuilder(this.internalValue);
682+
}
683+
}
684+
685+
/**
686+
* An ObjectValueBuilder provides APIs to set and delete fields from an
687+
* ObjectValue. All operations mutate the existing instance.
688+
*/
689+
export class ObjectValueBuilder {
690+
constructor(private internalValue: SortedMap<string, FieldValue>) {}
691+
692+
/**
693+
* Sets the field to the provided value.
694+
*
695+
* @param path The field path to set.
696+
* @param value The value to set.
697+
* @return The current Builder instance.
698+
*/
699+
set(path: FieldPath, value: FieldValue): ObjectValueBuilder {
700+
assert(!path.isEmpty(), 'Cannot set field for empty path on ObjectValue');
701+
const childName = path.firstSegment();
702+
if (path.length === 1) {
703+
this.internalValue = this.internalValue.insert(childName, value);
704+
} else {
705+
// nested field
706+
const child = this.internalValue.get(childName);
707+
let obj: ObjectValue;
708+
if (child instanceof ObjectValue) {
709+
obj = child;
710+
} else {
711+
obj = ObjectValue.EMPTY;
712+
}
713+
const newChild = obj
714+
.toBuilder()
715+
.set(path.popFirst(), value)
716+
.build();
717+
this.internalValue = this.internalValue.insert(childName, newChild);
718+
}
719+
return this;
708720
}
709721

710-
private setChild(childName: string, value: FieldValue): ObjectValue {
711-
return new ObjectValue(this.internalValue.insert(childName, value));
722+
/**
723+
* Removes the field at the current path. If there is no field at the
724+
* specified path, nothing is changed.
725+
*
726+
* @param path The field path to remove
727+
* @return The current Builder instance.
728+
*/
729+
delete(path: FieldPath): ObjectValueBuilder {
730+
assert(
731+
!path.isEmpty(),
732+
'Cannot delete field for empty path on ObjectValue'
733+
);
734+
const childName = path.firstSegment();
735+
if (path.length === 1) {
736+
this.internalValue = this.internalValue.remove(childName);
737+
} else {
738+
// nested field
739+
const child = this.internalValue.get(childName);
740+
if (child instanceof ObjectValue) {
741+
const newChild = child
742+
.toBuilder()
743+
.delete(path.popFirst())
744+
.build();
745+
this.internalValue = this.internalValue.insert(
746+
path.firstSegment(),
747+
newChild
748+
);
749+
} else {
750+
// Don't actually change a primitive value to an object for a delete
751+
}
752+
}
753+
return this;
712754
}
713755

714-
static EMPTY = new ObjectValue(
715-
new SortedMap<string, FieldValue>(primitiveComparator)
716-
);
756+
build(): ObjectValue {
757+
return new ObjectValue(this.internalValue);
758+
}
717759
}
718760

719761
export class ArrayValue extends FieldValue {

packages/firestore/src/model/mutation.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
UnknownDocument
2929
} from './document';
3030
import { DocumentKey } from './document_key';
31-
import { FieldValue, ObjectValue } from './field_value';
31+
import { FieldValue, ObjectValue, ObjectValueBuilder } from './field_value';
3232
import { FieldPath } from './path';
3333
import { TransformOperation } from './transform_operation';
3434

@@ -507,17 +507,18 @@ export class PatchMutation extends Mutation {
507507
}
508508

509509
private patchObject(data: ObjectValue): ObjectValue {
510+
const builder = data.toBuilder();
510511
this.fieldMask.fields.forEach(fieldPath => {
511512
if (!fieldPath.isEmpty()) {
512513
const newValue = this.data.field(fieldPath);
513514
if (newValue !== null) {
514-
data = data.set(fieldPath, newValue);
515+
builder.set(fieldPath, newValue);
515516
} else {
516-
data = data.delete(fieldPath);
517+
builder.delete(fieldPath);
517518
}
518519
}
519520
});
520-
return data;
521+
return builder.build();
521522
}
522523
}
523524

@@ -611,7 +612,7 @@ export class TransformMutation extends Mutation {
611612
}
612613

613614
extractBaseValue(maybeDoc: MaybeDocument | null): ObjectValue | null {
614-
let baseObject: ObjectValue | null = null;
615+
let baseObject: ObjectValueBuilder | null = null;
615616
for (const fieldTransform of this.fieldTransforms) {
616617
const existingValue =
617618
maybeDoc instanceof Document
@@ -623,7 +624,7 @@ export class TransformMutation extends Mutation {
623624

624625
if (coercedValue != null) {
625626
if (baseObject == null) {
626-
baseObject = ObjectValue.EMPTY.set(
627+
baseObject = ObjectValue.newBuilder().set(
627628
fieldTransform.field,
628629
coercedValue
629630
);
@@ -632,7 +633,7 @@ export class TransformMutation extends Mutation {
632633
}
633634
}
634635
}
635-
return baseObject;
636+
return baseObject ? baseObject.build() : null;
636637
}
637638

638639
isEqual(other: Mutation): boolean {
@@ -749,12 +750,13 @@ export class TransformMutation extends Mutation {
749750
'TransformResults length mismatch.'
750751
);
751752

753+
const builder = data.toBuilder();
752754
for (let i = 0; i < this.fieldTransforms.length; i++) {
753755
const fieldTransform = this.fieldTransforms[i];
754756
const fieldPath = fieldTransform.field;
755-
data = data.set(fieldPath, transformResults[i]);
757+
builder.set(fieldPath, transformResults[i]);
756758
}
757-
return data;
759+
return builder.build();
758760
}
759761
}
760762

packages/firestore/src/platform_browser/webchannel_connection.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,6 @@ export class WebChannelConnection implements Connection {
228228
];
229229
const webchannelTransport = createWebChannelTransport();
230230
const request: WebChannelOptions = {
231-
// Background channel test avoids the initial two test calls and decreases
232-
// initial cold start time.
233-
// TODO(dimond): wenboz@ mentioned this might affect use with proxies and
234-
// we should monitor closely for any reports.
235-
backgroundChannelTest: true,
236231
// Required for backend stickiness, routing behavior is based on this
237232
// parameter.
238233
httpSessionIdParam: 'gsessionid',

packages/firestore/src/remote/serializer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -577,11 +577,11 @@ export class JsonProtoSerializer {
577577
fromFields(object: {}): fieldValue.ObjectValue {
578578
// Proto map<string, Value> gets mapped to Object, so cast it.
579579
const map = object as { [key: string]: api.Value };
580-
let result = fieldValue.ObjectValue.EMPTY;
580+
const result = fieldValue.ObjectValue.newBuilder();
581581
obj.forEach(map, (key, value) => {
582-
result = result.set(new FieldPath([key]), this.fromValue(value));
582+
result.set(new FieldPath([key]), this.fromValue(value));
583583
});
584-
return result;
584+
return result.build();
585585
}
586586

587587
toMapValue(map: fieldValue.ObjectValue): api.MapValue {

0 commit comments

Comments
 (0)