Skip to content

Commit 2339f7b

Browse files
authored
Merge branch 'main' into davidmotson.ai_monitoring
2 parents 64a048b + ba69975 commit 2339f7b

File tree

25 files changed

+1569
-795
lines changed

25 files changed

+1569
-795
lines changed

firebase-dataconnect/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
* [changed] Removed the "beta" suffix from the version of the Firebase Data
33
Connect Android SDK, thus graduating it from "beta" to "generally available".
44
([#6792](https://github.com/firebase/firebase-android-sdk/pull/6792))
5+
* [changed] DataConnectOperationException added, enabling support for partial
6+
errors; that is, any data that was received and/or was able to be decoded is
7+
now available via the "response" property of the exception thrown when a
8+
query or mutation is executed.
9+
([#6794](https://github.com/firebase/firebase-android-sdk/pull/6794))
510

611
# 16.0.0-beta05
712
* [changed] Changed gRPC proto package to v1 (was v1beta).

firebase-dataconnect/api.txt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,47 @@ package com.google.firebase.dataconnect {
4242
ctor public DataConnectException(String message, Throwable? cause = null);
4343
}
4444

45+
public class DataConnectOperationException extends com.google.firebase.dataconnect.DataConnectException {
46+
ctor public DataConnectOperationException(String message, Throwable? cause = null, com.google.firebase.dataconnect.DataConnectOperationFailureResponse<? extends java.lang.Object?> response);
47+
method public final com.google.firebase.dataconnect.DataConnectOperationFailureResponse<? extends java.lang.Object?> getResponse();
48+
property public final com.google.firebase.dataconnect.DataConnectOperationFailureResponse<? extends java.lang.Object?> response;
49+
}
50+
51+
public interface DataConnectOperationFailureResponse<Data> {
52+
method public Data? getData();
53+
method public java.util.List<com.google.firebase.dataconnect.DataConnectOperationFailureResponse.ErrorInfo> getErrors();
54+
method public java.util.Map<java.lang.String,java.lang.Object?>? getRawData();
55+
method public String toString();
56+
property public abstract Data? data;
57+
property public abstract java.util.List<com.google.firebase.dataconnect.DataConnectOperationFailureResponse.ErrorInfo> errors;
58+
property public abstract java.util.Map<java.lang.String,java.lang.Object?>? rawData;
59+
}
60+
61+
public static interface DataConnectOperationFailureResponse.ErrorInfo {
62+
method public boolean equals(Object? other);
63+
method public String getMessage();
64+
method public java.util.List<com.google.firebase.dataconnect.DataConnectPathSegment> getPath();
65+
method public int hashCode();
66+
method public String toString();
67+
property public abstract String message;
68+
property public abstract java.util.List<com.google.firebase.dataconnect.DataConnectPathSegment> path;
69+
}
70+
71+
public sealed interface DataConnectPathSegment {
72+
}
73+
74+
@kotlin.jvm.JvmInline public static final value class DataConnectPathSegment.Field implements com.google.firebase.dataconnect.DataConnectPathSegment {
75+
ctor public DataConnectPathSegment.Field(String field);
76+
method public String getField();
77+
property public final String field;
78+
}
79+
80+
@kotlin.jvm.JvmInline public static final value class DataConnectPathSegment.ListIndex implements com.google.firebase.dataconnect.DataConnectPathSegment {
81+
ctor public DataConnectPathSegment.ListIndex(int index);
82+
method public int getIndex();
83+
property public final int index;
84+
}
85+
4586
public final class DataConnectSettings {
4687
ctor public DataConnectSettings(String host = "firebasedataconnect.googleapis.com", boolean sslEnabled = true);
4788
method public String getHost();

firebase-dataconnect/emulator/dataconnect/connector/person/person_ops.gql

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,14 @@ query getPersonAuth($id: String!) @auth(level: USER_ANON) {
8888
age
8989
}
9090
}
91+
92+
query getPersonWithPartialFailure($id: String!) @auth(level: PUBLIC) {
93+
person1: person(id: $id) { name }
94+
person2: person(id: $id) @check(expr: "false", message: "c8azjdwz2x") { name }
95+
}
96+
97+
mutation createPersonWithPartialFailure($id: String!, $name: String!) @auth(level: PUBLIC) {
98+
person1: person_insert(data: { id: $id, name: $name })
99+
person2: person_insert(data: { id_expr: "uuidV4()", name: $name }) @check(expr: "false", message: "ecxpjy4qfy")
100+
}
101+

firebase-dataconnect/emulator/emulator.sh

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@
1616

1717
set -euo pipefail
1818

19-
echo "[$0] PID=$$"
20-
21-
readonly SELF_DIR="$(dirname "$0")"
19+
export FIREBASE_DATACONNECT_POSTGRESQL_STRING='postgresql://postgres:postgres@localhost:5432?sslmode=disable'
20+
echo "[$0] export FIREBASE_DATACONNECT_POSTGRESQL_STRING='$FIREBASE_DATACONNECT_POSTGRESQL_STRING'"
2221

2322
readonly FIREBASE_ARGS=(
2423
firebase
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.dataconnect
18+
19+
import com.google.firebase.dataconnect.testutil.DataConnectIntegrationTestBase
20+
import com.google.firebase.dataconnect.testutil.schemas.PersonSchema
21+
import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.CreatePersonMutation
22+
import com.google.firebase.dataconnect.testutil.schemas.PersonSchema.GetPersonQuery
23+
import com.google.firebase.dataconnect.testutil.shouldSatisfy
24+
import io.kotest.assertions.throwables.shouldThrow
25+
import io.kotest.matchers.collections.shouldHaveAtLeastSize
26+
import io.kotest.property.Arb
27+
import io.kotest.property.arbitrary.map
28+
import io.kotest.property.arbitrary.next
29+
import kotlinx.coroutines.test.runTest
30+
import kotlinx.serialization.Serializable
31+
import kotlinx.serialization.SerializationException
32+
import kotlinx.serialization.serializer
33+
import org.junit.Test
34+
35+
class OperationExecutionErrorsIntegrationTest : DataConnectIntegrationTestBase() {
36+
37+
private val personSchema: PersonSchema by lazy { PersonSchema(dataConnectFactory) }
38+
private val dataConnect: FirebaseDataConnect by lazy { personSchema.dataConnect }
39+
40+
@Test
41+
fun executeQueryFailsWithNullDataNonEmptyErrors() = runTest {
42+
val queryRef =
43+
dataConnect.query(
44+
operationName = GetPersonQuery.operationName,
45+
variables = Arb.incompatibleVariables().next(rs),
46+
dataDeserializer = serializer<GetPersonQuery.Data>(),
47+
variablesSerializer = serializer(),
48+
optionsBuilder = {},
49+
)
50+
51+
val exception = shouldThrow<DataConnectOperationException> { queryRef.execute() }
52+
53+
exception.shouldSatisfy(
54+
expectedMessageSubstringCaseInsensitive = "operation encountered errors",
55+
expectedMessageSubstringCaseSensitive = "jwdbzka4k5",
56+
expectedCause = null,
57+
expectedRawData = null,
58+
expectedData = null,
59+
errorsValidator = { it.shouldHaveAtLeastSize(1) },
60+
)
61+
}
62+
63+
@Test
64+
fun executeMutationFailsWithNullDataNonEmptyErrors() = runTest {
65+
val mutationRef =
66+
dataConnect.mutation(
67+
operationName = CreatePersonMutation.operationName,
68+
variables = Arb.incompatibleVariables().next(rs),
69+
dataDeserializer = serializer<CreatePersonMutation.Data>(),
70+
variablesSerializer = serializer(),
71+
optionsBuilder = {},
72+
)
73+
74+
val exception = shouldThrow<DataConnectOperationException> { mutationRef.execute() }
75+
76+
exception.shouldSatisfy(
77+
expectedMessageSubstringCaseInsensitive = "operation encountered errors",
78+
expectedCause = null,
79+
expectedRawData = null,
80+
expectedData = null,
81+
errorsValidator = { it.shouldHaveAtLeastSize(1) },
82+
)
83+
}
84+
85+
@Test
86+
fun executeQueryFailsWithNonNullDataEmptyErrorsButDecodingResponseDataFails() = runTest {
87+
val id = Arb.alphanumericString().next()
88+
val queryRef =
89+
dataConnect.query(
90+
operationName = GetPersonQuery.operationName,
91+
variables = GetPersonQuery.Variables(id),
92+
dataDeserializer = serializer<IncompatibleData>(),
93+
variablesSerializer = serializer(),
94+
optionsBuilder = {},
95+
)
96+
97+
val exception = shouldThrow<DataConnectOperationException> { queryRef.execute() }
98+
99+
exception.shouldSatisfy(
100+
expectedMessageSubstringCaseInsensitive = "decoding data from the server's response failed",
101+
expectedCause = SerializationException::class,
102+
expectedRawData = mapOf("person" to null),
103+
expectedData = null,
104+
expectedErrors = emptyList(),
105+
)
106+
}
107+
108+
@Test
109+
fun executeMutationFailsWithNonNullDataEmptyErrorsButDecodingResponseDataFails() = runTest {
110+
val id = Arb.alphanumericString().next()
111+
val name = Arb.alphanumericString().next()
112+
val mutationRef =
113+
dataConnect.mutation(
114+
operationName = CreatePersonMutation.operationName,
115+
variables = CreatePersonMutation.Variables(id, name),
116+
dataDeserializer = serializer<IncompatibleData>(),
117+
variablesSerializer = serializer(),
118+
optionsBuilder = {},
119+
)
120+
121+
val exception = shouldThrow<DataConnectOperationException> { mutationRef.execute() }
122+
123+
exception.shouldSatisfy(
124+
expectedMessageSubstringCaseInsensitive = "decoding data from the server's response failed",
125+
expectedCause = SerializationException::class,
126+
expectedRawData = mapOf("person_insert" to mapOf("id" to id)),
127+
expectedData = null,
128+
expectedErrors = emptyList(),
129+
)
130+
}
131+
132+
@Test
133+
fun executeQueryFailsWithNonNullDataNonEmptyErrorsDecodingSucceeds() = runTest {
134+
val id = Arb.alphanumericString().next()
135+
val name = Arb.alphanumericString().next()
136+
personSchema.createPerson(CreatePersonMutation.Variables(id, name)).execute()
137+
val queryRef =
138+
dataConnect.query(
139+
operationName = "getPersonWithPartialFailure",
140+
variables = GetPersonWithPartialFailureVariables(id),
141+
dataDeserializer = serializer<GetPersonWithPartialFailureData>(),
142+
variablesSerializer = serializer(),
143+
optionsBuilder = {},
144+
)
145+
146+
val exception = shouldThrow<DataConnectOperationException> { queryRef.execute() }
147+
148+
exception.shouldSatisfy(
149+
expectedMessageSubstringCaseInsensitive = "operation encountered errors",
150+
expectedMessageSubstringCaseSensitive = "c8azjdwz2x",
151+
expectedCause = null,
152+
expectedRawData = mapOf("person1" to mapOf("name" to name), "person2" to null),
153+
expectedData = GetPersonWithPartialFailureData(name),
154+
errorsValidator = { it.shouldHaveAtLeastSize(1) },
155+
)
156+
}
157+
158+
@Test
159+
fun executeMutationFailsWithNonNullDataNonEmptyErrorsDecodingSucceeds() = runTest {
160+
val id = Arb.alphanumericString().next()
161+
val name = Arb.alphanumericString().next()
162+
val mutationRef =
163+
dataConnect.mutation(
164+
operationName = "createPersonWithPartialFailure",
165+
variables = CreatePersonWithPartialFailureVariables(id = id, name = name),
166+
dataDeserializer = serializer<CreatePersonWithPartialFailureData>(),
167+
variablesSerializer = serializer(),
168+
optionsBuilder = {},
169+
)
170+
171+
val exception = shouldThrow<DataConnectOperationException> { mutationRef.execute() }
172+
173+
exception.shouldSatisfy(
174+
expectedMessageSubstringCaseInsensitive = "operation encountered errors",
175+
expectedMessageSubstringCaseSensitive = "ecxpjy4qfy",
176+
expectedCause = null,
177+
expectedRawData = mapOf("person1" to mapOf("id" to id), "person2" to null),
178+
expectedData = CreatePersonWithPartialFailureData(id),
179+
errorsValidator = { it.shouldHaveAtLeastSize(1) },
180+
)
181+
}
182+
183+
@Test
184+
fun executeQueryFailsWithNonNullDataNonEmptyErrorsDecodingFails() = runTest {
185+
val id = Arb.alphanumericString().next()
186+
val name = Arb.alphanumericString().next()
187+
personSchema.createPerson(CreatePersonMutation.Variables(id, name)).execute()
188+
val queryRef =
189+
dataConnect.query(
190+
operationName = "getPersonWithPartialFailure",
191+
variables = GetPersonWithPartialFailureVariables(id),
192+
dataDeserializer = serializer<IncompatibleData>(),
193+
variablesSerializer = serializer(),
194+
optionsBuilder = {},
195+
)
196+
197+
val exception = shouldThrow<DataConnectOperationException> { queryRef.execute() }
198+
199+
exception.shouldSatisfy(
200+
expectedMessageSubstringCaseInsensitive = "operation encountered errors",
201+
expectedMessageSubstringCaseSensitive = "c8azjdwz2x",
202+
expectedCause = null,
203+
expectedRawData = mapOf("person1" to mapOf("name" to name), "person2" to null),
204+
expectedData = null,
205+
errorsValidator = { it.shouldHaveAtLeastSize(1) },
206+
)
207+
}
208+
209+
@Test
210+
fun executeMutationFailsWithNonNullDataNonEmptyErrorsDecodingFails() = runTest {
211+
val id = Arb.alphanumericString().next()
212+
val name = Arb.alphanumericString().next()
213+
val mutationRef =
214+
dataConnect.mutation(
215+
operationName = "createPersonWithPartialFailure",
216+
variables = CreatePersonWithPartialFailureVariables(id = id, name = name),
217+
dataDeserializer = serializer<IncompatibleData>(),
218+
variablesSerializer = serializer(),
219+
optionsBuilder = {},
220+
)
221+
222+
val exception = shouldThrow<DataConnectOperationException> { mutationRef.execute() }
223+
224+
exception.shouldSatisfy(
225+
expectedMessageSubstringCaseInsensitive = "operation encountered errors",
226+
expectedMessageSubstringCaseSensitive = "ecxpjy4qfy",
227+
expectedCause = null,
228+
expectedRawData = mapOf("person1" to mapOf("id" to id), "person2" to null),
229+
expectedData = null,
230+
errorsValidator = { it.shouldHaveAtLeastSize(1) },
231+
)
232+
}
233+
234+
@Serializable private data class IncompatibleVariables(val jwdbzka4k5: String)
235+
236+
@Serializable private data class IncompatibleData(val btzjhbfz7h: String)
237+
238+
private fun Arb.Companion.incompatibleVariables(string: Arb<String> = Arb.alphanumericString()) =
239+
string.map { IncompatibleVariables(it) }
240+
241+
@Serializable private data class GetPersonWithPartialFailureVariables(val id: String)
242+
243+
@Serializable
244+
private data class GetPersonWithPartialFailureData(val person1: Person, val person2: Nothing?) {
245+
constructor(person1Name: String) : this(Person(person1Name), null)
246+
247+
@Serializable private data class Person(val name: String)
248+
}
249+
250+
@Serializable
251+
private data class CreatePersonWithPartialFailureVariables(val id: String, val name: String)
252+
253+
@Serializable
254+
private data class CreatePersonWithPartialFailureData(
255+
val person1: Person,
256+
val person2: Nothing?
257+
) {
258+
constructor(person1Id: String) : this(Person(person1Id), null)
259+
260+
@Serializable private data class Person(val id: String)
261+
}
262+
}

firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/testutil/schemas/PersonSchema.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) {
5454
)
5555

5656
object CreatePersonMutation {
57+
const val operationName = "createPerson"
58+
5759
@Serializable
5860
data class Data(val person_insert: PersonKey) {
5961
@Serializable data class PersonKey(val id: String)
@@ -63,7 +65,7 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) {
6365

6466
fun createPerson(variables: CreatePersonMutation.Variables) =
6567
dataConnect.mutation(
66-
operationName = "createPerson",
68+
operationName = CreatePersonMutation.operationName,
6769
variables = variables,
6870
dataDeserializer = serializer<CreatePersonMutation.Data>(),
6971
variablesSerializer = serializer<CreatePersonMutation.Variables>(),
@@ -141,6 +143,8 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) {
141143
fun deletePerson(id: String) = deletePerson(DeletePersonMutation.Variables(id = id))
142144

143145
object GetPersonQuery {
146+
const val operationName = "getPerson"
147+
144148
@Serializable
145149
data class Data(val person: Person?) {
146150
@Serializable data class Person(val name: String, val age: Int? = null)
@@ -151,7 +155,7 @@ class PersonSchema(val dataConnect: FirebaseDataConnect) {
151155

152156
fun getPerson(variables: GetPersonQuery.Variables) =
153157
dataConnect.query(
154-
operationName = "getPerson",
158+
operationName = GetPersonQuery.operationName,
155159
variables = variables,
156160
dataDeserializer = serializer<GetPersonQuery.Data>(),
157161
variablesSerializer = serializer<GetPersonQuery.Variables>(),

0 commit comments

Comments
 (0)