Skip to content

Commit 1d76d33

Browse files
committed
Update invokeRun*QueryRpc functions to support paths with special characters.
1 parent 114bc6e commit 1d76d33

File tree

4 files changed

+142
-23
lines changed

4 files changed

+142
-23
lines changed

packages/firestore/src/remote/datastore.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ import {
5252
toMutation,
5353
toName,
5454
toQueryTarget,
55-
toRunAggregationQueryRequest
55+
toRunAggregationQueryRequest,
56+
toQueryTargetPath
5657
} from './serializer';
5758

5859
/**
@@ -225,11 +226,13 @@ export async function invokeRunQueryRpc(
225226
query: Query
226227
): Promise<Document[]> {
227228
const datastoreImpl = debugCast(datastore, DatastoreImpl);
228-
const request = toQueryTarget(datastoreImpl.serializer, queryToTarget(query));
229+
const target = queryToTarget(query);
230+
const request = toQueryTarget(datastoreImpl.serializer, target);
231+
const encodedPath = toQueryTargetPath(datastoreImpl.serializer, target, true);
229232
const response = await datastoreImpl.invokeStreamingRPC<
230233
ProtoRunQueryRequest,
231234
ProtoRunQueryResponse
232-
>('RunQuery', request.parent!, { structuredQuery: request.structuredQuery });
235+
>('RunQuery', encodedPath, { structuredQuery: request.structuredQuery });
233236
return (
234237
response
235238
// Omit RunQueryResponses that only contain readTimes.
@@ -246,20 +249,21 @@ export async function invokeRunAggregationQueryRpc(
246249
aggregates: Aggregate[]
247250
): Promise<ApiClientObjectMap<Value>> {
248251
const datastoreImpl = debugCast(datastore, DatastoreImpl);
252+
const target = queryToTarget(query);
249253
const { request, aliasMap } = toRunAggregationQueryRequest(
250254
datastoreImpl.serializer,
251-
queryToTarget(query),
255+
target,
252256
aggregates
253257
);
258+
const encodedPath = toQueryTargetPath(datastoreImpl.serializer, target, true);
254259

255-
const parent = request.parent;
256260
if (!datastoreImpl.connection.shouldResourcePathBeIncludedInRequest) {
257261
delete request.parent;
258262
}
259263
const response = await datastoreImpl.invokeStreamingRPC<
260264
ProtoRunAggregationQueryRequest,
261265
ProtoRunAggregationQueryResponse
262-
>('RunAggregationQuery', parent!, request, /*expectedResponseCount=*/ 1);
266+
>('RunAggregationQuery', encodedPath, request, /*expectedResponseCount=*/ 1);
263267

264268
// Omit RunAggregationQueryResponse that only contain readTimes.
265269
const filteredResult = response.filter(proto => !!proto.result);

packages/firestore/src/remote/serializer.ts

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -283,12 +283,21 @@ export function fromVersion(version: ProtoTimestamp): SnapshotVersion {
283283

284284
export function toResourceName(
285285
databaseId: DatabaseId,
286-
path: ResourcePath
286+
path: ResourcePath,
287+
uriEncoded: boolean = false
287288
): string {
288-
return fullyQualifiedPrefixPath(databaseId)
289+
const resourcePath = fullyQualifiedPrefixPath(databaseId)
289290
.child('documents')
290-
.child(path)
291-
.canonicalString();
291+
.child(path);
292+
293+
if (uriEncoded) {
294+
return resourcePath
295+
.toArray()
296+
.map(segment => encodeURIComponent(segment))
297+
.join('/');
298+
} else {
299+
return resourcePath.toArray().join('/');
300+
}
292301
}
293302

294303
function fromResourceName(name: string): ResourcePath {
@@ -337,9 +346,10 @@ export function fromName(
337346

338347
function toQueryPath(
339348
serializer: JsonProtoSerializer,
340-
path: ResourcePath
349+
path: ResourcePath,
350+
urlEncoded: boolean = false
341351
): string {
342-
return toResourceName(serializer.databaseId, path);
352+
return toResourceName(serializer.databaseId, path, urlEncoded);
343353
}
344354

345355
function fromQueryPath(name: string): ResourcePath {
@@ -841,24 +851,16 @@ export function toQueryTarget(
841851
// Dissect the path into parent, collectionId, and optional key filter.
842852
const result: ProtoQueryTarget = { structuredQuery: {} };
843853
const path = target.path;
854+
result.parent = toQueryTargetPath(serializer, target, false);
855+
844856
if (target.collectionGroup !== null) {
845-
debugAssert(
846-
path.length % 2 === 0,
847-
'Collection Group queries should be within a document path or root.'
848-
);
849-
result.parent = toQueryPath(serializer, path);
850857
result.structuredQuery!.from = [
851858
{
852859
collectionId: target.collectionGroup,
853860
allDescendants: true
854861
}
855862
];
856863
} else {
857-
debugAssert(
858-
path.length % 2 !== 0,
859-
'Document queries with filters are not supported.'
860-
);
861-
result.parent = toQueryPath(serializer, path.popLast());
862864
result.structuredQuery!.from = [{ collectionId: path.lastSegment() }];
863865
}
864866

@@ -887,6 +889,36 @@ export function toQueryTarget(
887889
return result;
888890
}
889891

892+
function getQueryParentResourcePath(
893+
serializer: JsonProtoSerializer,
894+
target: Target
895+
): ResourcePath {
896+
const path = target.path;
897+
898+
if (target.collectionGroup !== null) {
899+
debugAssert(
900+
path.length % 2 === 0,
901+
'Collection Group queries should be within a document path or root.'
902+
);
903+
return path;
904+
} else {
905+
debugAssert(
906+
path.length % 2 !== 0,
907+
'Document queries with filters are not supported.'
908+
);
909+
return path.popLast();
910+
}
911+
}
912+
913+
export function toQueryTargetPath(
914+
serializer: JsonProtoSerializer,
915+
target: Target,
916+
urlEncoded: boolean
917+
): string {
918+
const parentResourcePath = getQueryParentResourcePath(serializer, target);
919+
return toQueryPath(serializer, parentResourcePath, urlEncoded);
920+
}
921+
890922
export function toRunAggregationQueryRequest(
891923
serializer: JsonProtoSerializer,
892924
target: Target,

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ import {
3232
writeBatch,
3333
count,
3434
sum,
35-
average
35+
average,
36+
addDoc,
37+
setLogLevel
3638
} from '../util/firebase_export';
3739
import {
3840
apiDescribe,
@@ -54,6 +56,37 @@ apiDescribe('Count queries', (persistence: boolean) => {
5456
});
5557
});
5658

59+
it('can run count query getCountFromServer with + in document name', () => {
60+
setLogLevel('debug');
61+
const testDocs = {
62+
'a+1': { author: 'authorA' },
63+
'b1': { author: 'authorB' },
64+
'c1': { author: 'authorC' }
65+
};
66+
return withTestCollection(persistence, testDocs, async coll => {
67+
const subColl1 = collection(coll, 'a+1/sub');
68+
await addDoc(subColl1, { foo: 'bar' });
69+
await addDoc(subColl1, { foo: 'baz' });
70+
71+
const subColl2 = collection(coll, 'b1/su+b');
72+
await addDoc(subColl2, { foo: 'bar' });
73+
await addDoc(subColl2, { foo: 'baz' });
74+
75+
const subColl3 = collection(coll, 'c1/sub');
76+
await addDoc(subColl3, { foo: 'bar' });
77+
await addDoc(subColl3, { foo: 'baz' });
78+
79+
const snapshot1 = await getCountFromServer(subColl1);
80+
expect(snapshot1.data().count).to.equal(2);
81+
82+
const snapshot2 = await getCountFromServer(subColl2);
83+
expect(snapshot2.data().count).to.equal(2);
84+
85+
const snapshot3 = await getCountFromServer(subColl3);
86+
expect(snapshot3.data().count).to.equal(2);
87+
});
88+
});
89+
5790
it("count query doesn't use converter", () => {
5891
const testDocs = {
5992
a: { author: 'authorA', title: 'titleA' },

packages/firestore/test/lite/integration.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,27 @@ describe('Query', () => {
11181118
);
11191119
});
11201120
});
1121+
1122+
it('supports query over collection path with special characters', () => {
1123+
return withTestCollection(async collRef => {
1124+
const docWithSpecials = doc(collRef, 'so!@#$%^&*()_+special');
1125+
await setDoc(docWithSpecials, {});
1126+
1127+
const collectionWithSpecials = collection(
1128+
docWithSpecials,
1129+
'so!@#$%^&*()_+special'
1130+
);
1131+
await addDoc(collectionWithSpecials, { foo: 1 });
1132+
await addDoc(collectionWithSpecials, { foo: 2 });
1133+
1134+
const result = await getDocs(
1135+
query(collectionWithSpecials, orderBy('foo', 'asc'))
1136+
);
1137+
1138+
expect(result.size).to.equal(2);
1139+
verifyResults(result, { foo: 1 }, { foo: 2 });
1140+
});
1141+
});
11211142
});
11221143

11231144
describe('equality', () => {
@@ -2131,6 +2152,35 @@ describe('Count queries', () => {
21312152
});
21322153
});
21332154

2155+
it('can run count query getCountFromServer with + in document name', () => {
2156+
return withTestCollection(async coll => {
2157+
await setDoc(doc(coll, 'a+1'), {});
2158+
await setDoc(doc(coll, 'b1'), {});
2159+
await setDoc(doc(coll, 'c1'), {});
2160+
2161+
const subColl1 = collection(coll, 'a+1/sub');
2162+
await addDoc(subColl1, { foo: 'bar' });
2163+
await addDoc(subColl1, { foo: 'baz' });
2164+
2165+
const subColl2 = collection(coll, 'b1/su+b');
2166+
await addDoc(subColl2, { foo: 'bar' });
2167+
await addDoc(subColl2, { foo: 'baz' });
2168+
2169+
const subColl3 = collection(coll, 'c1/sub');
2170+
await addDoc(subColl3, { foo: 'bar' });
2171+
await addDoc(subColl3, { foo: 'baz' });
2172+
2173+
const snapshot1 = await getCount(subColl1);
2174+
expect(snapshot1.data().count).to.equal(2);
2175+
2176+
const snapshot2 = await getCount(subColl2);
2177+
expect(snapshot2.data().count).to.equal(2);
2178+
2179+
const snapshot3 = await getCount(subColl3);
2180+
expect(snapshot3.data().count).to.equal(2);
2181+
});
2182+
});
2183+
21342184
it('run count query on empty collection', () => {
21352185
return withTestCollection(async coll => {
21362186
const snapshot = await getCount(coll);

0 commit comments

Comments
 (0)