Skip to content

Commit 524d7d8

Browse files
committed
add in changes from stephenarosaj/fdc-impersonate
1 parent 40325df commit 524d7d8

File tree

4 files changed

+232
-51
lines changed

4 files changed

+232
-51
lines changed

src/data-connect/data-connect-api-client-internal.ts

Lines changed: 186 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,31 @@ import { ConnectorConfig, ExecuteGraphqlResponse, GraphqlOptions } from './data-
2727

2828
const API_VERSION = 'v1';
2929

30-
/** The Firebase Data Connect backend base URL format. */
31-
const FIREBASE_DATA_CONNECT_BASE_URL_FORMAT =
32-
'https://firebasedataconnect.googleapis.com/{version}/projects/{projectId}/locations/{locationId}/services/{serviceId}:{endpointId}';
33-
34-
/** Firebase Data Connect base URl format when using the Data Connect emultor. */
35-
const FIREBASE_DATA_CONNECT_EMULATOR_BASE_URL_FORMAT =
30+
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
31+
// TODO: CHANGE THIS BACK TO PROD - AUTOPUSH IS ONLY USED FOR LOCAL TESTING BEFORE CHANGES PROPAGATE
32+
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
33+
/** The Firebase Data Connect backend service URL format. */
34+
const FIREBASE_DATA_CONNECT_SERVICES_URL_FORMAT =
35+
'https://autopush-firebasedataconnect.sandbox.googleapis.com/{version}/projects/{projectId}/locations/{locationId}/services/{serviceId}:{endpointId}';
36+
37+
/** The Firebase Data Connect backend connector URL format. */
38+
const FIREBASE_DATA_CONNECT_CONNECTORS_URL_FORMAT =
39+
'https://autopush-firebasedataconnect.sandbox.googleapis.com/{version}/projects/{projectId}/locations/{locationId}/services/{serviceId}/connectors/{connectorId}:{endpointId}';
40+
41+
/** Firebase Data Connect service URL format when using the Data Connect emulator. */
42+
const FIREBASE_DATA_CONNECT_EMULATOR_SERVICES_URL_FORMAT =
3643
'http://{host}/{version}/projects/{projectId}/locations/{locationId}/services/{serviceId}:{endpointId}';
3744

45+
/** Firebase Data Connect connector URL format when using the Data Connect emulator. */
46+
const FIREBASE_DATA_CONNECT_EMULATOR_CONNECTORS_URL_FORMAT =
47+
'http://{host}/{version}/projects/{projectId}/locations/{locationId}/services/{serviceId}/connectors/{connectorId}:{endpointId}';
48+
3849
const EXECUTE_GRAPH_QL_ENDPOINT = 'executeGraphql';
3950
const EXECUTE_GRAPH_QL_READ_ENDPOINT = 'executeGraphqlRead';
4051

52+
const IMPERSONATE_QUERY_ENDPOINT = 'impersonateQuery';
53+
const IMPERSONATE_MUTATION_ENDPOINT = 'impersonateMutation';
54+
4155
const DATA_CONNECT_CONFIG_HEADERS = {
4256
'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`
4357
};
@@ -89,6 +103,15 @@ export class DataConnectApiClient {
89103
return this.executeGraphqlHelper(query, EXECUTE_GRAPH_QL_READ_ENDPOINT, options);
90104
}
91105

106+
107+
/**
108+
* A helper function to execute GraphQL queries.
109+
*
110+
* @param query - The arbitrary GraphQL query to execute.
111+
* @param endpoint - The endpoint to call.
112+
* @param options - The GraphQL options.
113+
* @returns A promise that fulfills with the GraphQL response, or throws an error.
114+
*/
92115
private async executeGraphqlHelper<GraphqlResponse, Variables>(
93116
query: string,
94117
endpoint: string,
@@ -112,24 +135,8 @@ export class DataConnectApiClient {
112135
...(options?.operationName && { operationName: options?.operationName }),
113136
...(options?.impersonate && { extensions: { impersonate: options?.impersonate } }),
114137
};
115-
return this.getUrl(API_VERSION, this.connectorConfig.location, this.connectorConfig.serviceId, endpoint)
116-
.then(async (url) => {
117-
const request: HttpRequestConfig = {
118-
method: 'POST',
119-
url,
120-
headers: DATA_CONNECT_CONFIG_HEADERS,
121-
data,
122-
};
123-
const resp = await this.httpClient.send(request);
124-
if (resp.data.errors && validator.isNonEmptyArray(resp.data.errors)) {
125-
const allMessages = resp.data.errors.map((error: { message: any; }) => error.message).join(' ');
126-
throw new FirebaseDataConnectError(
127-
DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR, allMessages);
128-
}
129-
return Promise.resolve({
130-
data: resp.data.data as GraphqlResponse,
131-
});
132-
})
138+
const url = await this.getUrl(API_VERSION, this.connectorConfig.location, this.connectorConfig.serviceId, endpoint);
139+
return this.makeGqlRequest<GraphqlResponse>(url, data)
133140
.then((resp) => {
134141
return resp;
135142
})
@@ -138,28 +145,138 @@ export class DataConnectApiClient {
138145
});
139146
}
140147

141-
private async getUrl(version: string, locationId: string, serviceId: string, endpointId: string): Promise<string> {
142-
return this.getProjectId()
143-
.then((projectId) => {
144-
const urlParams = {
145-
version,
146-
projectId,
147-
locationId,
148-
serviceId,
149-
endpointId
150-
};
151-
let urlFormat: string;
152-
if (useEmulator()) {
153-
urlFormat = utils.formatString(FIREBASE_DATA_CONNECT_EMULATOR_BASE_URL_FORMAT, {
154-
host: emulatorHost()
155-
});
156-
} else {
157-
urlFormat = FIREBASE_DATA_CONNECT_BASE_URL_FORMAT;
158-
}
159-
return utils.formatString(urlFormat, urlParams);
148+
/**
149+
* Executes a GraphQL query with impersonation.
150+
*
151+
* @param options - The GraphQL options. Must include impersonation details.
152+
* @returns A promise that fulfills with the GraphQL response.
153+
*/
154+
public async executeQuery<GraphqlResponse, Variables>(
155+
options: GraphqlOptions<Variables>
156+
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
157+
return this.executeOperationHelper(IMPERSONATE_QUERY_ENDPOINT, options);
158+
}
159+
160+
/**
161+
* Executes a GraphQL mutation with impersonation.
162+
*
163+
* @param options - The GraphQL options. Must include impersonation details.
164+
* @returns A promise that fulfills with the GraphQL response.
165+
*/
166+
public async executeMutation<GraphqlResponse, Variables>(
167+
options: GraphqlOptions<Variables>
168+
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
169+
return this.executeOperationHelper(IMPERSONATE_MUTATION_ENDPOINT, options);
170+
}
171+
172+
/**
173+
* A helper function to execute operations by making requests to FDC's impersonate
174+
* operations endpoints.
175+
*
176+
* @param endpoint - The endpoint to call.
177+
* @param options - The GraphQL options, including impersonation details.
178+
* @returns A promise that fulfills with the GraphQL response.
179+
*/
180+
private async executeOperationHelper<GraphqlResponse, Variables>(
181+
endpoint: string,
182+
options: GraphqlOptions<Variables>
183+
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
184+
if (
185+
typeof options.operationName === 'undefined' ||
186+
!validator.isNonEmptyString(options.operationName)
187+
) {
188+
throw new FirebaseDataConnectError(
189+
DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,
190+
'`options.operationName` must be a non-empty string.'
191+
);
192+
}
193+
if (
194+
typeof options.impersonate === 'undefined' ||
195+
!validator.isNonNullObject(options?.impersonate)
196+
) {
197+
throw new FirebaseDataConnectError(
198+
DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,
199+
'`options.impersonate` must be a non-null object.'
200+
);
201+
}
202+
203+
if (this.connectorConfig.connector === undefined || this.connectorConfig.connector === '') {
204+
throw new FirebaseDataConnectError(
205+
DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,
206+
`The 'connectorConfig.connector' field used to instantiate your Data Connect
207+
instance must be a non-empty string (the connectorId) when calling executeQuery or executeMutation.`);
208+
}
209+
210+
const data = {
211+
...(options.variables && { variables: options?.variables }),
212+
operationName: options.operationName,
213+
extensions: { impersonate: options.impersonate },
214+
};
215+
const url = await this.getUrl(
216+
API_VERSION,
217+
this.connectorConfig.location,
218+
this.connectorConfig.serviceId,
219+
endpoint,
220+
this.connectorConfig.connector,
221+
);
222+
return this.makeGqlRequest<GraphqlResponse>(url, data)
223+
.then((resp) => {
224+
return resp;
225+
})
226+
.catch((err) => {
227+
throw this.toFirebaseError(err);
160228
});
161229
}
162230

231+
/**
232+
* Constructs the URL for a Data Connect backend request.
233+
*
234+
* If no connectorId is provided, will direct the request to an endpoint under services:
235+
* .../services/{serviceId}:endpoint
236+
*
237+
* If connectorId is provided, will direct the request to an endpoint under connectors:
238+
* .../services/{serviceId}/connectors/{connectorId}:endpoint
239+
*
240+
* @param version - The API version.
241+
* @param locationId - The location of the Data Connect service.
242+
* @param serviceId - The ID of the Data Connect service.
243+
* @param endpointId - The endpoint to call.
244+
* @param connectorId - The ID of the connector, if applicable.
245+
* @returns A promise that fulfills with the constructed URL.
246+
*/
247+
private async getUrl(
248+
version: string,
249+
locationId: string,
250+
serviceId: string,
251+
endpointId: string,
252+
connectorId?: string,
253+
): Promise<string> {
254+
const projectId = await this.getProjectId();
255+
const urlParams = {
256+
version,
257+
projectId,
258+
locationId,
259+
serviceId,
260+
endpointId,
261+
connectorId
262+
};
263+
let urlFormat: string;
264+
if (useEmulator()) {
265+
(urlParams as any).host = emulatorHost();
266+
urlFormat = connectorId === undefined || connectorId === ''
267+
? FIREBASE_DATA_CONNECT_EMULATOR_SERVICES_URL_FORMAT
268+
: FIREBASE_DATA_CONNECT_EMULATOR_CONNECTORS_URL_FORMAT;
269+
} else {
270+
urlFormat = connectorId === undefined || connectorId === ''
271+
? FIREBASE_DATA_CONNECT_SERVICES_URL_FORMAT
272+
: FIREBASE_DATA_CONNECT_CONNECTORS_URL_FORMAT;
273+
}
274+
if (connectorId) {
275+
(urlParams as any).connectorId = connectorId;
276+
}
277+
return utils.formatString(urlFormat, urlParams);
278+
}
279+
163280
private getProjectId(): Promise<string> {
164281
if (this.projectId) {
165282
return Promise.resolve(this.projectId);
@@ -178,6 +295,32 @@ export class DataConnectApiClient {
178295
});
179296
}
180297

298+
/**
299+
* Makes a GraphQL request to the specified url.
300+
*
301+
* @param url - The URL to send the request to.
302+
* @param data - The GraphQL request payload.
303+
* @returns A promise that fulfills with the GraphQL response, or throws an error.
304+
*/
305+
private async makeGqlRequest<GraphqlResponse>(url: string, data: object):
306+
Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
307+
const request: HttpRequestConfig = {
308+
method: 'POST',
309+
url,
310+
headers: DATA_CONNECT_CONFIG_HEADERS,
311+
data,
312+
};
313+
const resp = await this.httpClient.send(request);
314+
if (resp.data.errors && validator.isNonEmptyArray(resp.data.errors)) {
315+
const allMessages = resp.data.errors.map((error: { message: any; }) => error.message).join(' ');
316+
throw new FirebaseDataConnectError(
317+
DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR, allMessages);
318+
}
319+
return Promise.resolve({
320+
data: resp.data.data as GraphqlResponse,
321+
});
322+
}
323+
181324
private toFirebaseError(err: RequestResponseError): PrefixedFirebaseError {
182325
if (err instanceof PrefixedFirebaseError) {
183326
return err;

src/data-connect/data-connect-api.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export interface ConnectorConfig {
3030
* Service ID of the Data Connect service.
3131
*/
3232
serviceId: string;
33+
34+
/**
35+
* Name of the Data Connect connector.
36+
* Required for operations that interact with connectors, such as executeQuery and executeMutation.
37+
*/
38+
connector?: string;
3339
}
3440

3541
/**
@@ -52,7 +58,10 @@ export interface GraphqlOptions<Variables> {
5258
variables?: Variables;
5359

5460
/**
55-
* The name of the GraphQL operation. Required only if `query` contains multiple operations.
61+
* The name of the GraphQL operation.
62+
* Required for operations that interact with connectors, such as executeQuery and executeMutation.
63+
* Required for operations that interact with services, such as executeGraphql, if
64+
* `query` contains multiple operations.
5665
*/
5766
operationName?: string;
5867

src/data-connect/data-connect.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,20 +88,48 @@ export class DataConnect {
8888
}
8989

9090
/**
91-
* Execute an arbitrary read-only GraphQL query
92-
*
93-
* @param query - The GraphQL read-only query.
94-
* @param options - Optional {@link GraphqlOptions} when executing a read-only GraphQL query.
95-
*
96-
* @returns A promise that fulfills with a `ExecuteGraphqlResponse`.
97-
*/
91+
* Execute an arbitrary read-only GraphQL query
92+
*
93+
* @param query - The GraphQL read-only query.
94+
* @param options - Optional {@link GraphqlOptions} when executing a read-only GraphQL query.
95+
*
96+
* @returns A promise that fulfills with a `ExecuteGraphqlResponse`.
97+
*/
9898
public executeGraphqlRead<GraphqlResponse, Variables>(
9999
query: string,
100100
options?: GraphqlOptions<Variables>,
101101
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
102102
return this.client.executeGraphqlRead(query, options);
103103
}
104104

105+
/**
106+
* Executes a pre-defined GraphQL query with impersonation.
107+
*
108+
* The query must be defined in your Data Connect GraphQL files.
109+
*
110+
* @param options - The GraphQL options, must include operationName and impersonation details.
111+
* @returns A promise that fulfills with the GraphQL response.
112+
*/
113+
public async executeQuery<GraphqlResponse, Variables>(
114+
options: GraphqlOptions<Variables>
115+
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
116+
return this.client.executeQuery(options);
117+
}
118+
119+
/**
120+
* Executes a pre-defined GraphQL mutation with impersonation.
121+
*
122+
* The mutation must be defined in your Data Connect GQL files.
123+
*
124+
* @param options - The GraphQL options, must include operationName and impersonation details.
125+
* @returns A promise that fulfills with the GraphQL response.
126+
*/
127+
public async executeMutation<GraphqlResponse, Variables>(
128+
options: GraphqlOptions<Variables>
129+
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
130+
return this.client.executeMutation(options);
131+
}
132+
105133
/**
106134
* Insert a single row into the specified table.
107135
*

src/data-connect/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export {
5151
* const connectorConfig: ConnectorConfig = {
5252
* location: 'us-west2',
5353
* serviceId: 'my-service',
54+
* connectorName: 'my-connector',
5455
* };
5556
*
5657
* // Get the `DataConnect` service for the default app

0 commit comments

Comments
 (0)