@@ -27,17 +27,31 @@ import { ConnectorConfig, ExecuteGraphqlResponse, GraphqlOptions } from './data-
2727
2828const 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+
3849const EXECUTE_GRAPH_QL_ENDPOINT = 'executeGraphql' ;
3950const EXECUTE_GRAPH_QL_READ_ENDPOINT = 'executeGraphqlRead' ;
4051
52+ const IMPERSONATE_QUERY_ENDPOINT = 'impersonateQuery' ;
53+ const IMPERSONATE_MUTATION_ENDPOINT = 'impersonateMutation' ;
54+
4155const 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 ;
0 commit comments