Skip to content
Open
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ test/resources/appid.txt
firebase-admin-*.tgz

docgen/markdown/
service_account.json
dataconnect/
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.

109 changes: 96 additions & 13 deletions src/data-connect/data-connect-api-client-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,24 @@ const API_VERSION = 'v1alpha';

/** 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}';
'https://firebasedataconnect.googleapis.com/{version}/projects/{projectId}/locations/{locationId}/services/{serviceId}:{endpointId}';

/** Firebase Data Connect base URl format when using the Data Connect emultor. */
/** The Firebase Data Connect backend base URL format including a connector. */
const FIREBASE_DATA_CONNECT_BASE_URL_FORMAT_WITH_CONNECTOR =
'https://firebasedataconnect.googleapis.com/{version}/projects/{projectId}/locations/{locationId}/services/{serviceId}/connectors/{connector}:{endpointId}';

/** Firebase Data Connect base URl format when using the Data Connect emulator. */
const FIREBASE_DATA_CONNECT_EMULATOR_BASE_URL_FORMAT =
'http://{host}/{version}/projects/{projectId}/locations/{locationId}/services/{serviceId}:{endpointId}';

/** Firebase Data Connect base URl format when using the Data Connect emulator including a connector. */
const FIREBASE_DATA_CONNECT_EMULATOR_BASE_URL_FORMAT_WITH_CONNECTOR =
'http://{host}/{version}/projects/{projectId}/locations/{locationId}/services/{serviceId}/connectors/{connector}:{endpointId}';

const EXECUTE_GRAPH_QL_ENDPOINT = 'executeGraphql';
const EXECUTE_GRAPH_QL_READ_ENDPOINT = 'executeGraphqlRead';
const EXECUTE_QUERY_ENDPOINT = 'executeQuery';
const EXECUTE_MUTATION_ENDPOINT = 'executeMutation';

const DATA_CONNECT_CONFIG_HEADERS = {
'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`
Expand Down Expand Up @@ -87,6 +97,30 @@ export class DataConnectApiClient {
options?: GraphqlOptions<Variables>,
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
return this.executeGraphqlHelper(query, EXECUTE_GRAPH_QL_READ_ENDPOINT, options);
// return this.executeHelper(EXECUTE_GRAPH_QL_READ_ENDPOINT,options, query);
}

/**
* Execute pre-existing read-only queries <QueryResult<Data, Variables>>
* @param options - GraphQL Options
* @returns A promise that fulfills with a `ExecuteGraphqlResponse`.
* @throws FirebaseDataConnectError
*/
public async executeQuery<Data, Variables>(
options: GraphqlOptions<Variables>,
): Promise<ExecuteGraphqlResponse<Data>> {
return this.executeOperationHelper(EXECUTE_QUERY_ENDPOINT, options);
}
/**
* Execute pre-existing read and write queries <MutationResult<Data, Variables>>
* @param options - GraphQL Options
* @returns A promise that fulfills with a `ExecuteGraphqlResponse`.
* @throws FirebaseDataConnectError
*/
public async executeMutation<Data, Variables>(
options: GraphqlOptions<Variables>,
): Promise<ExecuteGraphqlResponse<Data>> {
return this.executeOperationHelper(EXECUTE_MUTATION_ENDPOINT, options);
}

private async executeGraphqlHelper<GraphqlResponse, Variables>(
Expand All @@ -106,13 +140,49 @@ export class DataConnectApiClient {
'GraphqlOptions must be a non-null object');
}
}
return this.executeHelper(endpoint, options, query)
}

private async executeOperationHelper<GraphqlResponse, Variables>(
endpoint: string,
options: GraphqlOptions<Variables>,
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
if (typeof options == 'undefined') {
throw new FirebaseDataConnectError(
DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,
'GraphqlOptions should be a non-null object');
}
if (typeof options !== 'undefined') {
if (!validator.isNonNullObject(options)) {
throw new FirebaseDataConnectError(
DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,
'GraphqlOptions must be a non-null object');
}
}

if (!("operationName" in options)) {
throw new FirebaseDataConnectError(
DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,
'GraphqlOptions must contain `operationName`.');
}

return this.executeHelper(endpoint, options)
}


private async executeHelper<GraphqlResponse, Variables>(
endpoint: string,
options?: GraphqlOptions<Variables>,
gql?: string
): Promise<ExecuteGraphqlResponse<GraphqlResponse>> {
const data = {
query,
query: gql,
...(!gql && { name: options?.operationName }),
...(options?.variables && { variables: options?.variables }),
...(options?.operationName && { operationName: options?.operationName }),
...(options?.impersonate && { extensions: { impersonate: options?.impersonate } }),
};
return this.getUrl(API_VERSION, this.connectorConfig.location, this.connectorConfig.serviceId, endpoint)
return this.getUrl(API_VERSION, this.connectorConfig.location, this.connectorConfig.serviceId, endpoint, this.connectorConfig.connector)
.then(async (url) => {
const request: HttpRequestConfig = {
method: 'POST',
Expand All @@ -138,23 +208,36 @@ export class DataConnectApiClient {
});
}

private async getUrl(version: string, locationId: string, serviceId: string, endpointId: string): Promise<string> {
private async getUrl(version: string, locationId: string, serviceId: string, endpointId: string, connector?: string): Promise<string> {
return this.getProjectId()
.then((projectId) => {
const urlParams = {
version,
projectId,
locationId,
serviceId,
endpointId
endpointId,
...(connector && { connector })
};
let urlFormat: string;
if (useEmulator()) {
urlFormat = utils.formatString(FIREBASE_DATA_CONNECT_EMULATOR_BASE_URL_FORMAT, {
host: emulatorHost()
});
if ('connector' in urlParams && (endpointId === EXECUTE_QUERY_ENDPOINT || endpointId === EXECUTE_MUTATION_ENDPOINT)) {
urlFormat = utils.formatString(FIREBASE_DATA_CONNECT_EMULATOR_BASE_URL_FORMAT_WITH_CONNECTOR, {
host: emulatorHost()
});
}
else {
urlFormat = utils.formatString(FIREBASE_DATA_CONNECT_EMULATOR_BASE_URL_FORMAT, {
host: emulatorHost()
});
}
} else {
urlFormat = FIREBASE_DATA_CONNECT_BASE_URL_FORMAT;
if ('connector' in urlParams && (endpointId === EXECUTE_QUERY_ENDPOINT || endpointId === EXECUTE_MUTATION_ENDPOINT)) {
urlFormat = FIREBASE_DATA_CONNECT_BASE_URL_FORMAT_WITH_CONNECTOR
}
else {
urlFormat = FIREBASE_DATA_CONNECT_BASE_URL_FORMAT;
}
}
return utils.formatString(urlFormat, urlParams);
});
Expand Down Expand Up @@ -226,13 +309,13 @@ export class DataConnectApiClient {
// GraphQL object keys are typically unquoted.
return `${key}: ${this.objectToString(val)}`;
});

if (kvPairs.length === 0) {
return '{}'; // Represent an object with no defined properties as {}
}
return `{ ${kvPairs.join(', ')} }`;
}

// If value is undefined (and not an object property, which is handled above,
// e.g., if objectToString(undefined) is called directly or for an array element)
// it should be represented as 'null'.
Expand All @@ -255,7 +338,7 @@ export class DataConnectApiClient {
}

private handleBulkImportErrors(err: FirebaseDataConnectError): never {
if (err.code === `data-connect/${DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR}`){
if (err.code === `data-connect/${DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR}`) {
throw new FirebaseDataConnectError(
DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR,
`${err.message}. Make sure that your table name passed in matches the type name in your GraphQL schema file.`);
Expand Down
5 changes: 5 additions & 0 deletions 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;

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

/**
Expand Down
114 changes: 113 additions & 1 deletion src/data-connect/data-connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/

import { App } from '../app';
import { DataConnectApiClient } from './data-connect-api-client-internal';
import { DATA_CONNECT_ERROR_CODE_MAPPING, DataConnectApiClient, FirebaseDataConnectError } from './data-connect-api-client-internal';

import {
ConnectorConfig,
Expand Down Expand Up @@ -157,4 +157,116 @@ export class DataConnect {
): Promise<ExecuteGraphqlResponse<GraphQlResponse>> {
return this.client.upsertMany(tableName, variables);
}

/**
* Returns Query Reference
* @param name Name of Query
* @returns QueryRef
*/
public queryRef<Data>(name: string): QueryRef<Data, undefined>;
/**
*
* Returns Query Reference
* @param name Name of Query
* @param variables
* @returns QueryRef
*/
public queryRef<Data, Variables>(name: string, variables: Variables): QueryRef<Data, Variables>;
/**
*
* Returns Query Reference
* @param name Name of Query
* @param variables
* @returns QueryRef
*/
public queryRef<Data, Variables>(name: string, variables?: Variables): QueryRef<Data, Variables> {
if (!("connector" in this.connectorConfig)){
throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeQuery requires a connector');
}
return new QueryRef(this, name, variables as Variables, this.client);
}
/**
* Returns Mutation Reference
* @param name Name of Mutation
* @returns MutationRef
*/
public mutationRef<Data>(name: string): MutationRef<Data, undefined>;
/**
*
* Returns Mutation Reference
* @param name Name of Mutation
* @param variables
* @returns MutationRef
*/
public mutationRef<Data, Variables>(name: string, variables: Variables): MutationRef<Data, Variables>;
/**
*
* Returns Query Reference
* @param name Name of Mutation
* @param variables
* @returns MutationRef
*/
public mutationRef<Data, Variables>(name: string, variables?: Variables): MutationRef<Data, Variables> {
if (!("connector" in this.connectorConfig)){
throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeMutation requires a connector');
}
return new MutationRef(this, name, variables as Variables, this.client);
}
}

abstract class OperationRef<Data, Variables> {
_data?: Data;
constructor(public readonly dataConnect: DataConnect, public readonly name: string, public readonly variables: Variables, protected readonly client: DataConnectApiClient) {

}
abstract execute(): Promise<OperationResult<Data, Variables>>;
}

interface OperationResult<Data, Variables> {
ref: OperationRef<Data, Variables>;
data: Data;
variables: Variables;
dataConnect: DataConnect;
}
export interface QueryResult<Data, Variables> extends OperationResult<Data, Variables> {
ref: QueryRef<Data, Variables>;
}
export interface MutationResult<Data, Variables> extends OperationResult<Data, Variables> {
ref: MutationRef<Data, Variables>;
}

class QueryRef<Data, Variables> extends OperationRef<Data, Variables> {
option_params:GraphqlOptions<Variables>;
async execute(): Promise<QueryResult<Data, Variables>> {
const option_params = {
variables: this.variables,
operationName: this.name
};
const {data} = await this.client.executeQuery<Data, Variables>(option_params)

return {
ref: this,
data: data,
variables: this.variables,
dataConnect: this.dataConnect
}
}
}

class MutationRef<Data, Variables> extends OperationRef<Data, Variables> {
option_params:GraphqlOptions<Variables>;
async execute(): Promise<MutationResult<Data, Variables>> {
const option_params = {
variables: this.variables,
operationName: this.name
};
const {data} = await this.client.executeMutation<Data, Variables>(option_params)

return {
ref: this,
data: data,
variables: this.variables,
dataConnect: this.dataConnect
}
}
}
5 changes: 5 additions & 0 deletions src/data-connect/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ export {
* @returns The default `DataConnect` service with the provided connector configuration
* if no app is provided, or the `DataConnect` service associated with the provided app.
*/
// export function getDataConnect(connectorConfig: ConnectorConfig, app?: App): DataConnect {
// if (typeof app === 'undefined') {
// app = getApp();
// }

export function getDataConnect(connectorConfig: ConnectorConfig, app?: App): DataConnect {
if (typeof app === 'undefined') {
app = getApp();
Expand Down
Loading
Loading