Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 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
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
8 changes: 8 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 @@ -29,6 +30,13 @@ export class DataConnect {
executeGraphqlRead<GraphqlResponse, Variables>(query: string, options?: GraphqlOptions<Variables>): Promise<ExecuteGraphqlResponse<GraphqlResponse>>;
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>>;
// Warning: (ae-forgotten-export) The symbol "MutationRef" needs to be exported by the entry point index.d.ts
mutationRef<Data>(name: string, options?: RefOptions): MutationRef<Data, undefined>;
mutationRef<Data, Variables>(name: string, variables: Variables, options?: RefOptions): MutationRef<Data, Variables>;
// Warning: (ae-forgotten-export) The symbol "RefOptions" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "QueryRef" needs to be exported by the entry point index.d.ts
queryRef<Data>(name: string, options?: RefOptions): QueryRef<Data, undefined>;
queryRef<Data, Variables>(name: string, variables: Variables, options?: RefOptions): QueryRef<Data, Variables>;
upsert<GraphQlResponse, Variables extends object>(tableName: string, variables: Variables): Promise<ExecuteGraphqlResponse<GraphQlResponse>>;
upsertMany<GraphQlResponse, Variables extends Array<unknown>>(tableName: string, variables: Variables): Promise<ExecuteGraphqlResponse<GraphQlResponse>>;
}
Expand Down
4 changes: 2 additions & 2 deletions 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,25 +23,57 @@ 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, RefOptions } 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';

const DATA_CONNECT_CONFIG_HEADERS = {
'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`
};

/**
* 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 @@ -89,6 +121,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 @@ -112,52 +153,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: DATA_CONNECT_CONFIG_HEADERS,
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?: RefOptions
): 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?: RefOptions
): 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?: RefOptions
): 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 @@ -178,6 +330,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: DATA_CONNECT_CONFIG_HEADERS,
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
20 changes: 19 additions & 1 deletion src/data-connect/data-connect-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export interface ConnectorConfig {
* Service ID of the Data Connect service.
*/
serviceId: string;

/**
* Name of the Data Connect connector.
*/
connector?: string;
}

/**
Expand All @@ -52,7 +57,9 @@ export interface GraphqlOptions<Variables> {
variables?: Variables;

/**
* The name of the GraphQL operation. Required only if `query` contains multiple operations.
* The name of the GraphQL operation.
* Required for operations that interact with services, such as executeGraphql, if
* `query` contains multiple operations.
*/
operationName?: string;

Expand All @@ -63,6 +70,17 @@ export interface GraphqlOptions<Variables> {
impersonate?: ImpersonateAuthenticated | ImpersonateUnauthenticated;
}

/**
* Interface representing options for OperationRefs.
*/
export interface RefOptions {
/**
* If set, impersonate a request with given Firebase Auth context and evaluate the auth
* policies on the operation. If omitted, bypass any defined auth policies.
*/
impersonate?: ImpersonateAuthenticated | ImpersonateUnauthenticated;
}

/**
* Type representing the partial claims of a Firebase Auth token used to evaluate the
* Data Connect auth policy.
Expand Down
Loading
Loading