Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
53 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
fd61f1a
add invalidateAdminArgs to handle variadic JS executeOperation arguments
stephenarosaj Oct 8, 2025
104a1c5
npm run apidocs for validateAdminArgs
stephenarosaj Oct 13, 2025
652c059
update validateAdminArgs documentation
stephenarosaj Oct 14, 2025
79b55eb
address validateAdminArgs and some test comments
stephenarosaj Oct 14, 2025
97f75f3
update validate-admin-args and add tests, address existing test comme…
stephenarosaj Oct 15, 2025
f7d2a5b
update tests
stephenarosaj Oct 15, 2025
e95a035
update tests
stephenarosaj Oct 15, 2025
4e5fbcb
address mutation test comments
stephenarosaj Oct 15, 2025
b83b6da
address prod url comments
stephenarosaj Oct 15, 2025
39cfb9d
finally fixed unit tests
stephenarosaj Oct 15, 2025
5d4cbab
REALLY fixed unit tests
stephenarosaj Oct 15, 2025
69c180b
address comments, add DataConnect.executeQuery() and DataConnect.exec…
stephenarosaj Oct 18, 2025
e1d5c08
make validateAdminArgs internal
stephenarosaj Oct 20, 2025
a9b5f39
address comments
stephenarosaj Oct 21, 2025
b40871a
address documentation comment
stephenarosaj Oct 21, 2025
81d55c9
undo integration test changes
stephenarosaj Oct 21, 2025
07ba1e1
remove empty checks
stephenarosaj Oct 21, 2025
94d9147
remove length checks
stephenarosaj Oct 21, 2025
a84c639
remove foreach checks
stephenarosaj Oct 21, 2025
2578c3e
address test comments
stephenarosaj Oct 21, 2025
2590da7
revert package version
stephenarosaj Oct 28, 2025
9d93f5a
revert package version to master
stephenarosaj Oct 28, 2025
1d227b7
revert package version to master
stephenarosaj Oct 28, 2025
db410c2
update comments
stephenarosaj Oct 31, 2025
edcd9e1
Merge branch 'master' into rosa/impersonate
stephenarosaj Nov 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
Next Next commit
add in changes from stephenarosaj/fdc-impersonate
  • Loading branch information
stephenarosaj committed Sep 17, 2025
commit 524d7d81afbdab7c7f5a7c327763df792de8c78c
229 changes: 186 additions & 43 deletions src/data-connect/data-connect-api-client-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,31 @@ import { ConnectorConfig, ExecuteGraphqlResponse, GraphqlOptions } from './data-

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}';

/** Firebase Data Connect base URl format when using the Data Connect emultor. */
const FIREBASE_DATA_CONNECT_EMULATOR_BASE_URL_FORMAT =
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// TODO: CHANGE THIS BACK TO PROD - AUTOPUSH IS ONLY USED FOR LOCAL TESTING BEFORE CHANGES PROPAGATE
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
/** The Firebase Data Connect backend service URL format. */
const FIREBASE_DATA_CONNECT_SERVICES_URL_FORMAT =
'https://autopush-firebasedataconnect.sandbox.googleapis.com/{version}/projects/{projectId}/locations/{locationId}/services/{serviceId}:{endpointId}';

/** The Firebase Data Connect backend connector URL format. */
const FIREBASE_DATA_CONNECT_CONNECTORS_URL_FORMAT =
'https://autopush-firebasedataconnect.sandbox.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()}`
};
Expand Down Expand Up @@ -89,6 +103,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,24 +135,8 @@ 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,
});
})
const url = await this.getUrl(API_VERSION, this.connectorConfig.location, this.connectorConfig.serviceId, endpoint);
return this.makeGqlRequest<GraphqlResponse>(url, data)
.then((resp) => {
return resp;
})
Expand All @@ -138,28 +145,138 @@ export class DataConnectApiClient {
});
}

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>(
options: GraphqlOptions<Variables>
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
return this.executeOperationHelper(IMPERSONATE_QUERY_ENDPOINT, 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>(
options: GraphqlOptions<Variables>
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
return this.executeOperationHelper(IMPERSONATE_MUTATION_ENDPOINT, 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,
options: GraphqlOptions<Variables>
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
if (
typeof options.operationName === 'undefined' ||
!validator.isNonEmptyString(options.operationName)
) {
throw new FirebaseDataConnectError(
DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,
'`options.operationName` must be a non-empty string.'
);
}
if (
typeof options.impersonate === 'undefined' ||
!validator.isNonNullObject(options?.impersonate)
) {
throw new FirebaseDataConnectError(
DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,
'`options.impersonate` must be a non-null object.'
);
}

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 = {
...(options.variables && { variables: options?.variables }),
operationName: options.operationName,
extensions: { impersonate: options.impersonate },
};
const url = await this.getUrl(
API_VERSION,
this.connectorConfig.location,
this.connectorConfig.serviceId,
endpoint,
this.connectorConfig.connector,
);
return this.makeGqlRequest<GraphqlResponse>(url, data)
.then((resp) => {
return resp;
})
.catch((err) => {
throw this.toFirebaseError(err);
});
}

/**
* Constructs the URL for a Data Connect backend request.
*
* If no connectorId is provided, will direct the request to an endpoint under services:
* .../services/{serviceId}:endpoint
*
* If connectorId is provided, will direct the request to an endpoint under connectors:
* .../services/{serviceId}/connectors/{connectorId}: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.
* @param connectorId - The ID of the connector, if applicable.
* @returns A promise that fulfills with the constructed URL.
*/
private async getUrl(
version: string,
locationId: string,
serviceId: string,
endpointId: string,
connectorId?: string,
): Promise<string> {
const projectId = await this.getProjectId();
const urlParams = {
version,
projectId,
locationId,
serviceId,
endpointId,
connectorId
};
let urlFormat: string;
if (useEmulator()) {
(urlParams as any).host = emulatorHost();
urlFormat = connectorId === undefined || connectorId === ''
? FIREBASE_DATA_CONNECT_EMULATOR_SERVICES_URL_FORMAT
: FIREBASE_DATA_CONNECT_EMULATOR_CONNECTORS_URL_FORMAT;
} else {
urlFormat = connectorId === undefined || connectorId === ''
? FIREBASE_DATA_CONNECT_SERVICES_URL_FORMAT
: FIREBASE_DATA_CONNECT_CONNECTORS_URL_FORMAT;
}
if (connectorId) {
(urlParams as any).connectorId = connectorId;
}
return utils.formatString(urlFormat, urlParams);
}

private getProjectId(): Promise<string> {
if (this.projectId) {
return Promise.resolve(this.projectId);
Expand All @@ -178,6 +295,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
11 changes: 10 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,12 @@ export interface ConnectorConfig {
* Service ID of the Data Connect service.
*/
serviceId: string;

/**
* Name of the Data Connect connector.
* Required for operations that interact with connectors, such as executeQuery and executeMutation.
*/
connector?: string;
}

/**
Expand All @@ -52,7 +58,10 @@ 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 connectors, such as executeQuery and executeMutation.
* Required for operations that interact with services, such as executeGraphql, if
* `query` contains multiple operations.
*/
operationName?: string;

Expand Down
42 changes: 35 additions & 7 deletions src/data-connect/data-connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,48 @@ export class DataConnect {
}

/**
* Execute an arbitrary read-only GraphQL query
*
* @param query - The GraphQL read-only query.
* @param options - Optional {@link GraphqlOptions} when executing a read-only GraphQL query.
*
* @returns A promise that fulfills with a `ExecuteGraphqlResponse`.
*/
* Execute an arbitrary read-only GraphQL query
*
* @param query - The GraphQL read-only query.
* @param options - Optional {@link GraphqlOptions} when executing a read-only GraphQL query.
*
* @returns A promise that fulfills with a `ExecuteGraphqlResponse`.
*/
public executeGraphqlRead<GraphqlResponse, Variables>(
query: string,
options?: GraphqlOptions<Variables>,
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
return this.client.executeGraphqlRead(query, options);
}

/**
* Executes a pre-defined GraphQL query with impersonation.
*
* The query must be defined in your Data Connect GraphQL files.
*
* @param options - The GraphQL options, must include operationName and impersonation details.
* @returns A promise that fulfills with the GraphQL response.
*/
public async executeQuery<GraphqlResponse, Variables>(
options: GraphqlOptions<Variables>
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
return this.client.executeQuery(options);
}

/**
* Executes a pre-defined GraphQL mutation with impersonation.
*
* The mutation must be defined in your Data Connect GQL files.
*
* @param options - The GraphQL options, must include operationName and impersonation details.
* @returns A promise that fulfills with the GraphQL response.
*/
public async executeMutation<GraphqlResponse, Variables>(
options: GraphqlOptions<Variables>
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
return this.client.executeMutation(options);
}

/**
* Insert a single row into the specified table.
*
Expand Down
1 change: 1 addition & 0 deletions src/data-connect/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export {
* const connectorConfig: ConnectorConfig = {
* location: 'us-west2',
* serviceId: 'my-service',
* connectorName: 'my-connector',
* };
*
* // Get the `DataConnect` service for the default app
Expand Down