Skip to content

Commit 2cd9d7c

Browse files
Don't normalize Proto data during deepClone (#5147)
1 parent 1d10bba commit 2cd9d7c

File tree

8 files changed

+134
-88
lines changed

8 files changed

+134
-88
lines changed

.changeset/thirty-suits-switch.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"firebase": patch
3+
"@firebase/firestore": patch
4+
---
5+
6+
Fixed an issue that prevented Timestamps from being used via `update()` when connected to the Emulator

packages/firestore/src/exp/database.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
* limitations under the License.
1616
*/
1717

18+
// eslint-disable-next-line import/no-extraneous-dependencies
1819
import {
1920
_getProvider,
2021
_removeServiceInstance,
2122
FirebaseApp,
2223
getApp
23-
// eslint-disable-next-line import/no-extraneous-dependencies
2424
} from '@firebase/app-exp';
2525
import { FirebaseAuthInternalName } from '@firebase/auth-interop-types';
2626
import { Provider } from '@firebase/component';

packages/firestore/src/lite/database.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
* limitations under the License.
1616
*/
1717

18+
// eslint-disable-next-line import/no-extraneous-dependencies
1819
import {
1920
_getProvider,
2021
_removeServiceInstance,
2122
FirebaseApp,
2223
getApp
23-
// eslint-disable-next-line import/no-extraneous-dependencies
2424
} from '@firebase/app-exp';
2525
import { FirebaseAuthInternalName } from '@firebase/auth-interop-types';
2626
import { Provider } from '@firebase/component';

packages/firestore/src/model/values.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -559,8 +559,11 @@ export function isMapValue(
559559
export function deepClone(source: Value): Value {
560560
if (source.geoPointValue) {
561561
return { geoPointValue: { ...source.geoPointValue } };
562-
} else if (source.timestampValue) {
563-
return { timestampValue: { ...normalizeTimestamp(source.timestampValue) } };
562+
} else if (
563+
source.timestampValue &&
564+
typeof source.timestampValue === 'object'
565+
) {
566+
return { timestampValue: { ...source.timestampValue } };
564567
} else if (source.mapValue) {
565568
const target: Value = { mapValue: { fields: {} } };
566569
forEach(

packages/firestore/test/integration/api/type.test.ts

+69-72
Original file line numberDiff line numberDiff line change
@@ -33,49 +33,59 @@ apiDescribe('Firestore', (persistence: boolean) => {
3333
async function expectRoundtrip(
3434
db: firestore.FirebaseFirestore,
3535
data: {},
36-
validateSnapshots = true
37-
): Promise<void> {
36+
validateSnapshots = true,
37+
expectedData?: {}
38+
): Promise<firestore.DocumentSnapshot> {
39+
expectedData = expectedData ?? data;
40+
3841
const collection = db.collection(db.collection('a').doc().id);
3942
const doc = collection.doc();
40-
await doc.set(data);
4143

44+
await doc.set(data);
4245
let docSnapshot = await doc.get();
43-
expect(docSnapshot.data()).to.deep.equal(data);
46+
expect(docSnapshot.data()).to.deep.equal(expectedData);
47+
48+
await doc.update(data);
49+
docSnapshot = await doc.get();
50+
expect(docSnapshot.data()).to.deep.equal(expectedData);
4451

4552
// Validate that the transaction API returns the same types
4653
await db.runTransaction(async transaction => {
4754
docSnapshot = await transaction.get(doc);
48-
expect(docSnapshot.data()).to.deep.equal(data);
55+
expect(docSnapshot.data()).to.deep.equal(expectedData);
4956
});
5057

5158
if (validateSnapshots) {
5259
let querySnapshot = await collection.get();
5360
docSnapshot = querySnapshot.docs[0];
54-
expect(docSnapshot.data()).to.deep.equal(data);
61+
expect(docSnapshot.data()).to.deep.equal(expectedData);
5562

56-
const eventsAccumulator = new EventsAccumulator<firestore.QuerySnapshot>();
63+
const eventsAccumulator =
64+
new EventsAccumulator<firestore.QuerySnapshot>();
5765
const unlisten = collection.onSnapshot(eventsAccumulator.storeEvent);
5866
querySnapshot = await eventsAccumulator.awaitEvent();
5967
docSnapshot = querySnapshot.docs[0];
60-
expect(docSnapshot.data()).to.deep.equal(data);
68+
expect(docSnapshot.data()).to.deep.equal(expectedData);
6169

6270
unlisten();
6371
}
72+
73+
return docSnapshot;
6474
}
6575

6676
it('can read and write null fields', () => {
67-
return withTestDb(persistence, db => {
68-
return expectRoundtrip(db, { a: 1, b: null });
77+
return withTestDb(persistence, async db => {
78+
await expectRoundtrip(db, { a: 1, b: null });
6979
});
7080
});
7181

7282
it('can read and write number fields', () => {
73-
return withTestDb(persistence, db => {
83+
return withTestDb(persistence, async db => {
7484
// TODO(b/174486484): If we build ViewSnapshots from IndexedDb, this test
7585
// fails since we first store the backend proto in IndexedDb, which turns
7686
// -0.0 into 0.0.
7787
const validateSnapshots = !persistence;
78-
return expectRoundtrip(
88+
await expectRoundtrip(
7989
db,
8090
{ a: 1, b: NaN, c: Infinity, d: -0.0 },
8191
validateSnapshots
@@ -84,91 +94,78 @@ apiDescribe('Firestore', (persistence: boolean) => {
8494
});
8595

8696
it('can read and write array fields', () => {
87-
return withTestDb(persistence, db => {
88-
return expectRoundtrip(db, { array: [1, 'foo', { deep: true }, null] });
97+
return withTestDb(persistence, async db => {
98+
await expectRoundtrip(db, { array: [1, 'foo', { deep: true }, null] });
8999
});
90100
});
91101

92102
it('can read and write geo point fields', () => {
93-
return withTestDoc(persistence, doc => {
94-
return doc
95-
.set({
96-
geopoint1: new GeoPoint(1.23, 4.56),
97-
geopoint2: new GeoPoint(0, 0)
98-
})
99-
.then(() => {
100-
return doc.get();
101-
})
102-
.then(docSnapshot => {
103-
const latLong = docSnapshot.data()!['geopoint1'];
104-
expect(latLong instanceof GeoPoint).to.equal(true);
105-
expect(latLong.latitude).to.equal(1.23);
106-
expect(latLong.longitude).to.equal(4.56);
107-
108-
const zeroLatLong = docSnapshot.data()!['geopoint2'];
109-
expect(zeroLatLong instanceof GeoPoint).to.equal(true);
110-
expect(zeroLatLong.latitude).to.equal(0);
111-
expect(zeroLatLong.longitude).to.equal(0);
112-
});
103+
return withTestDb(persistence, async db => {
104+
const docSnapshot = await expectRoundtrip(db, {
105+
geopoint1: new GeoPoint(1.23, 4.56),
106+
geopoint2: new GeoPoint(0, 0)
107+
});
108+
109+
const latLong = docSnapshot.data()!['geopoint1'];
110+
expect(latLong instanceof GeoPoint).to.equal(true);
111+
expect(latLong.latitude).to.equal(1.23);
112+
expect(latLong.longitude).to.equal(4.56);
113+
114+
const zeroLatLong = docSnapshot.data()!['geopoint2'];
115+
expect(zeroLatLong instanceof GeoPoint).to.equal(true);
116+
expect(zeroLatLong.latitude).to.equal(0);
117+
expect(zeroLatLong.longitude).to.equal(0);
113118
});
114119
});
115120

116121
it('can read and write bytes fields', () => {
117-
return withTestDoc(persistence, doc => {
118-
return doc
119-
.set({
120-
bytes: Blob.fromUint8Array(new Uint8Array([0, 1, 255]))
121-
})
122-
.then(() => {
123-
return doc.get();
124-
})
125-
.then(docSnapshot => {
126-
const blob = docSnapshot.data()!['bytes'];
127-
// TODO(firestorexp): As part of the Compat migration, the SDK
128-
// should re-wrap the firestore-exp types into the Compat API.
129-
// Comment this change back in once this is complete (note that this
130-
// check passes in the legacy API).
131-
// expect(blob instanceof Blob).to.equal(true);
132-
expect(blob.toUint8Array()).to.deep.equal(
133-
new Uint8Array([0, 1, 255])
134-
);
135-
});
122+
return withTestDb(persistence, async db => {
123+
const docSnapshot = await expectRoundtrip(db, {
124+
bytes: Blob.fromUint8Array(new Uint8Array([0, 1, 255]))
125+
});
126+
127+
const blob = docSnapshot.data()!['bytes'];
128+
// TODO(firestorexp): As part of the Compat migration, the SDK
129+
// should re-wrap the firestore-exp types into the Compat API.
130+
// Comment this change back in once this is complete (note that this
131+
// check passes in the legacy API).
132+
// expect(blob instanceof Blob).to.equal(true);
133+
expect(blob.toUint8Array()).to.deep.equal(new Uint8Array([0, 1, 255]));
136134
});
137135
});
138136

139137
it('can read and write date fields', () => {
140-
return withTestDb(persistence, db => {
141-
const dateValue = new Date('2017-04-10T09:10:11.123Z');
142-
// Dates are returned as Timestamps, so expectRoundtrip can't be used
143-
// here.
144-
const doc = db.collection('rooms').doc();
145-
return doc
146-
.set({ date: dateValue })
147-
.then(() => doc.get())
148-
.then(snapshot => {
149-
expect(snapshot.data()).to.deep.equal({
150-
date: Timestamp.fromDate(dateValue)
151-
});
152-
});
138+
return withTestDb(persistence, async db => {
139+
const date = new Date('2017-04-10T09:10:11.123Z');
140+
// Dates are returned as Timestamps
141+
const data = { date };
142+
const expectedData = { date: Timestamp.fromDate(date) };
143+
144+
await expectRoundtrip(
145+
db,
146+
data,
147+
/* validateSnapshot= */ true,
148+
expectedData
149+
);
153150
});
154151
});
155152

156153
it('can read and write timestamp fields', () => {
157-
return withTestDb(persistence, db => {
154+
return withTestDb(persistence, async db => {
158155
const timestampValue = Timestamp.now();
159-
return expectRoundtrip(db, { timestamp: timestampValue });
156+
await expectRoundtrip(db, { timestamp: timestampValue });
160157
});
161158
});
162159

163160
it('can read and write document references', () => {
164-
return withTestDoc(persistence, doc => {
165-
return expectRoundtrip(doc.firestore, { a: 42, ref: doc });
161+
return withTestDoc(persistence, async doc => {
162+
await expectRoundtrip(doc.firestore, { a: 42, ref: doc });
166163
});
167164
});
168165

169166
it('can read and write document references in an array', () => {
170-
return withTestDoc(persistence, doc => {
171-
return expectRoundtrip(doc.firestore, { a: 42, refs: [doc] });
167+
return withTestDoc(persistence, async doc => {
168+
await expectRoundtrip(doc.firestore, { a: 42, refs: [doc] });
172169
});
173170
});
174171
});

packages/firestore/test/unit/model/values.test.ts

+39-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import {
2525
valueCompare,
2626
valueEquals,
2727
estimateByteSize,
28-
refValue
28+
refValue,
29+
deepClone
2930
} from '../../../src/model/values';
3031
import * as api from '../../../src/protos/firestore_proto_api';
3132
import { primitiveComparator } from '../../../src/util/misc';
@@ -405,4 +406,41 @@ describe('Values', () => {
405406
)
406407
).to.equal('{a:1,b:2,c:3}');
407408
});
409+
410+
it('clones properties without normalization', () => {
411+
const values = [
412+
{ integerValue: '1' },
413+
{ integerValue: 1 },
414+
{ doubleValue: '2' },
415+
{ doubleValue: 2 },
416+
{ timestampValue: '2007-04-05T14:30:01Z' },
417+
{ timestampValue: { seconds: 1175783401 } },
418+
{ timestampValue: '2007-04-05T14:30:01.999Z' },
419+
{
420+
timestampValue: { seconds: 1175783401, nanos: 999000000 }
421+
},
422+
{ timestampValue: '2007-04-05T14:30:02Z' },
423+
{ timestampValue: { seconds: 1175783402 } },
424+
{ timestampValue: '2007-04-05T14:30:02.100Z' },
425+
{
426+
timestampValue: { seconds: 1175783402, nanos: 100000000 }
427+
},
428+
{ timestampValue: '2007-04-05T14:30:02.100001Z' },
429+
{
430+
timestampValue: { seconds: 1175783402, nanos: 100001000 }
431+
},
432+
{ bytesValue: new Uint8Array([0, 1, 2]) },
433+
{ bytesValue: 'AAEC' },
434+
{ bytesValue: new Uint8Array([0, 1, 3]) },
435+
{ bytesValue: 'AAED' }
436+
];
437+
438+
for (const value of values) {
439+
expect(deepClone(value)).to.deep.equal(value);
440+
const mapValue = { mapValue: { fields: { foo: value } } };
441+
expect(deepClone(mapValue)).to.deep.equal(mapValue);
442+
const arrayValue = { arrayValue: { values: [value] } };
443+
expect(deepClone(arrayValue)).to.deep.equal(arrayValue);
444+
}
445+
});
408446
});

packages/firestore/test/unit/remote/rest_connection.test.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export class TestRestConnection extends RestConnection {
4747
body: Req
4848
): Promise<Resp> {
4949
this.lastUrl = url;
50-
this.lastRequestBody = (body as unknown) as Indexable;
50+
this.lastRequestBody = body as unknown as Indexable;
5151
this.lastHeaders = headers;
5252
const response = this.nextResponse;
5353
this.nextResponse = Promise.resolve<unknown>({});
@@ -63,7 +63,8 @@ describe('RestConnection', () => {
6363
'example.com',
6464
/*ssl=*/ false,
6565
/*forceLongPolling=*/ false,
66-
/*autoDetectLongPolling=*/ false
66+
/*autoDetectLongPolling=*/ false,
67+
/*useFetchStreams=*/ false
6768
);
6869
const connection = new TestRestConnection(testDatabaseInfo);
6970

packages/firestore/test/unit/specs/spec_test_runner.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,8 @@ abstract class TestRunner {
256256
'host',
257257
/*ssl=*/ false,
258258
/*forceLongPolling=*/ false,
259-
/*autoDetectLongPolling=*/ false
259+
/*autoDetectLongPolling=*/ false,
260+
/*useFetchStreams=*/ false
260261
);
261262

262263
// TODO(mrschmidt): During client startup in `firestore_client`, we block
@@ -297,11 +298,12 @@ abstract class TestRunner {
297298
const onlineComponentProvider = new MockOnlineComponentProvider(
298299
this.connection
299300
);
300-
const offlineComponentProvider = await this.initializeOfflineComponentProvider(
301-
onlineComponentProvider,
302-
configuration,
303-
this.useGarbageCollection
304-
);
301+
const offlineComponentProvider =
302+
await this.initializeOfflineComponentProvider(
303+
onlineComponentProvider,
304+
configuration,
305+
this.useGarbageCollection
306+
);
305307
await onlineComponentProvider.initialize(
306308
offlineComponentProvider,
307309
configuration
@@ -912,9 +914,8 @@ abstract class TestRunner {
912914
this.expectedActiveLimboDocs = expectedState.activeLimboDocs!.map(key);
913915
}
914916
if ('enqueuedLimboDocs' in expectedState) {
915-
this.expectedEnqueuedLimboDocs = expectedState.enqueuedLimboDocs!.map(
916-
key
917-
);
917+
this.expectedEnqueuedLimboDocs =
918+
expectedState.enqueuedLimboDocs!.map(key);
918919
}
919920
if ('activeTargets' in expectedState) {
920921
this.expectedActiveTargets.clear();

0 commit comments

Comments
 (0)