From 36fa0c848622a5c4a53d6edfba80c6e6e31c3d12 Mon Sep 17 00:00:00 2001 From: stephenarosaj Date: Thu, 11 Jun 2026 11:13:20 -0700 Subject: [PATCH 01/10] add tests checking for new GQL output for [insert,upsert](many) --- .../data-connect-api-client-internal.spec.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) 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 5951c29264..fd59ceeb46 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 @@ -774,6 +774,22 @@ describe('DataConnectApiClient CRUD helpers', () => { expect(err.cause).to.equal(expectedQueryError); } }); + + it('should call executeGraphql with variables and @allow directive for enums and other fields', async () => { + const data = { id: 'key1', name: 'Fred', status: 'ACTIVE' }; + const expectedMutation = ` + mutation($data: TestTable_Data! @allow(fields: "id name status")) { + testTable_insert(data: $data) + }`; + const expectedOptions = { + variables: { data } + }; + await apiClient.insert(tableName, data); + expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + expectedOptions + ); + }); }); // --- INSERT MANY TESTS --- @@ -866,6 +882,25 @@ describe('DataConnectApiClient CRUD helpers', () => { expect(err.cause).to.equal(expectedQueryError); } }); + + it('should call executeGraphql with variables and @allow directive for enums and other fields', async () => { + const data = [ + { id: 'key1', name: 'Fred', status: 'ACTIVE' }, + { id: 'key2', name: 'Bob', tags: ['cool'] } + ]; + const expectedMutation = ` + mutation($data: [TestTable_Data! @allow(fields: "id name status tags")]!) { + testTable_insertMany(data: $data) + }`; + const expectedOptions = { + variables: { data } + }; + await apiClient.insertMany(tableName, data); + expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + expectedOptions + ); + }); }); // --- UPSERT TESTS --- @@ -935,6 +970,22 @@ describe('DataConnectApiClient CRUD helpers', () => { expect(err.cause).to.equal(expectedQueryError); } }); + + it('should call executeGraphql with variables and @allow directive for enums and other fields', async () => { + const data = { id: 'key1', name: 'Fred', status: 'ACTIVE' }; + const expectedMutation = ` + mutation($data: TestTable_Data! @allow(fields: "id name status")) { + testTable_upsert(data: $data) + }`; + const expectedOptions = { + variables: { data } + }; + await apiClient.upsert(tableName, data); + expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + expectedOptions + ); + }); }); // --- UPSERT MANY TESTS --- @@ -1024,6 +1075,25 @@ describe('DataConnectApiClient CRUD helpers', () => { expect(err.cause).to.equal(expectedQueryError); } }); + + it('should call executeGraphql with variables and @allow directive for enums and other fields', async () => { + const data = [ + { id: 'key1', name: 'Fred', status: 'ACTIVE' }, + { id: 'key2', name: 'Bob', tags: ['cool'] } + ]; + const expectedMutation = ` + mutation($data: [TestTable_Data! @allow(fields: "id name status tags")]!) { + testTable_upsertMany(data: $data) + }`; + const expectedOptions = { + variables: { data } + }; + await apiClient.upsertMany(tableName, data); + expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + expectedOptions + ); + }); }); describe('String serialization', () => { From 1ed5b873702ca258017b9673ab2937b7bf211b0d Mon Sep 17 00:00:00 2001 From: stephenarosaj Date: Thu, 11 Jun 2026 11:27:28 -0700 Subject: [PATCH 02/10] change mutation functions to use @allow --- .../data-connect-api-client-internal.ts | 126 +++++++++--------- 1 file changed, 62 insertions(+), 64 deletions(-) diff --git a/src/data-connect/data-connect-api-client-internal.ts b/src/data-connect/data-connect-api-client-internal.ts index 8cb5ee289b..f4718a6a63 100644 --- a/src/data-connect/data-connect-api-client-internal.ts +++ b/src/data-connect/data-connect-api-client-internal.ts @@ -423,50 +423,6 @@ export class DataConnectApiClient { }); } - /** - * Converts JSON data into a GraphQL literal string. - * Handles nested objects, arrays, strings, numbers, and booleans. - * Ensures strings are properly escaped. - */ - private objectToString(data: unknown): string { - if (typeof data === 'string') { - return JSON.stringify(data); - } - if (typeof data === 'number' || typeof data === 'boolean' || data === null) { - return String(data); - } - if (validator.isArray(data)) { - const elements = data.map(item => this.objectToString(item)).join(', '); - return `[${elements}]`; - } - if (typeof data === 'object' && data !== null) { - // Filter out properties where the value is undefined BEFORE mapping - const kvPairs = Object.entries(data) - .filter(([, val]) => val !== undefined) - .map(([key, val]) => { - // 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'. - if (typeof data === 'undefined') { - return 'null'; - } - - // Fallback for any other types (e.g., Symbol, BigInt - though less common in GQL contexts) - // Consider how these should be handled or if an error should be thrown. - // For now, simple string conversion. - return String(data); - } - private formatTableName(tableName: string): string { // Format tableName: first character to lowercase if (tableName && tableName.length > 0) { @@ -515,11 +471,17 @@ export class DataConnectApiClient { } try { - tableName = this.formatTableName(tableName); - const gqlDataString = this.objectToString(data); - const mutation = `mutation { ${tableName}_insert(data: ${gqlDataString}) }`; - // Use internal executeGraphql - return this.executeGraphql(mutation).catch(this.handleBulkImportErrors); + const capitalizedTable = tableName.charAt(0).toUpperCase() + tableName.slice(1); + const formattedTableName = this.formatTableName(tableName); + + const keys = Object.keys(data) + .filter(key => (data as Record)[key] !== undefined) + .join(' '); + + const mutation = `mutation($data: ${capitalizedTable}_Data! @allow(fields: "${keys}")) { ${formattedTableName}_insert(data: $data) }`; + + return this.executeGraphql(mutation, { variables: { data } }) + .catch(this.handleBulkImportErrors); } catch (e: any) { throw new FirebaseDataConnectError({ code: DATA_CONNECT_ERROR_CODE_MAPPING.INTERNAL, @@ -550,11 +512,26 @@ export class DataConnectApiClient { } try { - tableName = this.formatTableName(tableName); - const gqlDataString = this.objectToString(data); - const mutation = `mutation { ${tableName}_insertMany(data: ${gqlDataString}) }`; - // Use internal executeGraphql - return this.executeGraphql(mutation).catch(this.handleBulkImportErrors); + const capitalizedTable = tableName.charAt(0).toUpperCase() + tableName.slice(1); + const formattedTableName = this.formatTableName(tableName); + + const allKeys = new Set(); + for (const element of data) { + if (validator.isNonNullObject(element)) { + const record = element as Record; + Object.keys(record).forEach(key => { + if (record[key] !== undefined) { + allKeys.add(key); + } + }); + } + } + const keys = Array.from(allKeys).join(' '); + + const mutation = `mutation($data: [${capitalizedTable}_Data! @allow(fields: "${keys}")]!) { ${formattedTableName}_insertMany(data: $data) }`; + + return this.executeGraphql(mutation, { variables: { data } }) + .catch(this.handleBulkImportErrors); } catch (e: any) { throw new FirebaseDataConnectError({ code: DATA_CONNECT_ERROR_CODE_MAPPING.INTERNAL, @@ -592,11 +569,17 @@ export class DataConnectApiClient { } try { - tableName = this.formatTableName(tableName); - const gqlDataString = this.objectToString(data); - const mutation = `mutation { ${tableName}_upsert(data: ${gqlDataString}) }`; - // Use internal executeGraphql - return this.executeGraphql(mutation).catch(this.handleBulkImportErrors); + const capitalizedTable = tableName.charAt(0).toUpperCase() + tableName.slice(1); + const formattedTableName = this.formatTableName(tableName); + + const keys = Object.keys(data) + .filter(key => (data as Record)[key] !== undefined) + .join(' '); + + const mutation = `mutation($data: ${capitalizedTable}_Data! @allow(fields: "${keys}")) { ${formattedTableName}_upsert(data: $data) }`; + + return this.executeGraphql(mutation, { variables: { data } }) + .catch(this.handleBulkImportErrors); } catch (e: any) { throw new FirebaseDataConnectError({ code: DATA_CONNECT_ERROR_CODE_MAPPING.INTERNAL, @@ -627,11 +610,26 @@ export class DataConnectApiClient { } try { - tableName = this.formatTableName(tableName); - const gqlDataString = this.objectToString(data); - const mutation = `mutation { ${tableName}_upsertMany(data: ${gqlDataString}) }`; - // Use internal executeGraphql - return this.executeGraphql(mutation).catch(this.handleBulkImportErrors); + const capitalizedTable = tableName.charAt(0).toUpperCase() + tableName.slice(1); + const formattedTableName = this.formatTableName(tableName); + + const allKeys = new Set(); + for (const element of data) { + if (validator.isNonNullObject(element)) { + const record = element as Record; + Object.keys(record).forEach(key => { + if (record[key] !== undefined) { + allKeys.add(key); + } + }); + } + } + const keys = Array.from(allKeys).join(' '); + + const mutation = `mutation($data: [${capitalizedTable}_Data! @allow(fields: "${keys}")]!) { ${formattedTableName}_upsertMany(data: $data) }`; + + return this.executeGraphql(mutation, { variables: { data } }) + .catch(this.handleBulkImportErrors); } catch (e: any) { throw new FirebaseDataConnectError({ code: DATA_CONNECT_ERROR_CODE_MAPPING.INTERNAL, From 4f6135e38c9bdb349df4e5b53e09d2c8a47eab24 Mon Sep 17 00:00:00 2001 From: stephenarosaj Date: Fri, 12 Jun 2026 10:13:08 -0700 Subject: [PATCH 03/10] factor out common code after @allow changes --- .../data-connect-api-client-internal.ts | 107 +++++++++--------- 1 file changed, 52 insertions(+), 55 deletions(-) diff --git a/src/data-connect/data-connect-api-client-internal.ts b/src/data-connect/data-connect-api-client-internal.ts index f4718a6a63..0fe2b74dc8 100644 --- a/src/data-connect/data-connect-api-client-internal.ts +++ b/src/data-connect/data-connect-api-client-internal.ts @@ -423,12 +423,47 @@ export class DataConnectApiClient { }); } - private formatTableName(tableName: string): string { - // Format tableName: first character to lowercase - if (tableName && tableName.length > 0) { - return tableName.charAt(0).toLowerCase() + tableName.slice(1); + /** + * Generates both capitalized and camel-cased variations of a table name. + * Capitalization matches the schema types, and camel-case matches mutations. + */ + private getTableNames(tableName: string): { capitalized: string; formatted: string } { + if (!tableName || tableName.length === 0) { + return { capitalized: tableName, formatted: tableName }; + } + const capitalized = tableName.charAt(0).toUpperCase() + tableName.slice(1); + const formatted = tableName.charAt(0).toLowerCase() + tableName.slice(1); + return { capitalized, formatted }; + } + + /** + * Extracts all defined property keys from an object as a space-separated string. + * Used to build the `@allow(fields: ...)` mutation directive for single operations. + */ + private getObjectKeys(data: Record | object): string { + return Object.keys(data) + .filter(key => (data as Record)[key] !== undefined) + .join(' '); + } + + /** + * Extracts the union of all defined property keys across an array of objects + * as a space-separated string. Used to build the `@allow(fields: ...)` mutation + * directive for bulk operations. + */ + private getArrayObjectsKeys(data: Array): string { + const allKeys = new Set(); + for (const element of data) { + if (validator.isNonNullObject(element)) { + const record = element as Record; + Object.keys(record).forEach(key => { + if (record[key] !== undefined) { + allKeys.add(key); + } + }); + } } - return tableName; + return Array.from(allKeys).join(' '); } private handleBulkImportErrors(err: FirebaseDataConnectError): never { @@ -471,14 +506,9 @@ export class DataConnectApiClient { } try { - const capitalizedTable = tableName.charAt(0).toUpperCase() + tableName.slice(1); - const formattedTableName = this.formatTableName(tableName); - - const keys = Object.keys(data) - .filter(key => (data as Record)[key] !== undefined) - .join(' '); - - const mutation = `mutation($data: ${capitalizedTable}_Data! @allow(fields: "${keys}")) { ${formattedTableName}_insert(data: $data) }`; + const { capitalized, formatted } = this.getTableNames(tableName); + const keys = this.getObjectKeys(data); + const mutation = `mutation($data: ${capitalized}_Data! @allow(fields: "${keys}")) { ${formatted}_insert(data: $data) }`; return this.executeGraphql(mutation, { variables: { data } }) .catch(this.handleBulkImportErrors); @@ -512,23 +542,9 @@ export class DataConnectApiClient { } try { - const capitalizedTable = tableName.charAt(0).toUpperCase() + tableName.slice(1); - const formattedTableName = this.formatTableName(tableName); - - const allKeys = new Set(); - for (const element of data) { - if (validator.isNonNullObject(element)) { - const record = element as Record; - Object.keys(record).forEach(key => { - if (record[key] !== undefined) { - allKeys.add(key); - } - }); - } - } - const keys = Array.from(allKeys).join(' '); - - const mutation = `mutation($data: [${capitalizedTable}_Data! @allow(fields: "${keys}")]!) { ${formattedTableName}_insertMany(data: $data) }`; + const { capitalized, formatted } = this.getTableNames(tableName); + const keys = this.getArrayObjectsKeys(data); + const mutation = `mutation($data: [${capitalized}_Data! @allow(fields: "${keys}")]!) { ${formatted}_insertMany(data: $data) }`; return this.executeGraphql(mutation, { variables: { data } }) .catch(this.handleBulkImportErrors); @@ -569,14 +585,9 @@ export class DataConnectApiClient { } try { - const capitalizedTable = tableName.charAt(0).toUpperCase() + tableName.slice(1); - const formattedTableName = this.formatTableName(tableName); - - const keys = Object.keys(data) - .filter(key => (data as Record)[key] !== undefined) - .join(' '); - - const mutation = `mutation($data: ${capitalizedTable}_Data! @allow(fields: "${keys}")) { ${formattedTableName}_upsert(data: $data) }`; + const { capitalized, formatted } = this.getTableNames(tableName); + const keys = this.getObjectKeys(data); + const mutation = `mutation($data: ${capitalized}_Data! @allow(fields: "${keys}")) { ${formatted}_upsert(data: $data) }`; return this.executeGraphql(mutation, { variables: { data } }) .catch(this.handleBulkImportErrors); @@ -610,23 +621,9 @@ export class DataConnectApiClient { } try { - const capitalizedTable = tableName.charAt(0).toUpperCase() + tableName.slice(1); - const formattedTableName = this.formatTableName(tableName); - - const allKeys = new Set(); - for (const element of data) { - if (validator.isNonNullObject(element)) { - const record = element as Record; - Object.keys(record).forEach(key => { - if (record[key] !== undefined) { - allKeys.add(key); - } - }); - } - } - const keys = Array.from(allKeys).join(' '); - - const mutation = `mutation($data: [${capitalizedTable}_Data! @allow(fields: "${keys}")]!) { ${formattedTableName}_upsertMany(data: $data) }`; + const { capitalized, formatted } = this.getTableNames(tableName); + const keys = this.getArrayObjectsKeys(data); + const mutation = `mutation($data: [${capitalized}_Data! @allow(fields: "${keys}")]!) { ${formatted}_upsertMany(data: $data) }`; return this.executeGraphql(mutation, { variables: { data } }) .catch(this.handleBulkImportErrors); From 8e12fc54ffbdef68f890106fff604ab99cf43531 Mon Sep 17 00:00:00 2001 From: stephenarosaj Date: Fri, 12 Jun 2026 10:13:24 -0700 Subject: [PATCH 04/10] update tests to expect @allow --- .../data-connect-api-client-internal.spec.ts | 225 ++++++++---------- 1 file changed, 104 insertions(+), 121 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 fd59ceeb46..38e21b1bcd 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 @@ -683,6 +683,10 @@ describe('DataConnectApiClient CRUD helpers', () => { .trim(); // Remove leading/trailing whitespace from the whole string }; + const capitalize = (str: string): string => { + return str.charAt(0).toUpperCase() + str.slice(1); + }; + beforeEach(() => { mockApp = mocks.appWithOptions(mockOptions); apiClient = new DataConnectApiClient(connectorConfig, mockApp); @@ -700,53 +704,45 @@ describe('DataConnectApiClient CRUD helpers', () => { // --- INSERT TESTS --- describe('insert()', () => { tableNames.forEach((tableName, index) => { - const expectedMutation = `mutation { ${formatedTableNames[index]}_insert(data: { name: "a" }) }`; + const capitalizedTable = capitalize(formatedTableNames[index]); + const expectedMutation = `mutation($data: ${capitalizedTable}_Data! @allow(fields: "name")) { ${formatedTableNames[index]}_insert(data: $data) }`; it(`should use the formatted tableName in the gql query: "${tableName}" as "${formatedTableNames[index]}"`, async () => { await apiClient.insert(tableName, { name: 'a' }); - await expect(executeGraphqlStub).to.have.been.calledOnceWithExactly(normalizeGraphQLString(expectedMutation)); + await expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + { variables: { data: { name: 'a' } } } + ); }); }); it('should call executeGraphql with the correct mutation for simple data', async () => { const simpleData = { name: 'test', value: 123 }; - const expectedMutation = ` - mutation { - ${formatedTableName}_insert(data: { - name: "test", - value: 123 - }) - }`; + const expectedMutation = `mutation($data: TestTable_Data! @allow(fields: "name value")) { ${formatedTableName}_insert(data: $data) }`; await apiClient.insert(tableName, simpleData); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly(normalizeGraphQLString(expectedMutation)); + expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + { variables: { data: simpleData } } + ); }); it('should call executeGraphql with the correct mutation for complex data', async () => { const complexData = { id: 'abc', active: true, scores: [10, 20], info: { nested: 'yes/no "quote" \\slash\\' } }; - const expectedMutation = ` - mutation { - ${formatedTableName}_insert(data: { - id: "abc", active: true, scores: [10, 20], - info: { nested: "yes/no \\"quote\\" \\\\slash\\\\" } - }) - }`; + const expectedMutation = `mutation($data: TestTable_Data! @allow(fields: "id active scores info")) { ${formatedTableName}_insert(data: $data) }`; await apiClient.insert(tableName, complexData); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly(normalizeGraphQLString(expectedMutation)); + expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + { variables: { data: complexData } } + ); }); it('should call executeGraphql with the correct mutation for undefined and null values', async () => { - const expectedMutation = ` - mutation { - ${formatedTableName}_insert(data: { - genre: "Action", - title: "Die Hard", - ratings: null, - director: {}, - extras: [1, null, "hello", null, { a: 1 }] - }) - }`; + const expectedMutation = `mutation($data: TestTable_Data! @allow(fields: "genre title ratings director extras")) { ${formatedTableName}_insert(data: $data) }`; await apiClient.insert(tableName, dataWithUndefined); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly(normalizeGraphQLString(expectedMutation)); + expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + { variables: { data: dataWithUndefined } } + ); }); it('should throw FirebaseDataConnectError for invalid tableName', async () => { @@ -795,21 +791,26 @@ describe('DataConnectApiClient CRUD helpers', () => { // --- INSERT MANY TESTS --- describe('insertMany()', () => { tableNames.forEach((tableName, index) => { - const expectedMutation = `mutation { ${formatedTableNames[index]}_insertMany(data: [{ name: "a" }]) }`; + const capitalizedTable = capitalize(formatedTableNames[index]); + const expectedMutation = `mutation($data: [${capitalizedTable}_Data! @allow(fields: "name")]!) { ${formatedTableNames[index]}_insertMany(data: $data) }`; it(`should use the formatted tableName in the gql query: "${tableName}" as "${formatedTableNames[index]}"`, async () => { await apiClient.insertMany(tableName, [{ name: 'a' }]); - await expect(executeGraphqlStub).to.have.been.calledOnceWithExactly(normalizeGraphQLString(expectedMutation)); + await expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + { variables: { data: [{ name: 'a' }] } } + ); }); }); it('should call executeGraphql with the correct mutation for simple data array', async () => { const simpleDataArray = [{ name: 'test1' }, { name: 'test2', value: 456 }]; - const expectedMutation = ` - mutation { - ${formatedTableName}_insertMany(data: [{ name: "test1" }, { name: "test2", value: 456 }]) }`; + const expectedMutation = `mutation($data: [TestTable_Data! @allow(fields: "name value")]!) { ${formatedTableName}_insertMany(data: $data) }`; await apiClient.insertMany(tableName, simpleDataArray); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly(normalizeGraphQLString(expectedMutation)); + expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + { variables: { data: simpleDataArray } } + ); }); it('should call executeGraphql with the correct mutation for complex data array', async () => { @@ -817,39 +818,25 @@ describe('DataConnectApiClient CRUD helpers', () => { { id: 'a', active: true, info: { nested: 'n1 "quote"' } }, { id: 'b', scores: [1, 2], info: { nested: 'n2/\\' } } ]; - const expectedMutation = ` - mutation { - ${formatedTableName}_insertMany(data: - [{ id: "a", active: true, info: { nested: "n1 \\"quote\\"" } }, { id: "b", scores: [1, 2], - info: { nested: "n2/\\\\" } }]) }`; + const expectedMutation = `mutation($data: [TestTable_Data! @allow(fields: "id active info scores")]!) { ${formatedTableName}_insertMany(data: $data) }`; await apiClient.insertMany(tableName, complexDataArray); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly(normalizeGraphQLString(expectedMutation)); + expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + { variables: { data: complexDataArray } } + ); }); it('should call executeGraphql with the correct mutation for undefined and null', async () => { const dataArray = [ dataWithUndefined, dataWithUndefined - ] - const expectedMutation = ` - mutation { - ${formatedTableName}_insertMany(data: [{ - genre: "Action", - title: "Die Hard", - ratings: null, - director: {}, - extras: [1, null, "hello", null, { a: 1 }] - }, - { - genre: "Action", - title: "Die Hard", - ratings: null, - director: {}, - extras: [1, null, "hello", null, { a: 1 }] - }]) - }`; + ]; + const expectedMutation = `mutation($data: [TestTable_Data! @allow(fields: "genre title ratings director extras")]!) { ${formatedTableName}_insertMany(data: $data) }`; await apiClient.insertMany(tableName, dataArray); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly(normalizeGraphQLString(expectedMutation)); + expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + { variables: { data: dataArray } } + ); }); it('should throw FirebaseDataConnectError for invalid tableName', async () => { @@ -906,43 +893,45 @@ describe('DataConnectApiClient CRUD helpers', () => { // --- UPSERT TESTS --- describe('upsert()', () => { tableNames.forEach((tableName, index) => { - const expectedMutation = `mutation { ${formatedTableNames[index]}_upsert(data: { name: "a" }) }`; + const capitalizedTable = capitalize(formatedTableNames[index]); + const expectedMutation = `mutation($data: ${capitalizedTable}_Data! @allow(fields: "name")) { ${formatedTableNames[index]}_upsert(data: $data) }`; it(`should use the formatted tableName in the gql query: "${tableName}" as "${formatedTableNames[index]}"`, async () => { await apiClient.upsert(tableName, { name: 'a' }); - await expect(executeGraphqlStub).to.have.been.calledOnceWithExactly(normalizeGraphQLString(expectedMutation)); + await expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + { variables: { data: { name: 'a' } } } + ); }); }); it('should call executeGraphql with the correct mutation for simple data', async () => { const simpleData = { id: 'key1', value: 'updated' }; - const expectedMutation = `mutation { ${formatedTableName}_upsert(data: { id: "key1", value: "updated" }) }`; + const expectedMutation = `mutation($data: TestTable_Data! @allow(fields: "id value")) { ${formatedTableName}_upsert(data: $data) }`; await apiClient.upsert(tableName, simpleData); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly(expectedMutation); + expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + { variables: { data: simpleData } } + ); }); it('should call executeGraphql with the correct mutation for complex data', async () => { const complexData = { id: 'key2', active: false, items: [1, null], detail: { status: 'done/\\' } }; - const expectedMutation = ` - mutation { ${formatedTableName}_upsert(data: - { id: "key2", active: false, items: [1, null], detail: { status: "done/\\\\" } }) }`; + const expectedMutation = `mutation($data: TestTable_Data! @allow(fields: "id active items detail")) { ${formatedTableName}_upsert(data: $data) }`; await apiClient.upsert(tableName, complexData); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly(normalizeGraphQLString(expectedMutation)); + expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + { variables: { data: complexData } } + ); }); it('should call executeGraphql with the correct mutation for undefined and null values', async () => { - const expectedMutation = ` - mutation { - ${formatedTableName}_upsert(data: { - genre: "Action", - title: "Die Hard", - ratings: null, - director: {}, - extras: [1, null, "hello", null, { a: 1 }] - }) - }`; + const expectedMutation = `mutation($data: TestTable_Data! @allow(fields: "genre title ratings director extras")) { ${formatedTableName}_upsert(data: $data) }`; await apiClient.upsert(tableName, dataWithUndefined); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly(normalizeGraphQLString(expectedMutation)); + expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + { variables: { data: dataWithUndefined } } + ); }); it('should throw FirebaseDataConnectError for invalid tableName', async () => { @@ -991,20 +980,26 @@ describe('DataConnectApiClient CRUD helpers', () => { // --- UPSERT MANY TESTS --- describe('upsertMany()', () => { tableNames.forEach((tableName, index) => { - const expectedMutation = `mutation { ${formatedTableNames[index]}_upsertMany(data: [{ name: "a" }]) }`; + const capitalizedTable = capitalize(formatedTableNames[index]); + const expectedMutation = `mutation($data: [${capitalizedTable}_Data! @allow(fields: "name")]!) { ${formatedTableNames[index]}_upsertMany(data: $data) }`; it(`should use the formatted tableName in the gql query: "${tableName}" as "${formatedTableNames[index]}"`, async () => { await apiClient.upsertMany(tableName, [{ name: 'a' }]); - await expect(executeGraphqlStub).to.have.been.calledOnceWithExactly(normalizeGraphQLString(expectedMutation)); + await expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + { variables: { data: [{ name: 'a' }] } } + ); }); }); it('should call executeGraphql with the correct mutation for simple data array', async () => { const simpleDataArray = [{ id: 'k1' }, { id: 'k2', value: 99 }]; - const expectedMutation = ` - mutation { ${formatedTableName}_upsertMany(data: [{ id: "k1" }, { id: "k2", value: 99 }]) }`; + const expectedMutation = `mutation($data: [TestTable_Data! @allow(fields: "id value")]!) { ${formatedTableName}_upsertMany(data: $data) }`; await apiClient.upsertMany(tableName, simpleDataArray); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly(normalizeGraphQLString(expectedMutation)); + expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + { variables: { data: simpleDataArray } } + ); }); it('should call executeGraphql with the correct mutation for complex data array', async () => { @@ -1012,37 +1007,25 @@ describe('DataConnectApiClient CRUD helpers', () => { { id: 'x', active: true, info: { nested: 'n1/\\"x' } }, { id: 'y', scores: [null, 2] } ]; - const expectedMutation = ` - mutation { ${formatedTableName}_upsertMany(data: - [{ id: "x", active: true, info: { nested: "n1/\\\\\\"x" } }, { id: "y", scores: [null, 2] }]) }`; + const expectedMutation = `mutation($data: [TestTable_Data! @allow(fields: "id active info scores")]!) { ${formatedTableName}_upsertMany(data: $data) }`; await apiClient.upsertMany(tableName, complexDataArray); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly(normalizeGraphQLString(expectedMutation)); + expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + { variables: { data: complexDataArray } } + ); }); it('should call executeGraphql with the correct mutation for undefined and null', async () => { const dataArray = [ dataWithUndefined, dataWithUndefined - ] - const expectedMutation = ` - mutation { - ${formatedTableName}_upsertMany(data: [{ - genre: "Action", - title: "Die Hard", - ratings: null, - director: {}, - extras: [1, null, "hello", null, { a: 1 }] - }, - { - genre: "Action", - title: "Die Hard", - ratings: null, - director: {}, - extras: [1, null, "hello", null, { a: 1 }] - }]) - }`; + ]; + const expectedMutation = `mutation($data: [TestTable_Data! @allow(fields: "genre title ratings director extras")]!) { ${formatedTableName}_upsertMany(data: $data) }`; await apiClient.upsertMany(tableName, dataArray); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly(normalizeGraphQLString(expectedMutation)); + expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( + normalizeGraphQLString(expectedMutation), + { variables: { data: dataArray } } + ); }); it('should throw FirebaseDataConnectError for invalid tableName', async () => { @@ -1097,48 +1080,48 @@ describe('DataConnectApiClient CRUD helpers', () => { }); describe('String serialization', () => { - it('should correctly escape special characters in strings during insert', async () => { + it('should correctly handle special characters in strings during insert', async () => { const data = { content: 'Line 1\nLine 2', }; await apiClient.insert(tableName, data); - const callArgs = executeGraphqlStub.firstCall.args[0]; + const callOptions = executeGraphqlStub.firstCall.args[1]; - expect(callArgs).to.include(String.raw`content: "Line 1\nLine 2"`); + expect(callOptions.variables.data.content).to.equal('Line 1\nLine 2'); }); - it('should correctly escape backslash', async () => { + it('should correctly handle backslash', async () => { const data = { content: 'Backslash \\', }; await apiClient.insert(tableName, data); - const callArgs = executeGraphqlStub.firstCall.args[0]; + const callOptions = executeGraphqlStub.firstCall.args[1]; - expect(callArgs).to.include(String.raw`content: "Backslash \\"`); + expect(callOptions.variables.data.content).to.equal('Backslash \\'); }); - it('should correctly escape double quotes', async () => { + it('should correctly handle double quotes', async () => { const data = { content: 'Quote "test"', }; await apiClient.insert(tableName, data); - const callArgs = executeGraphqlStub.firstCall.args[0]; + const callOptions = executeGraphqlStub.firstCall.args[1]; - expect(callArgs).to.include(String.raw`content: "Quote \"test\""`); + expect(callOptions.variables.data.content).to.equal('Quote "test"'); }); - it('should correctly escape tab character', async () => { + it('should correctly handle tab character', async () => { const data = { content: 'Tab\tCharacter', }; await apiClient.insert(tableName, data); - const callArgs = executeGraphqlStub.firstCall.args[0]; + const callOptions = executeGraphqlStub.firstCall.args[1]; - expect(callArgs).to.include(String.raw`content: "Tab\tCharacter"`); + expect(callOptions.variables.data.content).to.equal('Tab\tCharacter'); }); it('should correctly handle emojis', async () => { @@ -1147,9 +1130,9 @@ describe('DataConnectApiClient CRUD helpers', () => { }; await apiClient.insert(tableName, data); - const callArgs = executeGraphqlStub.firstCall.args[0]; + const callOptions = executeGraphqlStub.firstCall.args[1]; - expect(callArgs).to.include('content: "Emoji 😊"'); + expect(callOptions.variables.data.content).to.equal('Emoji 😊'); }); }); }); From ad84ef5dd57c02b0bb241d4e8b597cbda277f36f Mon Sep 17 00:00:00 2001 From: stephenarosaj Date: Fri, 12 Jun 2026 13:11:19 -0700 Subject: [PATCH 05/10] update the @allow directive for list inputs --- .../data-connect-api-client-internal.ts | 4 ++-- .../data-connect-api-client-internal.spec.ts | 20 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/data-connect/data-connect-api-client-internal.ts b/src/data-connect/data-connect-api-client-internal.ts index 0fe2b74dc8..e534671561 100644 --- a/src/data-connect/data-connect-api-client-internal.ts +++ b/src/data-connect/data-connect-api-client-internal.ts @@ -544,7 +544,7 @@ export class DataConnectApiClient { try { const { capitalized, formatted } = this.getTableNames(tableName); const keys = this.getArrayObjectsKeys(data); - const mutation = `mutation($data: [${capitalized}_Data! @allow(fields: "${keys}")]!) { ${formatted}_insertMany(data: $data) }`; + const mutation = `mutation($data: [${capitalized}_Data!]! @allow(fields: "${keys}")) { ${formatted}_insertMany(data: $data) }`; return this.executeGraphql(mutation, { variables: { data } }) .catch(this.handleBulkImportErrors); @@ -623,7 +623,7 @@ export class DataConnectApiClient { try { const { capitalized, formatted } = this.getTableNames(tableName); const keys = this.getArrayObjectsKeys(data); - const mutation = `mutation($data: [${capitalized}_Data! @allow(fields: "${keys}")]!) { ${formatted}_upsertMany(data: $data) }`; + const mutation = `mutation($data: [${capitalized}_Data!]! @allow(fields: "${keys}")) { ${formatted}_upsertMany(data: $data) }`; return this.executeGraphql(mutation, { variables: { data } }) .catch(this.handleBulkImportErrors); 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 38e21b1bcd..e53131e7c6 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 @@ -792,7 +792,7 @@ describe('DataConnectApiClient CRUD helpers', () => { describe('insertMany()', () => { tableNames.forEach((tableName, index) => { const capitalizedTable = capitalize(formatedTableNames[index]); - const expectedMutation = `mutation($data: [${capitalizedTable}_Data! @allow(fields: "name")]!) { ${formatedTableNames[index]}_insertMany(data: $data) }`; + const expectedMutation = `mutation($data: [${capitalizedTable}_Data!]! @allow(fields: "name")) { ${formatedTableNames[index]}_insertMany(data: $data) }`; it(`should use the formatted tableName in the gql query: "${tableName}" as "${formatedTableNames[index]}"`, async () => { await apiClient.insertMany(tableName, [{ name: 'a' }]); @@ -805,7 +805,7 @@ describe('DataConnectApiClient CRUD helpers', () => { it('should call executeGraphql with the correct mutation for simple data array', async () => { const simpleDataArray = [{ name: 'test1' }, { name: 'test2', value: 456 }]; - const expectedMutation = `mutation($data: [TestTable_Data! @allow(fields: "name value")]!) { ${formatedTableName}_insertMany(data: $data) }`; + const expectedMutation = `mutation($data: [TestTable_Data!]! @allow(fields: "name value")) { ${formatedTableName}_insertMany(data: $data) }`; await apiClient.insertMany(tableName, simpleDataArray); expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( normalizeGraphQLString(expectedMutation), @@ -818,7 +818,7 @@ describe('DataConnectApiClient CRUD helpers', () => { { id: 'a', active: true, info: { nested: 'n1 "quote"' } }, { id: 'b', scores: [1, 2], info: { nested: 'n2/\\' } } ]; - const expectedMutation = `mutation($data: [TestTable_Data! @allow(fields: "id active info scores")]!) { ${formatedTableName}_insertMany(data: $data) }`; + const expectedMutation = `mutation($data: [TestTable_Data!]! @allow(fields: "id active info scores")) { ${formatedTableName}_insertMany(data: $data) }`; await apiClient.insertMany(tableName, complexDataArray); expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( normalizeGraphQLString(expectedMutation), @@ -831,7 +831,7 @@ describe('DataConnectApiClient CRUD helpers', () => { dataWithUndefined, dataWithUndefined ]; - const expectedMutation = `mutation($data: [TestTable_Data! @allow(fields: "genre title ratings director extras")]!) { ${formatedTableName}_insertMany(data: $data) }`; + const expectedMutation = `mutation($data: [TestTable_Data!]! @allow(fields: "genre title ratings director extras")) { ${formatedTableName}_insertMany(data: $data) }`; await apiClient.insertMany(tableName, dataArray); expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( normalizeGraphQLString(expectedMutation), @@ -876,7 +876,7 @@ describe('DataConnectApiClient CRUD helpers', () => { { id: 'key2', name: 'Bob', tags: ['cool'] } ]; const expectedMutation = ` - mutation($data: [TestTable_Data! @allow(fields: "id name status tags")]!) { + mutation($data: [TestTable_Data!]! @allow(fields: "id name status tags")) { testTable_insertMany(data: $data) }`; const expectedOptions = { @@ -981,7 +981,7 @@ describe('DataConnectApiClient CRUD helpers', () => { describe('upsertMany()', () => { tableNames.forEach((tableName, index) => { const capitalizedTable = capitalize(formatedTableNames[index]); - const expectedMutation = `mutation($data: [${capitalizedTable}_Data! @allow(fields: "name")]!) { ${formatedTableNames[index]}_upsertMany(data: $data) }`; + const expectedMutation = `mutation($data: [${capitalizedTable}_Data!]! @allow(fields: "name")) { ${formatedTableNames[index]}_upsertMany(data: $data) }`; it(`should use the formatted tableName in the gql query: "${tableName}" as "${formatedTableNames[index]}"`, async () => { await apiClient.upsertMany(tableName, [{ name: 'a' }]); @@ -994,7 +994,7 @@ describe('DataConnectApiClient CRUD helpers', () => { it('should call executeGraphql with the correct mutation for simple data array', async () => { const simpleDataArray = [{ id: 'k1' }, { id: 'k2', value: 99 }]; - const expectedMutation = `mutation($data: [TestTable_Data! @allow(fields: "id value")]!) { ${formatedTableName}_upsertMany(data: $data) }`; + const expectedMutation = `mutation($data: [TestTable_Data!]! @allow(fields: "id value")) { ${formatedTableName}_upsertMany(data: $data) }`; await apiClient.upsertMany(tableName, simpleDataArray); expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( normalizeGraphQLString(expectedMutation), @@ -1007,7 +1007,7 @@ describe('DataConnectApiClient CRUD helpers', () => { { id: 'x', active: true, info: { nested: 'n1/\\"x' } }, { id: 'y', scores: [null, 2] } ]; - const expectedMutation = `mutation($data: [TestTable_Data! @allow(fields: "id active info scores")]!) { ${formatedTableName}_upsertMany(data: $data) }`; + const expectedMutation = `mutation($data: [TestTable_Data!]! @allow(fields: "id active info scores")) { ${formatedTableName}_upsertMany(data: $data) }`; await apiClient.upsertMany(tableName, complexDataArray); expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( normalizeGraphQLString(expectedMutation), @@ -1020,7 +1020,7 @@ describe('DataConnectApiClient CRUD helpers', () => { dataWithUndefined, dataWithUndefined ]; - const expectedMutation = `mutation($data: [TestTable_Data! @allow(fields: "genre title ratings director extras")]!) { ${formatedTableName}_upsertMany(data: $data) }`; + const expectedMutation = `mutation($data: [TestTable_Data!]! @allow(fields: "genre title ratings director extras")) { ${formatedTableName}_upsertMany(data: $data) }`; await apiClient.upsertMany(tableName, dataArray); expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( normalizeGraphQLString(expectedMutation), @@ -1065,7 +1065,7 @@ describe('DataConnectApiClient CRUD helpers', () => { { id: 'key2', name: 'Bob', tags: ['cool'] } ]; const expectedMutation = ` - mutation($data: [TestTable_Data! @allow(fields: "id name status tags")]!) { + mutation($data: [TestTable_Data!]! @allow(fields: "id name status tags")) { testTable_upsertMany(data: $data) }`; const expectedOptions = { From c00855bfc2860f96a72213882b1cf3f34fd29b4c Mon Sep 17 00:00:00 2001 From: stephenarosaj Date: Mon, 15 Jun 2026 16:38:51 -0700 Subject: [PATCH 06/10] initial fix --- .gitignore | 5 +++- .../data-connect-api-client-internal.ts | 24 ++++++++++++------- src/data-connect/error.ts | 23 ++++++++++++++++++ .../data-connect-api-client-internal.spec.ts | 21 ++++++++++++++++ 4 files changed, 64 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 141eeb7f06..dd780b4a90 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,7 @@ firebase-admin-*.tgz docgen/markdown/ # Dataconnect integration test artifacts should not be checked in -test/integration/dataconnect/dataconnect/.dataconnect \ No newline at end of file +test/integration/dataconnect/dataconnect/.dataconnect +test/integration/dataconnect/dataconnect-debug.log +test/integration/dataconnect/firebase-debug.log +test/integration/dataconnect/pglite-debug.log diff --git a/src/data-connect/data-connect-api-client-internal.ts b/src/data-connect/data-connect-api-client-internal.ts index 8cb5ee289b..a874dc5cd6 100644 --- a/src/data-connect/data-connect-api-client-internal.ts +++ b/src/data-connect/data-connect-api-client-internal.ts @@ -21,7 +21,12 @@ import { HttpRequestConfig, HttpClient, RequestResponseError, AuthorizedHttpClient } from '../utils/api-request'; import { FirebaseError, toHttpResponse } from '../utils/error'; -import { FirebaseDataConnectError, DataConnectErrorCode, DATA_CONNECT_ERROR_CODE_MAPPING } from './error'; +import { + FirebaseDataConnectError, + DataConnectErrorCode, + DATA_CONNECT_ERROR_CODE_MAPPING, + GRPC_STATUS_CODE_TO_STRING +} from './error'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { ConnectorConfig, ExecuteGraphqlResponse, GraphqlOptions, OperationOptions } from './data-connect-api'; @@ -409,10 +414,17 @@ export class DataConnectApiClient { }); } - const error: ServerError = (response.data as ErrorResponse).error || {}; + const data = response.data as any; + const error: ServerError = (validator.isNonNullObject(data) && 'error' in data) ? data.error : data || {}; + + let status = error.status; + if (!status && validator.isNumber(error.code)) { + status = GRPC_STATUS_CODE_TO_STRING[error.code as number]; + } + let code: DataConnectErrorCode = DATA_CONNECT_ERROR_CODE_MAPPING.UNKNOWN; - if (error.status && error.status in DATA_CONNECT_ERROR_CODE_MAPPING) { - code = DATA_CONNECT_ERROR_CODE_MAPPING[error.status]; + if (status && status in DATA_CONNECT_ERROR_CODE_MAPPING) { + code = DATA_CONNECT_ERROR_CODE_MAPPING[status]; } const message = error.message || 'Unknown server error'; return new FirebaseDataConnectError({ @@ -670,10 +682,6 @@ export function useEmulator(): boolean { return !!emulatorHost(); } -interface ErrorResponse { - error?: ServerError; -} - interface ServerError { code?: number; message?: string; diff --git a/src/data-connect/error.ts b/src/data-connect/error.ts index 4c92aa48ab..f547856fae 100644 --- a/src/data-connect/error.ts +++ b/src/data-connect/error.ts @@ -69,3 +69,26 @@ export class FirebaseDataConnectError extends FirebaseError { }); } } + +/** + * Mappings from gRPC status codes to their string equivalents. + * @internal + */ +export const GRPC_STATUS_CODE_TO_STRING: Record = { + 1: 'CANCELLED', + 2: 'UNKNOWN', + 3: 'INVALID_ARGUMENT', + 4: 'DEADLINE_EXCEEDED', + 5: 'NOT_FOUND', + 6: 'ALREADY_EXISTS', + 7: 'PERMISSION_DENIED', + 8: 'RESOURCE_EXHAUSTED', + 9: 'FAILED_PRECONDITION', + 10: 'ABORTED', + 11: 'OUT_OF_RANGE', + 12: 'UNIMPLEMENTED', + 13: 'INTERNAL', + 14: 'UNAVAILABLE', + 15: 'DATA_LOSS', + 16: 'UNAUTHENTICATED', +}; 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 5951c29264..7595928f05 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 @@ -179,6 +179,27 @@ describe('DataConnectApiClient', () => { .and.have.property('cause', expected.cause); }); + it('should reject when a gRPC-to-HTTP transcoded error response is received', () => { + const grpcError = { + code: 7, + message: 'Permission denied', + }; + const mockErr = utils.errorFrom(grpcError, 403); + sandbox + .stub(HttpClient.prototype, 'send') + .rejects(mockErr); + const expected = new FirebaseDataConnectError({ + code: 'permission-denied', + message: 'Permission denied', + httpResponse: toHttpResponse(mockErr.response), + cause: mockErr + }); + return apiClient.executeGraphql('query', {}) + .should.eventually.be.rejected + .and.deep.include(expected) + .and.have.property('cause', expected.cause); + }); + it('should reject with unknown-error when error code is not present', () => { const mockErr = utils.errorFrom({}, 404); sandbox From 27da9b2515c7ce42cb9f804da87455c81310ea0c Mon Sep 17 00:00:00 2001 From: stephenarosaj Date: Tue, 16 Jun 2026 11:00:05 -0700 Subject: [PATCH 07/10] address reviewer comments --- src/data-connect/data-connect-api-client-internal.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/data-connect/data-connect-api-client-internal.ts b/src/data-connect/data-connect-api-client-internal.ts index a874dc5cd6..fe0c56b36c 100644 --- a/src/data-connect/data-connect-api-client-internal.ts +++ b/src/data-connect/data-connect-api-client-internal.ts @@ -415,8 +415,10 @@ export class DataConnectApiClient { } const data = response.data as any; - const error: ServerError = (validator.isNonNullObject(data) && 'error' in data) ? data.error : data || {}; - + const error: ServerError = (validator.isNonNullObject(data) && validator.isNonNullObject(data.error)) + ? data.error + : (validator.isNonNullObject(data) ? data : {}); + let status = error.status; if (!status && validator.isNumber(error.code)) { status = GRPC_STATUS_CODE_TO_STRING[error.code as number]; From 6fe0ea29c5e4422649101a2843e9f72362a36f87 Mon Sep 17 00:00:00 2001 From: stephenarosaj Date: Tue, 16 Jun 2026 15:07:40 -0700 Subject: [PATCH 08/10] fix style and normalize expected + actual query strings in tests --- package-lock.json | 1 - .../data-connect-api-client-internal.ts | 20 +- .../data-connect-api-client-internal.spec.ts | 200 +++++++++--------- 3 files changed, 118 insertions(+), 103 deletions(-) diff --git a/package-lock.json b/package-lock.json index bdf6f376a4..8a9491c591 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@fastify/busboy": "^3.0.0", "@firebase/database-compat": "^2.1.4", "@firebase/database-types": "^1.0.20", - "@google-cloud/storage": "7.21.0", "farmhash-modern": "^1.1.0", "fast-deep-equal": "^3.1.1", "google-auth-library": "^10.6.2", diff --git a/src/data-connect/data-connect-api-client-internal.ts b/src/data-connect/data-connect-api-client-internal.ts index 6c661ec515..2affeb3bb2 100644 --- a/src/data-connect/data-connect-api-client-internal.ts +++ b/src/data-connect/data-connect-api-client-internal.ts @@ -522,7 +522,10 @@ export class DataConnectApiClient { try { const { capitalized, formatted } = this.getTableNames(tableName); const keys = this.getObjectKeys(data); - const mutation = `mutation($data: ${capitalized}_Data! @allow(fields: "${keys}")) { ${formatted}_insert(data: $data) }`; + const mutation = + `mutation($data: ${capitalized}_Data! @allow(fields: "${keys}")) { + ${formatted}_insert(data: $data) + }`; return this.executeGraphql(mutation, { variables: { data } }) .catch(this.handleBulkImportErrors); @@ -558,7 +561,10 @@ export class DataConnectApiClient { try { const { capitalized, formatted } = this.getTableNames(tableName); const keys = this.getArrayObjectsKeys(data); - const mutation = `mutation($data: [${capitalized}_Data!]! @allow(fields: "${keys}")) { ${formatted}_insertMany(data: $data) }`; + const mutation = + `mutation($data: [${capitalized}_Data!]! @allow(fields: "${keys}")) { + ${formatted}_insertMany(data: $data) + }`; return this.executeGraphql(mutation, { variables: { data } }) .catch(this.handleBulkImportErrors); @@ -601,7 +607,10 @@ export class DataConnectApiClient { try { const { capitalized, formatted } = this.getTableNames(tableName); const keys = this.getObjectKeys(data); - const mutation = `mutation($data: ${capitalized}_Data! @allow(fields: "${keys}")) { ${formatted}_upsert(data: $data) }`; + const mutation = + `mutation($data: ${capitalized}_Data! @allow(fields: "${keys}")) { + ${formatted}_upsert(data: $data) + }`; return this.executeGraphql(mutation, { variables: { data } }) .catch(this.handleBulkImportErrors); @@ -637,7 +646,10 @@ export class DataConnectApiClient { try { const { capitalized, formatted } = this.getTableNames(tableName); const keys = this.getArrayObjectsKeys(data); - const mutation = `mutation($data: [${capitalized}_Data!]! @allow(fields: "${keys}")) { ${formatted}_upsertMany(data: $data) }`; + const mutation = + `mutation($data: [${capitalized}_Data!]! @allow(fields: "${keys}")) { + ${formatted}_upsertMany(data: $data) + }`; return this.executeGraphql(mutation, { variables: { data } }) .catch(this.handleBulkImportErrors); 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 c87c8fb832..3d347bc9c9 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 @@ -699,11 +699,26 @@ describe('DataConnectApiClient CRUD helpers', () => { // Helper function to normalize GraphQL strings const normalizeGraphQLString = (str: string): string => { return str - .replace(/\s*\n\s*/g, '\n') // Remove leading/trailing whitespace around newlines - .replace(/\s+/g, ' ') // Replace multiple spaces with a single space + .replace(/\n/g, '') // Remove newlines + .replace(/\s+/g, ' ') // Replace multiple spaces with a single space .trim(); // Remove leading/trailing whitespace from the whole string }; + /** + * Helper function to normalize and validate the executeGraphql calls. Importantly, + * normalizes the actual input and the expected input to account for whitespace + * diffs. + */ + function expectNormalizedExecuteGraphqlCall( + expectedQuery: string, + expectedVariables: Record + ): void { + expect(executeGraphqlStub).to.have.been.calledOnce; + const call = executeGraphqlStub.getCall(0); + expect(normalizeGraphQLString(call.args[0])).to.equal(normalizeGraphQLString(expectedQuery)); + expect(call.args[1]).to.deep.equal(expectedVariables); + } + const capitalize = (str: string): string => { return str.charAt(0).toUpperCase() + str.slice(1); }; @@ -726,44 +741,44 @@ describe('DataConnectApiClient CRUD helpers', () => { describe('insert()', () => { tableNames.forEach((tableName, index) => { const capitalizedTable = capitalize(formatedTableNames[index]); - const expectedMutation = `mutation($data: ${capitalizedTable}_Data! @allow(fields: "name")) { ${formatedTableNames[index]}_insert(data: $data) }`; + const expectedMutation = + `mutation($data: ${capitalizedTable}_Data! @allow(fields: "name")) { + ${formatedTableNames[index]}_insert(data: $data) + }`; it(`should use the formatted tableName in the gql query: "${tableName}" as "${formatedTableNames[index]}"`, async () => { await apiClient.insert(tableName, { name: 'a' }); - await expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - { variables: { data: { name: 'a' } } } - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, { variables: { data: { name: 'a' } } }); }); }); it('should call executeGraphql with the correct mutation for simple data', async () => { const simpleData = { name: 'test', value: 123 }; - const expectedMutation = `mutation($data: TestTable_Data! @allow(fields: "name value")) { ${formatedTableName}_insert(data: $data) }`; + const expectedMutation = + `mutation($data: TestTable_Data! @allow(fields: "name value")) { + ${formatedTableName}_insert(data: $data) + }`; await apiClient.insert(tableName, simpleData); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - { variables: { data: simpleData } } - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, { variables: { data: simpleData } }); }); it('should call executeGraphql with the correct mutation for complex data', async () => { const complexData = { id: 'abc', active: true, scores: [10, 20], info: { nested: 'yes/no "quote" \\slash\\' } }; - const expectedMutation = `mutation($data: TestTable_Data! @allow(fields: "id active scores info")) { ${formatedTableName}_insert(data: $data) }`; + const expectedMutation = + `mutation($data: TestTable_Data! @allow(fields: "id active scores info")) { + ${formatedTableName}_insert(data: $data) + }`; await apiClient.insert(tableName, complexData); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - { variables: { data: complexData } } - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, { variables: { data: complexData } }); }); it('should call executeGraphql with the correct mutation for undefined and null values', async () => { - const expectedMutation = `mutation($data: TestTable_Data! @allow(fields: "genre title ratings director extras")) { ${formatedTableName}_insert(data: $data) }`; + const expectedMutation = + `mutation($data: TestTable_Data! @allow(fields: "genre title ratings director extras")) { + ${formatedTableName}_insert(data: $data) + }`; await apiClient.insert(tableName, dataWithUndefined); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - { variables: { data: dataWithUndefined } } - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, { variables: { data: dataWithUndefined } }); }); it('should throw FirebaseDataConnectError for invalid tableName', async () => { @@ -802,10 +817,7 @@ describe('DataConnectApiClient CRUD helpers', () => { variables: { data } }; await apiClient.insert(tableName, data); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - expectedOptions - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, expectedOptions); }); }); @@ -813,25 +825,25 @@ describe('DataConnectApiClient CRUD helpers', () => { describe('insertMany()', () => { tableNames.forEach((tableName, index) => { const capitalizedTable = capitalize(formatedTableNames[index]); - const expectedMutation = `mutation($data: [${capitalizedTable}_Data!]! @allow(fields: "name")) { ${formatedTableNames[index]}_insertMany(data: $data) }`; + const expectedMutation = ` + mutation($data: [${capitalizedTable}_Data!]! @allow(fields: "name")) { + ${formatedTableNames[index]}_insertMany(data: $data) + }`; it(`should use the formatted tableName in the gql query: "${tableName}" as "${formatedTableNames[index]}"`, async () => { await apiClient.insertMany(tableName, [{ name: 'a' }]); - await expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - { variables: { data: [{ name: 'a' }] } } - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, { variables: { data: [{ name: 'a' }] } }); }); }); it('should call executeGraphql with the correct mutation for simple data array', async () => { const simpleDataArray = [{ name: 'test1' }, { name: 'test2', value: 456 }]; - const expectedMutation = `mutation($data: [TestTable_Data!]! @allow(fields: "name value")) { ${formatedTableName}_insertMany(data: $data) }`; + const expectedMutation = ` + mutation($data: [TestTable_Data!]! @allow(fields: "name value")) { + ${formatedTableName}_insertMany(data: $data) + }`; await apiClient.insertMany(tableName, simpleDataArray); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - { variables: { data: simpleDataArray } } - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, { variables: { data: simpleDataArray } }); }); it('should call executeGraphql with the correct mutation for complex data array', async () => { @@ -839,12 +851,12 @@ describe('DataConnectApiClient CRUD helpers', () => { { id: 'a', active: true, info: { nested: 'n1 "quote"' } }, { id: 'b', scores: [1, 2], info: { nested: 'n2/\\' } } ]; - const expectedMutation = `mutation($data: [TestTable_Data!]! @allow(fields: "id active info scores")) { ${formatedTableName}_insertMany(data: $data) }`; + const expectedMutation = ` + mutation($data: [TestTable_Data!]! @allow(fields: "id active info scores")) { + ${formatedTableName}_insertMany(data: $data) + }`; await apiClient.insertMany(tableName, complexDataArray); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - { variables: { data: complexDataArray } } - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, { variables: { data: complexDataArray } }); }); it('should call executeGraphql with the correct mutation for undefined and null', async () => { @@ -852,12 +864,12 @@ describe('DataConnectApiClient CRUD helpers', () => { dataWithUndefined, dataWithUndefined ]; - const expectedMutation = `mutation($data: [TestTable_Data!]! @allow(fields: "genre title ratings director extras")) { ${formatedTableName}_insertMany(data: $data) }`; + const expectedMutation = ` + mutation($data: [TestTable_Data!]! @allow(fields: "genre title ratings director extras")) { + ${formatedTableName}_insertMany(data: $data) + }`; await apiClient.insertMany(tableName, dataArray); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - { variables: { data: dataArray } } - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, { variables: { data: dataArray } }); }); it('should throw FirebaseDataConnectError for invalid tableName', async () => { @@ -904,10 +916,7 @@ describe('DataConnectApiClient CRUD helpers', () => { variables: { data } }; await apiClient.insertMany(tableName, data); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - expectedOptions - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, expectedOptions); }); }); @@ -915,44 +924,44 @@ describe('DataConnectApiClient CRUD helpers', () => { describe('upsert()', () => { tableNames.forEach((tableName, index) => { const capitalizedTable = capitalize(formatedTableNames[index]); - const expectedMutation = `mutation($data: ${capitalizedTable}_Data! @allow(fields: "name")) { ${formatedTableNames[index]}_upsert(data: $data) }`; + const expectedMutation = ` + mutation($data: ${capitalizedTable}_Data! @allow(fields: "name")) { + ${formatedTableNames[index]}_upsert(data: $data) + }`; it(`should use the formatted tableName in the gql query: "${tableName}" as "${formatedTableNames[index]}"`, async () => { await apiClient.upsert(tableName, { name: 'a' }); - await expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - { variables: { data: { name: 'a' } } } - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, { variables: { data: { name: 'a' } } }); }); }); it('should call executeGraphql with the correct mutation for simple data', async () => { const simpleData = { id: 'key1', value: 'updated' }; - const expectedMutation = `mutation($data: TestTable_Data! @allow(fields: "id value")) { ${formatedTableName}_upsert(data: $data) }`; + const expectedMutation = ` + mutation($data: TestTable_Data! @allow(fields: "id value")) { + ${formatedTableName}_upsert(data: $data) + }`; await apiClient.upsert(tableName, simpleData); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - { variables: { data: simpleData } } - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, { variables: { data: simpleData } }); }); it('should call executeGraphql with the correct mutation for complex data', async () => { const complexData = { id: 'key2', active: false, items: [1, null], detail: { status: 'done/\\' } }; - const expectedMutation = `mutation($data: TestTable_Data! @allow(fields: "id active items detail")) { ${formatedTableName}_upsert(data: $data) }`; + const expectedMutation = ` + mutation($data: TestTable_Data! @allow(fields: "id active items detail")) { + ${formatedTableName}_upsert(data: $data) + }`; await apiClient.upsert(tableName, complexData); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - { variables: { data: complexData } } - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, { variables: { data: complexData } }); }); it('should call executeGraphql with the correct mutation for undefined and null values', async () => { - const expectedMutation = `mutation($data: TestTable_Data! @allow(fields: "genre title ratings director extras")) { ${formatedTableName}_upsert(data: $data) }`; + const expectedMutation = ` + mutation($data: TestTable_Data! @allow(fields: "genre title ratings director extras")) { + ${formatedTableName}_upsert(data: $data) + }`; await apiClient.upsert(tableName, dataWithUndefined); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - { variables: { data: dataWithUndefined } } - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, { variables: { data: dataWithUndefined } }); }); it('should throw FirebaseDataConnectError for invalid tableName', async () => { @@ -991,10 +1000,7 @@ describe('DataConnectApiClient CRUD helpers', () => { variables: { data } }; await apiClient.upsert(tableName, data); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - expectedOptions - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, expectedOptions); }); }); @@ -1002,25 +1008,25 @@ describe('DataConnectApiClient CRUD helpers', () => { describe('upsertMany()', () => { tableNames.forEach((tableName, index) => { const capitalizedTable = capitalize(formatedTableNames[index]); - const expectedMutation = `mutation($data: [${capitalizedTable}_Data!]! @allow(fields: "name")) { ${formatedTableNames[index]}_upsertMany(data: $data) }`; + const expectedMutation = ` + mutation($data: [${capitalizedTable}_Data!]! @allow(fields: "name")) { + ${formatedTableNames[index]}_upsertMany(data: $data) + }`; it(`should use the formatted tableName in the gql query: "${tableName}" as "${formatedTableNames[index]}"`, async () => { await apiClient.upsertMany(tableName, [{ name: 'a' }]); - await expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - { variables: { data: [{ name: 'a' }] } } - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, { variables: { data: [{ name: 'a' }] } }); }); }); it('should call executeGraphql with the correct mutation for simple data array', async () => { const simpleDataArray = [{ id: 'k1' }, { id: 'k2', value: 99 }]; - const expectedMutation = `mutation($data: [TestTable_Data!]! @allow(fields: "id value")) { ${formatedTableName}_upsertMany(data: $data) }`; + const expectedMutation = ` + mutation($data: [TestTable_Data!]! @allow(fields: "id value")) { + ${formatedTableName}_upsertMany(data: $data) + }`; await apiClient.upsertMany(tableName, simpleDataArray); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - { variables: { data: simpleDataArray } } - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, { variables: { data: simpleDataArray } }); }); it('should call executeGraphql with the correct mutation for complex data array', async () => { @@ -1028,12 +1034,12 @@ describe('DataConnectApiClient CRUD helpers', () => { { id: 'x', active: true, info: { nested: 'n1/\\"x' } }, { id: 'y', scores: [null, 2] } ]; - const expectedMutation = `mutation($data: [TestTable_Data!]! @allow(fields: "id active info scores")) { ${formatedTableName}_upsertMany(data: $data) }`; + const expectedMutation = ` + mutation($data: [TestTable_Data!]! @allow(fields: "id active info scores")) { + ${formatedTableName}_upsertMany(data: $data) + }`; await apiClient.upsertMany(tableName, complexDataArray); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - { variables: { data: complexDataArray } } - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, { variables: { data: complexDataArray } }); }); it('should call executeGraphql with the correct mutation for undefined and null', async () => { @@ -1041,12 +1047,12 @@ describe('DataConnectApiClient CRUD helpers', () => { dataWithUndefined, dataWithUndefined ]; - const expectedMutation = `mutation($data: [TestTable_Data!]! @allow(fields: "genre title ratings director extras")) { ${formatedTableName}_upsertMany(data: $data) }`; + const expectedMutation = ` + mutation($data: [TestTable_Data!]! @allow(fields: "genre title ratings director extras")) { + ${formatedTableName}_upsertMany(data: $data) + }`; await apiClient.upsertMany(tableName, dataArray); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - { variables: { data: dataArray } } - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, { variables: { data: dataArray } }); }); it('should throw FirebaseDataConnectError for invalid tableName', async () => { @@ -1093,10 +1099,7 @@ describe('DataConnectApiClient CRUD helpers', () => { variables: { data } }; await apiClient.upsertMany(tableName, data); - expect(executeGraphqlStub).to.have.been.calledOnceWithExactly( - normalizeGraphQLString(expectedMutation), - expectedOptions - ); + expectNormalizedExecuteGraphqlCall(expectedMutation, expectedOptions); }); }); @@ -1157,3 +1160,4 @@ describe('DataConnectApiClient CRUD helpers', () => { }); }); }); + From 629eeb9f5caf419fdcbb50d1398faed7ee44567b Mon Sep 17 00:00:00 2001 From: stephenarosaj Date: Wed, 17 Jun 2026 11:11:42 -0700 Subject: [PATCH 09/10] add fdc to integration test documentation and update integration test artifact ignores --- .gitignore | 8 ++++++-- CONTRIBUTING.md | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 32d4066dfc..e19489c327 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,10 @@ firebase-admin-*.tgz docgen/markdown/ -# Dataconnect integration test artifacts should not be checked in +# Integration test artifacts should not be checked in +**/database-debug.log +**/firestore-debug.log test/integration/dataconnect/dataconnect/.dataconnect -test/integration/dataconnect/*.log +**/dataconnect-debug.log +**/pglite-debug.log + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b1c626c611..2b6e250d89 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -150,9 +150,19 @@ And then: 'npx mocha \"test/integration/{auth,database,firestore}.spec.ts\" --slow 5000 --timeout 20000 --require ts-node/register' ``` -Currently, only the Auth, Database, and Firestore test suites work. Some test -cases will be automatically skipped due to lack of emulator support. The section -below covers how to run the full test suite against an actual Firebase project. +Currently, only the Auth, Database, and Firestore test suites work. Some test cases +will be automatically skipped due to lack of emulator support. + +You can also run the Data Connect test suite against the emulators using the same command, +but with a config file specific to Data Connect emulator testing: + +```bash + firebase emulators:exec \ + --project fake-project-id --only dataconnect --config test/integration/dataconnect/firebase.json \ + 'npx mocha \"test/integration/data-connect.spec.ts\" --slow 5000 --timeout 20000 --require ts-node/register' +``` + +The section below covers how to run the full test suite against an actual Firebase project. #### Integration Tests with an actual Firebase project From 746a37c6677fa32981268f576863b9fde8302523 Mon Sep 17 00:00:00 2001 From: stephenarosaj Date: Wed, 17 Jun 2026 15:46:17 -0700 Subject: [PATCH 10/10] improve getTableNames variable naming for clarity --- .../data-connect-api-client-internal.ts | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/data-connect/data-connect-api-client-internal.ts b/src/data-connect/data-connect-api-client-internal.ts index 943c5754c7..2eed0d5dbf 100644 --- a/src/data-connect/data-connect-api-client-internal.ts +++ b/src/data-connect/data-connect-api-client-internal.ts @@ -441,13 +441,13 @@ export class DataConnectApiClient { * Generates both capitalized and camel-cased variations of a table name. * Capitalization matches the schema types, and camel-case matches mutations. */ - private getTableNames(tableName: string): { capitalized: string; formatted: string } { + private getTableNames(tableName: string): { capitalized: string; camelCase: string } { if (!tableName || tableName.length === 0) { - return { capitalized: tableName, formatted: tableName }; + return { capitalized: tableName, camelCase: tableName }; } const capitalized = tableName.charAt(0).toUpperCase() + tableName.slice(1); - const formatted = tableName.charAt(0).toLowerCase() + tableName.slice(1); - return { capitalized, formatted }; + const camelCase = tableName.charAt(0).toLowerCase() + tableName.slice(1); + return { capitalized, camelCase }; } /** @@ -481,7 +481,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({ code: DATA_CONNECT_ERROR_CODE_MAPPING.QUERY_ERROR, message: `${err.message}. Make sure that your table name passed in matches the type name in your ` @@ -520,11 +520,11 @@ export class DataConnectApiClient { } try { - const { capitalized, formatted } = this.getTableNames(tableName); + const { capitalized, camelCase } = this.getTableNames(tableName); const keys = this.getObjectKeys(data); - const mutation = + const mutation = `mutation($data: ${capitalized}_Data! @allow(fields: "${keys}")) { - ${formatted}_insert(data: $data) + ${camelCase}_insert(data: $data) }`; return this.executeGraphql(mutation, { variables: { data } }) @@ -559,11 +559,11 @@ export class DataConnectApiClient { } try { - const { capitalized, formatted } = this.getTableNames(tableName); + const { capitalized, camelCase } = this.getTableNames(tableName); const keys = this.getArrayObjectsKeys(data); - const mutation = + const mutation = `mutation($data: [${capitalized}_Data!]! @allow(fields: "${keys}")) { - ${formatted}_insertMany(data: $data) + ${camelCase}_insertMany(data: $data) }`; return this.executeGraphql(mutation, { variables: { data } }) @@ -605,11 +605,11 @@ export class DataConnectApiClient { } try { - const { capitalized, formatted } = this.getTableNames(tableName); + const { capitalized, camelCase } = this.getTableNames(tableName); const keys = this.getObjectKeys(data); - const mutation = + const mutation = `mutation($data: ${capitalized}_Data! @allow(fields: "${keys}")) { - ${formatted}_upsert(data: $data) + ${camelCase}_upsert(data: $data) }`; return this.executeGraphql(mutation, { variables: { data } }) @@ -644,11 +644,11 @@ export class DataConnectApiClient { } try { - const { capitalized, formatted } = this.getTableNames(tableName); + const { capitalized, camelCase } = this.getTableNames(tableName); const keys = this.getArrayObjectsKeys(data); - const mutation = + const mutation = `mutation($data: [${capitalized}_Data!]! @allow(fields: "${keys}")) { - ${formatted}_upsertMany(data: $data) + ${camelCase}_upsertMany(data: $data) }`; return this.executeGraphql(mutation, { variables: { data } })