Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
36fa0c8
add tests checking for new GQL output for [insert,upsert](many)
stephenarosaj Jun 11, 2026
1ed5b87
change mutation functions to use @allow
stephenarosaj Jun 11, 2026
4f6135e
factor out common code after @allow changes
stephenarosaj Jun 12, 2026
8e12fc5
update tests to expect @allow
stephenarosaj Jun 12, 2026
ad84ef5
update the @allow directive for list inputs
stephenarosaj Jun 12, 2026
c00855b
initial fix
stephenarosaj Jun 15, 2026
27da9b2
address reviewer comments
stephenarosaj Jun 16, 2026
7c4de04
Merge branch 'rosa/emulator-error-fix' into rosa/enum-serialization
stephenarosaj Jun 16, 2026
6fe0ea2
fix style and normalize expected + actual query strings in tests
stephenarosaj Jun 16, 2026
3217d20
Merge branch 'main' into rosa/enum-serialization
stephenarosaj Jun 17, 2026
629eeb9
add fdc to integration test documentation and update integration test…
stephenarosaj Jun 17, 2026
746a37c
improve getTableNames variable naming for clarity
stephenarosaj Jun 17, 2026
863e3b2
add 10k limit and nested coalesced field keys for @allow
stephenarosaj Jun 25, 2026
f6eb83a
de-duplicate code in mutaiton CRUD functions, and refactor tests
stephenarosaj Jun 25, 2026
1451bba
update contributing
stephenarosaj Jun 25, 2026
6e91dff
add maxCount to array bulk insert variables
stephenarosaj Jun 26, 2026
b350dfd
limit @allow nesting to _on_ relational fields
stephenarosaj Jun 26, 2026
5627469
Merge branch 'main' into rosa/enum-serialization
stephenarosaj Jun 26, 2026
4f87c12
update tests
stephenarosaj Jun 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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

16 changes: 13 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
133 changes: 70 additions & 63 deletions src/data-connect/data-connect-api-client-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,55 +438,46 @@ export class DataConnectApiClient {
}

/**
* Converts JSON data into a GraphQL literal string.
* Handles nested objects, arrays, strings, numbers, and booleans.
* Ensures strings are properly escaped.
* Generates both capitalized and camel-cased variations of a table name.
* Capitalization matches the schema types, and camel-case matches mutations.
*/
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';
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 };
}
Comment thread
stephenarosaj marked this conversation as resolved.
Outdated

// 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);
/**
* 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<string, unknown> | object): string {
return Object.keys(data)
.filter(key => (data as Record<string, unknown>)[key] !== undefined)
.join(' ');
}
Comment thread
stephenarosaj marked this conversation as resolved.
Outdated

private formatTableName(tableName: string): string {
// Format tableName: first character to lowercase
if (tableName && tableName.length > 0) {
return tableName.charAt(0).toLowerCase() + tableName.slice(1);
/**
* 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<unknown>): string {
const allKeys = new Set<string>();
for (const element of data) {
if (validator.isNonNullObject(element)) {
const record = element as Record<string, unknown>;
Object.keys(record).forEach(key => {
if (record[key] !== undefined) {
allKeys.add(key);
}
});
}
}
return tableName;
return Array.from(allKeys).join(' ');
}
Comment thread
stephenarosaj marked this conversation as resolved.
Outdated

private handleBulkImportErrors(err: FirebaseDataConnectError): never {
Expand Down Expand Up @@ -529,11 +520,15 @@ 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<GraphQlResponse, Variables>(mutation).catch(this.handleBulkImportErrors);
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<GraphQlResponse, { data: Variables }>(mutation, { variables: { data } })
.catch(this.handleBulkImportErrors);
} catch (e: any) {
throw new FirebaseDataConnectError({
code: DATA_CONNECT_ERROR_CODE_MAPPING.INTERNAL,
Expand Down Expand Up @@ -564,11 +559,15 @@ 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<GraphQlResponse, Variables>(mutation).catch(this.handleBulkImportErrors);
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<GraphQlResponse, { data: Variables }>(mutation, { variables: { data } })
.catch(this.handleBulkImportErrors);
} catch (e: any) {
throw new FirebaseDataConnectError({
code: DATA_CONNECT_ERROR_CODE_MAPPING.INTERNAL,
Expand Down Expand Up @@ -606,11 +605,15 @@ 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<GraphQlResponse, Variables>(mutation).catch(this.handleBulkImportErrors);
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<GraphQlResponse, { data: Variables }>(mutation, { variables: { data } })
.catch(this.handleBulkImportErrors);
} catch (e: any) {
throw new FirebaseDataConnectError({
code: DATA_CONNECT_ERROR_CODE_MAPPING.INTERNAL,
Expand Down Expand Up @@ -641,11 +644,15 @@ 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<GraphQlResponse, Variables>(mutation).catch(this.handleBulkImportErrors);
const { capitalized, formatted } = this.getTableNames(tableName);
const keys = this.getArrayObjectsKeys(data);
const mutation =
`mutation($data: [${capitalized}_Data!]! @allow(fields: "${keys}")) {
Comment thread
stephenarosaj marked this conversation as resolved.
Outdated
${formatted}_upsertMany(data: $data)
}`;

return this.executeGraphql<GraphQlResponse, { data: Variables }>(mutation, { variables: { data } })
.catch(this.handleBulkImportErrors);
} catch (e: any) {
throw new FirebaseDataConnectError({
code: DATA_CONNECT_ERROR_CODE_MAPPING.INTERNAL,
Expand Down
Loading
Loading