Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
524d7d8
add in changes from stephenarosaj/fdc-impersonate
stephenarosaj Sep 17, 2025
bc92c5e
finish adding in changes from stephenarosaj/fdc-impersonate
stephenarosaj Sep 17, 2025
d43588e
update Google Inc. to Google LLC, run npm install; npm run build
stephenarosaj Sep 17, 2025
dcd493f
run npm apidocs
stephenarosaj Sep 17, 2025
23fe1f4
remove public execute apis
stephenarosaj Sep 24, 2025
ae8096a
convert executeOperation api to OperationRef(...).execute() api
stephenarosaj Sep 25, 2025
52a18d7
remove internal client from operation refs
stephenarosaj Sep 25, 2025
3cb6645
cleanup javadocs to address workflow failures
stephenarosaj Sep 25, 2025
bad9808
npm run apidocs
stephenarosaj Sep 25, 2025
6bdef60
spread GraphqlOptions arguments in OperationRefs and executeOperation…
stephenarosaj Sep 26, 2025
798c2dc
convert unit tests to use spread args
stephenarosaj Sep 26, 2025
85a6b4b
convert integration tests to use spread args
stephenarosaj Sep 26, 2025
5f34343
add executeQuery test cases which do not provide impersonation option…
stephenarosaj Sep 26, 2025
088d882
add executeMutation test cases which do not provide impersonation opt…
stephenarosaj Sep 26, 2025
216b3ac
run npm apidocs
stephenarosaj Sep 26, 2025
fb5a3de
address try/catch comment
stephenarosaj Sep 29, 2025
fd4ffb5
address await and reject grouping comment
stephenarosaj Sep 29, 2025
b118905
address getUrl comments
stephenarosaj Sep 29, 2025
cf72f38
address insecureReason comment
stephenarosaj Sep 29, 2025
43f223d
convert autopush resources to prod
stephenarosaj Sep 30, 2025
2c642fb
add RefOptions, [Operation,Query,Mutation]Ref, [Operation,Query,Mutat…
stephenarosaj Oct 1, 2025
12f1258
revert OperationRef.execute() API to executeOperation API
stephenarosaj Oct 3, 2025
60649e4
revert OperationRef.execute() API to executeOperation API
stephenarosaj Oct 3, 2025
918c4a0
revert tests to use DataConnect.executeOperation() API instead of Ope…
stephenarosaj Oct 3, 2025
4dad488
revert package version
stephenarosaj Oct 3, 2025
786c75f
merge master into rosa/impersonate
stephenarosaj Oct 3, 2025
89e691a
update executeOperation API to return executeOperationResponse
stephenarosaj Oct 3, 2025
5721657
update comments
stephenarosaj Oct 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions etc/firebase-admin.data-connect.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type AuthClaims = Partial<DecodedIdToken>;

// @public
export interface ConnectorConfig {
connector?: string;
location: string;
serviceId: string;
}
Expand All @@ -27,6 +28,10 @@ export class DataConnect {
readonly connectorConfig: ConnectorConfig;
executeGraphql<GraphqlResponse, Variables>(query: string, options?: GraphqlOptions<Variables>): Promise<ExecuteGraphqlResponse<GraphqlResponse>>;
executeGraphqlRead<GraphqlResponse, Variables>(query: string, options?: GraphqlOptions<Variables>): Promise<ExecuteGraphqlResponse<GraphqlResponse>>;
executeMutation<Data>(name: string, options?: OperationOptions): Promise<ExecuteOperationResponse<Data>>;
executeMutation<Data, Variables>(name: string, variables: Variables, options?: OperationOptions): Promise<ExecuteOperationResponse<Data>>;
executeQuery<Data>(name: string, options?: OperationOptions): Promise<ExecuteOperationResponse<Data>>;
executeQuery<Data, Variables>(name: string, variables: Variables, options?: OperationOptions): Promise<ExecuteOperationResponse<Data>>;
insert<GraphQlResponse, Variables extends object>(tableName: string, variables: Variables): Promise<ExecuteGraphqlResponse<GraphQlResponse>>;
insertMany<GraphQlResponse, Variables extends Array<unknown>>(tableName: string, variables: Variables): Promise<ExecuteGraphqlResponse<GraphQlResponse>>;
upsert<GraphQlResponse, Variables extends object>(tableName: string, variables: Variables): Promise<ExecuteGraphqlResponse<GraphQlResponse>>;
Expand All @@ -38,6 +43,11 @@ export interface ExecuteGraphqlResponse<GraphqlResponse> {
data: GraphqlResponse;
}

// @public
export interface ExecuteOperationResponse<GraphqlResponse> {
data: GraphqlResponse;
}

// @public
export function getDataConnect(connectorConfig: ConnectorConfig, app?: App): DataConnect;

Expand All @@ -60,4 +70,9 @@ export interface ImpersonateUnauthenticated {
unauthenticated: true;
}

// @public
export interface OperationOptions {
impersonate?: ImpersonateAuthenticated | ImpersonateUnauthenticated;
}

```
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

278 changes: 228 additions & 50 deletions src/data-connect/data-connect-api-client-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}';

/** 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 = {
Expand All @@ -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.
*
Expand Down Expand Up @@ -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,
Expand All @@ -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 }),
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> {
Expand All @@ -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;
Expand Down
Loading
Loading