From 0fcd4b0b1083f5febd9984386cb3b29fec6418ef Mon Sep 17 00:00:00 2001 From: FOLUSO ONATEMOWO Date: Wed, 16 Jul 2025 16:33:03 -0700 Subject: [PATCH 1/9] Implemented initilization and queries for the admin node core sdk, changes include:queryref --- .firebaserc | 7 ++ dataconnect_test.ts | 18 ++++ .../data-connect-api-client-internal.ts | 95 ++++++++++++++-- src/data-connect/data-connect-api.ts | 5 + src/data-connect/data-connect.ts | 101 +++++++++++++++++- 5 files changed, 219 insertions(+), 7 deletions(-) create mode 100644 .firebaserc create mode 100644 dataconnect_test.ts diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000000..b1ebad5e12 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,7 @@ +{ + "projects": { + "default": "test-suite-e6c23" + }, + "targets": {}, + "etags": {} +} \ No newline at end of file diff --git a/dataconnect_test.ts b/dataconnect_test.ts new file mode 100644 index 0000000000..f7a6957427 --- /dev/null +++ b/dataconnect_test.ts @@ -0,0 +1,18 @@ +const { getDataConnect } = require('./lib/data-connect'); +const { initializeApp } = require('./lib/app'); + +const app = initializeApp(); + +const config = { + serviceId: "your-service-id", + location: "us-central1", + connector:"movie-connector" +}; + +const dataConnect = getDataConnect({ + connectorConfig: config +}); + +const listOfMovies = dataConnect.queryRef('ListMovies').execute(); + +console.log(listOfMovies) \ No newline at end of file diff --git a/src/data-connect/data-connect-api-client-internal.ts b/src/data-connect/data-connect-api-client-internal.ts index e12005b8bb..cfd7f9a6e1 100644 --- a/src/data-connect/data-connect-api-client-internal.ts +++ b/src/data-connect/data-connect-api-client-internal.ts @@ -31,12 +31,22 @@ const API_VERSION = 'v1alpha'; 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. */ +/** 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()}` @@ -75,7 +85,7 @@ export class DataConnectApiClient { } /** - * Execute arbitrary read-only GraphQL queries + * Execute arbi>trary read-only GraphQL queries * * @param query - The GraphQL (read-only) string to be executed. * @param options - GraphQL Options @@ -89,6 +99,68 @@ export class DataConnectApiClient { return this.executeGraphqlHelper(query, EXECUTE_GRAPH_QL_READ_ENDPOINT, options); } + /** + * Uses the name and the variables parameters to execute a query. + */ + public async executeQuery( + options: GraphqlOptions, + ): Promise>{ + // const {data} = await this.executeHelper(options.operationName!, EXECUTE_QUERY_ENDPOINT, options); + return this.executeHelper(EXECUTE_QUERY_ENDPOINT,options); +} + + private async executeHelper( + endpoint: string, + options?: GraphqlOptions, + gql?: string + ): Promise> { + if (!validator.isNonEmptyString(gql)) { + throw new FirebaseDataConnectError( + DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + '`query` must be a non-empty string.'); + } + 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'); + } + } + const data = { + query: gql, + ...(!gql && { name: options?.operationName}), + ...(options?.variables && { variables: options?.variables }), + //change to if query != operationName for executeQuery and executeMutation + //Also how was this needed in conjuncton with executeGraphql before? Just the name of an operation normally doesn't that mean this is how it was used before? + ...(options?.operationName && { operationName: options?.operationName }), + ...(options?.impersonate && { extensions: { impersonate: options?.impersonate } }), + }; + return this.getUrl(API_VERSION, this.connectorConfig.location, this.connectorConfig.serviceId, endpoint,this.connectorConfig.connector) + .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); + }); + } + private async executeGraphqlHelper( query: string, endpoint: string, @@ -138,7 +210,7 @@ export class DataConnectApiClient { }); } - private async getUrl(version: string, locationId: string, serviceId: string, endpointId: string): Promise { + private async getUrl(version: string, locationId: string, serviceId: string, endpointId: string, connector?: string): Promise { return this.getProjectId() .then((projectId) => { const urlParams = { @@ -146,15 +218,26 @@ export class DataConnectApiClient { projectId, locationId, serviceId, - endpointId + endpointId, + ...(connector && { connector }) }; let urlFormat: string; if (useEmulator()) { - urlFormat = utils.formatString(FIREBASE_DATA_CONNECT_EMULATOR_BASE_URL_FORMAT, { + if ('connector' in urlParams){ + 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){ + urlFormat = FIREBASE_DATA_CONNECT_BASE_URL_FORMAT_WITH_CONNECTOR} + else{ + urlFormat = FIREBASE_DATA_CONNECT_BASE_URL_FORMAT;} } return utils.formatString(urlFormat, urlParams); }); diff --git a/src/data-connect/data-connect-api.ts b/src/data-connect/data-connect-api.ts index 1073437032..2e6146302f 100644 --- a/src/data-connect/data-connect-api.ts +++ b/src/data-connect/data-connect-api.ts @@ -30,6 +30,11 @@ export interface ConnectorConfig { * Service ID of the Data Connect service. */ serviceId: string; + + /** + * Connector of the Data Connect service. + */ + connector?: string; } /** diff --git a/src/data-connect/data-connect.ts b/src/data-connect/data-connect.ts index aa7d9d7e19..ddd37c78eb 100644 --- a/src/data-connect/data-connect.ts +++ b/src/data-connect/data-connect.ts @@ -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, @@ -157,4 +157,103 @@ export class DataConnect { ): Promise> { return this.client.upsertMany(tableName, variables); } + + /** + * Returns Query Reference + * @param name Name of Query + * @returns QueryRef + */ + public queryRef(name: string): QueryRef; + /** + * + * Returns Query Reference + * @param name Name of Query + * @param variables + * @returns QueryRef + */ + public queryRef(name: string, variables: Variables): QueryRef; + /** + * + * Returns Query Reference + * @param name Name of Query + * @param variables + * @returns QueryRef + */ + public queryRef(name: string, variables?: Variables): QueryRef { + console.log(this) + 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(name: string): MutationRef; + /** + * + * Returns Mutation Reference + * @param name Name of Mutation + * @param variables + * @returns MutationRef + */ + // public mutationRef(name: string, variables: Variables): MutationRef; + /** + * + * Returns Query Reference + * @param name Name of Mutation + * @param variables + * @returns MutationRef + */ + // public mutationRef(name: string, variables?: Variables): MutationRef { + // return new MutationRef(name, variables as Variables, this.client); + // } +} + +abstract class OperationRef { + _data?: Data; + constructor(public readonly dataConnect: DataConnect, public readonly name: string, public readonly variables: Variables, protected readonly client: DataConnectApiClient) { + + } + abstract execute(): Promise>; +} + +interface OperationResult { + ref: OperationRef; + data: Data; + variables: Variables; + dataConnect: DataConnect; } +export interface QueryResult extends OperationResult { + ref: QueryRef; +} +// interface MutationResult extends OperationResult { +// ref: MutationRef; +// } + +class QueryRef extends OperationRef { + option_params:GraphqlOptions; + async execute(): Promise> { + // return this.client.executeQuery(this.name, this.variables); + const option_params = { + variables: this.variables, + operationName: this.name + }; + const {data} = await this.client.executeQuery(option_params) + + return { + ref: this, + data: data, + variables: this.variables, + dataConnect: this.dataConnect + } + } +} + +// class MutationRef extends OperationRef { +// execute(): Promise> { +// return this.client.executeMutation(this.name, this.variables); +// } +// } \ No newline at end of file From f1ddd5c906e240b4f5fb950be21a7a97bc48eb3d Mon Sep 17 00:00:00 2001 From: FOLUSO ONATEMOWO Date: Fri, 18 Jul 2025 01:41:22 -0700 Subject: [PATCH 2/9] Draft: Implemented the initialization API and 'querRef' function; refactored and renamed the 'executeGraphqlHelper' function to support the 'executeQuery' and 'executeMutation' functions. --- .gitignore | 1 + dataconnect_test.ts | 20 +++++++++++-------- .../data-connect-api-client-internal.ts | 19 ++++++++++++------ src/data-connect/data-connect.ts | 4 ++-- src/data-connect/index.ts | 9 +++++++++ 5 files changed, 37 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 9331c650de..f9e33e377e 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ test/resources/appid.txt firebase-admin-*.tgz docgen/markdown/ +service_account.json diff --git a/dataconnect_test.ts b/dataconnect_test.ts index f7a6957427..2653eb6193 100644 --- a/dataconnect_test.ts +++ b/dataconnect_test.ts @@ -4,15 +4,19 @@ const { initializeApp } = require('./lib/app'); const app = initializeApp(); const config = { - serviceId: "your-service-id", - location: "us-central1", - connector:"movie-connector" + serviceId: "", + location: "", + // connector:", ): Promise> { - return this.executeGraphqlHelper(query, EXECUTE_GRAPH_QL_READ_ENDPOINT, options); + // return this.executeGraphqlHelper(query, EXECUTE_GRAPH_QL_READ_ENDPOINT, options); + return this.executeHelper(EXECUTE_GRAPH_QL_READ_ENDPOINT,options, query); } /** @@ -114,17 +115,22 @@ export class DataConnectApiClient { options?: GraphqlOptions, gql?: string ): Promise> { - if (!validator.isNonEmptyString(gql)) { + if (!validator.isNonEmptyString(gql) && typeof options == 'undefined') { throw new FirebaseDataConnectError( DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - '`query` must be a non-empty string.'); - } - if (typeof options !== 'undefined') { + '`gql` must be a non-empty string or GraphqlOptions should be a non-null object'); + } //How would we steer them in the right direction or let them know which area makes the most sense to follow for what they are trying to accomplish? they might want gql to be empty and the message should say + if (typeof options !== 'undefined' && !validator.isNonEmptyString(gql)) { 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, + '`gql` missing thus GraphqlOptions must contain `operationName`.');//Is this too descriptive? + } } const data = { query: gql, @@ -157,6 +163,7 @@ export class DataConnectApiClient { return resp; }) .catch((err) => { + console.log(err) throw this.toFirebaseError(err); }); } diff --git a/src/data-connect/data-connect.ts b/src/data-connect/data-connect.ts index ddd37c78eb..e97bfd9ef8 100644 --- a/src/data-connect/data-connect.ts +++ b/src/data-connect/data-connect.ts @@ -180,7 +180,7 @@ export class DataConnect { * @returns QueryRef */ public queryRef(name: string, variables?: Variables): QueryRef { - console.log(this) + // console.log(this) if (!("connector" in this.connectorConfig)){ throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeQuery requires a connector'); } @@ -242,7 +242,7 @@ class QueryRef extends OperationRef { operationName: this.name }; const {data} = await this.client.executeQuery(option_params) - + return { ref: this, data: data, diff --git a/src/data-connect/index.ts b/src/data-connect/index.ts index ab262682f2..9356d9eb27 100644 --- a/src/data-connect/index.ts +++ b/src/data-connect/index.ts @@ -71,6 +71,15 @@ 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(); +// } + +// const firebaseApp: FirebaseApp = app as FirebaseApp; +// const dataConnectService = firebaseApp.getOrInitService('dataConnect', (app) => new DataConnectService(app)); +// return dataConnectService.getDataConnect(connectorConfig); +// } export function getDataConnect(connectorConfig: ConnectorConfig, app?: App): DataConnect { if (typeof app === 'undefined') { app = getApp(); From 019faba73653c67270b38ab60aff518ea0b5271c Mon Sep 17 00:00:00 2001 From: FOLUSO ONATEMOWO Date: Fri, 18 Jul 2025 08:50:59 -0700 Subject: [PATCH 3/9] Commented out potential issues in dataconnect_test.ts --- dataconnect_test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dataconnect_test.ts b/dataconnect_test.ts index 2653eb6193..7cb3a10259 100644 --- a/dataconnect_test.ts +++ b/dataconnect_test.ts @@ -1,13 +1,13 @@ -const { getDataConnect } = require('./lib/data-connect'); -const { initializeApp } = require('./lib/app'); +// const { getDataConnect } = require('./lib/data-connect'); +// const { initializeApp } = require('./lib/app'); -const app = initializeApp(); +// const app = initializeApp(); -const config = { - serviceId: "", - location: "", +// const config = { +// serviceId: "", +// location: "", // connector:" Date: Mon, 21 Jul 2025 20:24:28 -0700 Subject: [PATCH 4/9] Added mutationRef's functionality accourding to the api proposal and cleaned up unnecessary code. --- dataconnect_test.ts | 22 ------ .../data-connect-api-client-internal.ts | 75 +++++-------------- src/data-connect/data-connect.ts | 42 +++++++---- test-dataconnect/dataconnect_test.ts | 27 +++++++ 4 files changed, 74 insertions(+), 92 deletions(-) delete mode 100644 dataconnect_test.ts create mode 100644 test-dataconnect/dataconnect_test.ts diff --git a/dataconnect_test.ts b/dataconnect_test.ts deleted file mode 100644 index 7cb3a10259..0000000000 --- a/dataconnect_test.ts +++ /dev/null @@ -1,22 +0,0 @@ -// const { getDataConnect } = require('./lib/data-connect'); -// const { initializeApp } = require('./lib/app'); - -// const app = initializeApp(); - -// const config = { -// serviceId: "", -// location: "", - // connector:", ): Promise> { - return this.executeGraphqlHelper(query, EXECUTE_GRAPH_QL_ENDPOINT, options); + // return this.executeGraphqlHelper(query, EXECUTE_GRAPH_QL_ENDPOINT, options); + return this.executeHelper(EXECUTE_GRAPH_QL_ENDPOINT,options, query); } /** @@ -100,15 +101,28 @@ export class DataConnectApiClient { return this.executeHelper(EXECUTE_GRAPH_QL_READ_ENDPOINT,options, query); } - /** - * Uses the name and the variables parameters to execute a query. + /** + * Execute pre-existing > read-only queries + * @param options - GraphQL Options + * @returns A promise that fulfills with a `ExecuteGraphqlResponse`. + * @throws FirebaseDataConnectError */ public async executeQuery( options: GraphqlOptions, ): Promise>{ - // const {data} = await this.executeHelper(options.operationName!, EXECUTE_QUERY_ENDPOINT, options); return this.executeHelper(EXECUTE_QUERY_ENDPOINT,options); } + /** + * Execute pre-existing > read and write queries + * @param options - GraphQL Options + * @returns A promise that fulfills with a `ExecuteGraphqlResponse`. + * @throws FirebaseDataConnectError + */ + public async executeMutation( + options: GraphqlOptions, + ): Promise>{ + return this.executeHelper(EXECUTE_MUTATION_ENDPOINT,options); +} private async executeHelper( endpoint: string, @@ -136,8 +150,6 @@ export class DataConnectApiClient { query: gql, ...(!gql && { name: options?.operationName}), ...(options?.variables && { variables: options?.variables }), - //change to if query != operationName for executeQuery and executeMutation - //Also how was this needed in conjuncton with executeGraphql before? Just the name of an operation normally doesn't that mean this is how it was used before? ...(options?.operationName && { operationName: options?.operationName }), ...(options?.impersonate && { extensions: { impersonate: options?.impersonate } }), }; @@ -168,55 +180,6 @@ export class DataConnectApiClient { }); } - private async executeGraphqlHelper( - query: string, - endpoint: string, - options?: GraphqlOptions, - ): Promise> { - if (!validator.isNonEmptyString(query)) { - throw new FirebaseDataConnectError( - DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - '`query` must be a non-empty string.'); - } - 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'); - } - } - const data = { - query, - ...(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) - .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); - }); - } - private async getUrl(version: string, locationId: string, serviceId: string, endpointId: string, connector?: string): Promise { return this.getProjectId() .then((projectId) => { diff --git a/src/data-connect/data-connect.ts b/src/data-connect/data-connect.ts index e97bfd9ef8..b085c7b732 100644 --- a/src/data-connect/data-connect.ts +++ b/src/data-connect/data-connect.ts @@ -191,7 +191,7 @@ export class DataConnect { * @param name Name of Mutation * @returns MutationRef */ - // public mutationRef(name: string): MutationRef; + public mutationRef(name: string): MutationRef; /** * * Returns Mutation Reference @@ -199,7 +199,7 @@ export class DataConnect { * @param variables * @returns MutationRef */ - // public mutationRef(name: string, variables: Variables): MutationRef; + public mutationRef(name: string, variables: Variables): MutationRef; /** * * Returns Query Reference @@ -207,9 +207,12 @@ export class DataConnect { * @param variables * @returns MutationRef */ - // public mutationRef(name: string, variables?: Variables): MutationRef { - // return new MutationRef(name, variables as Variables, this.client); - // } + public mutationRef(name: string, variables?: Variables): MutationRef { + if (!("connector" in this.connectorConfig)){ + throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeQuery requires a connector'); + } + return new MutationRef(this, name, variables as Variables, this.client); + } } abstract class OperationRef { @@ -229,14 +232,13 @@ interface OperationResult { export interface QueryResult extends OperationResult { ref: QueryRef; } -// interface MutationResult extends OperationResult { -// ref: MutationRef; -// } +export interface MutationResult extends OperationResult { + ref: MutationRef; +} class QueryRef extends OperationRef { option_params:GraphqlOptions; async execute(): Promise> { - // return this.client.executeQuery(this.name, this.variables); const option_params = { variables: this.variables, operationName: this.name @@ -252,8 +254,20 @@ class QueryRef extends OperationRef { } } -// class MutationRef extends OperationRef { -// execute(): Promise> { -// return this.client.executeMutation(this.name, this.variables); -// } -// } \ No newline at end of file +class MutationRef extends OperationRef { + option_params:GraphqlOptions; + async execute(): Promise> { + const option_params = { + variables: this.variables, + operationName: this.name + }; + const {data} = await this.client.executeMutation(option_params) + + return { + ref: this, + data: data, + variables: this.variables, + dataConnect: this.dataConnect + } + } +} \ No newline at end of file diff --git a/test-dataconnect/dataconnect_test.ts b/test-dataconnect/dataconnect_test.ts new file mode 100644 index 0000000000..4c3f2569a5 --- /dev/null +++ b/test-dataconnect/dataconnect_test.ts @@ -0,0 +1,27 @@ +import { getDataConnect } from '../lib/data-connect'; +import { initializeApp } from '../lib/app'; + +initializeApp(); + +const config = { + serviceId: "your-service-id", + location: "us-central1", + connector:"movie-connector" +}; + +const dataConnect = getDataConnect(config); + +async function queryref_test (){ + // const listOfMovies_res = await dataConnect.queryRef('ListMovies', {orderByRating: "DESC",orderByReleaseYear: "DESC", limit: 3}).execute(); + // const getActorsDetails = await dataConnect.queryRef('GetActorById', {id: "11111111222233334444555555555555"}).execute() + // const graphqlRead_listOfMovies_res = await dataConnect.executeGraphqlRead(" query { movies {id, title}}"); + // const graphql_listOfMovies_res= await dataConnect.executeGraphql(`mutation {user_insert(data: {id: "66666666667777777770", username: "coder_bot"})}`); + // const graphql_listOfMovies_res_with_operation= await dataConnect.executeGraphql(`mutation {user_insert(data: {id: "66666666667777777773", username: "night_agent"})}`); + // const graphql_query_listOfMovies_res = await dataConnect.executeGraphql(" query { movies {id, title}}"); + // const Upsert_user_res = await dataConnect.mutationRef('UpsertUser', {id: "66666666667777777771",username: "Hola"}).execute(); + const GetUserBy_Id_res = await dataConnect.queryRef("GetUserById",{id:"66666666667777777771"}).execute() + + console.log(GetUserBy_Id_res.data) +} + +queryref_test() \ No newline at end of file From 3f4528575df0a5ae5ea0d5463ee0da6afb76b1da Mon Sep 17 00:00:00 2001 From: FOLUSO ONATEMOWO Date: Thu, 24 Jul 2025 04:51:41 -0700 Subject: [PATCH 5/9] Created two new helper functions that use the executeHelper function. The two new functions are used to get different parameters and potentially throw detailed and tailored errors. --- package/LICENSE | 201 +++++++++++++ package/README.md | 94 ++++++ package/package.json | 274 ++++++++++++++++++ .../data-connect-api-client-internal.ts | 56 ++-- src/data-connect/data-connect.ts | 7 +- test-dataconnect/dataconnect_test.ts | 12 +- .../data-connect-api-client-internal.spec.ts | 6 + 7 files changed, 626 insertions(+), 24 deletions(-) create mode 100644 package/LICENSE create mode 100644 package/README.md create mode 100644 package/package.json diff --git a/package/LICENSE b/package/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/package/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/package/README.md b/package/README.md new file mode 100644 index 0000000000..8ebe57eab4 --- /dev/null +++ b/package/README.md @@ -0,0 +1,94 @@ +[![Build Status](https://github.com/firebase/firebase-admin-node/workflows/Continuous%20Integration/badge.svg)](https://github.com/firebase/firebase-admin-node/actions) + +# Firebase Admin Node.js SDK + + +## Table of Contents + + * [Overview](#overview) + * [Installation](#installation) + * [Contributing](#contributing) + * [Documentation](#documentation) + * [Supported Environments](#supported-environments) + * [Acknowledgments](#acknowledgments) + * [License](#license) + + +## Overview + +[Firebase](https://firebase.google.com) provides the tools and infrastructure +you need to develop your app, grow your user base, and earn money. The Firebase +Admin Node.js SDK enables access to Firebase services from privileged environments +(such as servers or cloud) in Node.js. + +For more information, visit the +[Firebase Admin SDK setup guide](https://firebase.google.com/docs/admin/setup/). + + +## Installation + +The Firebase Admin Node.js SDK is available on npm as `firebase-admin`: + +```bash +$ npm install --save firebase-admin +``` + +To use the module in your application, `require` it from any JavaScript file: + +```js +const { initializeApp } = require("firebase-admin/app"); + +initializeApp(); +``` + +If you are using ES2015, you can `import` the module instead: + +```js +import { initializeApp } from "firebase-admin/app"; + +initializeApp(); +``` + + +## Contributing + +Please refer to the [CONTRIBUTING page](./CONTRIBUTING.md) for more information +about how you can contribute to this project. We welcome bug reports, feature +requests, code review feedback, and also pull requests. + + +## Supported Environments + +We support Node.js 18 and higher. + +Please also note that the Admin SDK should only +be used in server-side/back-end environments controlled by the app developer. +This includes most server and serverless platforms (both on-premise and in +the cloud). It is not recommended to use the Admin SDK in client-side +environments. + + +## Documentation + +* [Setup Guide](https://firebase.google.com/docs/admin/setup/) +* [Database Guide](https://firebase.google.com/docs/database/admin/start/) +* [Authentication Guide](https://firebase.google.com/docs/auth/admin/) +* [Cloud Messaging Guide](https://firebase.google.com/docs/cloud-messaging/admin/) +* [API Reference](https://firebase.google.com/docs/reference/admin/node/) +* [Release Notes](https://firebase.google.com/support/release-notes/admin/node/) + + +## Acknowledgments + +Thanks to the team at [Casetext](https://casetext.com/) for transferring +ownership of the `firebase-admin` npm module over to the Firebase team +and for their longtime use and support of the Firebase platform. + + +## License + +Firebase Admin Node.js SDK is licensed under the +[Apache License, version 2.0](http://www.apache.org/licenses/LICENSE-2.0). + +Your use of Firebase is governed by the +[Terms of Service for Firebase Services](https://firebase.google.com/terms/). diff --git a/package/package.json b/package/package.json new file mode 100644 index 0000000000..1401c8ce6a --- /dev/null +++ b/package/package.json @@ -0,0 +1,274 @@ +{ + "name": "firebase-admin", + "version": "13.4.0", + "description": "Firebase admin SDK for Node.js", + "author": "Firebase (https://firebase.google.com/)", + "license": "Apache-2.0", + "homepage": "https://firebase.google.com/", + "engines": { + "node": ">=18" + }, + "scripts": { + "build": "gulp build", + "build:tests": "gulp compile_test", + "prepare": "npm run build && npm run esm-wrap", + "lint": "run-p lint:src lint:test", + "test": "run-s lint test:unit", + "integration": "run-s build test:integration", + "test:unit": "mocha test/unit/*.spec.ts --require ts-node/register", + "test:integration": "mocha test/integration/*.ts --slow 5000 --timeout 20000 --require ts-node/register", + "test:coverage": "nyc npm run test:unit", + "lint:src": "eslint src/ --ext .ts", + "lint:test": "eslint test/ --ext .ts", + "apidocs": "run-s api-extractor:local api-documenter", + "api-extractor": "node generate-reports.js", + "api-extractor:local": "npm run build && node generate-reports.js --local", + "esm-wrap": "node generate-esm-wrapper.js", + "api-documenter": "run-s api-documenter:markdown api-documenter:toc api-documenter:post", + "api-documenter:markdown": "api-documenter-fire markdown --input temp --output docgen/markdown -s --project admin", + "api-documenter:toc": "api-documenter-fire toc --input temp --output docgen/markdown -p /docs/reference/admin/node -s", + "api-documenter:post": "node docgen/post-process.js" + }, + "nyc": { + "extension": [ + ".ts" + ], + "include": [ + "src" + ], + "exclude": [ + "**/*.d.ts" + ], + "all": true + }, + "keywords": [ + "admin", + "database", + "Firebase", + "realtime", + "authentication" + ], + "repository": { + "type": "git", + "url": "https://github.com/firebase/firebase-admin-node" + }, + "main": "lib/index.js", + "files": [ + "lib/", + "LICENSE", + "README.md", + "package.json" + ], + "types": "./lib/index.d.ts", + "typesVersions": { + "*": { + "app": [ + "lib/app" + ], + "app-check": [ + "lib/app-check" + ], + "auth": [ + "lib/auth" + ], + "eventarc": [ + "lib/eventarc" + ], + "extensions": [ + "lib/extensions" + ], + "database": [ + "lib/database" + ], + "data-connect": [ + "lib/data-connect" + ], + "firestore": [ + "lib/firestore" + ], + "functions": [ + "lib/functions" + ], + "installations": [ + "lib/installations" + ], + "instance-id": [ + "lib/instance-id" + ], + "machine-learning": [ + "lib/machine-learning" + ], + "messaging": [ + "lib/messaging" + ], + "project-management": [ + "lib/project-management" + ], + "remote-config": [ + "lib/remote-config" + ], + "security-rules": [ + "lib/security-rules" + ], + "storage": [ + "lib/storage" + ] + } + }, + "exports": { + ".": "./lib/index.js", + "./app": { + "types": "./lib/app/index.d.ts", + "require": "./lib/app/index.js", + "import": "./lib/esm/app/index.js" + }, + "./app-check": { + "types": "./lib/app-check/index.d.ts", + "require": "./lib/app-check/index.js", + "import": "./lib/esm/app-check/index.js" + }, + "./auth": { + "types": "./lib/auth/index.d.ts", + "require": "./lib/auth/index.js", + "import": "./lib/esm/auth/index.js" + }, + "./database": { + "types": "./lib/database/index.d.ts", + "require": "./lib/database/index.js", + "import": "./lib/esm/database/index.js" + }, + "./data-connect": { + "types": "./lib/data-connect/index.d.ts", + "require": "./lib/data-connect/index.js", + "import": "./lib/esm/data-connect/index.js" + }, + "./eventarc": { + "types": "./lib/eventarc/index.d.ts", + "require": "./lib/eventarc/index.js", + "import": "./lib/esm/eventarc/index.js" + }, + "./extensions": { + "types": "./lib/extensions/index.d.ts", + "require": "./lib/extensions/index.js", + "import": "./lib/esm/extensions/index.js" + }, + "./firestore": { + "types": "./lib/firestore/index.d.ts", + "require": "./lib/firestore/index.js", + "import": "./lib/esm/firestore/index.js" + }, + "./functions": { + "types": "./lib/functions/index.d.ts", + "require": "./lib/functions/index.js", + "import": "./lib/esm/functions/index.js" + }, + "./installations": { + "types": "./lib/installations/index.d.ts", + "require": "./lib/installations/index.js", + "import": "./lib/esm/installations/index.js" + }, + "./instance-id": { + "types": "./lib/instance-id/index.d.ts", + "require": "./lib/instance-id/index.js", + "import": "./lib/esm/instance-id/index.js" + }, + "./machine-learning": { + "types": "./lib/machine-learning/index.d.ts", + "require": "./lib/machine-learning/index.js", + "import": "./lib/esm/machine-learning/index.js" + }, + "./messaging": { + "types": "./lib/messaging/index.d.ts", + "require": "./lib/messaging/index.js", + "import": "./lib/esm/messaging/index.js" + }, + "./project-management": { + "types": "./lib/project-management/index.d.ts", + "require": "./lib/project-management/index.js", + "import": "./lib/esm/project-management/index.js" + }, + "./remote-config": { + "types": "./lib/remote-config/index.d.ts", + "require": "./lib/remote-config/index.js", + "import": "./lib/esm/remote-config/index.js" + }, + "./security-rules": { + "types": "./lib/security-rules/index.d.ts", + "require": "./lib/security-rules/index.js", + "import": "./lib/esm/security-rules/index.js" + }, + "./storage": { + "types": "./lib/storage/index.d.ts", + "require": "./lib/storage/index.js", + "import": "./lib/esm/storage/index.js" + } + }, + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@firebase/database-compat": "^2.0.0", + "@firebase/database-types": "^1.0.6", + "@types/node": "^22.8.7", + "farmhash-modern": "^1.1.0", + "google-auth-library": "^9.14.2", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.1.0", + "node-forge": "^1.3.1", + "uuid": "^11.0.2" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^7.11.0", + "@google-cloud/storage": "^7.14.0" + }, + "devDependencies": { + "@firebase/api-documenter": "^0.4.0", + "@firebase/app-compat": "^0.2.1", + "@firebase/auth-compat": "^0.5.13", + "@firebase/auth-types": "^0.12.0", + "@microsoft/api-extractor": "^7.11.2", + "@types/bcrypt": "^5.0.0", + "@types/chai": "^4.0.0", + "@types/chai-as-promised": "^7.1.0", + "@types/firebase-token-generator": "^2.0.28", + "@types/jsonwebtoken": "8.5.1", + "@types/lodash": "^4.14.104", + "@types/minimist": "^1.2.2", + "@types/mocha": "^10.0.0", + "@types/nock": "^11.1.0", + "@types/request": "^2.47.0", + "@types/request-promise": "^4.1.41", + "@types/sinon": "^17.0.2", + "@types/sinon-chai": "^3.0.0", + "@types/uuid": "^10.0.0", + "@typescript-eslint/eslint-plugin": "^7.16.1", + "@typescript-eslint/parser": "^7.16.1", + "bcrypt": "^5.0.0", + "chai": "^4.2.0", + "chai-as-promised": "^7.0.0", + "chai-exclude": "^2.1.0", + "chalk": "^4.1.1", + "child-process-promise": "^2.2.1", + "del": "^6.0.0", + "eslint": "^8.56.0", + "firebase-token-generator": "^2.0.0", + "gulp": "^5.0.0", + "gulp-filter": "^7.0.0", + "gulp-header": "^2.0.9", + "gulp-typescript": "^5.0.1", + "http-message-parser": "^0.0.34", + "lodash": "^4.17.15", + "minimist": "^1.2.6", + "mocha": "^10.0.0", + "mz": "^2.7.0", + "nock": "^13.0.0", + "npm-run-all": "^4.1.5", + "nyc": "^17.0.0", + "request": "^2.75.0", + "request-promise": "^4.1.1", + "run-sequence": "^2.2.1", + "sinon": "^18.0.0", + "sinon-chai": "^3.0.0", + "ts-node": "^10.2.0", + "typescript": "5.5.4", + "yargs": "^17.0.1" + } +} diff --git a/src/data-connect/data-connect-api-client-internal.ts b/src/data-connect/data-connect-api-client-internal.ts index dce5d7df5d..e60e5e1e63 100644 --- a/src/data-connect/data-connect-api-client-internal.ts +++ b/src/data-connect/data-connect-api-client-internal.ts @@ -81,8 +81,8 @@ export class DataConnectApiClient { query: string, options?: GraphqlOptions, ): Promise> { - // return this.executeGraphqlHelper(query, EXECUTE_GRAPH_QL_ENDPOINT, options); - return this.executeHelper(EXECUTE_GRAPH_QL_ENDPOINT,options, query); + return this.executeGraphqlHelper(query, EXECUTE_GRAPH_QL_ENDPOINT, options); + // return this.executeHelper(EXECUTE_GRAPH_QL_ENDPOINT,options, query); } /** @@ -97,8 +97,8 @@ export class DataConnectApiClient { query: string, options?: GraphqlOptions, ): Promise> { - // return this.executeGraphqlHelper(query, EXECUTE_GRAPH_QL_READ_ENDPOINT, options); - return this.executeHelper(EXECUTE_GRAPH_QL_READ_ENDPOINT,options, query); + return this.executeGraphqlHelper(query, EXECUTE_GRAPH_QL_READ_ENDPOINT, options); + // return this.executeHelper(EXECUTE_GRAPH_QL_READ_ENDPOINT,options, query); } /** @@ -110,7 +110,7 @@ export class DataConnectApiClient { public async executeQuery( options: GraphqlOptions, ): Promise>{ - return this.executeHelper(EXECUTE_QUERY_ENDPOINT,options); + return this.executeOperationHelper(EXECUTE_QUERY_ENDPOINT,options); } /** * Execute pre-existing > read and write queries @@ -121,31 +121,53 @@ export class DataConnectApiClient { public async executeMutation( options: GraphqlOptions, ): Promise>{ - return this.executeHelper(EXECUTE_MUTATION_ENDPOINT,options); + return this.executeOperationHelper(EXECUTE_MUTATION_ENDPOINT,options); } - private async executeHelper( + private async executeGraphqlHelper( + query: string, endpoint: string, options?: GraphqlOptions, - gql?: string ): Promise> { - if (!validator.isNonEmptyString(gql) && typeof options == 'undefined') { + if (!validator.isNonEmptyString(query)) { throw new FirebaseDataConnectError( DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, - '`gql` must be a non-empty string or GraphqlOptions should be a non-null object'); - } //How would we steer them in the right direction or let them know which area makes the most sense to follow for what they are trying to accomplish? they might want gql to be empty and the message should say - if (typeof options !== 'undefined' && !validator.isNonEmptyString(gql)) { + '`query` must be a non-empty string.'); + } + 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, - '`gql` missing thus GraphqlOptions must contain `operationName`.');//Is this too descriptive? - } } + return this.executeHelper(endpoint, options, query) + } + + private async executeOperationHelper( + endpoint: string, + options?: GraphqlOptions, + ): Promise> { + if (typeof options == 'undefined') { + throw new FirebaseDataConnectError( + DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, + 'GraphqlOptions should 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( + endpoint: string, + options?: GraphqlOptions, + gql?: string + ): Promise> { const data = { query: gql, ...(!gql && { name: options?.operationName}), diff --git a/src/data-connect/data-connect.ts b/src/data-connect/data-connect.ts index b085c7b732..2c61f1023c 100644 --- a/src/data-connect/data-connect.ts +++ b/src/data-connect/data-connect.ts @@ -84,6 +84,9 @@ export class DataConnect { query: string, options?: GraphqlOptions, ): Promise> { + if ("connector" in this.connectorConfig){ + throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeGraphql does not require a connector'); + } return this.client.executeGraphql(query, options); } @@ -99,6 +102,9 @@ export class DataConnect { query: string, options?: GraphqlOptions, ): Promise> { + if ("connector" in this.connectorConfig){ + throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeGraphqlRead does not require a connector'); + } return this.client.executeGraphqlRead(query, options); } @@ -180,7 +186,6 @@ export class DataConnect { * @returns QueryRef */ public queryRef(name: string, variables?: Variables): QueryRef { - // console.log(this) if (!("connector" in this.connectorConfig)){ throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeQuery requires a connector'); } diff --git a/test-dataconnect/dataconnect_test.ts b/test-dataconnect/dataconnect_test.ts index 4c3f2569a5..37addc856a 100644 --- a/test-dataconnect/dataconnect_test.ts +++ b/test-dataconnect/dataconnect_test.ts @@ -6,7 +6,7 @@ initializeApp(); const config = { serviceId: "your-service-id", location: "us-central1", - connector:"movie-connector" + connector:"movie-connector" //If the connector is included when you run executeGraphqlRead and executeGraphql(Mutations and Queries) you get a 404 error possibly because of the generated link/url. Writing a condtition to check to stop it from getting server error, what would happen if we got server error normally on the client's end? }; const dataConnect = getDataConnect(config); @@ -14,14 +14,14 @@ const dataConnect = getDataConnect(config); async function queryref_test (){ // const listOfMovies_res = await dataConnect.queryRef('ListMovies', {orderByRating: "DESC",orderByReleaseYear: "DESC", limit: 3}).execute(); // const getActorsDetails = await dataConnect.queryRef('GetActorById', {id: "11111111222233334444555555555555"}).execute() - // const graphqlRead_listOfMovies_res = await dataConnect.executeGraphqlRead(" query { movies {id, title}}"); - // const graphql_listOfMovies_res= await dataConnect.executeGraphql(`mutation {user_insert(data: {id: "66666666667777777770", username: "coder_bot"})}`); + // // const graphqlRead_listOfMovies_res = await dataConnect.executeGraphqlRead(" query { movies {id, title}}"); + // const graphql_listOfMovies_res= await dataConnect.executeGraphql(`mutation {user_insert(data: {id: "666666666677777777711", username: "coder_bot"})}`); // const graphql_listOfMovies_res_with_operation= await dataConnect.executeGraphql(`mutation {user_insert(data: {id: "66666666667777777773", username: "night_agent"})}`); - // const graphql_query_listOfMovies_res = await dataConnect.executeGraphql(" query { movies {id, title}}"); + const graphql_query_listOfMovies_res = await dataConnect.executeGraphql(" query { movies {id, title}}"); // const Upsert_user_res = await dataConnect.mutationRef('UpsertUser', {id: "66666666667777777771",username: "Hola"}).execute(); - const GetUserBy_Id_res = await dataConnect.queryRef("GetUserById",{id:"66666666667777777771"}).execute() + // const GetUserBy_Id_res = await dataConnect.queryRef("GetUserById",{id:"666666666677777777711"}).execute() - console.log(GetUserBy_Id_res.data) + console.log(graphql_query_listOfMovies_res.data) } queryref_test() \ No newline at end of file diff --git a/test/unit/data-connect/data-connect-api-client-internal.spec.ts b/test/unit/data-connect/data-connect-api-client-internal.spec.ts index a5798703e5..15998a35f5 100644 --- a/test/unit/data-connect/data-connect-api-client-internal.spec.ts +++ b/test/unit/data-connect/data-connect-api-client-internal.spec.ts @@ -71,6 +71,12 @@ describe('DataConnectApiClient', () => { serviceId: 'my-service', }; + const connectorConfig_with_connector: ConnectorConfig = { + location: 'us-west2', + serviceId: 'my-service', + connector: 'mock-connector' //this is fake, I need to verify where to put it officially within mock.ts perhaps- still unsure + }; + const clientWithoutProjectId = new DataConnectApiClient( connectorConfig, mocks.mockCredentialApp()); From fe78b87af3ed896b34166ce9ef217cdacf19d3a9 Mon Sep 17 00:00:00 2001 From: FOLUSO ONATEMOWO Date: Thu, 31 Jul 2025 16:17:40 -0700 Subject: [PATCH 6/9] Modified the geturl function to account for a connector being passed into the connectorConfig so that executeGraphql and executeGraphqlRead don't try to retrieve data from wrong endpoints. Included unit tests for the executeMutation function. --- .gitignore | 1 + .../data-connect-api-client-internal.ts | 15 +- src/data-connect/data-connect.ts | 12 +- .../data-connect-api-client-internal.spec.ts | 177 +++++++++++++++++- 4 files changed, 195 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index f9e33e377e..2005e95795 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ firebase-admin-*.tgz docgen/markdown/ service_account.json +dataconnect/ \ No newline at end of file diff --git a/src/data-connect/data-connect-api-client-internal.ts b/src/data-connect/data-connect-api-client-internal.ts index e60e5e1e63..9e80cb7049 100644 --- a/src/data-connect/data-connect-api-client-internal.ts +++ b/src/data-connect/data-connect-api-client-internal.ts @@ -33,7 +33,7 @@ const FIREBASE_DATA_CONNECT_BASE_URL_FORMAT = /** 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}'; + '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 = @@ -153,6 +153,15 @@ export class DataConnectApiClient { DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'GraphqlOptions should be a non-null object'); } + //Recent addition + 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, @@ -215,7 +224,7 @@ export class DataConnectApiClient { }; let urlFormat: string; if (useEmulator()) { - if ('connector' in urlParams){ + 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() }); @@ -226,7 +235,7 @@ export class DataConnectApiClient { }); } } else { - if ('connector' in urlParams){ + 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;} diff --git a/src/data-connect/data-connect.ts b/src/data-connect/data-connect.ts index 2c61f1023c..c45a9367a1 100644 --- a/src/data-connect/data-connect.ts +++ b/src/data-connect/data-connect.ts @@ -84,9 +84,9 @@ export class DataConnect { query: string, options?: GraphqlOptions, ): Promise> { - if ("connector" in this.connectorConfig){ - throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeGraphql does not require a connector'); - } + // if ("connector" in this.connectorConfig){ + // throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeGraphql does not require a connector'); + // } return this.client.executeGraphql(query, options); } @@ -102,9 +102,9 @@ export class DataConnect { query: string, options?: GraphqlOptions, ): Promise> { - if ("connector" in this.connectorConfig){ - throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeGraphqlRead does not require a connector'); - } + // if ("connector" in this.connectorConfig){ + // throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeGraphqlRead does not require a connector'); + // } return this.client.executeGraphqlRead(query, options); } diff --git a/test/unit/data-connect/data-connect-api-client-internal.spec.ts b/test/unit/data-connect/data-connect-api-client-internal.spec.ts index 15998a35f5..5ff8c1cf71 100644 --- a/test/unit/data-connect/data-connect-api-client-internal.spec.ts +++ b/test/unit/data-connect/data-connect-api-client-internal.spec.ts @@ -81,6 +81,10 @@ describe('DataConnectApiClient', () => { connectorConfig, mocks.mockCredentialApp()); + const clientWithoutProjectId_with_connector = new DataConnectApiClient( + connectorConfig_with_connector, + mocks.mockCredentialApp()); + const mockOptions = { credential: new mocks.MockCredential(), projectId: 'test-project', @@ -89,12 +93,14 @@ describe('DataConnectApiClient', () => { let app: FirebaseApp; let apiClient: DataConnectApiClient; + let apiClient_with_connector: DataConnectApiClient; let sandbox: sinon.SinonSandbox; beforeEach(() => { sandbox = sinon.createSandbox(); app = mocks.appWithOptions(mockOptions); apiClient = new DataConnectApiClient(connectorConfig, app); + apiClient_with_connector = new DataConnectApiClient(connectorConfig_with_connector, app); }); afterEach(() => { @@ -116,6 +122,17 @@ describe('DataConnectApiClient', () => { it('should initialize httpClient with the provided app', () => { expect((apiClient as any).httpClient).to.be.an.instanceOf(AuthorizedHttpClient); }); + // Test for an app instance with a connector within the connector config + it('should throw an error if app is not a valid Firebase app instance', () => { + expect(() => new DataConnectApiClient(connectorConfig_with_connector, null as unknown as FirebaseApp)).to.throw( + FirebaseDataConnectError, + 'First argument passed to getDataConnect() must be a valid Firebase app instance.' + ); + }); + + it('should initialize httpClient with the provided app', () => { + expect((apiClient_with_connector as any).httpClient).to.be.an.instanceOf(AuthorizedHttpClient); + }); }); describe('executeGraphql', () => { @@ -215,7 +232,7 @@ describe('DataConnectApiClient', () => { method: 'POST', url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig.location}/services/${connectorConfig.serviceId}:executeGraphql`, headers: EXPECTED_HEADERS, - data: { query: 'query' } + data: { query: 'query' } }); }); }); @@ -235,6 +252,164 @@ describe('DataConnectApiClient', () => { }); }); }); + + it('should resolve with the GraphQL response on success when a connector is passed in', () => { + interface UsersResponse { + users: [ + user: { + id: string; + name: string; + address: string; + } + ]; + } + const stub = sandbox + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200)); + return apiClient_with_connector.executeGraphql('query', {}) + .then((resp) => { + expect(resp.data.users).to.be.not.empty; + expect(resp.data.users[0].name).to.be.not.undefined; + expect(resp.data.users[0].address).to.be.not.undefined; + expect(resp.data.users).to.deep.equal(TEST_RESPONSE.data.users); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig.location}/services/${connectorConfig.serviceId}:executeGraphql`, + headers: EXPECTED_HEADERS, + data: { query: 'query' } + }); + }); + }); + + it('should use DATA_CONNECT_EMULATOR_HOST if set', () => { + process.env.DATA_CONNECT_EMULATOR_HOST = 'localhost:9399'; + const stub = sandbox + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200)); + return apiClient_with_connector.executeGraphql('query', {}) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: `http://localhost:9399/v1alpha/projects/test-project/locations/${connectorConfig.location}/services/${connectorConfig.serviceId}:executeGraphql`, + headers: EMULATOR_EXPECTED_HEADERS, + data: { query: 'query' } + }); + }); + }); + + }); + + describe('executeMutation', () => { + //what if there's no project id and also there's no connector, what error would that be? -> should error the connector first because you won't even reach the endpoint to find out if there's a project id or not + it('should reject when project id is not available', () => { + return clientWithoutProjectId_with_connector.executeMutation({operationName: 'getById'}) + .should.eventually.be.rejectedWith(noProjectId); + }); + + it('should throw an error if no arguments are passed in', async () => { + await expect(apiClient_with_connector.executeMutation(undefined as any)).to.be.rejectedWith( + FirebaseDataConnectError, + 'GraphqlOptions should be a non-null object' + ); + }); + + const invalidOptions = [null, NaN, 0, 1, true, false, [], _.noop]; + invalidOptions.forEach((invalidOption) => { + it('should throw given an invalid options object: ' + JSON.stringify(invalidOption), async () => { + await expect(apiClient_with_connector.executeMutation(invalidOption as any)).to.be.rejectedWith( + FirebaseDataConnectError, + 'GraphqlOptions must be a non-null object' + ); + }); + }); + //could this pass as a null object, also what if the wrong operaton was passed in, would it be handled in another test- say integration? + it('should throw an error if there is no operationName', async () => { + await expect(apiClient_with_connector.executeMutation({})).to.be.rejectedWith( + FirebaseDataConnectError, + 'GraphqlOptions must contain `operationName`.' + ); + }); + + it('should reject when a full platform error response is received', () => { + sandbox + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + const expected = new FirebaseDataConnectError('not-found', 'Requested entity not found'); + return apiClient_with_connector.executeMutation({operationName: 'getById'}) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject with unknown-error when error code is not present', () => { + sandbox + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom({}, 404)); + const expected = new FirebaseDataConnectError('unknown-error', 'Unknown server error: {}'); + return apiClient_with_connector.executeMutation({operationName: 'getById'}) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject with unknown-error for non-json response', () => { + sandbox + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom('not json', 404)); + const expected = new FirebaseDataConnectError( + 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + return apiClient_with_connector.executeMutation({operationName: 'getById'}) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject when rejected with a FirebaseDataConnectError', () => { + const expected = new FirebaseDataConnectError('internal-error', 'socket hang up'); + sandbox + .stub(HttpClient.prototype, 'send') + .rejects(expected); + return apiClient_with_connector.executeMutation({operationName: 'getById'}) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should resolve with the Mutation response on success', () => { + interface UsersResponse { + users: [ + user: { + id: string; + name: string; + address: string; + } + ]; + } + const stub = sandbox + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200)); + return apiClient_with_connector.executeMutation({operationName: 'getById'}) + .then((resp) => { + expect(resp.data.users).to.be.not.empty; + expect(resp.data.users[0].name).to.be.not.undefined; + expect(resp.data.users[0].address).to.be.not.undefined; + expect(resp.data.users).to.deep.equal(TEST_RESPONSE.data.users); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig_with_connector.location}/services/${connectorConfig_with_connector.serviceId}/connectors/${connectorConfig_with_connector.connector}:executeMutation`, + headers: EXPECTED_HEADERS, + data: { query: undefined, name: 'getById', operationName: 'getById' } + }); + }); + }); + + it('should use DATA_CONNECT_EMULATOR_HOST if set', () => { + process.env.DATA_CONNECT_EMULATOR_HOST = 'localhost:9399'; + const stub = sandbox + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200)); + return apiClient_with_connector.executeMutation({operationName: 'getById'}) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: `http://localhost:9399/v1alpha/projects/test-project/locations/${connectorConfig_with_connector.location}/services/${connectorConfig_with_connector.serviceId}/connectors/${connectorConfig_with_connector.connector}:executeMutation`, + headers: EMULATOR_EXPECTED_HEADERS, + data: { query: undefined, name: 'getById', operationName: 'getById' } + }); + }); + }); }); }); From 1c5e1cefb3cd9ab82e4537c1ad8841a5786a1b93 Mon Sep 17 00:00:00 2001 From: FOLUSO ONATEMOWO Date: Thu, 31 Jul 2025 17:02:42 -0700 Subject: [PATCH 7/9] Replaced some urls present in the executeMutation unit tests and cleaned up some unnecessary comments. --- src/data-connect/data-connect-api-client-internal.ts | 5 ++--- src/data-connect/data-connect.ts | 8 +------- .../data-connect-api-client-internal.spec.ts | 9 ++++----- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/data-connect/data-connect-api-client-internal.ts b/src/data-connect/data-connect-api-client-internal.ts index 9e80cb7049..012dbafd12 100644 --- a/src/data-connect/data-connect-api-client-internal.ts +++ b/src/data-connect/data-connect-api-client-internal.ts @@ -102,7 +102,7 @@ export class DataConnectApiClient { } /** - * Execute pre-existing > read-only queries + * Execute pre-existing read-only queries > * @param options - GraphQL Options * @returns A promise that fulfills with a `ExecuteGraphqlResponse`. * @throws FirebaseDataConnectError @@ -113,7 +113,7 @@ export class DataConnectApiClient { return this.executeOperationHelper(EXECUTE_QUERY_ENDPOINT,options); } /** - * Execute pre-existing > read and write queries + * Execute pre-existing read and write queries > * @param options - GraphQL Options * @returns A promise that fulfills with a `ExecuteGraphqlResponse`. * @throws FirebaseDataConnectError @@ -153,7 +153,6 @@ export class DataConnectApiClient { DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT, 'GraphqlOptions should be a non-null object'); } - //Recent addition if (typeof options !== 'undefined') { if (!validator.isNonNullObject(options)) { throw new FirebaseDataConnectError( diff --git a/src/data-connect/data-connect.ts b/src/data-connect/data-connect.ts index c45a9367a1..d0dbcc3cef 100644 --- a/src/data-connect/data-connect.ts +++ b/src/data-connect/data-connect.ts @@ -84,9 +84,6 @@ export class DataConnect { query: string, options?: GraphqlOptions, ): Promise> { - // if ("connector" in this.connectorConfig){ - // throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeGraphql does not require a connector'); - // } return this.client.executeGraphql(query, options); } @@ -102,9 +99,6 @@ export class DataConnect { query: string, options?: GraphqlOptions, ): Promise> { - // if ("connector" in this.connectorConfig){ - // throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeGraphqlRead does not require a connector'); - // } return this.client.executeGraphqlRead(query, options); } @@ -214,7 +208,7 @@ export class DataConnect { */ public mutationRef(name: string, variables?: Variables): MutationRef { if (!("connector" in this.connectorConfig)){ - throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeQuery requires a connector'); + throw new FirebaseDataConnectError(DATA_CONNECT_ERROR_CODE_MAPPING.INVALID_ARGUMENT,'executeMutation requires a connector'); } return new MutationRef(this, name, variables as Variables, this.client); } diff --git a/test/unit/data-connect/data-connect-api-client-internal.spec.ts b/test/unit/data-connect/data-connect-api-client-internal.spec.ts index 5ff8c1cf71..9f9797c315 100644 --- a/test/unit/data-connect/data-connect-api-client-internal.spec.ts +++ b/test/unit/data-connect/data-connect-api-client-internal.spec.ts @@ -74,7 +74,7 @@ describe('DataConnectApiClient', () => { const connectorConfig_with_connector: ConnectorConfig = { location: 'us-west2', serviceId: 'my-service', - connector: 'mock-connector' //this is fake, I need to verify where to put it officially within mock.ts perhaps- still unsure + connector: 'mock-connector' }; const clientWithoutProjectId = new DataConnectApiClient( @@ -274,7 +274,7 @@ describe('DataConnectApiClient', () => { expect(resp.data.users).to.deep.equal(TEST_RESPONSE.data.users); expect(stub).to.have.been.calledOnce.and.calledWith({ method: 'POST', - url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig.location}/services/${connectorConfig.serviceId}:executeGraphql`, + url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig_with_connector.location}/services/${connectorConfig_with_connector.serviceId}:executeGraphql`, headers: EXPECTED_HEADERS, data: { query: 'query' } }); @@ -290,7 +290,7 @@ describe('DataConnectApiClient', () => { .then(() => { expect(stub).to.have.been.calledOnce.and.calledWith({ method: 'POST', - url: `http://localhost:9399/v1alpha/projects/test-project/locations/${connectorConfig.location}/services/${connectorConfig.serviceId}:executeGraphql`, + url: `http://localhost:9399/v1alpha/projects/test-project/locations/${connectorConfig_with_connector.location}/services/${connectorConfig_with_connector.serviceId}:executeGraphql`, headers: EMULATOR_EXPECTED_HEADERS, data: { query: 'query' } }); @@ -300,7 +300,6 @@ describe('DataConnectApiClient', () => { }); describe('executeMutation', () => { - //what if there's no project id and also there's no connector, what error would that be? -> should error the connector first because you won't even reach the endpoint to find out if there's a project id or not it('should reject when project id is not available', () => { return clientWithoutProjectId_with_connector.executeMutation({operationName: 'getById'}) .should.eventually.be.rejectedWith(noProjectId); @@ -322,7 +321,7 @@ describe('DataConnectApiClient', () => { ); }); }); - //could this pass as a null object, also what if the wrong operaton was passed in, would it be handled in another test- say integration? + it('should throw an error if there is no operationName', async () => { await expect(apiClient_with_connector.executeMutation({})).to.be.rejectedWith( FirebaseDataConnectError, From c99c078453983acf4daad86571a83be47ff3cc16 Mon Sep 17 00:00:00 2001 From: FOLUSO ONATEMOWO Date: Mon, 4 Aug 2025 11:25:01 -0700 Subject: [PATCH 8/9] Incorporated feedback from a PR review and added unit tests for . --- .firebaserc | 7 - package-lock.json | 4 +- package/LICENSE | 201 ------------- package/README.md | 94 ------ package/package.json | 274 ------------------ .../data-connect-api-client-internal.ts | 58 ++-- src/data-connect/data-connect.ts | 2 +- src/data-connect/index.ts | 4 - test-dataconnect/dataconnect_test.ts | 27 -- .../data-connect-api-client-internal.spec.ts | 204 +++++++++++-- 10 files changed, 218 insertions(+), 657 deletions(-) delete mode 100644 .firebaserc delete mode 100644 package/LICENSE delete mode 100644 package/README.md delete mode 100644 package/package.json delete mode 100644 test-dataconnect/dataconnect_test.ts diff --git a/.firebaserc b/.firebaserc deleted file mode 100644 index b1ebad5e12..0000000000 --- a/.firebaserc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "projects": { - "default": "test-suite-e6c23" - }, - "targets": {}, - "etags": {} -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fc2251b97e..bba8a0fd5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "firebase-admin", - "version": "13.4.0", + "version": "13.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "firebase-admin", - "version": "13.4.0", + "version": "13.3.0", "license": "Apache-2.0", "dependencies": { "@fastify/busboy": "^3.0.0", diff --git a/package/LICENSE b/package/LICENSE deleted file mode 100644 index 261eeb9e9f..0000000000 --- a/package/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/package/README.md b/package/README.md deleted file mode 100644 index 8ebe57eab4..0000000000 --- a/package/README.md +++ /dev/null @@ -1,94 +0,0 @@ -[![Build Status](https://github.com/firebase/firebase-admin-node/workflows/Continuous%20Integration/badge.svg)](https://github.com/firebase/firebase-admin-node/actions) - -# Firebase Admin Node.js SDK - - -## Table of Contents - - * [Overview](#overview) - * [Installation](#installation) - * [Contributing](#contributing) - * [Documentation](#documentation) - * [Supported Environments](#supported-environments) - * [Acknowledgments](#acknowledgments) - * [License](#license) - - -## Overview - -[Firebase](https://firebase.google.com) provides the tools and infrastructure -you need to develop your app, grow your user base, and earn money. The Firebase -Admin Node.js SDK enables access to Firebase services from privileged environments -(such as servers or cloud) in Node.js. - -For more information, visit the -[Firebase Admin SDK setup guide](https://firebase.google.com/docs/admin/setup/). - - -## Installation - -The Firebase Admin Node.js SDK is available on npm as `firebase-admin`: - -```bash -$ npm install --save firebase-admin -``` - -To use the module in your application, `require` it from any JavaScript file: - -```js -const { initializeApp } = require("firebase-admin/app"); - -initializeApp(); -``` - -If you are using ES2015, you can `import` the module instead: - -```js -import { initializeApp } from "firebase-admin/app"; - -initializeApp(); -``` - - -## Contributing - -Please refer to the [CONTRIBUTING page](./CONTRIBUTING.md) for more information -about how you can contribute to this project. We welcome bug reports, feature -requests, code review feedback, and also pull requests. - - -## Supported Environments - -We support Node.js 18 and higher. - -Please also note that the Admin SDK should only -be used in server-side/back-end environments controlled by the app developer. -This includes most server and serverless platforms (both on-premise and in -the cloud). It is not recommended to use the Admin SDK in client-side -environments. - - -## Documentation - -* [Setup Guide](https://firebase.google.com/docs/admin/setup/) -* [Database Guide](https://firebase.google.com/docs/database/admin/start/) -* [Authentication Guide](https://firebase.google.com/docs/auth/admin/) -* [Cloud Messaging Guide](https://firebase.google.com/docs/cloud-messaging/admin/) -* [API Reference](https://firebase.google.com/docs/reference/admin/node/) -* [Release Notes](https://firebase.google.com/support/release-notes/admin/node/) - - -## Acknowledgments - -Thanks to the team at [Casetext](https://casetext.com/) for transferring -ownership of the `firebase-admin` npm module over to the Firebase team -and for their longtime use and support of the Firebase platform. - - -## License - -Firebase Admin Node.js SDK is licensed under the -[Apache License, version 2.0](http://www.apache.org/licenses/LICENSE-2.0). - -Your use of Firebase is governed by the -[Terms of Service for Firebase Services](https://firebase.google.com/terms/). diff --git a/package/package.json b/package/package.json deleted file mode 100644 index 1401c8ce6a..0000000000 --- a/package/package.json +++ /dev/null @@ -1,274 +0,0 @@ -{ - "name": "firebase-admin", - "version": "13.4.0", - "description": "Firebase admin SDK for Node.js", - "author": "Firebase (https://firebase.google.com/)", - "license": "Apache-2.0", - "homepage": "https://firebase.google.com/", - "engines": { - "node": ">=18" - }, - "scripts": { - "build": "gulp build", - "build:tests": "gulp compile_test", - "prepare": "npm run build && npm run esm-wrap", - "lint": "run-p lint:src lint:test", - "test": "run-s lint test:unit", - "integration": "run-s build test:integration", - "test:unit": "mocha test/unit/*.spec.ts --require ts-node/register", - "test:integration": "mocha test/integration/*.ts --slow 5000 --timeout 20000 --require ts-node/register", - "test:coverage": "nyc npm run test:unit", - "lint:src": "eslint src/ --ext .ts", - "lint:test": "eslint test/ --ext .ts", - "apidocs": "run-s api-extractor:local api-documenter", - "api-extractor": "node generate-reports.js", - "api-extractor:local": "npm run build && node generate-reports.js --local", - "esm-wrap": "node generate-esm-wrapper.js", - "api-documenter": "run-s api-documenter:markdown api-documenter:toc api-documenter:post", - "api-documenter:markdown": "api-documenter-fire markdown --input temp --output docgen/markdown -s --project admin", - "api-documenter:toc": "api-documenter-fire toc --input temp --output docgen/markdown -p /docs/reference/admin/node -s", - "api-documenter:post": "node docgen/post-process.js" - }, - "nyc": { - "extension": [ - ".ts" - ], - "include": [ - "src" - ], - "exclude": [ - "**/*.d.ts" - ], - "all": true - }, - "keywords": [ - "admin", - "database", - "Firebase", - "realtime", - "authentication" - ], - "repository": { - "type": "git", - "url": "https://github.com/firebase/firebase-admin-node" - }, - "main": "lib/index.js", - "files": [ - "lib/", - "LICENSE", - "README.md", - "package.json" - ], - "types": "./lib/index.d.ts", - "typesVersions": { - "*": { - "app": [ - "lib/app" - ], - "app-check": [ - "lib/app-check" - ], - "auth": [ - "lib/auth" - ], - "eventarc": [ - "lib/eventarc" - ], - "extensions": [ - "lib/extensions" - ], - "database": [ - "lib/database" - ], - "data-connect": [ - "lib/data-connect" - ], - "firestore": [ - "lib/firestore" - ], - "functions": [ - "lib/functions" - ], - "installations": [ - "lib/installations" - ], - "instance-id": [ - "lib/instance-id" - ], - "machine-learning": [ - "lib/machine-learning" - ], - "messaging": [ - "lib/messaging" - ], - "project-management": [ - "lib/project-management" - ], - "remote-config": [ - "lib/remote-config" - ], - "security-rules": [ - "lib/security-rules" - ], - "storage": [ - "lib/storage" - ] - } - }, - "exports": { - ".": "./lib/index.js", - "./app": { - "types": "./lib/app/index.d.ts", - "require": "./lib/app/index.js", - "import": "./lib/esm/app/index.js" - }, - "./app-check": { - "types": "./lib/app-check/index.d.ts", - "require": "./lib/app-check/index.js", - "import": "./lib/esm/app-check/index.js" - }, - "./auth": { - "types": "./lib/auth/index.d.ts", - "require": "./lib/auth/index.js", - "import": "./lib/esm/auth/index.js" - }, - "./database": { - "types": "./lib/database/index.d.ts", - "require": "./lib/database/index.js", - "import": "./lib/esm/database/index.js" - }, - "./data-connect": { - "types": "./lib/data-connect/index.d.ts", - "require": "./lib/data-connect/index.js", - "import": "./lib/esm/data-connect/index.js" - }, - "./eventarc": { - "types": "./lib/eventarc/index.d.ts", - "require": "./lib/eventarc/index.js", - "import": "./lib/esm/eventarc/index.js" - }, - "./extensions": { - "types": "./lib/extensions/index.d.ts", - "require": "./lib/extensions/index.js", - "import": "./lib/esm/extensions/index.js" - }, - "./firestore": { - "types": "./lib/firestore/index.d.ts", - "require": "./lib/firestore/index.js", - "import": "./lib/esm/firestore/index.js" - }, - "./functions": { - "types": "./lib/functions/index.d.ts", - "require": "./lib/functions/index.js", - "import": "./lib/esm/functions/index.js" - }, - "./installations": { - "types": "./lib/installations/index.d.ts", - "require": "./lib/installations/index.js", - "import": "./lib/esm/installations/index.js" - }, - "./instance-id": { - "types": "./lib/instance-id/index.d.ts", - "require": "./lib/instance-id/index.js", - "import": "./lib/esm/instance-id/index.js" - }, - "./machine-learning": { - "types": "./lib/machine-learning/index.d.ts", - "require": "./lib/machine-learning/index.js", - "import": "./lib/esm/machine-learning/index.js" - }, - "./messaging": { - "types": "./lib/messaging/index.d.ts", - "require": "./lib/messaging/index.js", - "import": "./lib/esm/messaging/index.js" - }, - "./project-management": { - "types": "./lib/project-management/index.d.ts", - "require": "./lib/project-management/index.js", - "import": "./lib/esm/project-management/index.js" - }, - "./remote-config": { - "types": "./lib/remote-config/index.d.ts", - "require": "./lib/remote-config/index.js", - "import": "./lib/esm/remote-config/index.js" - }, - "./security-rules": { - "types": "./lib/security-rules/index.d.ts", - "require": "./lib/security-rules/index.js", - "import": "./lib/esm/security-rules/index.js" - }, - "./storage": { - "types": "./lib/storage/index.d.ts", - "require": "./lib/storage/index.js", - "import": "./lib/esm/storage/index.js" - } - }, - "dependencies": { - "@fastify/busboy": "^3.0.0", - "@firebase/database-compat": "^2.0.0", - "@firebase/database-types": "^1.0.6", - "@types/node": "^22.8.7", - "farmhash-modern": "^1.1.0", - "google-auth-library": "^9.14.2", - "jsonwebtoken": "^9.0.0", - "jwks-rsa": "^3.1.0", - "node-forge": "^1.3.1", - "uuid": "^11.0.2" - }, - "optionalDependencies": { - "@google-cloud/firestore": "^7.11.0", - "@google-cloud/storage": "^7.14.0" - }, - "devDependencies": { - "@firebase/api-documenter": "^0.4.0", - "@firebase/app-compat": "^0.2.1", - "@firebase/auth-compat": "^0.5.13", - "@firebase/auth-types": "^0.12.0", - "@microsoft/api-extractor": "^7.11.2", - "@types/bcrypt": "^5.0.0", - "@types/chai": "^4.0.0", - "@types/chai-as-promised": "^7.1.0", - "@types/firebase-token-generator": "^2.0.28", - "@types/jsonwebtoken": "8.5.1", - "@types/lodash": "^4.14.104", - "@types/minimist": "^1.2.2", - "@types/mocha": "^10.0.0", - "@types/nock": "^11.1.0", - "@types/request": "^2.47.0", - "@types/request-promise": "^4.1.41", - "@types/sinon": "^17.0.2", - "@types/sinon-chai": "^3.0.0", - "@types/uuid": "^10.0.0", - "@typescript-eslint/eslint-plugin": "^7.16.1", - "@typescript-eslint/parser": "^7.16.1", - "bcrypt": "^5.0.0", - "chai": "^4.2.0", - "chai-as-promised": "^7.0.0", - "chai-exclude": "^2.1.0", - "chalk": "^4.1.1", - "child-process-promise": "^2.2.1", - "del": "^6.0.0", - "eslint": "^8.56.0", - "firebase-token-generator": "^2.0.0", - "gulp": "^5.0.0", - "gulp-filter": "^7.0.0", - "gulp-header": "^2.0.9", - "gulp-typescript": "^5.0.1", - "http-message-parser": "^0.0.34", - "lodash": "^4.17.15", - "minimist": "^1.2.6", - "mocha": "^10.0.0", - "mz": "^2.7.0", - "nock": "^13.0.0", - "npm-run-all": "^4.1.5", - "nyc": "^17.0.0", - "request": "^2.75.0", - "request-promise": "^4.1.1", - "run-sequence": "^2.2.1", - "sinon": "^18.0.0", - "sinon-chai": "^3.0.0", - "ts-node": "^10.2.0", - "typescript": "5.5.4", - "yargs": "^17.0.1" - } -} diff --git a/src/data-connect/data-connect-api-client-internal.ts b/src/data-connect/data-connect-api-client-internal.ts index 012dbafd12..d666d2b6b4 100644 --- a/src/data-connect/data-connect-api-client-internal.ts +++ b/src/data-connect/data-connect-api-client-internal.ts @@ -29,11 +29,11 @@ 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}'; /** 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}'; + '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 = @@ -82,11 +82,10 @@ export class DataConnectApiClient { options?: GraphqlOptions, ): Promise> { return this.executeGraphqlHelper(query, EXECUTE_GRAPH_QL_ENDPOINT, options); - // return this.executeHelper(EXECUTE_GRAPH_QL_ENDPOINT,options, query); } /** - * Execute arbi>trary read-only GraphQL queries + * Execute arbitrary read-only GraphQL queries * * @param query - The GraphQL (read-only) string to be executed. * @param options - GraphQL Options @@ -109,9 +108,9 @@ export class DataConnectApiClient { */ public async executeQuery( options: GraphqlOptions, - ): Promise>{ - return this.executeOperationHelper(EXECUTE_QUERY_ENDPOINT,options); -} + ): Promise> { + return this.executeOperationHelper(EXECUTE_QUERY_ENDPOINT, options); + } /** * Execute pre-existing read and write queries > * @param options - GraphQL Options @@ -120,9 +119,9 @@ export class DataConnectApiClient { */ public async executeMutation( options: GraphqlOptions, - ): Promise>{ - return this.executeOperationHelper(EXECUTE_MUTATION_ENDPOINT,options); -} + ): Promise> { + return this.executeOperationHelper(EXECUTE_MUTATION_ENDPOINT, options); + } private async executeGraphqlHelper( query: string, @@ -142,11 +141,11 @@ export class DataConnectApiClient { } } return this.executeHelper(endpoint, options, query) - } - + } + private async executeOperationHelper( endpoint: string, - options?: GraphqlOptions, + options: GraphqlOptions, ): Promise> { if (typeof options == 'undefined') { throw new FirebaseDataConnectError( @@ -168,7 +167,7 @@ export class DataConnectApiClient { } return this.executeHelper(endpoint, options) - } + } private async executeHelper( @@ -178,12 +177,12 @@ export class DataConnectApiClient { ): Promise> { const data = { query: gql, - ...(!gql && { name: options?.operationName}), + ...(!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,this.connectorConfig.connector) + return this.getUrl(API_VERSION, this.connectorConfig.location, this.connectorConfig.serviceId, endpoint, this.connectorConfig.connector) .then(async (url) => { const request: HttpRequestConfig = { method: 'POST', @@ -205,7 +204,6 @@ export class DataConnectApiClient { return resp; }) .catch((err) => { - console.log(err) throw this.toFirebaseError(err); }); } @@ -223,21 +221,23 @@ export class DataConnectApiClient { }; let urlFormat: string; if (useEmulator()) { - if ('connector' in urlParams && (endpointId === EXECUTE_QUERY_ENDPOINT || endpointId === EXECUTE_MUTATION_ENDPOINT)){ + 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{ + host: emulatorHost() + }); + } + else { urlFormat = utils.formatString(FIREBASE_DATA_CONNECT_EMULATOR_BASE_URL_FORMAT, { host: emulatorHost() }); } } else { - 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;} + 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); }); @@ -309,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'. @@ -338,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.`); diff --git a/src/data-connect/data-connect.ts b/src/data-connect/data-connect.ts index d0dbcc3cef..7476db4157 100644 --- a/src/data-connect/data-connect.ts +++ b/src/data-connect/data-connect.ts @@ -269,4 +269,4 @@ class MutationRef extends OperationRef { dataConnect: this.dataConnect } } -} \ No newline at end of file +} diff --git a/src/data-connect/index.ts b/src/data-connect/index.ts index 9356d9eb27..1b1d71f66e 100644 --- a/src/data-connect/index.ts +++ b/src/data-connect/index.ts @@ -76,10 +76,6 @@ export { // app = getApp(); // } -// const firebaseApp: FirebaseApp = app as FirebaseApp; -// const dataConnectService = firebaseApp.getOrInitService('dataConnect', (app) => new DataConnectService(app)); -// return dataConnectService.getDataConnect(connectorConfig); -// } export function getDataConnect(connectorConfig: ConnectorConfig, app?: App): DataConnect { if (typeof app === 'undefined') { app = getApp(); diff --git a/test-dataconnect/dataconnect_test.ts b/test-dataconnect/dataconnect_test.ts deleted file mode 100644 index 37addc856a..0000000000 --- a/test-dataconnect/dataconnect_test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { getDataConnect } from '../lib/data-connect'; -import { initializeApp } from '../lib/app'; - -initializeApp(); - -const config = { - serviceId: "your-service-id", - location: "us-central1", - connector:"movie-connector" //If the connector is included when you run executeGraphqlRead and executeGraphql(Mutations and Queries) you get a 404 error possibly because of the generated link/url. Writing a condtition to check to stop it from getting server error, what would happen if we got server error normally on the client's end? -}; - -const dataConnect = getDataConnect(config); - -async function queryref_test (){ - // const listOfMovies_res = await dataConnect.queryRef('ListMovies', {orderByRating: "DESC",orderByReleaseYear: "DESC", limit: 3}).execute(); - // const getActorsDetails = await dataConnect.queryRef('GetActorById', {id: "11111111222233334444555555555555"}).execute() - // // const graphqlRead_listOfMovies_res = await dataConnect.executeGraphqlRead(" query { movies {id, title}}"); - // const graphql_listOfMovies_res= await dataConnect.executeGraphql(`mutation {user_insert(data: {id: "666666666677777777711", username: "coder_bot"})}`); - // const graphql_listOfMovies_res_with_operation= await dataConnect.executeGraphql(`mutation {user_insert(data: {id: "66666666667777777773", username: "night_agent"})}`); - const graphql_query_listOfMovies_res = await dataConnect.executeGraphql(" query { movies {id, title}}"); - // const Upsert_user_res = await dataConnect.mutationRef('UpsertUser', {id: "66666666667777777771",username: "Hola"}).execute(); - // const GetUserBy_Id_res = await dataConnect.queryRef("GetUserById",{id:"666666666677777777711"}).execute() - - console.log(graphql_query_listOfMovies_res.data) -} - -queryref_test() \ No newline at end of file diff --git a/test/unit/data-connect/data-connect-api-client-internal.spec.ts b/test/unit/data-connect/data-connect-api-client-internal.spec.ts index 9f9797c315..629363e3e3 100644 --- a/test/unit/data-connect/data-connect-api-client-internal.spec.ts +++ b/test/unit/data-connect/data-connect-api-client-internal.spec.ts @@ -81,7 +81,7 @@ describe('DataConnectApiClient', () => { connectorConfig, mocks.mockCredentialApp()); - const clientWithoutProjectId_with_connector = new DataConnectApiClient( + const clientWithoutProjectIdWithConnector = new DataConnectApiClient( connectorConfig_with_connector, mocks.mockCredentialApp()); @@ -93,14 +93,14 @@ describe('DataConnectApiClient', () => { let app: FirebaseApp; let apiClient: DataConnectApiClient; - let apiClient_with_connector: DataConnectApiClient; + let apiClientWithConnector: DataConnectApiClient; let sandbox: sinon.SinonSandbox; beforeEach(() => { sandbox = sinon.createSandbox(); app = mocks.appWithOptions(mockOptions); apiClient = new DataConnectApiClient(connectorConfig, app); - apiClient_with_connector = new DataConnectApiClient(connectorConfig_with_connector, app); + apiClientWithConnector = new DataConnectApiClient(connectorConfig_with_connector, app); }); afterEach(() => { @@ -131,7 +131,7 @@ describe('DataConnectApiClient', () => { }); it('should initialize httpClient with the provided app', () => { - expect((apiClient_with_connector as any).httpClient).to.be.an.instanceOf(AuthorizedHttpClient); + expect((apiClientWithConnector as any).httpClient).to.be.an.instanceOf(AuthorizedHttpClient); }); }); @@ -266,7 +266,7 @@ describe('DataConnectApiClient', () => { const stub = sandbox .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(TEST_RESPONSE, 200)); - return apiClient_with_connector.executeGraphql('query', {}) + return apiClientWithConnector.executeGraphql('query', {}) .then((resp) => { expect(resp.data.users).to.be.not.empty; expect(resp.data.users[0].name).to.be.not.undefined; @@ -286,7 +286,7 @@ describe('DataConnectApiClient', () => { const stub = sandbox .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(TEST_RESPONSE, 200)); - return apiClient_with_connector.executeGraphql('query', {}) + return apiClientWithConnector.executeGraphql('query', {}) .then(() => { expect(stub).to.have.been.calledOnce.and.calledWith({ method: 'POST', @@ -299,14 +299,154 @@ describe('DataConnectApiClient', () => { }); + describe('executeQuery', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectIdWithConnector.executeQuery({operationName: 'getById'}) + .should.eventually.be.rejectedWith(noProjectId); + }); + + it('should throw an error if no arguments are passed in', async () => { + await expect(apiClientWithConnector.executeQuery(undefined as any)).to.be.rejectedWith( + FirebaseDataConnectError, + 'GraphqlOptions should be a non-null object' + ); + }); + + const invalidOptions = [null, NaN, 0, 1, true, false, [], _.noop]; + invalidOptions.forEach((invalidOption) => { + it('should throw given an invalid options object: ' + JSON.stringify(invalidOption), async () => { + await expect(apiClientWithConnector.executeQuery(invalidOption as any)).to.be.rejectedWith( + FirebaseDataConnectError, + 'GraphqlOptions must be a non-null object' + ); + }); + }); + + it('should throw an error if there is no operationName', async () => { + await expect(apiClientWithConnector.executeQuery({})).to.be.rejectedWith( + FirebaseDataConnectError, + 'GraphqlOptions must contain `operationName`.' + ); + }); + + it('should reject when a full platform error response is received', () => { + sandbox + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + const expected = new FirebaseDataConnectError('not-found', 'Requested entity not found'); + return apiClientWithConnector.executeQuery({operationName: 'getById'}) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject with unknown-error when error code is not present', () => { + sandbox + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom({}, 404)); + const expected = new FirebaseDataConnectError('unknown-error', 'Unknown server error: {}'); + return apiClientWithConnector.executeQuery({operationName: 'getById'}) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject with unknown-error for non-json response', () => { + sandbox + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom('not json', 404)); + const expected = new FirebaseDataConnectError( + 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + return apiClientWithConnector.executeQuery({operationName: 'getById'}) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject when rejected with a FirebaseDataConnectError', () => { + const expected = new FirebaseDataConnectError('internal-error', 'socket hang up'); + sandbox + .stub(HttpClient.prototype, 'send') + .rejects(expected); + return apiClientWithConnector.executeQuery({operationName: 'getById'}) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should resolve with the Query response on success', () => { + interface UsersResponse { + users: [ + user: { + id: string; + name: string; + address: string; + } + ]; + } + const stub = sandbox + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200)); + return apiClientWithConnector.executeQuery({operationName: 'getById'}) + .then((resp) => { + expect(resp.data.users).to.be.not.empty; + expect(resp.data.users[0].name).to.be.not.undefined; + expect(resp.data.users[0].address).to.be.not.undefined; + expect(resp.data.users).to.deep.equal(TEST_RESPONSE.data.users); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig_with_connector.location}/services/${connectorConfig_with_connector.serviceId}/connectors/${connectorConfig_with_connector.connector}:executeQuery`, + headers: EXPECTED_HEADERS, + data: { query: undefined, name: 'getById', operationName: 'getById'} + }); + }); + }); + + it('should resolve with the Query response on success', () => { + interface UsersResponse { + users: [ + user: { + id: string; + name: string; + address: string; + } + ]; + } + const stub = sandbox + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200)); + return apiClientWithConnector.executeQuery({operationName: 'getById', variables: {} }) + .then((resp) => { + expect(resp.data.users).to.be.not.empty; + expect(resp.data.users[0].name).to.be.not.undefined; + expect(resp.data.users[0].address).to.be.not.undefined; + expect(resp.data.users).to.deep.equal(TEST_RESPONSE.data.users); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig_with_connector.location}/services/${connectorConfig_with_connector.serviceId}/connectors/${connectorConfig_with_connector.connector}:executeQuery`, + headers: EXPECTED_HEADERS, + data: { query: undefined, name: 'getById', operationName: 'getById', variables: {} } + }); + }); + }); + + it('should use DATA_CONNECT_EMULATOR_HOST if set', () => { + process.env.DATA_CONNECT_EMULATOR_HOST = 'localhost:9399'; + const stub = sandbox + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200)); + return apiClientWithConnector.executeQuery({operationName: 'getById'}) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: `http://localhost:9399/v1alpha/projects/test-project/locations/${connectorConfig_with_connector.location}/services/${connectorConfig_with_connector.serviceId}/connectors/${connectorConfig_with_connector.connector}:executeQuery`, + headers: EMULATOR_EXPECTED_HEADERS, + data: { query: undefined, name: 'getById', operationName: 'getById' } + }); + }); + }); + }); + describe('executeMutation', () => { it('should reject when project id is not available', () => { - return clientWithoutProjectId_with_connector.executeMutation({operationName: 'getById'}) + return clientWithoutProjectIdWithConnector.executeMutation({operationName: 'getById'}) .should.eventually.be.rejectedWith(noProjectId); }); it('should throw an error if no arguments are passed in', async () => { - await expect(apiClient_with_connector.executeMutation(undefined as any)).to.be.rejectedWith( + await expect(apiClientWithConnector.executeMutation(undefined as any)).to.be.rejectedWith( FirebaseDataConnectError, 'GraphqlOptions should be a non-null object' ); @@ -315,15 +455,15 @@ describe('DataConnectApiClient', () => { const invalidOptions = [null, NaN, 0, 1, true, false, [], _.noop]; invalidOptions.forEach((invalidOption) => { it('should throw given an invalid options object: ' + JSON.stringify(invalidOption), async () => { - await expect(apiClient_with_connector.executeMutation(invalidOption as any)).to.be.rejectedWith( + await expect(apiClientWithConnector.executeMutation(invalidOption as any)).to.be.rejectedWith( FirebaseDataConnectError, 'GraphqlOptions must be a non-null object' ); }); }); - + it('should throw an error if there is no operationName', async () => { - await expect(apiClient_with_connector.executeMutation({})).to.be.rejectedWith( + await expect(apiClientWithConnector.executeMutation({})).to.be.rejectedWith( FirebaseDataConnectError, 'GraphqlOptions must contain `operationName`.' ); @@ -334,7 +474,7 @@ describe('DataConnectApiClient', () => { .stub(HttpClient.prototype, 'send') .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); const expected = new FirebaseDataConnectError('not-found', 'Requested entity not found'); - return apiClient_with_connector.executeMutation({operationName: 'getById'}) + return apiClientWithConnector.executeMutation({operationName: 'getById'}) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -343,7 +483,7 @@ describe('DataConnectApiClient', () => { .stub(HttpClient.prototype, 'send') .rejects(utils.errorFrom({}, 404)); const expected = new FirebaseDataConnectError('unknown-error', 'Unknown server error: {}'); - return apiClient_with_connector.executeMutation({operationName: 'getById'}) + return apiClientWithConnector.executeMutation({operationName: 'getById'}) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -353,7 +493,7 @@ describe('DataConnectApiClient', () => { .rejects(utils.errorFrom('not json', 404)); const expected = new FirebaseDataConnectError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); - return apiClient_with_connector.executeMutation({operationName: 'getById'}) + return apiClientWithConnector.executeMutation({operationName: 'getById'}) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -362,7 +502,7 @@ describe('DataConnectApiClient', () => { sandbox .stub(HttpClient.prototype, 'send') .rejects(expected); - return apiClient_with_connector.executeMutation({operationName: 'getById'}) + return apiClientWithConnector.executeMutation({operationName: 'getById'}) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -379,7 +519,7 @@ describe('DataConnectApiClient', () => { const stub = sandbox .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(TEST_RESPONSE, 200)); - return apiClient_with_connector.executeMutation({operationName: 'getById'}) + return apiClientWithConnector.executeMutation({operationName: 'getById'}) .then((resp) => { expect(resp.data.users).to.be.not.empty; expect(resp.data.users[0].name).to.be.not.undefined; @@ -389,7 +529,35 @@ describe('DataConnectApiClient', () => { method: 'POST', url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig_with_connector.location}/services/${connectorConfig_with_connector.serviceId}/connectors/${connectorConfig_with_connector.connector}:executeMutation`, headers: EXPECTED_HEADERS, - data: { query: undefined, name: 'getById', operationName: 'getById' } + data: { query: undefined, name: 'getById', operationName: 'getById'} + }); + }); + }); + + it('should resolve with the Mutation response on success', () => { + interface UsersResponse { + users: [ + user: { + id: string; + name: string; + address: string; + } + ]; + } + const stub = sandbox + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200)); + return apiClientWithConnector.executeMutation({operationName: 'getById', variables: {} }) + .then((resp) => { + expect(resp.data.users).to.be.not.empty; + expect(resp.data.users[0].name).to.be.not.undefined; + expect(resp.data.users[0].address).to.be.not.undefined; + expect(resp.data.users).to.deep.equal(TEST_RESPONSE.data.users); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig_with_connector.location}/services/${connectorConfig_with_connector.serviceId}/connectors/${connectorConfig_with_connector.connector}:executeMutation`, + headers: EXPECTED_HEADERS, + data: { query: undefined, name: 'getById', operationName: 'getById', variables: {} } }); }); }); @@ -399,7 +567,7 @@ describe('DataConnectApiClient', () => { const stub = sandbox .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(TEST_RESPONSE, 200)); - return apiClient_with_connector.executeMutation({operationName: 'getById'}) + return apiClientWithConnector.executeMutation({operationName: 'getById'}) .then(() => { expect(stub).to.have.been.calledOnce.and.calledWith({ method: 'POST', From 4f8ccdfbd75ff878941dda21e134f2d4d1fc5df3 Mon Sep 17 00:00:00 2001 From: FOLUSO ONATEMOWO Date: Mon, 4 Aug 2025 11:55:29 -0700 Subject: [PATCH 9/9] Formatted the data-connect-api-client-internals.spec.ts file --- .../data-connect-api-client-internal.spec.ts | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/test/unit/data-connect/data-connect-api-client-internal.spec.ts b/test/unit/data-connect/data-connect-api-client-internal.spec.ts index 629363e3e3..a607491025 100644 --- a/test/unit/data-connect/data-connect-api-client-internal.spec.ts +++ b/test/unit/data-connect/data-connect-api-client-internal.spec.ts @@ -53,8 +53,8 @@ describe('DataConnectApiClient', () => { }; const noProjectId = 'Failed to determine project ID. Initialize the SDK with service ' - + 'account credentials or set project ID as an app option. Alternatively, set the ' - + 'GOOGLE_CLOUD_PROJECT environment variable.'; + + 'account credentials or set project ID as an app option. Alternatively, set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'; const TEST_RESPONSE = { data: { @@ -75,7 +75,7 @@ describe('DataConnectApiClient', () => { location: 'us-west2', serviceId: 'my-service', connector: 'mock-connector' - }; + }; const clientWithoutProjectId = new DataConnectApiClient( connectorConfig, @@ -232,7 +232,7 @@ describe('DataConnectApiClient', () => { method: 'POST', url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig.location}/services/${connectorConfig.serviceId}:executeGraphql`, headers: EXPECTED_HEADERS, - data: { query: 'query' } + data: { query: 'query' } }); }); }); @@ -301,7 +301,7 @@ describe('DataConnectApiClient', () => { describe('executeQuery', () => { it('should reject when project id is not available', () => { - return clientWithoutProjectIdWithConnector.executeQuery({operationName: 'getById'}) + return clientWithoutProjectIdWithConnector.executeQuery({ operationName: 'getById' }) .should.eventually.be.rejectedWith(noProjectId); }); @@ -334,7 +334,7 @@ describe('DataConnectApiClient', () => { .stub(HttpClient.prototype, 'send') .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); const expected = new FirebaseDataConnectError('not-found', 'Requested entity not found'); - return apiClientWithConnector.executeQuery({operationName: 'getById'}) + return apiClientWithConnector.executeQuery({ operationName: 'getById' }) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -343,7 +343,7 @@ describe('DataConnectApiClient', () => { .stub(HttpClient.prototype, 'send') .rejects(utils.errorFrom({}, 404)); const expected = new FirebaseDataConnectError('unknown-error', 'Unknown server error: {}'); - return apiClientWithConnector.executeQuery({operationName: 'getById'}) + return apiClientWithConnector.executeQuery({ operationName: 'getById' }) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -353,7 +353,7 @@ describe('DataConnectApiClient', () => { .rejects(utils.errorFrom('not json', 404)); const expected = new FirebaseDataConnectError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); - return apiClientWithConnector.executeQuery({operationName: 'getById'}) + return apiClientWithConnector.executeQuery({ operationName: 'getById' }) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -362,7 +362,7 @@ describe('DataConnectApiClient', () => { sandbox .stub(HttpClient.prototype, 'send') .rejects(expected); - return apiClientWithConnector.executeQuery({operationName: 'getById'}) + return apiClientWithConnector.executeQuery({ operationName: 'getById' }) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -379,7 +379,7 @@ describe('DataConnectApiClient', () => { const stub = sandbox .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(TEST_RESPONSE, 200)); - return apiClientWithConnector.executeQuery({operationName: 'getById'}) + return apiClientWithConnector.executeQuery({ operationName: 'getById' }) .then((resp) => { expect(resp.data.users).to.be.not.empty; expect(resp.data.users[0].name).to.be.not.undefined; @@ -389,7 +389,7 @@ describe('DataConnectApiClient', () => { method: 'POST', url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig_with_connector.location}/services/${connectorConfig_with_connector.serviceId}/connectors/${connectorConfig_with_connector.connector}:executeQuery`, headers: EXPECTED_HEADERS, - data: { query: undefined, name: 'getById', operationName: 'getById'} + data: { query: undefined, name: 'getById', operationName: 'getById' } }); }); }); @@ -407,7 +407,7 @@ describe('DataConnectApiClient', () => { const stub = sandbox .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(TEST_RESPONSE, 200)); - return apiClientWithConnector.executeQuery({operationName: 'getById', variables: {} }) + return apiClientWithConnector.executeQuery({ operationName: 'getById', variables: {} }) .then((resp) => { expect(resp.data.users).to.be.not.empty; expect(resp.data.users[0].name).to.be.not.undefined; @@ -417,7 +417,7 @@ describe('DataConnectApiClient', () => { method: 'POST', url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig_with_connector.location}/services/${connectorConfig_with_connector.serviceId}/connectors/${connectorConfig_with_connector.connector}:executeQuery`, headers: EXPECTED_HEADERS, - data: { query: undefined, name: 'getById', operationName: 'getById', variables: {} } + data: { query: undefined, name: 'getById', operationName: 'getById', variables: {} } }); }); }); @@ -427,21 +427,21 @@ describe('DataConnectApiClient', () => { const stub = sandbox .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(TEST_RESPONSE, 200)); - return apiClientWithConnector.executeQuery({operationName: 'getById'}) + return apiClientWithConnector.executeQuery({ operationName: 'getById' }) .then(() => { expect(stub).to.have.been.calledOnce.and.calledWith({ method: 'POST', url: `http://localhost:9399/v1alpha/projects/test-project/locations/${connectorConfig_with_connector.location}/services/${connectorConfig_with_connector.serviceId}/connectors/${connectorConfig_with_connector.connector}:executeQuery`, headers: EMULATOR_EXPECTED_HEADERS, - data: { query: undefined, name: 'getById', operationName: 'getById' } + data: { query: undefined, name: 'getById', operationName: 'getById' } }); }); }); }); - + describe('executeMutation', () => { it('should reject when project id is not available', () => { - return clientWithoutProjectIdWithConnector.executeMutation({operationName: 'getById'}) + return clientWithoutProjectIdWithConnector.executeMutation({ operationName: 'getById' }) .should.eventually.be.rejectedWith(noProjectId); }); @@ -474,7 +474,7 @@ describe('DataConnectApiClient', () => { .stub(HttpClient.prototype, 'send') .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); const expected = new FirebaseDataConnectError('not-found', 'Requested entity not found'); - return apiClientWithConnector.executeMutation({operationName: 'getById'}) + return apiClientWithConnector.executeMutation({ operationName: 'getById' }) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -483,7 +483,7 @@ describe('DataConnectApiClient', () => { .stub(HttpClient.prototype, 'send') .rejects(utils.errorFrom({}, 404)); const expected = new FirebaseDataConnectError('unknown-error', 'Unknown server error: {}'); - return apiClientWithConnector.executeMutation({operationName: 'getById'}) + return apiClientWithConnector.executeMutation({ operationName: 'getById' }) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -493,7 +493,7 @@ describe('DataConnectApiClient', () => { .rejects(utils.errorFrom('not json', 404)); const expected = new FirebaseDataConnectError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); - return apiClientWithConnector.executeMutation({operationName: 'getById'}) + return apiClientWithConnector.executeMutation({ operationName: 'getById' }) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -502,7 +502,7 @@ describe('DataConnectApiClient', () => { sandbox .stub(HttpClient.prototype, 'send') .rejects(expected); - return apiClientWithConnector.executeMutation({operationName: 'getById'}) + return apiClientWithConnector.executeMutation({ operationName: 'getById' }) .should.eventually.be.rejected.and.deep.include(expected); }); @@ -519,7 +519,7 @@ describe('DataConnectApiClient', () => { const stub = sandbox .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(TEST_RESPONSE, 200)); - return apiClientWithConnector.executeMutation({operationName: 'getById'}) + return apiClientWithConnector.executeMutation({ operationName: 'getById' }) .then((resp) => { expect(resp.data.users).to.be.not.empty; expect(resp.data.users[0].name).to.be.not.undefined; @@ -529,7 +529,7 @@ describe('DataConnectApiClient', () => { method: 'POST', url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig_with_connector.location}/services/${connectorConfig_with_connector.serviceId}/connectors/${connectorConfig_with_connector.connector}:executeMutation`, headers: EXPECTED_HEADERS, - data: { query: undefined, name: 'getById', operationName: 'getById'} + data: { query: undefined, name: 'getById', operationName: 'getById' } }); }); }); @@ -547,7 +547,7 @@ describe('DataConnectApiClient', () => { const stub = sandbox .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(TEST_RESPONSE, 200)); - return apiClientWithConnector.executeMutation({operationName: 'getById', variables: {} }) + return apiClientWithConnector.executeMutation({ operationName: 'getById', variables: {} }) .then((resp) => { expect(resp.data.users).to.be.not.empty; expect(resp.data.users[0].name).to.be.not.undefined; @@ -557,7 +557,7 @@ describe('DataConnectApiClient', () => { method: 'POST', url: `https://firebasedataconnect.googleapis.com/v1alpha/projects/test-project/locations/${connectorConfig_with_connector.location}/services/${connectorConfig_with_connector.serviceId}/connectors/${connectorConfig_with_connector.connector}:executeMutation`, headers: EXPECTED_HEADERS, - data: { query: undefined, name: 'getById', operationName: 'getById', variables: {} } + data: { query: undefined, name: 'getById', operationName: 'getById', variables: {} } }); }); }); @@ -567,13 +567,13 @@ describe('DataConnectApiClient', () => { const stub = sandbox .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(TEST_RESPONSE, 200)); - return apiClientWithConnector.executeMutation({operationName: 'getById'}) + return apiClientWithConnector.executeMutation({ operationName: 'getById' }) .then(() => { expect(stub).to.have.been.calledOnce.and.calledWith({ method: 'POST', url: `http://localhost:9399/v1alpha/projects/test-project/locations/${connectorConfig_with_connector.location}/services/${connectorConfig_with_connector.serviceId}/connectors/${connectorConfig_with_connector.connector}:executeMutation`, headers: EMULATOR_EXPECTED_HEADERS, - data: { query: undefined, name: 'getById', operationName: 'getById' } + data: { query: undefined, name: 'getById', operationName: 'getById' } }); }); }); @@ -708,11 +708,11 @@ describe('DataConnectApiClient CRUD helpers', () => { .to.be.rejectedWith(FirebaseDataConnectError, /`data` must be a non-null object./); }); - it('should throw FirebaseDataConnectError for array data', async() => { + it('should throw FirebaseDataConnectError for array data', async () => { await expect(apiClient.insert(tableName, [])) .to.be.rejectedWith(FirebaseDataConnectError, /`data` must be an object, not an array, for single insert./); }); - + it('should amend the message for query errors', async () => { await expect(apiClientQueryError.insert(tableName, { data: 1 })) .to.be.rejectedWith(FirebaseDataConnectError, `${serverErrorString}. ${additionalErrorMessageForBulkImport}`);