-
Notifications
You must be signed in to change notification settings - Fork 409
feat(dc): Add executeQuery and executeMutation APIs to Data Connect #2979
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
stephenarosaj
wants to merge
32
commits into
master
Choose a base branch
from
rosa/impersonate
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
524d7d8
add in changes from stephenarosaj/fdc-impersonate
stephenarosaj bc92c5e
finish adding in changes from stephenarosaj/fdc-impersonate
stephenarosaj d43588e
update Google Inc. to Google LLC, run npm install; npm run build
stephenarosaj dcd493f
run npm apidocs
stephenarosaj 23fe1f4
remove public execute apis
stephenarosaj ae8096a
convert executeOperation api to OperationRef(...).execute() api
stephenarosaj 52a18d7
remove internal client from operation refs
stephenarosaj 3cb6645
cleanup javadocs to address workflow failures
stephenarosaj bad9808
npm run apidocs
stephenarosaj 6bdef60
spread GraphqlOptions arguments in OperationRefs and executeOperation…
stephenarosaj 798c2dc
convert unit tests to use spread args
stephenarosaj 85a6b4b
convert integration tests to use spread args
stephenarosaj 5f34343
add executeQuery test cases which do not provide impersonation option…
stephenarosaj 088d882
add executeMutation test cases which do not provide impersonation opt…
stephenarosaj 216b3ac
run npm apidocs
stephenarosaj fb5a3de
address try/catch comment
stephenarosaj fd4ffb5
address await and reject grouping comment
stephenarosaj b118905
address getUrl comments
stephenarosaj cf72f38
address insecureReason comment
stephenarosaj 43f223d
convert autopush resources to prod
stephenarosaj 2c642fb
add RefOptions, [Operation,Query,Mutation]Ref, [Operation,Query,Mutat…
stephenarosaj 12f1258
revert OperationRef.execute() API to executeOperation API
stephenarosaj 60649e4
revert OperationRef.execute() API to executeOperation API
stephenarosaj 918c4a0
revert tests to use DataConnect.executeOperation() API instead of Ope…
stephenarosaj 4dad488
revert package version
stephenarosaj 786c75f
merge master into rosa/impersonate
stephenarosaj 89e691a
update executeOperation API to return executeOperationResponse
stephenarosaj 5721657
update comments
stephenarosaj fd61f1a
add invalidateAdminArgs to handle variadic JS executeOperation arguments
stephenarosaj 104a1c5
npm run apidocs for validateAdminArgs
stephenarosaj 652c059
update validateAdminArgs documentation
stephenarosaj 79b55eb
address validateAdminArgs and some test comments
stephenarosaj File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,21 +23,32 @@ import { | |
import { PrefixedFirebaseError } from '../utils/error'; | ||
import * as utils from '../utils/index'; | ||
import * as validator from '../utils/validator'; | ||
import { ConnectorConfig, ExecuteGraphqlResponse, GraphqlOptions } from './data-connect-api'; | ||
import { ConnectorConfig, ExecuteGraphqlResponse, GraphqlOptions, OperationOptions } from './data-connect-api'; | ||
|
||
const API_VERSION = 'v1'; | ||
|
||
/** The Firebase Data Connect backend base URL format. */ | ||
const FIREBASE_DATA_CONNECT_BASE_URL_FORMAT = | ||
'https://firebasedataconnect.googleapis.com/{version}/projects/{projectId}/locations/{locationId}/services/{serviceId}:{endpointId}'; | ||
/** The Firebase Data Connect backend service URL format. */ | ||
const FIREBASE_DATA_CONNECT_SERVICES_URL_FORMAT = | ||
'https://firebasedataconnect.googleapis.com/{version}/projects/{projectId}/locations/{locationId}/services/{serviceId}:{endpointId}'; | ||
|
||
/** Firebase Data Connect base URl format when using the Data Connect emultor. */ | ||
const FIREBASE_DATA_CONNECT_EMULATOR_BASE_URL_FORMAT = | ||
/** The Firebase Data Connect backend connector URL format. */ | ||
const FIREBASE_DATA_CONNECT_CONNECTORS_URL_FORMAT = | ||
'https://firebasedataconnect.googleapis.com/{version}/projects/{projectId}/locations/{locationId}/services/{serviceId}/connectors/{connectorId}:{endpointId}'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Maybe we should extract out the production URL, just in case we mis-spell it somewhere |
||
|
||
/** Firebase Data Connect service URL format when using the Data Connect emulator. */ | ||
const FIREBASE_DATA_CONNECT_EMULATOR_SERVICES_URL_FORMAT = | ||
'http://{host}/{version}/projects/{projectId}/locations/{locationId}/services/{serviceId}:{endpointId}'; | ||
|
||
/** Firebase Data Connect connector URL format when using the Data Connect emulator. */ | ||
const FIREBASE_DATA_CONNECT_EMULATOR_CONNECTORS_URL_FORMAT = | ||
'http://{host}/{version}/projects/{projectId}/locations/{locationId}/services/{serviceId}/connectors/{connectorId}:{endpointId}'; | ||
|
||
const EXECUTE_GRAPH_QL_ENDPOINT = 'executeGraphql'; | ||
const EXECUTE_GRAPH_QL_READ_ENDPOINT = 'executeGraphqlRead'; | ||
|
||
const IMPERSONATE_QUERY_ENDPOINT = 'impersonateQuery'; | ||
const IMPERSONATE_MUTATION_ENDPOINT = 'impersonateMutation'; | ||
|
||
|
||
function getHeaders(isUsingGen: boolean): { [key: string]: string } { | ||
const headerValue = { | ||
|
@@ -50,6 +61,27 @@ function getHeaders(isUsingGen: boolean): { [key: string]: string } { | |
return headerValue; | ||
} | ||
|
||
/** | ||
* URL params for requests to an endpoint under services: | ||
* .../services/{serviceId}:endpoint | ||
*/ | ||
interface ServicesUrlParams { | ||
version: string; | ||
projectId: string; | ||
locationId: string; | ||
serviceId: string; | ||
endpointId: string; | ||
host?: string; // Present only when using the emulator | ||
} | ||
|
||
/** | ||
* URL params for requests to an endpoint under connectors: | ||
* .../services/{serviceId}/connectors/{connectorId}:endpoint | ||
*/ | ||
interface ConnectorsUrlParams extends ServicesUrlParams { | ||
connectorId: string; | ||
} | ||
|
||
/** | ||
* Class that facilitates sending requests to the Firebase Data Connect backend API. | ||
* | ||
|
@@ -106,6 +138,15 @@ export class DataConnectApiClient { | |
return this.executeGraphqlHelper(query, EXECUTE_GRAPH_QL_READ_ENDPOINT, options); | ||
} | ||
|
||
|
||
/** | ||
* A helper function to execute GraphQL queries. | ||
* | ||
* @param query - The arbitrary GraphQL query to execute. | ||
* @param endpoint - The endpoint to call. | ||
* @param options - The GraphQL options. | ||
* @returns A promise that fulfills with the GraphQL response, or throws an error. | ||
*/ | ||
private async executeGraphqlHelper<GraphqlResponse, Variables>( | ||
query: string, | ||
endpoint: string, | ||
|
@@ -129,52 +170,163 @@ export class DataConnectApiClient { | |
...(options?.operationName && { operationName: options?.operationName }), | ||
...(options?.impersonate && { extensions: { impersonate: options?.impersonate } }), | ||
}; | ||
return this.getUrl(API_VERSION, this.connectorConfig.location, this.connectorConfig.serviceId, endpoint) | ||
.then(async (url) => { | ||
const request: HttpRequestConfig = { | ||
method: 'POST', | ||
url, | ||
headers: getHeaders(this.isUsingGen), | ||
data, | ||
}; | ||
const resp = await this.httpClient.send(request); | ||
if (resp.data.errors && validator.isNonEmptyArray(resp.data.errors)) { | ||
const allMessages = resp.data.errors.map((error: { message: any; }) => error.message).join(' '); | ||
throw new FirebaseDataConnectError( | ||
DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR, allMessages); | ||
} | ||
return Promise.resolve({ | ||
data: resp.data.data as GraphqlResponse, | ||
}); | ||
}) | ||
.then((resp) => { | ||
return resp; | ||
}) | ||
.catch((err) => { | ||
throw this.toFirebaseError(err); | ||
}); | ||
const url = await this.getServicesUrl( | ||
API_VERSION, | ||
this.connectorConfig.location, | ||
this.connectorConfig.serviceId, | ||
endpoint | ||
); | ||
try { | ||
const resp = await this.makeGqlRequest<GraphqlResponse>(url, data); | ||
return resp; | ||
} catch (err: any) { | ||
throw this.toFirebaseError(err); | ||
} | ||
} | ||
|
||
private async getUrl(version: string, locationId: string, serviceId: string, endpointId: string): Promise<string> { | ||
return this.getProjectId() | ||
.then((projectId) => { | ||
const urlParams = { | ||
version, | ||
projectId, | ||
locationId, | ||
serviceId, | ||
endpointId | ||
}; | ||
let urlFormat: string; | ||
if (useEmulator()) { | ||
urlFormat = utils.formatString(FIREBASE_DATA_CONNECT_EMULATOR_BASE_URL_FORMAT, { | ||
host: emulatorHost() | ||
}); | ||
} else { | ||
urlFormat = FIREBASE_DATA_CONNECT_BASE_URL_FORMAT; | ||
} | ||
return utils.formatString(urlFormat, urlParams); | ||
}); | ||
/** | ||
* Executes a GraphQL query with impersonation. | ||
* | ||
* @param options - The GraphQL options. Must include impersonation details. | ||
* @returns A promise that fulfills with the GraphQL response. | ||
*/ | ||
public async executeQuery<GraphqlResponse, Variables>( | ||
name: string, | ||
variables: Variables, | ||
options?: OperationOptions | ||
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> { | ||
return this.executeOperationHelper(IMPERSONATE_QUERY_ENDPOINT, name, variables, options); | ||
} | ||
|
||
/** | ||
* Executes a GraphQL mutation with impersonation. | ||
* | ||
* @param options - The GraphQL options. Must include impersonation details. | ||
* @returns A promise that fulfills with the GraphQL response. | ||
*/ | ||
public async executeMutation<GraphqlResponse, Variables>( | ||
name: string, | ||
variables: Variables, | ||
options?: OperationOptions | ||
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> { | ||
return this.executeOperationHelper(IMPERSONATE_MUTATION_ENDPOINT, name, variables, options); | ||
} | ||
|
||
/** | ||
* A helper function to execute operations by making requests to FDC's impersonate | ||
* operations endpoints. | ||
* | ||
* @param endpoint - The endpoint to call. | ||
* @param options - The GraphQL options, including impersonation details. | ||
* @returns A promise that fulfills with the GraphQL response. | ||
*/ | ||
private async executeOperationHelper<GraphqlResponse, Variables>( | ||
endpoint: string, | ||
name: string, | ||
variables: Variables, | ||
options?: OperationOptions | ||
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> { | ||
if ( | ||
typeof name === 'undefined' || | ||
!validator.isNonEmptyString(name) | ||
) { | ||
throw new FirebaseDataConnectError( | ||
DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, | ||
'`name` must be a non-empty string.' | ||
); | ||
} | ||
|
||
if (this.connectorConfig.connector === undefined || this.connectorConfig.connector === '') { | ||
throw new FirebaseDataConnectError( | ||
DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, | ||
`The 'connectorConfig.connector' field used to instantiate your Data Connect | ||
instance must be a non-empty string (the connectorId) when calling executeQuery or executeMutation.`); | ||
} | ||
|
||
const data = { | ||
...(variables && { variables: variables }), | ||
stephenarosaj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
operationName: name, | ||
extensions: { impersonate: options?.impersonate }, | ||
}; | ||
const url = await this.getConnectorsUrl( | ||
API_VERSION, | ||
this.connectorConfig.location, | ||
this.connectorConfig.serviceId, | ||
this.connectorConfig.connector, | ||
endpoint, | ||
); | ||
try { | ||
const resp = await this.makeGqlRequest<GraphqlResponse>(url, data); | ||
return resp; | ||
} catch (err: any) { | ||
throw this.toFirebaseError(err); | ||
} | ||
} | ||
|
||
/** | ||
* Constructs the URL for a Data Connect request to a service endpoint. | ||
* | ||
* @param version - The API version. | ||
* @param locationId - The location of the Data Connect service. | ||
* @param serviceId - The ID of the Data Connect service. | ||
* @param endpointId - The endpoint to call. | ||
* @returns A promise which resolves to the formatted URL string. | ||
*/ | ||
private async getServicesUrl( | ||
version: string, | ||
locationId: string, | ||
serviceId: string, | ||
endpointId: string, | ||
): Promise<string> { | ||
const projectId = await this.getProjectId(); | ||
const params: ServicesUrlParams = { | ||
version, | ||
projectId, | ||
locationId, | ||
serviceId, | ||
endpointId, | ||
}; | ||
let urlFormat = FIREBASE_DATA_CONNECT_SERVICES_URL_FORMAT; | ||
if (useEmulator()) { | ||
urlFormat = FIREBASE_DATA_CONNECT_EMULATOR_SERVICES_URL_FORMAT; | ||
params.host = emulatorHost(); | ||
} | ||
return utils.formatString(urlFormat, params); | ||
} | ||
|
||
/** | ||
* Constructs the URL for a Data Connect request to a connector endpoint. | ||
* | ||
* @param version - The API version. | ||
* @param locationId - The location of the Data Connect service. | ||
* @param serviceId - The ID of the Data Connect service. | ||
* @param connectorId - The ID of the Connector. | ||
* @param endpointId - The endpoint to call. | ||
* @returns A promise which resolves to the formatted URL string. | ||
|
||
*/ | ||
private async getConnectorsUrl( | ||
version: string, | ||
locationId: string, | ||
serviceId: string, | ||
connectorId: string, | ||
endpointId: string, | ||
): Promise<string> { | ||
const projectId = await this.getProjectId(); | ||
const params: ConnectorsUrlParams = { | ||
version, | ||
projectId, | ||
locationId, | ||
serviceId, | ||
connectorId, | ||
endpointId, | ||
}; | ||
let urlFormat = FIREBASE_DATA_CONNECT_CONNECTORS_URL_FORMAT; | ||
if (useEmulator()) { | ||
urlFormat = FIREBASE_DATA_CONNECT_EMULATOR_CONNECTORS_URL_FORMAT; | ||
params.host = emulatorHost(); | ||
} | ||
return utils.formatString(urlFormat, params); | ||
} | ||
|
||
private getProjectId(): Promise<string> { | ||
|
@@ -195,6 +347,32 @@ export class DataConnectApiClient { | |
}); | ||
} | ||
|
||
/** | ||
* Makes a GraphQL request to the specified url. | ||
* | ||
* @param url - The URL to send the request to. | ||
* @param data - The GraphQL request payload. | ||
* @returns A promise that fulfills with the GraphQL response, or throws an error. | ||
*/ | ||
private async makeGqlRequest<GraphqlResponse>(url: string, data: object): | ||
Promise<ExecuteGraphqlResponse<GraphqlResponse>> { | ||
const request: HttpRequestConfig = { | ||
method: 'POST', | ||
url, | ||
headers: getHeaders(this.isUsingGen), | ||
data, | ||
}; | ||
const resp = await this.httpClient.send(request); | ||
if (resp.data.errors && validator.isNonEmptyArray(resp.data.errors)) { | ||
const allMessages = resp.data.errors.map((error: { message: any; }) => error.message).join(' '); | ||
throw new FirebaseDataConnectError( | ||
DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR, allMessages); | ||
} | ||
return Promise.resolve({ | ||
data: resp.data.data as GraphqlResponse, | ||
}); | ||
} | ||
|
||
private toFirebaseError(err: RequestResponseError): PrefixedFirebaseError { | ||
if (err instanceof PrefixedFirebaseError) { | ||
return err; | ||
|
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lahirumaramba Any opinions on this? We only need this for the generated SDK, and we don't intend for developers to use this.