diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/queries.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/queries.gql index 3c1ce8191a13..b6553b6fd49e 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/queries.gql +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/queries.gql @@ -10,6 +10,13 @@ query ListMovies @auth(level: USER) { } } +query GetMovie($key: Movie_Key!) @auth(level: USER) { + movie(key: $key) { + id + title + } +} + # List movies by partial title match query ListMoviesByPartialTitle($input: String!) @auth(level: PUBLIC) { movies(where: { title: { contains: $input } }) { diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/README.md b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/README.md new file mode 100644 index 000000000000..afeeea8892ee --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/README.md @@ -0,0 +1,736 @@ +# movies SDK + +## Installation +```sh +flutter pub get firebase_data_connect +flutterfire configure +``` +For more information, see [Flutter for Firebase installation documentation](https://firebase.google.com/docs/data-connect/flutter-sdk#use-core). + +## Data Connect instance +Each connector creates a static class, with an instance of the `DataConnect` class that can be used to connect to your Data Connect backend and call operations. + +### Connecting to the emulator + +```dart +String host = 'localhost'; // or your host name +int port = 9399; // or your port number +MoviesConnector.instance.dataConnect.useDataConnectEmulator(host, port); +``` + +You can also call queries and mutations by using the connector class. +## Queries + +### ListMovies +#### Required Arguments +```dart +// No required arguments +MoviesConnector.instance.listMovies().execute(); +``` + + + +#### Return Type +`execute()` returns a `QueryResult` +```dart +/// Result of an Operation Request (query/mutation). +class OperationResult { + OperationResult(this.dataConnect, this.data, this.ref); + Data data; + OperationRef ref; + FirebaseDataConnect dataConnect; +} + +/// Result of a query request. Created to hold extra variables in the future. +class QueryResult extends OperationResult { + QueryResult(super.dataConnect, super.data, super.ref); +} + +final result = await MoviesConnector.instance.listMovies(); +ListMoviesData data = result.data; +final ref = result.ref; +``` + +#### Getting the Ref +Each builder returns an `execute` function, which is a helper function that creates a `Ref` object, and executes the underlying operation. +An example of how to use the `Ref` object is shown below: +```dart +final ref = MoviesConnector.instance.listMovies().ref(); +ref.execute(); + +ref.subscribe(...); +``` + + +### GetMovie +#### Required Arguments +```dart +GetMovieVariablesKey key = ...; +MoviesConnector.instance.getMovie( + key: key, +).execute(); +``` + + + +#### Return Type +`execute()` returns a `QueryResult` +```dart +/// Result of an Operation Request (query/mutation). +class OperationResult { + OperationResult(this.dataConnect, this.data, this.ref); + Data data; + OperationRef ref; + FirebaseDataConnect dataConnect; +} + +/// Result of a query request. Created to hold extra variables in the future. +class QueryResult extends OperationResult { + QueryResult(super.dataConnect, super.data, super.ref); +} + +final result = await MoviesConnector.instance.getMovie( + key: key, +); +GetMovieData data = result.data; +final ref = result.ref; +``` + +#### Getting the Ref +Each builder returns an `execute` function, which is a helper function that creates a `Ref` object, and executes the underlying operation. +An example of how to use the `Ref` object is shown below: +```dart +GetMovieVariablesKey key = ...; + +final ref = MoviesConnector.instance.getMovie( + key: key, +).ref(); +ref.execute(); + +ref.subscribe(...); +``` + + +### ListMoviesByPartialTitle +#### Required Arguments +```dart +String input = ...; +MoviesConnector.instance.listMoviesByPartialTitle( + input: input, +).execute(); +``` + + + +#### Return Type +`execute()` returns a `QueryResult` +```dart +/// Result of an Operation Request (query/mutation). +class OperationResult { + OperationResult(this.dataConnect, this.data, this.ref); + Data data; + OperationRef ref; + FirebaseDataConnect dataConnect; +} + +/// Result of a query request. Created to hold extra variables in the future. +class QueryResult extends OperationResult { + QueryResult(super.dataConnect, super.data, super.ref); +} + +final result = await MoviesConnector.instance.listMoviesByPartialTitle( + input: input, +); +ListMoviesByPartialTitleData data = result.data; +final ref = result.ref; +``` + +#### Getting the Ref +Each builder returns an `execute` function, which is a helper function that creates a `Ref` object, and executes the underlying operation. +An example of how to use the `Ref` object is shown below: +```dart +String input = ...; + +final ref = MoviesConnector.instance.listMoviesByPartialTitle( + input: input, +).ref(); +ref.execute(); + +ref.subscribe(...); +``` + + +### ListPersons +#### Required Arguments +```dart +// No required arguments +MoviesConnector.instance.listPersons().execute(); +``` + + + +#### Return Type +`execute()` returns a `QueryResult` +```dart +/// Result of an Operation Request (query/mutation). +class OperationResult { + OperationResult(this.dataConnect, this.data, this.ref); + Data data; + OperationRef ref; + FirebaseDataConnect dataConnect; +} + +/// Result of a query request. Created to hold extra variables in the future. +class QueryResult extends OperationResult { + QueryResult(super.dataConnect, super.data, super.ref); +} + +final result = await MoviesConnector.instance.listPersons(); +ListPersonsData data = result.data; +final ref = result.ref; +``` + +#### Getting the Ref +Each builder returns an `execute` function, which is a helper function that creates a `Ref` object, and executes the underlying operation. +An example of how to use the `Ref` object is shown below: +```dart +final ref = MoviesConnector.instance.listPersons().ref(); +ref.execute(); + +ref.subscribe(...); +``` + + +### ListThing +#### Required Arguments +```dart +// No required arguments +MoviesConnector.instance.listThing().execute(); +``` + +#### Optional Arguments +We return a builder for each query. For ListThing, we created `ListThingBuilder`. For queries and mutations with optional parameters, we return a builder class. +The builder pattern allows Data Connect to distinguish between fields that haven't been set and fields that have been set to null. A field can be set by calling its respective setter method like below: +```dart +class ListThingVariablesBuilder { + ... + + ListThingVariablesBuilder data(AnyValue? t) { + _data.value = t; + return this; + } + + ... +} +MoviesConnector.instance.listThing() +.data(data) +.execute(); +``` + +#### Return Type +`execute()` returns a `QueryResult` +```dart +/// Result of an Operation Request (query/mutation). +class OperationResult { + OperationResult(this.dataConnect, this.data, this.ref); + Data data; + OperationRef ref; + FirebaseDataConnect dataConnect; +} + +/// Result of a query request. Created to hold extra variables in the future. +class QueryResult extends OperationResult { + QueryResult(super.dataConnect, super.data, super.ref); +} + +final result = await MoviesConnector.instance.listThing(); +ListThingData data = result.data; +final ref = result.ref; +``` + +#### Getting the Ref +Each builder returns an `execute` function, which is a helper function that creates a `Ref` object, and executes the underlying operation. +An example of how to use the `Ref` object is shown below: +```dart +final ref = MoviesConnector.instance.listThing().ref(); +ref.execute(); + +ref.subscribe(...); +``` + + +### ListTimestamps +#### Required Arguments +```dart +// No required arguments +MoviesConnector.instance.listTimestamps().execute(); +``` + + + +#### Return Type +`execute()` returns a `QueryResult` +```dart +/// Result of an Operation Request (query/mutation). +class OperationResult { + OperationResult(this.dataConnect, this.data, this.ref); + Data data; + OperationRef ref; + FirebaseDataConnect dataConnect; +} + +/// Result of a query request. Created to hold extra variables in the future. +class QueryResult extends OperationResult { + QueryResult(super.dataConnect, super.data, super.ref); +} + +final result = await MoviesConnector.instance.listTimestamps(); +ListTimestampsData data = result.data; +final ref = result.ref; +``` + +#### Getting the Ref +Each builder returns an `execute` function, which is a helper function that creates a `Ref` object, and executes the underlying operation. +An example of how to use the `Ref` object is shown below: +```dart +final ref = MoviesConnector.instance.listTimestamps().ref(); +ref.execute(); + +ref.subscribe(...); +``` + +## Mutations + +### addPerson +#### Required Arguments +```dart +// No required arguments +MoviesConnector.instance.addPerson().execute(); +``` + +#### Optional Arguments +We return a builder for each query. For addPerson, we created `addPersonBuilder`. For queries and mutations with optional parameters, we return a builder class. +The builder pattern allows Data Connect to distinguish between fields that haven't been set and fields that have been set to null. A field can be set by calling its respective setter method like below: +```dart +class AddPersonVariablesBuilder { + ... + + AddPersonVariablesBuilder name(String? t) { + _name.value = t; + return this; + } + + ... +} +MoviesConnector.instance.addPerson() +.name(name) +.execute(); +``` + +#### Return Type +`execute()` returns a `OperationResult` +```dart +/// Result of an Operation Request (query/mutation). +class OperationResult { + OperationResult(this.dataConnect, this.data, this.ref); + Data data; + OperationRef ref; + FirebaseDataConnect dataConnect; +} + +final result = await MoviesConnector.instance.addPerson(); +addPersonData data = result.data; +final ref = result.ref; +``` + +#### Getting the Ref +Each builder returns an `execute` function, which is a helper function that creates a `Ref` object, and executes the underlying operation. +An example of how to use the `Ref` object is shown below: +```dart +final ref = MoviesConnector.instance.addPerson().ref(); +ref.execute(); +``` + + +### addDirectorToMovie +#### Required Arguments +```dart +// No required arguments +MoviesConnector.instance.addDirectorToMovie().execute(); +``` + +#### Optional Arguments +We return a builder for each query. For addDirectorToMovie, we created `addDirectorToMovieBuilder`. For queries and mutations with optional parameters, we return a builder class. +The builder pattern allows Data Connect to distinguish between fields that haven't been set and fields that have been set to null. A field can be set by calling its respective setter method like below: +```dart +class AddDirectorToMovieVariablesBuilder { + ... + + AddDirectorToMovieVariablesBuilder personId(AddDirectorToMovieVariablesPersonId? t) { + _personId.value = t; + return this; + } + AddDirectorToMovieVariablesBuilder movieId(String? t) { + _movieId.value = t; + return this; + } + + ... +} +MoviesConnector.instance.addDirectorToMovie() +.personId(personId) +.movieId(movieId) +.execute(); +``` + +#### Return Type +`execute()` returns a `OperationResult` +```dart +/// Result of an Operation Request (query/mutation). +class OperationResult { + OperationResult(this.dataConnect, this.data, this.ref); + Data data; + OperationRef ref; + FirebaseDataConnect dataConnect; +} + +final result = await MoviesConnector.instance.addDirectorToMovie(); +addDirectorToMovieData data = result.data; +final ref = result.ref; +``` + +#### Getting the Ref +Each builder returns an `execute` function, which is a helper function that creates a `Ref` object, and executes the underlying operation. +An example of how to use the `Ref` object is shown below: +```dart +final ref = MoviesConnector.instance.addDirectorToMovie().ref(); +ref.execute(); +``` + + +### addTimestamp +#### Required Arguments +```dart +Timestamp timestamp = ...; +MoviesConnector.instance.addTimestamp( + timestamp: timestamp, +).execute(); +``` + + + +#### Return Type +`execute()` returns a `OperationResult` +```dart +/// Result of an Operation Request (query/mutation). +class OperationResult { + OperationResult(this.dataConnect, this.data, this.ref); + Data data; + OperationRef ref; + FirebaseDataConnect dataConnect; +} + +final result = await MoviesConnector.instance.addTimestamp( + timestamp: timestamp, +); +addTimestampData data = result.data; +final ref = result.ref; +``` + +#### Getting the Ref +Each builder returns an `execute` function, which is a helper function that creates a `Ref` object, and executes the underlying operation. +An example of how to use the `Ref` object is shown below: +```dart +Timestamp timestamp = ...; + +final ref = MoviesConnector.instance.addTimestamp( + timestamp: timestamp, +).ref(); +ref.execute(); +``` + + +### addDateAndTimestamp +#### Required Arguments +```dart +DateTime date = ...; +Timestamp timestamp = ...; +MoviesConnector.instance.addDateAndTimestamp( + date: date, + timestamp: timestamp, +).execute(); +``` + + + +#### Return Type +`execute()` returns a `OperationResult` +```dart +/// Result of an Operation Request (query/mutation). +class OperationResult { + OperationResult(this.dataConnect, this.data, this.ref); + Data data; + OperationRef ref; + FirebaseDataConnect dataConnect; +} + +final result = await MoviesConnector.instance.addDateAndTimestamp( + date: date, + timestamp: timestamp, +); +addDateAndTimestampData data = result.data; +final ref = result.ref; +``` + +#### Getting the Ref +Each builder returns an `execute` function, which is a helper function that creates a `Ref` object, and executes the underlying operation. +An example of how to use the `Ref` object is shown below: +```dart +DateTime date = ...; +Timestamp timestamp = ...; + +final ref = MoviesConnector.instance.addDateAndTimestamp( + date: date, + timestamp: timestamp, +).ref(); +ref.execute(); +``` + + +### seedMovies +#### Required Arguments +```dart +// No required arguments +MoviesConnector.instance.seedMovies().execute(); +``` + + + +#### Return Type +`execute()` returns a `OperationResult` +```dart +/// Result of an Operation Request (query/mutation). +class OperationResult { + OperationResult(this.dataConnect, this.data, this.ref); + Data data; + OperationRef ref; + FirebaseDataConnect dataConnect; +} + +final result = await MoviesConnector.instance.seedMovies(); +seedMoviesData data = result.data; +final ref = result.ref; +``` + +#### Getting the Ref +Each builder returns an `execute` function, which is a helper function that creates a `Ref` object, and executes the underlying operation. +An example of how to use the `Ref` object is shown below: +```dart +final ref = MoviesConnector.instance.seedMovies().ref(); +ref.execute(); +``` + + +### createMovie +#### Required Arguments +```dart +String title = ...; +int releaseYear = ...; +String genre = ...; +MoviesConnector.instance.createMovie( + title: title, + releaseYear: releaseYear, + genre: genre, +).execute(); +``` + +#### Optional Arguments +We return a builder for each query. For createMovie, we created `createMovieBuilder`. For queries and mutations with optional parameters, we return a builder class. +The builder pattern allows Data Connect to distinguish between fields that haven't been set and fields that have been set to null. A field can be set by calling its respective setter method like below: +```dart +class CreateMovieVariablesBuilder { + ... + CreateMovieVariablesBuilder rating(double? t) { + _rating.value = t; + return this; + } + CreateMovieVariablesBuilder description(String? t) { + _description.value = t; + return this; + } + + ... +} +MoviesConnector.instance.createMovie( + title: title, + releaseYear: releaseYear, + genre: genre, +) +.rating(rating) +.description(description) +.execute(); +``` + +#### Return Type +`execute()` returns a `OperationResult` +```dart +/// Result of an Operation Request (query/mutation). +class OperationResult { + OperationResult(this.dataConnect, this.data, this.ref); + Data data; + OperationRef ref; + FirebaseDataConnect dataConnect; +} + +final result = await MoviesConnector.instance.createMovie( + title: title, + releaseYear: releaseYear, + genre: genre, +); +createMovieData data = result.data; +final ref = result.ref; +``` + +#### Getting the Ref +Each builder returns an `execute` function, which is a helper function that creates a `Ref` object, and executes the underlying operation. +An example of how to use the `Ref` object is shown below: +```dart +String title = ...; +int releaseYear = ...; +String genre = ...; + +final ref = MoviesConnector.instance.createMovie( + title: title, + releaseYear: releaseYear, + genre: genre, +).ref(); +ref.execute(); +``` + + +### deleteMovie +#### Required Arguments +```dart +String id = ...; +MoviesConnector.instance.deleteMovie( + id: id, +).execute(); +``` + + + +#### Return Type +`execute()` returns a `OperationResult` +```dart +/// Result of an Operation Request (query/mutation). +class OperationResult { + OperationResult(this.dataConnect, this.data, this.ref); + Data data; + OperationRef ref; + FirebaseDataConnect dataConnect; +} + +final result = await MoviesConnector.instance.deleteMovie( + id: id, +); +deleteMovieData data = result.data; +final ref = result.ref; +``` + +#### Getting the Ref +Each builder returns an `execute` function, which is a helper function that creates a `Ref` object, and executes the underlying operation. +An example of how to use the `Ref` object is shown below: +```dart +String id = ...; + +final ref = MoviesConnector.instance.deleteMovie( + id: id, +).ref(); +ref.execute(); +``` + + +### thing +#### Required Arguments +```dart +// No required arguments +MoviesConnector.instance.thing().execute(); +``` + +#### Optional Arguments +We return a builder for each query. For thing, we created `thingBuilder`. For queries and mutations with optional parameters, we return a builder class. +The builder pattern allows Data Connect to distinguish between fields that haven't been set and fields that have been set to null. A field can be set by calling its respective setter method like below: +```dart +class ThingVariablesBuilder { + ... + + ThingVariablesBuilder title(AnyValue t) { + _title.value = t; + return this; + } + + ... +} +MoviesConnector.instance.thing() +.title(title) +.execute(); +``` + +#### Return Type +`execute()` returns a `OperationResult` +```dart +/// Result of an Operation Request (query/mutation). +class OperationResult { + OperationResult(this.dataConnect, this.data, this.ref); + Data data; + OperationRef ref; + FirebaseDataConnect dataConnect; +} + +final result = await MoviesConnector.instance.thing(); +thingData data = result.data; +final ref = result.ref; +``` + +#### Getting the Ref +Each builder returns an `execute` function, which is a helper function that creates a `Ref` object, and executes the underlying operation. +An example of how to use the `Ref` object is shown below: +```dart +final ref = MoviesConnector.instance.thing().ref(); +ref.execute(); +``` + + +### seedData +#### Required Arguments +```dart +// No required arguments +MoviesConnector.instance.seedData().execute(); +``` + + + +#### Return Type +`execute()` returns a `OperationResult` +```dart +/// Result of an Operation Request (query/mutation). +class OperationResult { + OperationResult(this.dataConnect, this.data, this.ref); + Data data; + OperationRef ref; + FirebaseDataConnect dataConnect; +} + +final result = await MoviesConnector.instance.seedData(); +seedDataData data = result.data; +final ref = result.ref; +``` + +#### Getting the Ref +Each builder returns an `execute` function, which is a helper function that creates a `Ref` object, and executes the underlying operation. +An example of how to use the `Ref` object is shown below: +```dart +final ref = MoviesConnector.instance.seedData().ref(); +ref.execute(); +``` + diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/get_movie.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/get_movie.dart new file mode 100644 index 000000000000..5e09901aef92 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/get_movie.dart @@ -0,0 +1,100 @@ +part of 'movies.dart'; + +class GetMovieVariablesBuilder { + GetMovieVariablesKey key; + + final FirebaseDataConnect _dataConnect; + GetMovieVariablesBuilder( + this._dataConnect, { + required this.key, + }); + Deserializer dataDeserializer = + (dynamic json) => GetMovieData.fromJson(jsonDecode(json)); + Serializer varsSerializer = + (GetMovieVariables vars) => jsonEncode(vars.toJson()); + Future> execute() { + return ref().execute(); + } + + QueryRef ref() { + GetMovieVariables vars = GetMovieVariables( + key: key, + ); + return _dataConnect.query( + "GetMovie", dataDeserializer, varsSerializer, vars); + } +} + +class GetMovieMovie { + String id; + String title; + GetMovieMovie.fromJson(dynamic json) + : id = nativeFromJson(json['id']), + title = nativeFromJson(json['title']); + + Map toJson() { + Map json = {}; + json['id'] = nativeToJson(id); + json['title'] = nativeToJson(title); + return json; + } + + GetMovieMovie({ + required this.id, + required this.title, + }); +} + +class GetMovieData { + GetMovieMovie? movie; + GetMovieData.fromJson(dynamic json) + : movie = json['movie'] == null + ? null + : GetMovieMovie.fromJson(json['movie']); + + Map toJson() { + Map json = {}; + if (movie != null) { + json['movie'] = movie!.toJson(); + } + return json; + } + + GetMovieData({ + this.movie, + }); +} + +class GetMovieVariablesKey { + String id; + GetMovieVariablesKey.fromJson(dynamic json) + : id = nativeFromJson(json['id']); + + Map toJson() { + Map json = {}; + json['id'] = nativeToJson(id); + return json; + } + + GetMovieVariablesKey({ + required this.id, + }); +} + +class GetMovieVariables { + GetMovieVariablesKey key; + @Deprecated( + 'fromJson is deprecated for Variable classes as they are no longer required for deserialization.') + GetMovieVariables.fromJson(Map json) + : key = GetMovieVariablesKey.fromJson(json['key']); + + Map toJson() { + Map json = {}; + json['key'] = key.toJson(); + return json; + } + + GetMovieVariables({ + required this.key, + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart index 1bd4739e61b1..ef27424855d7 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart @@ -3,16 +3,6 @@ library movies; import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'dart:convert'; -part 'list_movies.dart'; - -part 'list_movies_by_partial_title.dart'; - -part 'list_persons.dart'; - -part 'list_thing.dart'; - -part 'list_timestamps.dart'; - part 'add_person.dart'; part 'add_director_to_movie.dart'; @@ -31,40 +21,19 @@ part 'thing.dart'; part 'seed_data.dart'; -class MoviesConnector { - ListMoviesVariablesBuilder listMovies() { - return ListMoviesVariablesBuilder( - dataConnect, - ); - } +part 'list_movies.dart'; - ListMoviesByPartialTitleVariablesBuilder listMoviesByPartialTitle({ - required String input, - }) { - return ListMoviesByPartialTitleVariablesBuilder( - dataConnect, - input: input, - ); - } +part 'get_movie.dart'; - ListPersonsVariablesBuilder listPersons() { - return ListPersonsVariablesBuilder( - dataConnect, - ); - } +part 'list_movies_by_partial_title.dart'; - ListThingVariablesBuilder listThing() { - return ListThingVariablesBuilder( - dataConnect, - ); - } +part 'list_persons.dart'; - ListTimestampsVariablesBuilder listTimestamps() { - return ListTimestampsVariablesBuilder( - dataConnect, - ); - } +part 'list_thing.dart'; + +part 'list_timestamps.dart'; +class MoviesConnector { AddPersonVariablesBuilder addPerson() { return AddPersonVariablesBuilder( dataConnect, @@ -137,6 +106,48 @@ class MoviesConnector { ); } + ListMoviesVariablesBuilder listMovies() { + return ListMoviesVariablesBuilder( + dataConnect, + ); + } + + GetMovieVariablesBuilder getMovie({ + required GetMovieVariablesKey key, + }) { + return GetMovieVariablesBuilder( + dataConnect, + key: key, + ); + } + + ListMoviesByPartialTitleVariablesBuilder listMoviesByPartialTitle({ + required String input, + }) { + return ListMoviesByPartialTitleVariablesBuilder( + dataConnect, + input: input, + ); + } + + ListPersonsVariablesBuilder listPersons() { + return ListPersonsVariablesBuilder( + dataConnect, + ); + } + + ListThingVariablesBuilder listThing() { + return ListThingVariablesBuilder( + dataConnect, + ); + } + + ListTimestampsVariablesBuilder listTimestamps() { + return ListTimestampsVariablesBuilder( + dataConnect, + ); + } + static ConnectorConfig connectorConfig = ConnectorConfig( 'us-west2', 'movies', diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart index 4bf47a5bc25e..bf39d32283b3 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart @@ -115,9 +115,9 @@ class RestTransport implements DataConnectTransport { body: json.encode(body), headers: headers, ); + Map bodyJson = + jsonDecode(r.body) as Map; if (r.statusCode != 200) { - Map bodyJson = - jsonDecode(r.body) as Map; String message = bodyJson.containsKey('message') ? bodyJson['message']! : r.body; throw DataConnectError( @@ -127,44 +127,58 @@ class RestTransport implements DataConnectTransport { "Received a status code of ${r.statusCode} with a message '$message'", ); } - Map bodyJson = - jsonDecode(r.body) as Map; - if (bodyJson.containsKey('errors') && - (bodyJson['errors'] as List).isNotEmpty) { - Map? data = bodyJson['data']; - Data? decodedData; - if (data != null) { - try { - decodedData = deserializer(jsonEncode(bodyJson['data'])); - } catch (e) { - // nothing required - } - } - List errors = - jsonDecode(jsonEncode(bodyJson['errors'])) as List; - List suberrors = errors - .map((e) { - return jsonDecode(jsonEncode(e)) as Map; - }) - .map((e) => DataConnectOperationFailureResponseErrorInfo( - (e['path'] as List) - .map((val) => val.runtimeType == String - ? DataConnectFieldPathSegment(val) - : DataConnectListIndexPathSegment(val)) - .toList(), - e['message'])) - .toList(); + List errors = bodyJson['errors'] ?? []; + final data = bodyJson['data']; + List suberrors = errors + .map((e) => switch (e) { + {'path': List? path, 'message': String? message} => + DataConnectOperationFailureResponseErrorInfo( + (path ?? []) + .map((val) => switch (val) { + String() => DataConnectFieldPathSegment(val), + int() => DataConnectListIndexPathSegment(val), + _ => throw DataConnectError( + DataConnectErrorCode.other, + 'Incorrect type for $val') + }) + .toList(), + message ?? + (throw DataConnectError( + DataConnectErrorCode.other, 'Missing message'))), + _ => throw DataConnectError( + DataConnectErrorCode.other, 'Unable to parse JSON: $e') + }) + .toList(); + Data? decodedData; + Object? decodeError; + try { + /// The response we get is in the data field of the response + /// Once we get the data back, it's not quite json-encoded, + /// so we have to encode it and then send it to the user's deserializer. + decodedData = deserializer(jsonEncode(bodyJson['data'])); + } catch (e) { + decodeError = e; + } + if (suberrors.isNotEmpty) { final response = DataConnectOperationFailureResponse(suberrors, data, decodedData); + throw DataConnectOperationError(DataConnectErrorCode.other, 'Failed to invoke operation: ', response); + } else { + if (decodeError != null) { + throw DataConnectError(DataConnectErrorCode.other, + 'Unable to decode data: $decodeError'); + } + if (decodedData is! Data) { + throw DataConnectError( + DataConnectErrorCode.other, + "Decoded data wasn't parsed properly. Expected $Data, got $decodedData", + ); + } + return decodedData; } - - /// The response we get is in the data field of the response - /// Once we get the data back, it's not quite json-encoded, - /// so we have to encode it and then send it to the user's deserializer. - return deserializer(jsonEncode(bodyJson['data'])); } on Exception catch (e) { if (e is DataConnectError) { rethrow; diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.dart index 3492e58ef1a5..0635e066c04c 100644 --- a/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.dart +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.dart @@ -429,6 +429,105 @@ void main() { (e.response.data as AbcHolder).abc == 'def')), ); }); + test( + 'invokeOperation should decode a partial error if no path is specified', + () async { + final mockResponse = http.Response( + ''' + { + "data": {"abc": "def"}, + "errors": [ + { + "message": "invalid pkey", + "locations": [], + "path": null, + "extensions": null + } + ] + }''', + 200, + ); + when( + mockHttpClient.post( + any, + headers: anyNamed('headers'), + body: anyNamed('body'), + ), + ).thenAnswer((_) async => mockResponse); + + final deserializer = (String data) { + Map decoded = jsonDecode(data) as Map; + return AbcHolder(decoded['abc']!); + }; + + expect( + () => transport.invokeOperation( + 'testQuery', + 'executeQuery', + deserializer, + null, + null, + null, + ), + throwsA(predicate((e) => + e is DataConnectOperationError && + e.response.rawData!['abc'] == 'def' && + e.response.errors.first.message == 'invalid pkey' && + e.response.errors.first.path.isEmpty && + e.response.data is AbcHolder && + (e.response.data as AbcHolder).abc == 'def')), + ); + }); + test( + 'invokeOperation should decode a partial error if list path is specified', + () async { + final mockResponse = http.Response( + ''' + { + "data": {"abc": "def"}, + "errors": [ + { + "message": "invalid pkey", + "locations": [], + "path": [1,2,3], + "extensions": null + } + ] + }''', + 200, + ); + when( + mockHttpClient.post( + any, + headers: anyNamed('headers'), + body: anyNamed('body'), + ), + ).thenAnswer((_) async => mockResponse); + + final deserializer = (String data) { + Map decoded = jsonDecode(data) as Map; + return AbcHolder(decoded['abc']!); + }; + + expect( + () => transport.invokeOperation( + 'testQuery', + 'executeQuery', + deserializer, + null, + null, + null, + ), throwsA(predicate((e) { + return e is DataConnectOperationError && + e.response.rawData!['abc'] == 'def' && + e.response.errors.first.message == 'invalid pkey' && + e.response.errors.first.path.length == 3 && + e.response.errors.first.path.first + is DataConnectListIndexPathSegment && + e.response.data is AbcHolder && + (e.response.data as AbcHolder).abc == 'def'; + }))); + }); }); }