Skip to content

Commit c923efa

Browse files
schmidt-sebastianmikelehen
authored andcommitted
Ensure consistent JSON Serialization of DocumentKeys (#1596)
Stop relying on JSON.stringify() as part of generating canonicalId() since we haven't made any effort to implement sane toJSON() methods on our types. We now use toString() instead, which required implementing it on SortedMap.
1 parent a738a9d commit c923efa

File tree

8 files changed

+95
-8
lines changed

8 files changed

+95
-8
lines changed

packages/firestore/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
While this feature is not yet available, all schema changes are included
99
in this release. Once you upgrade, you will not be able to use an older version
1010
of the Firestore SDK with persistence enabled.
11+
- [fixed] Fixed an issue with IndexedDb persistence that triggered an internal
12+
assert for Queries that use nested DocumentReferences in where() clauses
13+
(#1524, #1596).
1114

1215
# 1.0.4
1316
- [fixed] Fixed an uncaught promise error occurring when `enablePersistence()`

packages/firestore/src/core/sync_engine.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ export class SyncEngine implements RemoteSyncer, SharedClientStateSyncer {
204204

205205
const queryView = this.queryViewsByQuery.get(query);
206206
if (queryView) {
207-
// PORTING NOTE: With Mult-Tab Web, it is possible that a query view
207+
// PORTING NOTE: With Multi-Tab Web, it is possible that a query view
208208
// already exists when EventManager calls us for the first time. This
209209
// happens when the primary tab is already listening to this query on
210210
// behalf of another tab and the user of the primary also starts listening

packages/firestore/src/model/field_value.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ export class ObjectValue extends FieldValue {
614614
}
615615

616616
toString(): string {
617-
return JSON.stringify(this.value());
617+
return this.internalValue.toString();
618618
}
619619

620620
private child(childName: string): FieldValue | undefined {
@@ -688,6 +688,7 @@ export class ArrayValue extends FieldValue {
688688
}
689689

690690
toString(): string {
691-
return JSON.stringify(this.value());
691+
const descriptions = this.internalValue.map(v => v.toString());
692+
return `[${descriptions.join(',')}]`;
692693
}
693694
}

packages/firestore/src/model/path.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const DOCUMENT_KEY_NAME = '__name__';
2323
/**
2424
* Path represents an ordered sequence of string segments.
2525
*/
26-
export abstract class Path {
26+
abstract class Path {
2727
private segments: string[];
2828
private offset: number;
2929
private len: number;
@@ -32,6 +32,14 @@ export abstract class Path {
3232
this.init(segments, offset, length);
3333
}
3434

35+
/**
36+
* Returns a String representation.
37+
*
38+
* Implementing classes are required to provide deterministic implementations as
39+
* the String representation is used to obtain canonical Query IDs.
40+
*/
41+
abstract toString(): string;
42+
3543
/**
3644
* An initialization method that can be called from outside the constructor.
3745
* We need this so that we can have a non-static construct method that returns

packages/firestore/src/util/sorted_map.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,15 @@ export class SortedMap<K, V> {
143143
});
144144
}
145145

146+
toString(): string {
147+
const descriptions: string[] = [];
148+
this.inorderTraversal((k, v) => {
149+
descriptions.push(`${k}:${v}`);
150+
return false;
151+
});
152+
return `{${descriptions.join(', ')}}`;
153+
}
154+
146155
// Traverses the map in reverse key order and calls the specified action
147156
// function for each key/value pair. If action returns true, traversal is
148157
// aborted.

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ import {
3232
withTestDb
3333
} from '../util/helpers';
3434

35-
const Timestamp = firebase.firestore!.Timestamp;
35+
const Blob = firebase.firestore!.Blob;
3636
const FieldPath = firebase.firestore!.FieldPath;
37+
const GeoPoint = firebase.firestore!.GeoPoint;
38+
const Timestamp = firebase.firestore!.Timestamp;
3739

3840
// TODO(b/116617988): Use public API.
3941
interface FirestoreInternal extends firestore.FirebaseFirestore {
@@ -694,4 +696,41 @@ apiDescribe('Queries', persistence => {
694696
expect(querySnapshot.docs.map(d => d.id)).to.deep.equal(['cg-doc2']);
695697
});
696698
});
699+
700+
it('can query custom types', () => {
701+
return withTestCollection(persistence, {}, async ref => {
702+
const data = {
703+
ref: ref.firestore.doc('f/c'),
704+
geoPoint: new GeoPoint(0, 0),
705+
buffer: Blob.fromBase64String('Zm9v'),
706+
time: Timestamp.now(),
707+
array: [
708+
ref.firestore.doc('f/c'),
709+
new GeoPoint(0, 0),
710+
Blob.fromBase64String('Zm9v'),
711+
Timestamp.now()
712+
]
713+
};
714+
await ref.add({ data });
715+
716+
// In https://github.com/firebase/firebase-js-sdk/issues/1524, a
717+
// customer was not able to unlisten from a query that contained a
718+
// nested object with a DocumentReference. The cause of it was that our
719+
// serialization of nested references via JSON.stringify() was different
720+
// for Queries created via the API layer versus Queries read from
721+
// persistence. To simulate this issue, we have to listen and unlisten
722+
// to the same query twice.
723+
const query = ref.where('data', '==', data);
724+
725+
for (let i = 0; i < 2; ++i) {
726+
const deferred = new Deferred();
727+
const unsubscribe = query.onSnapshot(snapshot => {
728+
expect(snapshot.size).to.equal(1);
729+
deferred.resolve();
730+
});
731+
await deferred.promise;
732+
unsubscribe();
733+
}
734+
});
735+
});
697736
});

packages/firestore/test/unit/core/query.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,27 @@ describe('Query', () => {
383383
// .addOrderBy(orderBy(DOCUMENT_KEY_NAME, 'desc'))
384384
// .withUpperBound(lip3, 'exclusive');
385385

386+
const relativeReference = ref('project1/database1', 'col/doc');
387+
const absoluteReference = ref(
388+
'project1/database1',
389+
'projects/project1/databases/database1/documents/col/doc',
390+
5
391+
);
392+
393+
const q16a = Query.atPath(path('foo')).addFilter(
394+
filter('object', '==', { ref: relativeReference })
395+
);
396+
const q16b = Query.atPath(path('foo')).addFilter(
397+
filter('object', '==', { ref: absoluteReference })
398+
);
399+
400+
const q17a = Query.atPath(path('foo')).addFilter(
401+
filter('array', '==', [relativeReference])
402+
);
403+
const q17b = Query.atPath(path('foo')).addFilter(
404+
filter('array', '==', [absoluteReference])
405+
);
406+
386407
const queries = [
387408
[q1a, q1b],
388409
[q2a, q2b],
@@ -396,9 +417,11 @@ describe('Query', () => {
396417
[q10a],
397418
[q11a],
398419
[q12a],
399-
[q13a]
420+
[q13a],
400421
//[q14a],
401422
//[q15a],
423+
[q16a, q16b],
424+
[q17a, q17b]
402425
];
403426

404427
expectEqualitySets(queries, (q1, q2) => {

packages/firestore/test/util/helpers.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,14 @@ export function version(v: TestSnapshotVersion): SnapshotVersion {
105105
return SnapshotVersion.fromMicroseconds(v);
106106
}
107107

108-
export function ref(dbIdStr: string, keyStr: string): DocumentKeyReference {
108+
export function ref(
109+
dbIdStr: string,
110+
keyStr: string,
111+
offset?: number
112+
): DocumentKeyReference {
109113
const [project, database] = dbIdStr.split('/', 2);
110114
const dbId = new DatabaseId(project, database);
111-
return new DocumentKeyReference(dbId, key(keyStr));
115+
return new DocumentKeyReference(dbId, new DocumentKey(path(keyStr, offset)));
112116
}
113117

114118
export function doc(

0 commit comments

Comments
 (0)