Skip to content

Commit f478845

Browse files
Update invokeRun*QueryRpc functions to support paths with special characters (#7402)
Update invokeRun*QueryRpc functions to support paths with special characters. --------- Co-authored-by: Denver Coneybeare <[email protected]>
1 parent f854abe commit f478845

16 files changed

+220
-68
lines changed

.changeset/shaggy-garlics-leave.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@firebase/firestore": patch
3+
---
4+
5+
Support special characters in query paths sent to `getCountFromServer(...)`, `getCount(...)` (lite API), and `getDocs(...)` (lite API).

packages/firestore/src/local/local_serializer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ export function toDbTarget(
285285
queryProto = toQueryTarget(
286286
localSerializer.remoteSerializer,
287287
targetData.target
288-
);
288+
).queryTarget;
289289
}
290290

291291
// We can't store the resumeToken as a ByteString in IndexedDb, so we

packages/firestore/src/model/path.ts

+9
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,15 @@ export class ResourcePath extends BasePath<ResourcePath> {
215215
return this.canonicalString();
216216
}
217217

218+
/**
219+
* Returns a string representation of this path
220+
* where each path segment has been encoded with
221+
* `encodeURIComponent`.
222+
*/
223+
toUriEncodedString(): string {
224+
return this.toArray().map(encodeURIComponent).join('/');
225+
}
226+
218227
/**
219228
* Creates a resource path from the given slash-delimited string. If multiple
220229
* arguments are provided, all components are combined. Leading and trailing

packages/firestore/src/platform/node/grpc_connection.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import * as grpc from '@grpc/grpc-js';
2222
import { Token } from '../../api/credentials';
2323
import { DatabaseInfo } from '../../core/database_info';
2424
import { SDK_VERSION } from '../../core/version';
25+
import { ResourcePath } from '../../model/path';
2526
import { Connection, Stream } from '../../remote/connection';
2627
import { mapCodeFromRpcCode } from '../../remote/rpc_error';
2728
import { StreamBridge } from '../../remote/stream_bridge';
@@ -114,7 +115,7 @@ export class GrpcConnection implements Connection {
114115

115116
invokeRPC<Req, Resp>(
116117
rpcName: string,
117-
path: string,
118+
path: ResourcePath,
118119
request: Req,
119120
authToken: Token | null,
120121
appCheckToken: Token | null
@@ -166,7 +167,7 @@ export class GrpcConnection implements Connection {
166167

167168
invokeStreamingRPC<Req, Resp>(
168169
rpcName: string,
169-
path: string,
170+
path: ResourcePath,
170171
request: Req,
171172
authToken: Token | null,
172173
appCheckToken: Token | null,

packages/firestore/src/remote/connection.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717

1818
import { Token } from '../api/credentials';
19+
import { ResourcePath } from '../model/path';
1920
import { FirestoreError } from '../util/error';
2021

2122
/**
@@ -38,14 +39,15 @@ export interface Connection {
3839
* representing the JSON to send.
3940
*
4041
* @param rpcName - the name of the RPC to invoke
41-
* @param path - the path to invoke this RPC on
42+
* @param path - the path to invoke this RPC on. An array of path segments
43+
* that will be encoded and joined with path separators when required.
4244
* @param request - the Raw JSON object encoding of the request message
4345
* @param token - the Token to use for the RPC.
4446
* @returns a Promise containing the JSON object encoding of the response
4547
*/
4648
invokeRPC<Req, Resp>(
4749
rpcName: string,
48-
path: string,
50+
path: ResourcePath,
4951
request: Req,
5052
authToken: Token | null,
5153
appCheckToken: Token | null
@@ -57,15 +59,16 @@ export interface Connection {
5759
* completion and then returned as an array.
5860
*
5961
* @param rpcName - the name of the RPC to invoke
60-
* @param path - the path to invoke this RPC on
62+
* @param path - the path to invoke this RPC on. An array of path segments
63+
* that will be encoded and joined with path separators when required.
6164
* @param request - the Raw JSON object encoding of the request message
6265
* @param token - the Token to use for the RPC.
6366
* @returns a Promise containing an array with the JSON object encodings of the
6467
* responses
6568
*/
6669
invokeStreamingRPC<Req, Resp>(
6770
rpcName: string,
68-
path: string,
71+
path: ResourcePath,
6972
request: Req,
7073
authToken: Token | null,
7174
appCheckToken: Token | null,

packages/firestore/src/remote/datastore.ts

+37-14
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
import { CredentialsProvider } from '../api/credentials';
1919
import { User } from '../auth/user';
2020
import { Aggregate } from '../core/aggregate';
21+
import { DatabaseId } from '../core/database_info';
2122
import { queryToAggregateTarget, Query, queryToTarget } from '../core/query';
2223
import { Document } from '../model/document';
2324
import { DocumentKey } from '../model/document_key';
2425
import { Mutation } from '../model/mutation';
26+
import { ResourcePath } from '../model/path';
2527
import {
2628
ApiClientObjectMap,
2729
BatchGetDocumentsRequest as ProtoBatchGetDocumentsRequest,
@@ -47,11 +49,11 @@ import {
4749
import {
4850
fromDocument,
4951
fromBatchGetDocumentsResponse,
50-
getEncodedDatabaseId,
5152
JsonProtoSerializer,
5253
toMutation,
5354
toName,
5455
toQueryTarget,
56+
toResourcePath,
5557
toRunAggregationQueryRequest
5658
} from './serializer';
5759

@@ -94,7 +96,8 @@ class DatastoreImpl extends Datastore {
9496
/** Invokes the provided RPC with auth and AppCheck tokens. */
9597
invokeRPC<Req, Resp>(
9698
rpcName: string,
97-
path: string,
99+
databaseId: DatabaseId,
100+
resourcePath: ResourcePath,
98101
request: Req
99102
): Promise<Resp> {
100103
this.verifyInitialized();
@@ -105,7 +108,7 @@ class DatastoreImpl extends Datastore {
105108
.then(([authToken, appCheckToken]) => {
106109
return this.connection.invokeRPC<Req, Resp>(
107110
rpcName,
108-
path,
111+
toResourcePath(databaseId, resourcePath),
109112
request,
110113
authToken,
111114
appCheckToken
@@ -127,7 +130,8 @@ class DatastoreImpl extends Datastore {
127130
/** Invokes the provided RPC with streamed results with auth and AppCheck tokens. */
128131
invokeStreamingRPC<Req, Resp>(
129132
rpcName: string,
130-
path: string,
133+
databaseId: DatabaseId,
134+
resourcePath: ResourcePath,
131135
request: Req,
132136
expectedResponseCount?: number
133137
): Promise<Resp[]> {
@@ -139,7 +143,7 @@ class DatastoreImpl extends Datastore {
139143
.then(([authToken, appCheckToken]) => {
140144
return this.connection.invokeStreamingRPC<Req, Resp>(
141145
rpcName,
142-
path,
146+
toResourcePath(databaseId, resourcePath),
143147
request,
144148
authToken,
145149
appCheckToken,
@@ -186,26 +190,35 @@ export async function invokeCommitRpc(
186190
mutations: Mutation[]
187191
): Promise<void> {
188192
const datastoreImpl = debugCast(datastore, DatastoreImpl);
189-
const path = getEncodedDatabaseId(datastoreImpl.serializer) + '/documents';
190193
const request = {
191194
writes: mutations.map(m => toMutation(datastoreImpl.serializer, m))
192195
};
193-
await datastoreImpl.invokeRPC('Commit', path, request);
196+
await datastoreImpl.invokeRPC(
197+
'Commit',
198+
datastoreImpl.serializer.databaseId,
199+
ResourcePath.emptyPath(),
200+
request
201+
);
194202
}
195203

196204
export async function invokeBatchGetDocumentsRpc(
197205
datastore: Datastore,
198206
keys: DocumentKey[]
199207
): Promise<Document[]> {
200208
const datastoreImpl = debugCast(datastore, DatastoreImpl);
201-
const path = getEncodedDatabaseId(datastoreImpl.serializer) + '/documents';
202209
const request = {
203210
documents: keys.map(k => toName(datastoreImpl.serializer, k))
204211
};
205212
const response = await datastoreImpl.invokeStreamingRPC<
206213
ProtoBatchGetDocumentsRequest,
207214
ProtoBatchGetDocumentsResponse
208-
>('BatchGetDocuments', path, request, keys.length);
215+
>(
216+
'BatchGetDocuments',
217+
datastoreImpl.serializer.databaseId,
218+
ResourcePath.emptyPath(),
219+
request,
220+
keys.length
221+
);
209222

210223
const docs = new Map<string, Document>();
211224
response.forEach(proto => {
@@ -226,11 +239,16 @@ export async function invokeRunQueryRpc(
226239
query: Query
227240
): Promise<Document[]> {
228241
const datastoreImpl = debugCast(datastore, DatastoreImpl);
229-
const request = toQueryTarget(datastoreImpl.serializer, queryToTarget(query));
242+
const { queryTarget, parent } = toQueryTarget(
243+
datastoreImpl.serializer,
244+
queryToTarget(query)
245+
);
230246
const response = await datastoreImpl.invokeStreamingRPC<
231247
ProtoRunQueryRequest,
232248
ProtoRunQueryResponse
233-
>('RunQuery', request.parent!, { structuredQuery: request.structuredQuery });
249+
>('RunQuery', datastoreImpl.serializer.databaseId, parent, {
250+
structuredQuery: queryTarget.structuredQuery
251+
});
234252
return (
235253
response
236254
// Omit RunQueryResponses that only contain readTimes.
@@ -247,20 +265,25 @@ export async function invokeRunAggregationQueryRpc(
247265
aggregates: Aggregate[]
248266
): Promise<ApiClientObjectMap<Value>> {
249267
const datastoreImpl = debugCast(datastore, DatastoreImpl);
250-
const { request, aliasMap } = toRunAggregationQueryRequest(
268+
const { request, aliasMap, parent } = toRunAggregationQueryRequest(
251269
datastoreImpl.serializer,
252270
queryToAggregateTarget(query),
253271
aggregates
254272
);
255273

256-
const parent = request.parent;
257274
if (!datastoreImpl.connection.shouldResourcePathBeIncludedInRequest) {
258275
delete request.parent;
259276
}
260277
const response = await datastoreImpl.invokeStreamingRPC<
261278
ProtoRunAggregationQueryRequest,
262279
ProtoRunAggregationQueryResponse
263-
>('RunAggregationQuery', parent!, request, /*expectedResponseCount=*/ 1);
280+
>(
281+
'RunAggregationQuery',
282+
datastoreImpl.serializer.databaseId,
283+
parent,
284+
request,
285+
/*expectedResponseCount=*/ 1
286+
);
264287

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

packages/firestore/src/remote/rest_connection.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
DatabaseInfo,
2323
DEFAULT_DATABASE_NAME
2424
} from '../core/database_info';
25+
import { ResourcePath } from '../model/path';
2526
import { debugAssert } from '../util/assert';
2627
import { generateUniqueDebugId } from '../util/debug_uid';
2728
import { FirestoreError } from '../util/error';
@@ -82,13 +83,13 @@ export abstract class RestConnection implements Connection {
8283

8384
invokeRPC<Req, Resp>(
8485
rpcName: string,
85-
path: string,
86+
path: ResourcePath,
8687
req: Req,
8788
authToken: Token | null,
8889
appCheckToken: Token | null
8990
): Promise<Resp> {
9091
const streamId = generateUniqueDebugId();
91-
const url = this.makeUrl(rpcName, path);
92+
const url = this.makeUrl(rpcName, path.toUriEncodedString());
9293
logDebug(LOG_TAG, `Sending RPC '${rpcName}' ${streamId}:`, url, req);
9394

9495
const headers: StringMap = {
@@ -119,7 +120,7 @@ export abstract class RestConnection implements Connection {
119120

120121
invokeStreamingRPC<Req, Resp>(
121122
rpcName: string,
122-
path: string,
123+
path: ResourcePath,
123124
request: Req,
124125
authToken: Token | null,
125126
appCheckToken: Token | null,

0 commit comments

Comments
 (0)