From 03113a3c3580a4c424b31c7646cf75a021ac9b73 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Thu, 26 May 2022 14:49:49 -0700 Subject: [PATCH 01/26] chore!(api): migrate API category type definitions --- .../src/category/amplify_api_category.dart | 134 +++++---- .../lib/src/category/amplify_categories.dart | 1 + .../plugin/amplify_api_plugin_interface.dart | 74 +++-- .../lib/src/types/api/api_types.dart | 7 +- .../legacy_api_operation.dart} | 15 +- .../types/api/exceptions/api_exception.dart | 9 - .../lib/src/types/api/rest/http_payload.dart | 53 ++++ .../src/types/api/rest/rest_exception.dart | 34 --- .../src/types/api/rest/rest_operation.dart | 23 -- .../lib/src/types/api/rest/rest_response.dart | 67 ----- packages/amplify_core/pubspec.yaml | 1 + packages/api/amplify_api/.gitignore | 40 ++- .../example/lib/graphql_api_view.dart | 3 +- .../example/lib/rest_api_view.dart | 70 +++-- packages/api/amplify_api/example/pubspec.yaml | 1 + .../lib/src/method_channel_api.dart | 199 ++++++++---- packages/api/amplify_api/pubspec.yaml | 3 +- .../test/amplify_rest_api_methods_test.dart | 282 ++++++++---------- 18 files changed, 551 insertions(+), 465 deletions(-) rename packages/amplify_core/lib/src/types/api/{graphql/graphql_operation.dart => async/legacy_api_operation.dart} (54%) create mode 100644 packages/amplify_core/lib/src/types/api/rest/http_payload.dart delete mode 100644 packages/amplify_core/lib/src/types/api/rest/rest_exception.dart delete mode 100644 packages/amplify_core/lib/src/types/api/rest/rest_operation.dart delete mode 100644 packages/amplify_core/lib/src/types/api/rest/rest_response.dart diff --git a/packages/amplify_core/lib/src/category/amplify_api_category.dart b/packages/amplify_core/lib/src/category/amplify_api_category.dart index 99406d31fc..ca6c666638 100644 --- a/packages/amplify_core/lib/src/category/amplify_api_category.dart +++ b/packages/amplify_core/lib/src/category/amplify_api_category.dart @@ -1,5 +1,5 @@ /* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. @@ -21,17 +21,13 @@ class APICategory extends AmplifyCategory { Category get category => Category.api; // ====== GraphQL ======= - GraphQLOperation query({required GraphQLRequest request}) { - return plugins.length == 1 - ? plugins[0].query(request: request) - : throw _pluginNotAddedException('Api'); - } + CancelableOperation> query( + {required GraphQLRequest request}) => + defaultPlugin.query(request: request); - GraphQLOperation mutate({required GraphQLRequest request}) { - return plugins.length == 1 - ? plugins[0].mutate(request: request) - : throw _pluginNotAddedException('Api'); - } + CancelableOperation> mutate( + {required GraphQLRequest request}) => + defaultPlugin.mutate(request: request); /// Subscribes to the given [request] and returns the stream of response events. /// An optional [onEstablished] callback can be used to be alerted when the @@ -42,52 +38,88 @@ class APICategory extends AmplifyCategory { Stream> subscribe( GraphQLRequest request, { void Function()? onEstablished, - }) { - return plugins.length == 1 - ? plugins[0].subscribe(request, onEstablished: onEstablished) - : throw _pluginNotAddedException('Api'); - } + }) => + defaultPlugin.subscribe(request, onEstablished: onEstablished); // ====== RestAPI ====== - void cancelRequest(String cancelToken) { - return plugins.length == 1 - ? plugins[0].cancelRequest(cancelToken) - : throw _pluginNotAddedException('Api'); - } - RestOperation get({required RestOptions restOptions}) { - return plugins.length == 1 - ? plugins[0].get(restOptions: restOptions) - : throw _pluginNotAddedException('Api'); - } + CancelableOperation delete( + String path, { + Map? headers, + HttpPayload? body, + Map? queryParameters, + String? apiName, + }) => + defaultPlugin.delete( + path, + headers: headers, + body: body, + apiName: apiName, + ); - RestOperation put({required RestOptions restOptions}) { - return plugins.length == 1 - ? plugins[0].put(restOptions: restOptions) - : throw _pluginNotAddedException('Api'); - } + CancelableOperation get( + String path, { + Map? headers, + Map? queryParameters, + String? apiName, + }) => + defaultPlugin.get( + path, + headers: headers, + apiName: apiName, + ); - RestOperation post({required RestOptions restOptions}) { - return plugins.length == 1 - ? plugins[0].post(restOptions: restOptions) - : throw _pluginNotAddedException('Api'); - } + CancelableOperation head( + String path, { + Map? headers, + Map? queryParameters, + String? apiName, + }) => + defaultPlugin.head( + path, + headers: headers, + apiName: apiName, + ); - RestOperation delete({required RestOptions restOptions}) { - return plugins.length == 1 - ? plugins[0].delete(restOptions: restOptions) - : throw _pluginNotAddedException('Api'); - } + CancelableOperation patch( + String path, { + Map? headers, + HttpPayload? body, + Map? queryParameters, + String? apiName, + }) => + defaultPlugin.patch( + path, + headers: headers, + body: body, + apiName: apiName, + ); - RestOperation head({required RestOptions restOptions}) { - return plugins.length == 1 - ? plugins[0].head(restOptions: restOptions) - : throw _pluginNotAddedException('Api'); - } + CancelableOperation post( + String path, { + Map? headers, + HttpPayload? body, + Map? queryParameters, + String? apiName, + }) => + defaultPlugin.post( + path, + headers: headers, + body: body, + apiName: apiName, + ); - RestOperation patch({required RestOptions restOptions}) { - return plugins.length == 1 - ? plugins[0].patch(restOptions: restOptions) - : throw _pluginNotAddedException('Api'); - } + CancelableOperation put( + String path, { + Map? headers, + HttpPayload? body, + Map? queryParameters, + String? apiName, + }) => + defaultPlugin.put( + path, + headers: headers, + body: body, + apiName: apiName, + ); } diff --git a/packages/amplify_core/lib/src/category/amplify_categories.dart b/packages/amplify_core/lib/src/category/amplify_categories.dart index 8b53362a39..46b5ea30e3 100644 --- a/packages/amplify_core/lib/src/category/amplify_categories.dart +++ b/packages/amplify_core/lib/src/category/amplify_categories.dart @@ -19,6 +19,7 @@ import 'dart:async'; import 'dart:io'; import 'package:amplify_core/amplify_core.dart'; +import 'package:async/async.dart'; import 'package:collection/collection.dart'; import 'package:flutter/services.dart'; import 'package:meta/meta.dart'; diff --git a/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart b/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart index d318db6e13..ba63eb5814 100644 --- a/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart +++ b/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart @@ -1,5 +1,5 @@ /* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ */ import 'package:amplify_core/amplify_core.dart'; +import 'package:async/async.dart'; import 'package:meta/meta.dart'; abstract class APIPluginInterface extends AmplifyPluginInterface { @@ -25,11 +26,13 @@ abstract class APIPluginInterface extends AmplifyPluginInterface { ModelProviderInterface? get modelProvider => throw UnimplementedError(); // ====== GraphQL ======= - GraphQLOperation query({required GraphQLRequest request}) { + CancelableOperation> query( + {required GraphQLRequest request}) { throw UnimplementedError('query() has not been implemented.'); } - GraphQLOperation mutate({required GraphQLRequest request}) { + CancelableOperation> mutate( + {required GraphQLRequest request}) { throw UnimplementedError('mutate() has not been implemented.'); } @@ -50,31 +53,64 @@ abstract class APIPluginInterface extends AmplifyPluginInterface { void registerAuthProvider(APIAuthProvider authProvider); // ====== RestAPI ====== - void cancelRequest(String cancelToken) { - throw UnimplementedError('cancelRequest has not been implemented.'); - } - - RestOperation get({required RestOptions restOptions}) { - throw UnimplementedError('get has not been implemented.'); + CancelableOperation delete( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + throw UnimplementedError('delete() has not been implemented'); } - RestOperation put({required RestOptions restOptions}) { - throw UnimplementedError('put has not been implemented.'); + /// Uses Amplify configuration to authorize request to [path] and returns + /// [CancelableOperation] which resolves to standard HTTP + /// [Response](https://pub.dev/documentation/http/latest/http/Response-class.html). + CancelableOperation get( + String path, { + Map? headers, + Map? queryParameters, + String? apiName, + }) { + throw UnimplementedError('get() has not been implemented'); } - RestOperation post({required RestOptions restOptions}) { - throw UnimplementedError('post has not been implemented.'); + CancelableOperation head( + String path, { + Map? headers, + Map? queryParameters, + String? apiName, + }) { + throw UnimplementedError('head() has not been implemented'); } - RestOperation delete({required RestOptions restOptions}) { - throw UnimplementedError('delete has not been implemented.'); + CancelableOperation patch( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + throw UnimplementedError('patch() has not been implemented'); } - RestOperation head({required RestOptions restOptions}) { - throw UnimplementedError('head has not been implemented.'); + CancelableOperation post( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + throw UnimplementedError('post() has not been implemented'); } - RestOperation patch({required RestOptions restOptions}) { - throw UnimplementedError('patch has not been implemented.'); + CancelableOperation put( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + throw UnimplementedError('put() has not been implemented'); } } diff --git a/packages/amplify_core/lib/src/types/api/api_types.dart b/packages/amplify_core/lib/src/types/api/api_types.dart index 3e69a1dc4b..d0e8f21a0e 100644 --- a/packages/amplify_core/lib/src/types/api/api_types.dart +++ b/packages/amplify_core/lib/src/types/api/api_types.dart @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +export 'async/legacy_api_operation.dart'; + // API Authorization export 'auth/api_auth_provider.dart'; export 'auth/api_authorization_type.dart'; @@ -20,17 +22,14 @@ export 'auth/api_authorization_type.dart'; export 'exceptions/api_exception.dart'; export 'graphql/graphql_helpers.dart'; -export 'graphql/graphql_operation.dart'; export 'graphql/graphql_request.dart'; export 'graphql/graphql_request_type.dart'; export 'graphql/graphql_response.dart'; export 'graphql/graphql_response_error.dart'; export 'graphql/graphql_subscription_operation.dart'; -export 'rest/rest_exception.dart'; -export 'rest/rest_operation.dart'; +export 'rest/http_payload.dart'; export 'rest/rest_options.dart'; -export 'rest/rest_response.dart'; export 'types/pagination/paginated_model_type.dart'; export 'types/pagination/paginated_result.dart'; diff --git a/packages/amplify_core/lib/src/types/api/graphql/graphql_operation.dart b/packages/amplify_core/lib/src/types/api/async/legacy_api_operation.dart similarity index 54% rename from packages/amplify_core/lib/src/types/api/graphql/graphql_operation.dart rename to packages/amplify_core/lib/src/types/api/async/legacy_api_operation.dart index 94035a8997..41c36eb711 100644 --- a/packages/amplify_core/lib/src/types/api/graphql/graphql_operation.dart +++ b/packages/amplify_core/lib/src/types/api/async/legacy_api_operation.dart @@ -1,5 +1,5 @@ /* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. @@ -13,11 +13,12 @@ * permissions and limitations under the License. */ -import 'package:amplify_core/amplify_core.dart'; +import 'package:async/async.dart'; -class GraphQLOperation { - final Function cancel; - final Future> response; - - const GraphQLOperation({required this.response, required this.cancel}); +/// Eventually this should be deprecated and just use [CancelableOperation]. +/// Until then, this is used to make `.response` available like it was for older +/// [GraphQLOperation] and [RestOperation] classes. +extension LegacyApiOperation on CancelableOperation { + @Deprecated('Use .value instead.') + Future get response => value; } diff --git a/packages/amplify_core/lib/src/types/api/exceptions/api_exception.dart b/packages/amplify_core/lib/src/types/api/exceptions/api_exception.dart index 9f9d833110..2ec1bf37ac 100644 --- a/packages/amplify_core/lib/src/types/api/exceptions/api_exception.dart +++ b/packages/amplify_core/lib/src/types/api/exceptions/api_exception.dart @@ -19,18 +19,11 @@ import 'package:amplify_core/amplify_core.dart'; /// Exception thrown from the API Category. /// {@endtemplate} class ApiException extends AmplifyException { - /// HTTP status of response, only available if error - @Deprecated( - 'Use RestException instead to retrieve the HTTP response. Existing uses of ' - 'ApiException for handling REST errors can be safely replaced with RestException') - final int? httpStatusCode; - /// {@macro api_exception} const ApiException( String message, { String? recoverySuggestion, String? underlyingException, - this.httpStatusCode, }) : super( message, recoverySuggestion: recoverySuggestion, @@ -40,7 +33,6 @@ class ApiException extends AmplifyException { /// Constructor for down casting an AmplifyException to this exception ApiException._private( AmplifyException exception, - this.httpStatusCode, ) : super( exception.message, recoverySuggestion: exception.recoverySuggestion, @@ -57,7 +49,6 @@ class ApiException extends AmplifyException { } return ApiException._private( AmplifyException.fromMap(serializedException), - statusCode, ); } } diff --git a/packages/amplify_core/lib/src/types/api/rest/http_payload.dart b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart new file mode 100644 index 0000000000..47b03ce9da --- /dev/null +++ b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart @@ -0,0 +1,53 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +import 'dart:async'; +import 'dart:convert'; + +import 'package:async/async.dart'; + +/// {@template amplify_common.http_payload} +/// An HTTP request's payload. +/// {@endtemplate} +class HttpPayload extends StreamView> { + /// {@macro amplify_common.http_payload} + factory HttpPayload([Object? body]) { + if (body == null) { + return const HttpPayload.empty(); + } + if (body is String) { + return HttpPayload.string(body); + } + if (body is List) { + return HttpPayload.bytes(body); + } + if (body is Stream>) { + return HttpPayload.streaming(body); + } + throw ArgumentError('Invalid HTTP payload type: ${body.runtimeType}'); + } + + /// An empty HTTP body. + const HttpPayload.empty() : super(const Stream.empty()); + + /// A UTF-8 encoded HTTP body. + HttpPayload.string(String body, {Encoding encoding = utf8}) + : super(LazyStream(() => Stream.value(encoding.encode(body)))); + + /// A byte buffer HTTP body. + HttpPayload.bytes(List body) : super(Stream.value(body)); + + /// A streaming HTTP body. + const HttpPayload.streaming(Stream> body) : super(body); +} diff --git a/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart b/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart deleted file mode 100644 index fe6a6a8ee5..0000000000 --- a/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -import 'package:amplify_core/amplify_core.dart'; - -/// {@template rest_exception} -/// An HTTP error encountered during a REST API call, i.e. for calls returning -/// non-2xx status codes. -/// {@endtemplate} -class RestException extends ApiException { - /// The HTTP response from the server. - final RestResponse response; - - /// {@macro rest_exception} - RestException(this.response) - : super(response.body, httpStatusCode: response.statusCode); - - @override - String toString() { - return 'RestException{response=$response}'; - } -} diff --git a/packages/amplify_core/lib/src/types/api/rest/rest_operation.dart b/packages/amplify_core/lib/src/types/api/rest/rest_operation.dart deleted file mode 100644 index eb84a0ea42..0000000000 --- a/packages/amplify_core/lib/src/types/api/rest/rest_operation.dart +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -import 'rest_response.dart'; - -class RestOperation { - final Function cancel; - final Future response; - - const RestOperation({required this.response, required this.cancel}); -} diff --git a/packages/amplify_core/lib/src/types/api/rest/rest_response.dart b/packages/amplify_core/lib/src/types/api/rest/rest_response.dart deleted file mode 100644 index 9596694dee..0000000000 --- a/packages/amplify_core/lib/src/types/api/rest/rest_response.dart +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -/// {@template rest_response} -/// An HTTP response from a REST API call. -/// {@endtemplate} -@immutable -class RestResponse { - /// The response status code. - final int statusCode; - - /// The response headers. - /// - /// Will be `null` if unavailable from the platform. - final Map? headers; - - /// The response body bytes. - final Uint8List data; - - /// The decoded body (using UTF-8). - /// - /// For custom processing, use [data] to get the raw body bytes. - late final String body; - - /// {@macro rest_response} - RestResponse({ - required Uint8List? data, - required this.headers, - required this.statusCode, - }) : data = data ?? Uint8List(0) { - body = utf8.decode(this.data, allowMalformed: true); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is RestResponse && - statusCode == other.statusCode && - headers == other.headers && - listEquals(data, other.data); - - @override - int get hashCode => hashValues(statusCode, headers, hashList(data)); - - @override - String toString() { - return 'RestResponse{statusCode=$statusCode, headers=$headers, body=$body}'; - } -} diff --git a/packages/amplify_core/pubspec.yaml b/packages/amplify_core/pubspec.yaml index 90a63f3893..0d53d414dd 100644 --- a/packages/amplify_core/pubspec.yaml +++ b/packages/amplify_core/pubspec.yaml @@ -8,6 +8,7 @@ environment: flutter: ">=3.0.0" dependencies: + async: ^2.8.2 aws_common: ^0.1.0 aws_signature_v4: ^0.1.0 collection: ^1.15.0 diff --git a/packages/api/amplify_api/.gitignore b/packages/api/amplify_api/.gitignore index e9dc58d3d6..6bb69a50e0 100644 --- a/packages/api/amplify_api/.gitignore +++ b/packages/api/amplify_api/.gitignore @@ -1,7 +1,43 @@ +# See https://dart.dev/guides/libraries/private-files + +# Miscellaneous +*.class +*.log +*.pyc +*.swp .DS_Store -.dart_tool/ +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies .packages +.pub-cache/ .pub/ - build/ + +# Code coverage +coverage/ + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/api/amplify_api/example/lib/graphql_api_view.dart b/packages/api/amplify_api/example/lib/graphql_api_view.dart index 53a218efcd..6644dad380 100644 --- a/packages/api/amplify_api/example/lib/graphql_api_view.dart +++ b/packages/api/amplify_api/example/lib/graphql_api_view.dart @@ -14,6 +14,7 @@ */ import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:async/async.dart'; import 'package:flutter/material.dart'; class GraphQLApiView extends StatefulWidget { @@ -29,7 +30,7 @@ class GraphQLApiView extends StatefulWidget { class _GraphQLApiViewState extends State { String _result = ''; void Function()? _unsubscribe; - late GraphQLOperation _lastOperation; + late CancelableOperation _lastOperation; Future subscribe() async { String graphQLDocument = '''subscription MySubscription { diff --git a/packages/api/amplify_api/example/lib/rest_api_view.dart b/packages/api/amplify_api/example/lib/rest_api_view.dart index aeca89c97f..2e55e01e73 100644 --- a/packages/api/amplify_api/example/lib/rest_api_view.dart +++ b/packages/api/amplify_api/example/lib/rest_api_view.dart @@ -13,9 +13,9 @@ * permissions and limitations under the License. */ -import 'dart:convert'; - +import 'package:amplify_api/amplify_api.dart'; import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:async/async.dart'; import 'package:flutter/material.dart'; class RestApiView extends StatefulWidget { @@ -27,7 +27,10 @@ class RestApiView extends StatefulWidget { class _RestApiViewState extends State { late TextEditingController _apiPathController; - late RestOperation _lastRestOperation; + late CancelableOperation _lastRestOperation; + + // TEMP until Amplify methods implemented in dart. + late AmplifyAPI api; @override void initState() { @@ -39,18 +42,16 @@ class _RestApiViewState extends State { void onPutPressed() async { try { - RestOperation restOperation = Amplify.API.put( - restOptions: RestOptions( - path: _apiPathController.text, - body: ascii.encode('{"name":"Mow the lawn"}'), - ), + final restOperation = Amplify.API.put( + _apiPathController.text, + body: HttpPayload.string('{"name":"Mow the lawn"}'), ); _lastRestOperation = restOperation; - RestResponse response = await restOperation.response; + final response = await restOperation.value; print('Put SUCCESS'); - print(response); + print(await response.decodeBody()); } on Exception catch (e) { print('Put FAILED'); print(e); @@ -59,18 +60,16 @@ class _RestApiViewState extends State { void onPostPressed() async { try { - RestOperation restOperation = Amplify.API.post( - restOptions: RestOptions( - path: _apiPathController.text, - body: ascii.encode('{"name":"Mow the lawn"}'), - ), + final restOperation = Amplify.API.post( + _apiPathController.text, + body: HttpPayload.string('{"name":"Mow the lawn"}'), ); _lastRestOperation = restOperation; - RestResponse response = await restOperation.response; + final response = await restOperation.value; print('Post SUCCESS'); - print(response); + print(await response.decodeBody()); } on Exception catch (e) { print('Post FAILED'); print(e); @@ -79,16 +78,16 @@ class _RestApiViewState extends State { void onGetPressed() async { try { - RestOperation restOperation = Amplify.API.get( - restOptions: RestOptions( - path: _apiPathController.text, - )); + final restOperation = Amplify.API.get( + _apiPathController.text, + ); _lastRestOperation = restOperation; - RestResponse response = await restOperation.response; + final response = await restOperation.value; print('Get SUCCESS'); - print(response); + print(response.statusCode); + print(await response.decodeBody()); } on ApiException catch (e) { print('Get FAILED'); print(e.toString()); @@ -97,15 +96,14 @@ class _RestApiViewState extends State { void onDeletePressed() async { try { - RestOperation restOperation = Amplify.API.delete( - restOptions: RestOptions(path: _apiPathController.text), + final restOperation = Amplify.API.delete( + _apiPathController.text, ); - _lastRestOperation = restOperation; - RestResponse response = await restOperation.response; + final response = await restOperation.value; print('Delete SUCCESS'); - print(response); + print(await response.decodeBody()); } on Exception catch (e) { print('Delete FAILED'); print(e); @@ -123,15 +121,14 @@ class _RestApiViewState extends State { void onHeadPressed() async { try { - RestOperation restOperation = Amplify.API.head( - restOptions: RestOptions(path: _apiPathController.text), + final restOperation = Amplify.API.head( + _apiPathController.text, ); _lastRestOperation = restOperation; - RestResponse response = await restOperation.response; + final response = await restOperation.response; print('Head SUCCESS'); - print(response); } on ApiException catch (e) { print('Head FAILED'); print(e.toString()); @@ -140,15 +137,16 @@ class _RestApiViewState extends State { void onPatchPressed() async { try { - RestOperation restOperation = Amplify.API.patch( - restOptions: RestOptions(path: _apiPathController.text), + final restOperation = Amplify.API.patch( + _apiPathController.text, + body: HttpPayload.string('{"name":"Mow the lawn"}'), ); _lastRestOperation = restOperation; - RestResponse response = await restOperation.response; + final response = await restOperation.response; print('Patch SUCCESS'); - print(response); + print(await response.decodeBody()); } on ApiException catch (e) { print('Patch FAILED'); print(e.toString()); diff --git a/packages/api/amplify_api/example/pubspec.yaml b/packages/api/amplify_api/example/pubspec.yaml index 4e57ade772..7b017f9370 100644 --- a/packages/api/amplify_api/example/pubspec.yaml +++ b/packages/api/amplify_api/example/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: path: ../../../auth/amplify_auth_cognito amplify_flutter: path: ../../../amplify/amplify_flutter + async: ^2.8.2 aws_common: ^0.1.0 # The following adds the Cupertino Icons font to your application. diff --git a/packages/api/amplify_api/lib/src/method_channel_api.dart b/packages/api/amplify_api/lib/src/method_channel_api.dart index 59deb7fca0..9d662abe00 100644 --- a/packages/api/amplify_api/lib/src/method_channel_api.dart +++ b/packages/api/amplify_api/lib/src/method_channel_api.dart @@ -20,7 +20,7 @@ import 'package:amplify_api/src/graphql/graphql_response_decoder.dart'; import 'package:amplify_api/src/graphql/graphql_subscription_event.dart'; import 'package:amplify_api/src/graphql/graphql_subscription_transformer.dart'; import 'package:amplify_core/amplify_core.dart'; - +import 'package:async/async.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -150,31 +150,19 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { } @override - GraphQLOperation query({required GraphQLRequest request}) { - Future> response = + CancelableOperation> query( + {required GraphQLRequest request}) { + Future> responseFuture = _getMethodChannelResponse(methodName: 'query', request: request); - - //TODO: Cancel implementation will be added along with REST API as it is shared - GraphQLOperation result = GraphQLOperation( - cancel: () => cancelRequest(request.id), - response: response, - ); - - return result; + return CancelableOperation.fromFuture(responseFuture); } @override - GraphQLOperation mutate({required GraphQLRequest request}) { - Future> response = + CancelableOperation> mutate( + {required GraphQLRequest request}) { + Future> responseFuture = _getMethodChannelResponse(methodName: 'mutate', request: request); - - //TODO: Cancel implementation will be added along with REST API as it is shared - GraphQLOperation result = GraphQLOperation( - cancel: () => cancelRequest(request.id), - response: response, - ); - - return result; + return CancelableOperation.fromFuture(responseFuture); } @override @@ -248,21 +236,51 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { } // ====== RestAPI ====== - RestOperation _restFunctionHelper( - {required String methodName, required RestOptions restOptions}) { - // Send Request cancelToken to Native - String cancelToken = UUID.getUUID(); - Future futureResponse = - _callNativeRestMethod(methodName, cancelToken, restOptions); + Future _restResponseHelper({ + required String methodName, + required String path, + required String cancelToken, + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) async { + final bodyBytes = + body != null ? Uint8List.fromList(await body.single) : null; + final restOptions = RestOptions( + path: path, + body: bodyBytes, + apiName: apiName, + queryParameters: queryParameters as Map?, + headers: headers); + return _callNativeRestMethod(methodName, cancelToken, restOptions); + } - return RestOperation( - response: futureResponse, - cancel: () => cancelRequest(cancelToken), - ); + CancelableOperation _restFunctionHelper({ + required String methodName, + required String path, + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + // Send Request cancelToken to Native + String cancelToken = UUID.getUUID(); + final responseFuture = _restResponseHelper( + methodName: methodName, + path: path, + cancelToken: cancelToken, + body: body, + headers: headers, + queryParameters: queryParameters, + apiName: apiName); + + return CancelableOperation.fromFuture(responseFuture, + onCancel: () => cancelRequest(cancelToken)); } - Future _callNativeRestMethod( + Future _callNativeRestMethod( String methodName, String cancelToken, RestOptions restOptions) async { // Prepare map input Map inputsMap = {}; @@ -284,55 +302,116 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { } } - bool _shouldThrow(int statusCode) { - return statusCode < 200 || statusCode > 299; - } - - RestResponse _formatRestResponse(Map res) { + AWSStreamedHttpResponse _formatRestResponse(Map res) { final statusCode = res['statusCode'] as int; final headers = res['headers'] as Map?; - final response = RestResponse( - data: res['data'] as Uint8List?, - headers: headers?.cast(), - statusCode: statusCode, - ); - if (_shouldThrow(statusCode)) { - throw RestException(response); - } - return response; + final rawResponseBody = res['data'] as Uint8List?; + + return AWSStreamedHttpResponse( + statusCode: statusCode, + body: Stream.value(rawResponseBody?.toList() ?? [])); } @override - RestOperation get({required RestOptions restOptions}) { - return _restFunctionHelper(methodName: 'get', restOptions: restOptions); + CancelableOperation get( + String path, { + Map? headers, + Map? queryParameters, + String? apiName, + }) { + return _restFunctionHelper( + methodName: 'get', + path: path, + headers: headers, + queryParameters: queryParameters, + apiName: apiName); } @override - RestOperation put({required RestOptions restOptions}) { - return _restFunctionHelper(methodName: 'put', restOptions: restOptions); + CancelableOperation put( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + return _restFunctionHelper( + methodName: 'put', + path: path, + body: body, + headers: headers, + queryParameters: queryParameters, + apiName: apiName); } @override - RestOperation post({required RestOptions restOptions}) { - return _restFunctionHelper(methodName: 'post', restOptions: restOptions); + CancelableOperation post( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + return _restFunctionHelper( + methodName: 'post', + path: path, + body: body, + headers: headers, + queryParameters: queryParameters, + apiName: apiName); } @override - RestOperation delete({required RestOptions restOptions}) { - return _restFunctionHelper(methodName: 'delete', restOptions: restOptions); + CancelableOperation delete( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + return _restFunctionHelper( + methodName: 'delete', + path: path, + body: body, + headers: headers, + queryParameters: queryParameters, + apiName: apiName); } @override - RestOperation head({required RestOptions restOptions}) { - return _restFunctionHelper(methodName: 'head', restOptions: restOptions); + CancelableOperation head( + String path, { + Map? headers, + Map? queryParameters, + String? apiName, + }) { + return _restFunctionHelper( + methodName: 'head', + path: path, + headers: headers, + queryParameters: queryParameters, + apiName: apiName); } @override - RestOperation patch({required RestOptions restOptions}) { - return _restFunctionHelper(methodName: 'patch', restOptions: restOptions); + CancelableOperation patch( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + return _restFunctionHelper( + methodName: 'patch', + path: path, + body: body, + headers: headers, + queryParameters: queryParameters, + apiName: apiName); } - @override + /// Cancels a request with a given request ID. + @Deprecated('Use .cancel() on CancelableOperation instead.') Future cancelRequest(String cancelToken) async { print('Attempting to cancel Operation $cancelToken'); @@ -347,6 +426,8 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { // ====== GENERAL METHODS ====== ApiException _deserializeException(PlatformException e) { + print(e.message); + if (e.code == 'ApiException') { return ApiException.fromMap( Map.from(e.details as Map), diff --git a/packages/api/amplify_api/pubspec.yaml b/packages/api/amplify_api/pubspec.yaml index 4305f09b30..869779172e 100644 --- a/packages/api/amplify_api/pubspec.yaml +++ b/packages/api/amplify_api/pubspec.yaml @@ -2,6 +2,7 @@ name: amplify_api description: The Amplify Flutter API category plugin, supporting GraphQL and REST operations. version: 0.5.0 homepage: https://github.com/aws-amplify/amplify-flutter/tree/main/packages/amplify_api +publish_to: none # until finalized environment: sdk: ">=2.17.0 <3.0.0" @@ -12,6 +13,7 @@ dependencies: amplify_api_ios: 0.5.0 amplify_core: 0.5.0 amplify_flutter: 0.5.0 + async: ^2.8.2 aws_common: ^0.1.0 collection: ^1.15.0 flutter: @@ -23,7 +25,6 @@ dev_dependencies: amplify_lints: ^2.0.0 amplify_test: path: ../../amplify_test - async: ^2.6.0 build_runner: ^2.0.0 flutter_test: sdk: flutter diff --git a/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart b/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart index f8ebb5d817..9f020a7aad 100644 --- a/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart +++ b/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart @@ -25,9 +25,15 @@ import 'graphql_helpers_test.dart'; const statusOK = 200; const statusBadRequest = 400; - -// Matchers -final throwsRestException = throwsA(isA()); +const mowLawnBody = '{"name": "Mow the lawn"}'; +const hello = 'Hello from lambda!'; +final helloResponse = ascii.encode(hello); +final encodedMowLoanBody = ascii.encode(mowLawnBody); +const queryParameters = { + 'queryParameterA': 'queryValueA', + 'queryParameterB': 'queryValueB' +}; +const headers = {'headerA': 'headerValueA', 'headerB': 'headerValueB'}; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -41,185 +47,165 @@ void main() { await Amplify.addPlugin(api); }); - test('PUT returns proper response.data', () async { - var responseData = Uint8List.fromList( - '{"success": "put call succeed!","url":/items?queryParameterA=queryValueA&queryParameterB=queryValueB","body": {"name": "Mow the lawn"}}' - .codeUnits); - var body = Uint8List.fromList('{"name":"Mow the lawn"}'.codeUnits); - var queryParameters = { - 'queryParameterA': 'queryValueA', - 'queryParameterB': 'queryValueB' - }; - var headers = {'headerA': 'headerValueA', 'headerB': 'headerValueB'}; + Future _assertResponse(AWSStreamedHttpResponse response) async { + final actualResponseBody = await response.decodeBody(); + expect(actualResponseBody, hello); + expect(response.statusCode, statusOK); + } + test('PUT returns proper response.data', () async { apiChannel.setMockMethodCallHandler((MethodCall methodCall) async { if (methodCall.method == 'put') { Map restOptions = methodCall.arguments['restOptions'] as Map; expect(restOptions['apiName'], 'restapi'); expect(restOptions['path'], '/items'); - expect(restOptions['body'], body); + expect(restOptions['body'], encodedMowLoanBody); expect(restOptions['queryParameters'], queryParameters); expect(restOptions['headers'], headers); - - return {'data': responseData, 'statusCode': statusOK}; + return {'data': helloResponse, 'statusCode': statusOK}; } }); - RestOperation restOperation = api.put( - restOptions: RestOptions( - path: '/items', - body: body, - apiName: 'restapi', - queryParameters: queryParameters, - headers: headers, - ), + final restOperation = api.put( + '/items', + body: HttpPayload.string(mowLawnBody), + apiName: 'restapi', + queryParameters: queryParameters, + headers: headers, ); - RestResponse response = await restOperation.response; - - expect(response.data, responseData); + final response = await restOperation.value; + await _assertResponse(response); }); test('POST returns proper response.data', () async { - var responseData = Uint8List.fromList( - '{"success": "post call succeed!","url":"/items?queryParameterA=queryValueA&queryParameterB=queryValueB","body": {"name": "Mow the lawn"}}' - .codeUnits); - var body = Uint8List.fromList('{"name":"Mow the lawn"}'.codeUnits); - var queryParameters = { - 'queryParameterA': 'queryValueA', - 'queryParameterB': 'queryValueB' - }; - var headers = {'headerA': 'headerValueA', 'headerB': 'headerValueB'}; - apiChannel.setMockMethodCallHandler((MethodCall methodCall) async { if (methodCall.method == 'post') { Map restOptions = methodCall.arguments['restOptions'] as Map; expect(restOptions['apiName'], 'restapi'); expect(restOptions['path'], '/items'); - expect(restOptions['body'], body); + expect(restOptions['body'], encodedMowLoanBody); expect(restOptions['queryParameters'], queryParameters); expect(restOptions['headers'], headers); - - return {'data': responseData, 'statusCode': statusOK}; + return {'data': helloResponse, 'statusCode': statusOK}; } }); - RestOperation restOperation = api.post( - restOptions: RestOptions( - path: '/items', - body: body, - apiName: 'restapi', - headers: headers, - queryParameters: queryParameters, - ), + final restOperation = api.post( + '/items', + body: HttpPayload.string(mowLawnBody), + apiName: 'restapi', + queryParameters: queryParameters, + headers: headers, ); - RestResponse response = await restOperation.response; - - expect(response.data, responseData); + final response = await restOperation.value; + await _assertResponse(response); }); test('GET returns proper response.data', () async { - var responseData = Uint8List.fromList( - '{"success":"get call succeed!","url":"/items"}'.codeUnits); - apiChannel.setMockMethodCallHandler((MethodCall methodCall) async { if (methodCall.method == 'get') { Map restOptions = methodCall.arguments['restOptions'] as Map; + expect(restOptions['apiName'], 'restapi'); expect(restOptions['path'], '/items'); - - return {'data': responseData, 'statusCode': statusOK}; + expect(restOptions['queryParameters'], queryParameters); + expect(restOptions['headers'], headers); + return {'data': helloResponse, 'statusCode': statusOK}; } }); - RestOperation restOperation = api.get( - restOptions: const RestOptions( - path: '/items', - )); - - RestResponse response = await restOperation.response; + final restOperation = api.get( + '/items', + apiName: 'restapi', + queryParameters: queryParameters, + headers: headers, + ); - expect(response.data, responseData); + final response = await restOperation.value; + await _assertResponse(response); }); test('DELETE returns proper response.data', () async { - var responseData = Uint8List.fromList( - '{"success":"delete call succeed!","url":"/items"}'.codeUnits); - apiChannel.setMockMethodCallHandler((MethodCall methodCall) async { if (methodCall.method == 'delete') { Map restOptions = methodCall.arguments['restOptions'] as Map; + expect(restOptions['apiName'], 'restapi'); expect(restOptions['path'], '/items'); - - return {'data': responseData, 'statusCode': statusOK}; + expect(restOptions['body'], encodedMowLoanBody); + expect(restOptions['queryParameters'], queryParameters); + expect(restOptions['headers'], headers); + return {'data': helloResponse, 'statusCode': statusOK}; } }); - RestOperation restOperation = api.delete( - restOptions: const RestOptions( - path: '/items', - )); - - RestResponse response = await restOperation.response; - - expect(response.data, responseData); - }); - - test('GET Status Code Error throws proper error', () async { - apiChannel.setMockMethodCallHandler((MethodCall methodCall) async { - if (methodCall.method == 'get') { - throw PlatformException(code: 'ApiException', details: { - 'message': 'AMPLIFY_API_MUTATE_FAILED', - 'recoverySuggestion': 'some insightful suggestion', - 'underlyingException': 'Act of God' - }); - } - }); + final restOperation = api.delete( + '/items', + body: HttpPayload.string(mowLawnBody), + apiName: 'restapi', + queryParameters: queryParameters, + headers: headers, + ); - try { - RestOperation restOperation = api.get( - restOptions: const RestOptions( - path: '/items', - )); - await restOperation.response; - } on ApiException catch (e) { - expect(e.message, 'AMPLIFY_API_MUTATE_FAILED'); - expect(e.recoverySuggestion, 'some insightful suggestion'); - expect(e.underlyingException, 'Act of God'); - } + final response = await restOperation.value; + await _assertResponse(response); }); - test('GET exception adds the httpStatusCode to exception if available', - () async { - const statusCode = 500; - const data = 'Internal server error'; - - apiChannel.setMockMethodCallHandler((MethodCall methodCall) async { - if (methodCall.method == 'get') { - return { - 'statusCode': statusCode, - 'headers': {}, - 'data': Uint8List.fromList(data.codeUnits), - }; - } - }); - - try { - RestOperation restOperation = api.get( - restOptions: const RestOptions( - path: '/items', - ), - ); - await restOperation.response; - } on RestException catch (e) { - expect(e.response.statusCode, 500); - expect(e.response.body, data); - } - }); + // test('GET Status Code Error throws proper error', () async { + // apiChannel.setMockMethodCallHandler((MethodCall methodCall) async { + // if (methodCall.method == 'get') { + // throw PlatformException(code: 'ApiException', details: { + // 'message': 'AMPLIFY_API_MUTATE_FAILED', + // 'recoverySuggestion': 'some insightful suggestion', + // 'underlyingException': 'Act of God' + // }); + // } + // }); + + // try { + // RestOperation restOperation = api.get( + // restOptions: const RestOptions( + // path: '/items', + // )); + // await restOperation.response; + // } on ApiException catch (e) { + // expect(e.message, 'AMPLIFY_API_MUTATE_FAILED'); + // expect(e.recoverySuggestion, 'some insightful suggestion'); + // expect(e.underlyingException, 'Act of God'); + // } + // }); + + // test('GET exception adds the httpStatusCode to exception if available', + // () async { + // const statusCode = 500; + // const data = 'Internal server error'; + + // apiChannel.setMockMethodCallHandler((MethodCall methodCall) async { + // if (methodCall.method == 'get') { + // return { + // 'statusCode': statusCode, + // 'headers': {}, + // 'data': Uint8List.fromList(data.codeUnits), + // }; + // } + // }); + + // try { + // RestOperation restOperation = api.get( + // restOptions: const RestOptions( + // path: '/items', + // ), + // ); + // await restOperation.response; + // } on RestException catch (e) { + // expect(e.response.statusCode, 500); + // expect(e.response.body, data); + // } + // }); test('CANCEL success does not throw error', () async { // Need to reply with PLACEHOLDER to avoid null issues in _formatRestResponse @@ -236,10 +222,7 @@ void main() { } }); - RestOperation restOperation = api.get( - restOptions: const RestOptions( - path: '/items', - )); + final restOperation = api.get('/items'); //RestResponse response = await restOperation.response; restOperation.cancel(); @@ -259,27 +242,22 @@ void main() { }); }); - test('throws RestException', () async { - final restOp = api.get(restOptions: const RestOptions(path: '/')); - await expectLater(restOp.response, throwsRestException); - }); - - test('has valid RestResponse', () async { - final restOp = api.get(restOptions: const RestOptions(path: '/')); - - RestException restException; - try { - await restOp.response; - fail('RestOperation should throw'); - } on Exception catch (e) { - expect(e, isA()); - restException = e as RestException; - } - - final response = restException.response; - expect(response.statusCode, statusBadRequest); - expect(response.headers, testResponseHeaders); - expect(response.body, testBody); - }); + // test('has valid RestResponse', () async { + // final restOp = api.get(restOptions: const RestOptions(path: '/')); + + // RestException restException; + // try { + // await restOp.response; + // fail('RestOperation should throw'); + // } on Exception catch (e) { + // expect(e, isA()); + // restException = e as RestException; + // } + + // final response = restException.response; + // expect(response.statusCode, statusBadRequest); + // expect(response.headers, testResponseHeaders); + // expect(response.body, testBody); + // }); }); } From 5f57bdcb2c6e6720998c1d8b7c353a1e4cff1319 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Fri, 27 May 2022 15:21:08 -0700 Subject: [PATCH 02/26] deprecate rest exception --- .../lib/src/types/api/api_types.dart | 1 + .../lib/src/types/api/rest/http_payload.dart | 4 +-- .../src/types/api/rest/rest_exception.dart | 27 +++++++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 packages/amplify_core/lib/src/types/api/rest/rest_exception.dart diff --git a/packages/amplify_core/lib/src/types/api/api_types.dart b/packages/amplify_core/lib/src/types/api/api_types.dart index d0e8f21a0e..c038b0c239 100644 --- a/packages/amplify_core/lib/src/types/api/api_types.dart +++ b/packages/amplify_core/lib/src/types/api/api_types.dart @@ -29,6 +29,7 @@ export 'graphql/graphql_response_error.dart'; export 'graphql/graphql_subscription_operation.dart'; export 'rest/http_payload.dart'; +export 'rest/rest_exception.dart'; export 'rest/rest_options.dart'; export 'types/pagination/paginated_model_type.dart'; diff --git a/packages/amplify_core/lib/src/types/api/rest/http_payload.dart b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart index 47b03ce9da..8aceeb3b9b 100644 --- a/packages/amplify_core/lib/src/types/api/rest/http_payload.dart +++ b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart @@ -17,11 +17,11 @@ import 'dart:convert'; import 'package:async/async.dart'; -/// {@template amplify_common.http_payload} +/// {@template amplify_core.http_payload} /// An HTTP request's payload. /// {@endtemplate} class HttpPayload extends StreamView> { - /// {@macro amplify_common.http_payload} + /// {@macro amplify_core.http_payload} factory HttpPayload([Object? body]) { if (body == null) { return const HttpPayload.empty(); diff --git a/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart b/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart new file mode 100644 index 0000000000..6ccd359798 --- /dev/null +++ b/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart @@ -0,0 +1,27 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import 'package:amplify_core/amplify_core.dart'; + +/// {@template rest_exception} +/// An HTTP error encountered during a REST API call, i.e. for calls returning +/// non-2xx status codes. +/// {@endtemplate} +@Deprecated( + 'No longer thrown for non-200 responses. Handle responses as desired.') +class RestException extends ApiException { + /// {@macro rest_exception} + RestException() : super('REST exception.'); +} From 774d48eb43729c8c7bef84ec2413b3708ba93a31 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Fri, 27 May 2022 15:22:19 -0700 Subject: [PATCH 03/26] change deprecation message --- .../amplify_core/lib/src/types/api/rest/rest_exception.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart b/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart index 6ccd359798..b73115aea1 100644 --- a/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart +++ b/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart @@ -19,8 +19,7 @@ import 'package:amplify_core/amplify_core.dart'; /// An HTTP error encountered during a REST API call, i.e. for calls returning /// non-2xx status codes. /// {@endtemplate} -@Deprecated( - 'No longer thrown for non-200 responses. Handle responses as desired.') +@Deprecated('No longer thrown for non-200 responses. Will soon be removed') class RestException extends ApiException { /// {@macro rest_exception} RestException() : super('REST exception.'); From 5198f9b3f2a68086f8a87d418caf304d5f0b98aa Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Fri, 27 May 2022 15:23:06 -0700 Subject: [PATCH 04/26] change constuctor --- .../amplify_core/lib/src/types/api/rest/rest_exception.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart b/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart index b73115aea1..4eea7fd4f3 100644 --- a/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart +++ b/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart @@ -22,5 +22,5 @@ import 'package:amplify_core/amplify_core.dart'; @Deprecated('No longer thrown for non-200 responses. Will soon be removed') class RestException extends ApiException { /// {@macro rest_exception} - RestException() : super('REST exception.'); + const RestException() : super('REST exception.'); } From 3f1472a8d747e8ed0ba7fe02b2a51ce86e8b5886 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Tue, 31 May 2022 11:19:14 -0700 Subject: [PATCH 05/26] change types for qp --- .../src/category/amplify_api_category.dart | 12 +++--- .../plugin/amplify_api_plugin_interface.dart | 12 +++--- .../lib/src/method_channel_api.dart | 38 +++++++++++++------ 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/packages/amplify_core/lib/src/category/amplify_api_category.dart b/packages/amplify_core/lib/src/category/amplify_api_category.dart index ca6c666638..7d9692a725 100644 --- a/packages/amplify_core/lib/src/category/amplify_api_category.dart +++ b/packages/amplify_core/lib/src/category/amplify_api_category.dart @@ -47,7 +47,7 @@ class APICategory extends AmplifyCategory { String path, { Map? headers, HttpPayload? body, - Map? queryParameters, + Map? queryParameters, String? apiName, }) => defaultPlugin.delete( @@ -60,7 +60,7 @@ class APICategory extends AmplifyCategory { CancelableOperation get( String path, { Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) => defaultPlugin.get( @@ -72,7 +72,7 @@ class APICategory extends AmplifyCategory { CancelableOperation head( String path, { Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) => defaultPlugin.head( @@ -85,7 +85,7 @@ class APICategory extends AmplifyCategory { String path, { Map? headers, HttpPayload? body, - Map? queryParameters, + Map? queryParameters, String? apiName, }) => defaultPlugin.patch( @@ -99,7 +99,7 @@ class APICategory extends AmplifyCategory { String path, { Map? headers, HttpPayload? body, - Map? queryParameters, + Map? queryParameters, String? apiName, }) => defaultPlugin.post( @@ -113,7 +113,7 @@ class APICategory extends AmplifyCategory { String path, { Map? headers, HttpPayload? body, - Map? queryParameters, + Map? queryParameters, String? apiName, }) => defaultPlugin.put( diff --git a/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart b/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart index ba63eb5814..5169acb091 100644 --- a/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart +++ b/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart @@ -57,7 +57,7 @@ abstract class APIPluginInterface extends AmplifyPluginInterface { String path, { HttpPayload? body, Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { throw UnimplementedError('delete() has not been implemented'); @@ -69,7 +69,7 @@ abstract class APIPluginInterface extends AmplifyPluginInterface { CancelableOperation get( String path, { Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { throw UnimplementedError('get() has not been implemented'); @@ -78,7 +78,7 @@ abstract class APIPluginInterface extends AmplifyPluginInterface { CancelableOperation head( String path, { Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { throw UnimplementedError('head() has not been implemented'); @@ -88,7 +88,7 @@ abstract class APIPluginInterface extends AmplifyPluginInterface { String path, { HttpPayload? body, Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { throw UnimplementedError('patch() has not been implemented'); @@ -98,7 +98,7 @@ abstract class APIPluginInterface extends AmplifyPluginInterface { String path, { HttpPayload? body, Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { throw UnimplementedError('post() has not been implemented'); @@ -108,7 +108,7 @@ abstract class APIPluginInterface extends AmplifyPluginInterface { String path, { HttpPayload? body, Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { throw UnimplementedError('put() has not been implemented'); diff --git a/packages/api/amplify_api/lib/src/method_channel_api.dart b/packages/api/amplify_api/lib/src/method_channel_api.dart index 9d662abe00..476980d527 100644 --- a/packages/api/amplify_api/lib/src/method_channel_api.dart +++ b/packages/api/amplify_api/lib/src/method_channel_api.dart @@ -14,6 +14,7 @@ */ import 'dart:async'; +import 'dart:convert'; import 'dart:typed_data'; import 'package:amplify_api/src/graphql/graphql_response_decoder.dart'; @@ -243,16 +244,29 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { required String cancelToken, HttpPayload? body, Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) async { - final bodyBytes = - body != null ? Uint8List.fromList(await body.single) : null; + Uint8List? bodyBytes; + if (body != null) { + final completer = Completer(); + final sink = ByteConversionSink.withCallback( + (bytes) => completer.complete(Uint8List.fromList(bytes)), + ); + body.listen( + sink.add, + onError: completer.completeError, + onDone: sink.close, + cancelOnError: true, + ); + bodyBytes = await completer.future; + } + final restOptions = RestOptions( path: path, body: bodyBytes, apiName: apiName, - queryParameters: queryParameters as Map?, + queryParameters: queryParameters, headers: headers); return _callNativeRestMethod(methodName, cancelToken, restOptions); } @@ -262,11 +276,11 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { required String path, HttpPayload? body, Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { // Send Request cancelToken to Native - String cancelToken = UUID.getUUID(); + String cancelToken = uuid(); final responseFuture = _restResponseHelper( methodName: methodName, path: path, @@ -316,7 +330,7 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { CancelableOperation get( String path, { Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { return _restFunctionHelper( @@ -332,7 +346,7 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { String path, { HttpPayload? body, Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { return _restFunctionHelper( @@ -349,7 +363,7 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { String path, { HttpPayload? body, Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { return _restFunctionHelper( @@ -366,7 +380,7 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { String path, { HttpPayload? body, Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { return _restFunctionHelper( @@ -382,7 +396,7 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { CancelableOperation head( String path, { Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { return _restFunctionHelper( @@ -398,7 +412,7 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { String path, { HttpPayload? body, Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { return _restFunctionHelper( From c2aa31b6db5c35d9347a6c586059dbb03c9910d4 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Tue, 31 May 2022 11:25:18 -0700 Subject: [PATCH 06/26] cleanup --- .../example/lib/rest_api_view.dart | 3 -- .../test/amplify_rest_api_methods_test.dart | 52 ------------------- 2 files changed, 55 deletions(-) diff --git a/packages/api/amplify_api/example/lib/rest_api_view.dart b/packages/api/amplify_api/example/lib/rest_api_view.dart index 2e55e01e73..d6c489f6b3 100644 --- a/packages/api/amplify_api/example/lib/rest_api_view.dart +++ b/packages/api/amplify_api/example/lib/rest_api_view.dart @@ -29,9 +29,6 @@ class _RestApiViewState extends State { late TextEditingController _apiPathController; late CancelableOperation _lastRestOperation; - // TEMP until Amplify methods implemented in dart. - late AmplifyAPI api; - @override void initState() { super.initState(); diff --git a/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart b/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart index 9f020a7aad..c94ad8806d 100644 --- a/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart +++ b/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart @@ -155,58 +155,6 @@ void main() { await _assertResponse(response); }); - // test('GET Status Code Error throws proper error', () async { - // apiChannel.setMockMethodCallHandler((MethodCall methodCall) async { - // if (methodCall.method == 'get') { - // throw PlatformException(code: 'ApiException', details: { - // 'message': 'AMPLIFY_API_MUTATE_FAILED', - // 'recoverySuggestion': 'some insightful suggestion', - // 'underlyingException': 'Act of God' - // }); - // } - // }); - - // try { - // RestOperation restOperation = api.get( - // restOptions: const RestOptions( - // path: '/items', - // )); - // await restOperation.response; - // } on ApiException catch (e) { - // expect(e.message, 'AMPLIFY_API_MUTATE_FAILED'); - // expect(e.recoverySuggestion, 'some insightful suggestion'); - // expect(e.underlyingException, 'Act of God'); - // } - // }); - - // test('GET exception adds the httpStatusCode to exception if available', - // () async { - // const statusCode = 500; - // const data = 'Internal server error'; - - // apiChannel.setMockMethodCallHandler((MethodCall methodCall) async { - // if (methodCall.method == 'get') { - // return { - // 'statusCode': statusCode, - // 'headers': {}, - // 'data': Uint8List.fromList(data.codeUnits), - // }; - // } - // }); - - // try { - // RestOperation restOperation = api.get( - // restOptions: const RestOptions( - // path: '/items', - // ), - // ); - // await restOperation.response; - // } on RestException catch (e) { - // expect(e.response.statusCode, 500); - // expect(e.response.body, data); - // } - // }); - test('CANCEL success does not throw error', () async { // Need to reply with PLACEHOLDER to avoid null issues in _formatRestResponse // In actual production code, the methodChannel doesn't respond to the future response From f23b403710d16575dfe6965788981d18982d171e Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Tue, 31 May 2022 11:25:54 -0700 Subject: [PATCH 07/26] more cleanup --- .../test/amplify_rest_api_methods_test.dart | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart b/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart index c94ad8806d..91f273def3 100644 --- a/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart +++ b/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart @@ -189,23 +189,5 @@ void main() { }; }); }); - - // test('has valid RestResponse', () async { - // final restOp = api.get(restOptions: const RestOptions(path: '/')); - - // RestException restException; - // try { - // await restOp.response; - // fail('RestOperation should throw'); - // } on Exception catch (e) { - // expect(e, isA()); - // restException = e as RestException; - // } - - // final response = restException.response; - // expect(response.statusCode, statusBadRequest); - // expect(response.headers, testResponseHeaders); - // expect(response.body, testBody); - // }); }); } From 6fa8dede501e7e83e635d6f9db6a3cdcff801956 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Wed, 1 Jun 2022 14:25:27 -0700 Subject: [PATCH 08/26] fix response headers, other PT changes --- .../lib/src/types/api/rest/http_payload.dart | 23 +++++++++ .../src/types/api/rest/rest_exception.dart | 2 +- .../lib/src/method_channel_api.dart | 7 +-- .../test/amplify_rest_api_methods_test.dart | 48 +++++++++++++------ 4 files changed, 61 insertions(+), 19 deletions(-) diff --git a/packages/amplify_core/lib/src/types/api/rest/http_payload.dart b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart index 8aceeb3b9b..2a11432b55 100644 --- a/packages/amplify_core/lib/src/types/api/rest/http_payload.dart +++ b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart @@ -35,6 +35,9 @@ class HttpPayload extends StreamView> { if (body is Stream>) { return HttpPayload.streaming(body); } + if (body is Map) { + return HttpPayload.fields(body.cast()); + } throw ArgumentError('Invalid HTTP payload type: ${body.runtimeType}'); } @@ -48,6 +51,26 @@ class HttpPayload extends StreamView> { /// A byte buffer HTTP body. HttpPayload.bytes(List body) : super(Stream.value(body)); + /// Form-encodes the body. + HttpPayload.fields(Map body, {Encoding encoding = utf8}) + : super(LazyStream(() => Stream.value( + encoding.encode(_mapToQuery(body, encoding: encoding))))); + /// A streaming HTTP body. const HttpPayload.streaming(Stream> body) : super(body); } + +/// Converts a [Map] from parameter names to values to a URL query string. +/// +/// mapToQuery({"foo": "bar", "baz": "bang"}); +/// //=> "foo=bar&baz=bang" +/// +/// Copied from similar util. https://github.com/dart-lang/http/blob/06649afbb5847dbb0293816ba8348766b116e419/pkgs/http/lib/src/utils.dart#L15 +String _mapToQuery(Map map, {Encoding? encoding}) { + var pairs = >[]; + map.forEach((key, value) => pairs.add([ + Uri.encodeQueryComponent(key, encoding: encoding ?? utf8), + Uri.encodeQueryComponent(value, encoding: encoding ?? utf8) + ])); + return pairs.map((pair) => '${pair[0]}=${pair[1]}').join('&'); +} diff --git a/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart b/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart index 4eea7fd4f3..4d4df8f57a 100644 --- a/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart +++ b/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart @@ -20,7 +20,7 @@ import 'package:amplify_core/amplify_core.dart'; /// non-2xx status codes. /// {@endtemplate} @Deprecated('No longer thrown for non-200 responses. Will soon be removed') -class RestException extends ApiException { +abstract class RestException extends ApiException { /// {@macro rest_exception} const RestException() : super('REST exception.'); } diff --git a/packages/api/amplify_api/lib/src/method_channel_api.dart b/packages/api/amplify_api/lib/src/method_channel_api.dart index 476980d527..d37a73621e 100644 --- a/packages/api/amplify_api/lib/src/method_channel_api.dart +++ b/packages/api/amplify_api/lib/src/method_channel_api.dart @@ -318,11 +318,14 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { AWSStreamedHttpResponse _formatRestResponse(Map res) { final statusCode = res['statusCode'] as int; - final headers = res['headers'] as Map?; + // Make type-safe version of response headers. + final serializedHeaders = res['headers'] as Map?; + final headers = serializedHeaders?.cast(); final rawResponseBody = res['data'] as Uint8List?; return AWSStreamedHttpResponse( statusCode: statusCode, + headers: headers, body: Stream.value(rawResponseBody?.toList() ?? [])); } @@ -440,8 +443,6 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { // ====== GENERAL METHODS ====== ApiException _deserializeException(PlatformException e) { - print(e.message); - if (e.code == 'ApiException') { return ApiException.fromMap( Map.from(e.details as Map), diff --git a/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart b/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart index 91f273def3..67e5e5fe11 100644 --- a/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart +++ b/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart @@ -155,6 +155,39 @@ void main() { await _assertResponse(response); }); + test( + 'POST with form-encoded body gets proper response with response headers included', + () async { + apiChannel.setMockMethodCallHandler((MethodCall methodCall) async { + if (methodCall.method == 'post') { + Map restOptions = + methodCall.arguments['restOptions'] as Map; + expect(restOptions['apiName'], 'restapi'); + expect(restOptions['path'], '/items'); + expect(restOptions['queryParameters'], queryParameters); + expect(restOptions['headers'], headers); + expect(utf8.decode(restOptions['body'] as List), 'foo=bar'); + return { + 'data': helloResponse, + 'statusCode': statusOK, + 'headers': {'foo': 'bar'} + }; + } + }); + + final restOperation = api.post( + '/items', + apiName: 'restapi', + body: HttpPayload.fields({'foo': 'bar'}), + queryParameters: queryParameters, + headers: headers, + ); + + final response = await restOperation.value; + expect(response.headers['foo'], 'bar'); + await _assertResponse(response); + }); + test('CANCEL success does not throw error', () async { // Need to reply with PLACEHOLDER to avoid null issues in _formatRestResponse // In actual production code, the methodChannel doesn't respond to the future response @@ -175,19 +208,4 @@ void main() { //RestResponse response = await restOperation.response; restOperation.cancel(); }); - - group('non-2xx status code', () { - const testBody = 'test'; - const testResponseHeaders = {'key': 'value'}; - - setUpAll(() { - apiChannel.setMockMethodCallHandler((call) async { - return { - 'data': utf8.encode(testBody), - 'statusCode': statusBadRequest, - 'headers': testResponseHeaders, - }; - }); - }); - }); } From 1c9e8cb831f36a2cfd1f45b66b9007de191ff3dd Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Thu, 2 Jun 2022 09:25:13 -0700 Subject: [PATCH 09/26] change example app --- .../amplify_auth_cognito/example/lib/main.dart | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/auth/amplify_auth_cognito/example/lib/main.dart b/packages/auth/amplify_auth_cognito/example/lib/main.dart index 4e3441eae7..a70803bd32 100644 --- a/packages/auth/amplify_auth_cognito/example/lib/main.dart +++ b/packages/auth/amplify_auth_cognito/example/lib/main.dart @@ -12,9 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'dart:convert'; -import 'dart:typed_data'; - import 'package:amplify_api/amplify_api.dart'; import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; import 'package:amplify_authenticator/amplify_authenticator.dart'; @@ -94,14 +91,13 @@ class _MyHomePageState extends State { try { final response = await Amplify.API .post( - restOptions: RestOptions( - path: '/hello', - body: utf8.encode(_controller.text) as Uint8List, - ), + '/hello', + body: HttpPayload.string(_controller.text), ) - .response; + .value; + final decodedBody = await response.decodeBody(); setState(() { - _greeting = response.body; + _greeting = decodedBody; }); } on Exception catch (e) { setState(() { From 6034d6b29102be3c0e78ed51a063f50a8e362ec2 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Thu, 2 Jun 2022 14:58:33 -0700 Subject: [PATCH 10/26] feat(api): REST API in dart, auth mode NONE --- .../amplify_flutter/lib/src/dart_impl.dart | 2 + .../api/amplify_api/example/lib/main.dart | 2 +- packages/api/amplify_api/lib/amplify_api.dart | 5 +- .../lib/src/amplify_api_config.dart | 75 ++++++ .../amplify_authorization_rest_client.dart | 58 +++++ .../amplify_api/lib/src/api_plugin_impl.dart | 216 ++++++++++++++++++ packages/api/amplify_api/pubspec.yaml | 1 + 7 files changed, 357 insertions(+), 2 deletions(-) create mode 100644 packages/api/amplify_api/lib/src/amplify_api_config.dart create mode 100644 packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart create mode 100644 packages/api/amplify_api/lib/src/api_plugin_impl.dart diff --git a/packages/amplify/amplify_flutter/lib/src/dart_impl.dart b/packages/amplify/amplify_flutter/lib/src/dart_impl.dart index d70003e515..66862546e6 100644 --- a/packages/amplify/amplify_flutter/lib/src/dart_impl.dart +++ b/packages/amplify/amplify_flutter/lib/src/dart_impl.dart @@ -30,6 +30,7 @@ class AmplifyDartImpl extends AmplifyClass { ); await Future.wait( [ + ...API.plugins, ...Auth.plugins, ].map((p) => p.configure(config: amplifyConfig)), eagerError: true, @@ -60,6 +61,7 @@ class AmplifyDartImpl extends AmplifyClass { case Category.analytics: case Category.storage: case Category.api: + return API.addPlugin(plugin.cast()); case Category.dataStore: case Category.hub: throw UnimplementedError(); diff --git a/packages/api/amplify_api/example/lib/main.dart b/packages/api/amplify_api/example/lib/main.dart index 5c044e7aec..6e5dbf862d 100644 --- a/packages/api/amplify_api/example/lib/main.dart +++ b/packages/api/amplify_api/example/lib/main.dart @@ -44,7 +44,7 @@ class _MyAppState extends State { } void _configureAmplify() async { - Amplify.addPlugins([AmplifyAuthCognito(), AmplifyAPI()]); + await Amplify.addPlugins([AmplifyAuthCognito(), AmplifyAPI()]); try { await Amplify.configure(amplifyconfig); diff --git a/packages/api/amplify_api/lib/amplify_api.dart b/packages/api/amplify_api/lib/amplify_api.dart index f0ca3c2c4f..808520d03a 100644 --- a/packages/api/amplify_api/lib/amplify_api.dart +++ b/packages/api/amplify_api/lib/amplify_api.dart @@ -21,6 +21,8 @@ import 'package:amplify_api/src/method_channel_api.dart'; import 'package:amplify_core/amplify_core.dart'; import 'package:meta/meta.dart'; +import 'src/api_plugin_impl.dart'; + export 'package:amplify_core/src/types/api/api_types.dart'; export './model_mutations.dart'; @@ -37,7 +39,8 @@ abstract class AmplifyAPI extends APIPluginInterface { ModelProviderInterface? modelProvider, }) { if (zIsWeb || Platform.isWindows || Platform.isMacOS || Platform.isLinux) { - throw UnsupportedError('This platform is not supported yet'); + return AmplifyAPIDart( + authProviders: authProviders, modelProvider: modelProvider); } return AmplifyAPIMethodChannel( authProviders: authProviders, diff --git a/packages/api/amplify_api/lib/src/amplify_api_config.dart b/packages/api/amplify_api/lib/src/amplify_api_config.dart new file mode 100644 index 0000000000..fff942e19b --- /dev/null +++ b/packages/api/amplify_api/lib/src/amplify_api_config.dart @@ -0,0 +1,75 @@ +// +// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// A copy of the License is located at +// +// http://aws.amazon.com/apache2.0 +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. +// + +import 'package:amplify_core/amplify_core.dart'; +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; + +const _slash = '/'; + +@internal +class EndpointConfig with AWSEquatable { + const EndpointConfig(this.name, this.config); + + final String name; + final AWSApiConfig config; + + @override + List get props => [name, config]; + + /// Gets the host with environment path prefix from Amplify config and combines + /// with [path] and [queryParameters] to return a full [Uri]. + Uri getUri(String path, Map? queryParameters) { + final parsed = Uri.parse(config.endpoint); + // Remove leading slashes which are suggested in public documentation. + if (path.startsWith(_slash)) { + path = path.substring(1); + } + // Avoid URI-encoding slashes in path from caller. + final pathSegmentsFromPath = path.split(_slash); + return parsed.replace(pathSegments: [ + ...parsed.pathSegments, + ...pathSegmentsFromPath, + ], queryParameters: queryParameters); + } +} + +@internal +extension AWSApiPluginConfigHelpers on AWSApiPluginConfig { + EndpointConfig getEndpoint({ + required EndpointType type, + String? apiName, + }) { + final typeConfigs = + entries.where((config) => config.value.endpointType == type); + if (apiName != null) { + final config = typeConfigs.firstWhere( + (config) => config.key == apiName, + orElse: () => throw ApiException( + 'No API endpoint found matching apiName $apiName.', + ), + ); + return EndpointConfig(config.key, config.value); + } + final onlyConfig = typeConfigs.singleOrNull; + if (onlyConfig == null) { + throw const ApiException( + 'Multiple API endpoints defined. Pass apiName parameter to specify ' + 'which one to use.', + ); + } + return EndpointConfig(onlyConfig.key, onlyConfig.value); + } +} diff --git a/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart b/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart new file mode 100644 index 0000000000..1ece436cb4 --- /dev/null +++ b/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart @@ -0,0 +1,58 @@ +// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:async'; + +import 'package:amplify_core/amplify_core.dart'; +import 'package:aws_common/aws_common.dart'; +import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; + +/// Implementation of http [http.Client] that authorizes HTTP requests with +/// Amplify. +@internal +class AmplifyAuthorizationRestClient extends http.BaseClient { + /// Determines how requests with this client are authorized. + final AWSApiConfig endpointConfig; + final http.Client _baseClient; + final bool _useDefaultBaseClient; + + /// Provide an [AWSApiConfig] which will determine how requests from this + /// client are authorized. + AmplifyAuthorizationRestClient({ + required this.endpointConfig, + http.Client? baseClient, + }) : _useDefaultBaseClient = baseClient == null, + _baseClient = baseClient ?? http.Client(); + + /// Implementation of [send] that authorizes any request without "Authorization" + /// header already set. + @override + Future send(http.BaseRequest request) async => + _baseClient.send(_authorizeRequest(request)); + + @override + void close() { + if (_useDefaultBaseClient) _baseClient.close(); + } + + http.BaseRequest _authorizeRequest(http.BaseRequest request) { + if (!request.headers.containsKey(AWSHeaders.authorization) && + endpointConfig.authorizationType != APIAuthorizationType.none) { + // ignore: todo + // TODO: Use auth providers from core to transform the request. + } + return request; + } +} diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart new file mode 100644 index 0000000000..cf2521fadb --- /dev/null +++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart @@ -0,0 +1,216 @@ +// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +library amplify_api; + +import 'package:amplify_api/amplify_api.dart'; +import 'package:amplify_core/amplify_core.dart'; +import 'package:async/async.dart'; +import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; + +import 'amplify_api_config.dart'; +import 'amplify_authorization_rest_client.dart'; + +/// {@template amplify_api.amplify_api_dart} +/// The AWS implementation of the Amplify API category. +/// {@endtemplate} +class AmplifyAPIDart extends AmplifyAPI { + late final AWSApiPluginConfig _apiConfig; + final http.Client? _baseHttpClient; + + /// A map of the keys from the Amplify API config to HTTP clients to use for + /// requests to that endpoint. + final Map _clientPool = {}; + + /// {@macro amplify_api.amplify_api_dart} + AmplifyAPIDart({ + List authProviders = const [], + http.Client? baseHttpClient, + this.modelProvider, + }) : _baseHttpClient = baseHttpClient, + super.protected() { + authProviders.forEach(registerAuthProvider); + } + + @override + Future configure({AmplifyConfig? config}) async { + final apiConfig = config?.api?.awsPlugin; + if (apiConfig == null) { + throw const ApiException('No AWS API config found', + recoverySuggestion: 'Add API from the Amplify CLI. See ' + 'https://docs.amplify.aws/lib/graphqlapi/getting-started/q/platform/flutter/#configure-api'); + } + _apiConfig = apiConfig; + } + + /// Returns the HTTP client to be used for REST operations. + /// + /// Use [apiName] if there are multiple REST endpoints. + @visibleForTesting + http.Client getRestClient({String? apiName}) { + final endpoint = _apiConfig.getEndpoint( + type: EndpointType.rest, + apiName: apiName, + ); + return _clientPool[endpoint.name] ??= AmplifyAuthorizationRestClient( + endpointConfig: endpoint.config, + baseClient: _baseHttpClient, + ); + } + + Uri _getRestUri( + String path, String? apiName, Map? queryParameters) { + final endpoint = _apiConfig.getEndpoint( + type: EndpointType.rest, + apiName: apiName, + ); + return endpoint.getUri(path, queryParameters); + } + + /// NOTE: http does not support request abort https://github.com/dart-lang/http/issues/424. + /// For now, just make a [CancelableOperation] to cancel the future. + /// To actually abort calls at network layer, need to call in + /// dart:io/dart:html or other library with custom http default Client() implementation. + CancelableOperation _makeCancelable(Future responseFuture) { + return CancelableOperation.fromFuture(responseFuture); + } + + CancelableOperation _prepareRestResponse( + Future responseFuture) { + return _makeCancelable(responseFuture); + } + + @override + final ModelProviderInterface? modelProvider; + + @override + void registerAuthProvider(APIAuthProvider authProvider) { + // ignore: todo + // TODO: integrate with auth provider implementation. + throw UnimplementedError( + 'registerAuthProvider() has not been implemented.'); + } + + // ====== REST ======= + + @override + CancelableOperation delete( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + final uri = _getRestUri(path, apiName, queryParameters); + final client = getRestClient(apiName: apiName); + return _prepareRestResponse(AWSStreamedHttpRequest.delete( + uri, + body: body ?? const HttpPayload.empty(), + headers: headers, + ).send(client)); + } + + @override + CancelableOperation get( + String path, { + Map? headers, + Map? queryParameters, + String? apiName, + }) { + final uri = _getRestUri(path, apiName, queryParameters); + final client = getRestClient(apiName: apiName); + return _prepareRestResponse( + AWSHttpRequest.get( + uri, + headers: headers, + ).send(client), + ); + } + + @override + CancelableOperation head( + String path, { + Map? headers, + Map? queryParameters, + String? apiName, + }) { + final uri = _getRestUri(path, apiName, queryParameters); + final client = getRestClient(apiName: apiName); + return _prepareRestResponse( + AWSHttpRequest( + method: AWSHttpMethod.head, + uri: uri, + headers: headers, + ).send(client), + ); + } + + @override + CancelableOperation patch( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + final uri = _getRestUri(path, apiName, queryParameters); + final client = getRestClient(apiName: apiName); + return _prepareRestResponse( + AWSStreamedHttpRequest.patch( + uri, + headers: headers, + body: body ?? const HttpPayload.empty(), + ).send(client), + ); + } + + @override + CancelableOperation post( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + final uri = _getRestUri(path, apiName, queryParameters); + final client = getRestClient(apiName: apiName); + return _prepareRestResponse( + AWSStreamedHttpRequest.post( + uri, + headers: headers, + body: body ?? const HttpPayload.empty(), + ).send(client), + ); + } + + @override + CancelableOperation put( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + final uri = _getRestUri(path, apiName, queryParameters); + final client = getRestClient(apiName: apiName); + return _prepareRestResponse( + AWSStreamedHttpRequest.put( + uri, + headers: headers, + body: body ?? const HttpPayload.empty(), + ).send(client), + ); + } +} diff --git a/packages/api/amplify_api/pubspec.yaml b/packages/api/amplify_api/pubspec.yaml index 869779172e..08b59af397 100644 --- a/packages/api/amplify_api/pubspec.yaml +++ b/packages/api/amplify_api/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: collection: ^1.15.0 flutter: sdk: flutter + http: ^0.13.4 meta: ^1.7.0 plugin_platform_interface: ^2.0.0 From 5b8a8bdd26519e7e225b2e710b4460a3ab58b6c8 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Thu, 9 Jun 2022 09:08:11 -0700 Subject: [PATCH 11/26] Update packages/amplify_core/lib/src/types/api/rest/http_payload.dart Co-authored-by: Jordan Nelson --- .../amplify_core/lib/src/types/api/rest/http_payload.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/amplify_core/lib/src/types/api/rest/http_payload.dart b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart index 2a11432b55..ddadc750a7 100644 --- a/packages/amplify_core/lib/src/types/api/rest/http_payload.dart +++ b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart @@ -35,8 +35,8 @@ class HttpPayload extends StreamView> { if (body is Stream>) { return HttpPayload.streaming(body); } - if (body is Map) { - return HttpPayload.fields(body.cast()); + if (body is Map) { + return HttpPayload.fields(body); } throw ArgumentError('Invalid HTTP payload type: ${body.runtimeType}'); } From cfb1c4e63cffec9f8913e8aa40daf24127ac0a9e Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Thu, 9 Jun 2022 15:49:17 -0700 Subject: [PATCH 12/26] address some comments --- .../src/types/api/rest/rest_exception.dart | 2 +- .../example/lib/rest_api_view.dart | 1 - .../lib/src/method_channel_api.dart | 100 ++++++++++-------- 3 files changed, 55 insertions(+), 48 deletions(-) diff --git a/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart b/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart index 4d4df8f57a..1f6dc18c2e 100644 --- a/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart +++ b/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart @@ -19,7 +19,7 @@ import 'package:amplify_core/amplify_core.dart'; /// An HTTP error encountered during a REST API call, i.e. for calls returning /// non-2xx status codes. /// {@endtemplate} -@Deprecated('No longer thrown for non-200 responses. Will soon be removed') +@Deprecated('BREAKING CHANGE: No longer thrown for non-200 responses.') abstract class RestException extends ApiException { /// {@macro rest_exception} const RestException() : super('REST exception.'); diff --git a/packages/api/amplify_api/example/lib/rest_api_view.dart b/packages/api/amplify_api/example/lib/rest_api_view.dart index d6c489f6b3..9a30d68e41 100644 --- a/packages/api/amplify_api/example/lib/rest_api_view.dart +++ b/packages/api/amplify_api/example/lib/rest_api_view.dart @@ -13,7 +13,6 @@ * permissions and limitations under the License. */ -import 'package:amplify_api/amplify_api.dart'; import 'package:amplify_flutter/amplify_flutter.dart'; import 'package:async/async.dart'; import 'package:flutter/material.dart'; diff --git a/packages/api/amplify_api/lib/src/method_channel_api.dart b/packages/api/amplify_api/lib/src/method_channel_api.dart index d37a73621e..3fa5773faa 100644 --- a/packages/api/amplify_api/lib/src/method_channel_api.dart +++ b/packages/api/amplify_api/lib/src/method_channel_api.dart @@ -263,11 +263,12 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { } final restOptions = RestOptions( - path: path, - body: bodyBytes, - apiName: apiName, - queryParameters: queryParameters, - headers: headers); + path: path, + body: bodyBytes, + apiName: apiName, + queryParameters: queryParameters, + headers: headers, + ); return _callNativeRestMethod(methodName, cancelToken, restOptions); } @@ -282,13 +283,14 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { // Send Request cancelToken to Native String cancelToken = uuid(); final responseFuture = _restResponseHelper( - methodName: methodName, - path: path, - cancelToken: cancelToken, - body: body, - headers: headers, - queryParameters: queryParameters, - apiName: apiName); + methodName: methodName, + path: path, + cancelToken: cancelToken, + body: body, + headers: headers, + queryParameters: queryParameters, + apiName: apiName, + ); return CancelableOperation.fromFuture(responseFuture, onCancel: () => cancelRequest(cancelToken)); @@ -337,11 +339,12 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { String? apiName, }) { return _restFunctionHelper( - methodName: 'get', - path: path, - headers: headers, - queryParameters: queryParameters, - apiName: apiName); + methodName: 'get', + path: path, + headers: headers, + queryParameters: queryParameters, + apiName: apiName, + ); } @override @@ -353,12 +356,13 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { String? apiName, }) { return _restFunctionHelper( - methodName: 'put', - path: path, - body: body, - headers: headers, - queryParameters: queryParameters, - apiName: apiName); + methodName: 'put', + path: path, + body: body, + headers: headers, + queryParameters: queryParameters, + apiName: apiName, + ); } @override @@ -370,12 +374,13 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { String? apiName, }) { return _restFunctionHelper( - methodName: 'post', - path: path, - body: body, - headers: headers, - queryParameters: queryParameters, - apiName: apiName); + methodName: 'post', + path: path, + body: body, + headers: headers, + queryParameters: queryParameters, + apiName: apiName, + ); } @override @@ -387,12 +392,13 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { String? apiName, }) { return _restFunctionHelper( - methodName: 'delete', - path: path, - body: body, - headers: headers, - queryParameters: queryParameters, - apiName: apiName); + methodName: 'delete', + path: path, + body: body, + headers: headers, + queryParameters: queryParameters, + apiName: apiName, + ); } @override @@ -403,11 +409,12 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { String? apiName, }) { return _restFunctionHelper( - methodName: 'head', - path: path, - headers: headers, - queryParameters: queryParameters, - apiName: apiName); + methodName: 'head', + path: path, + headers: headers, + queryParameters: queryParameters, + apiName: apiName, + ); } @override @@ -419,12 +426,13 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { String? apiName, }) { return _restFunctionHelper( - methodName: 'patch', - path: path, - body: body, - headers: headers, - queryParameters: queryParameters, - apiName: apiName); + methodName: 'patch', + path: path, + body: body, + headers: headers, + queryParameters: queryParameters, + apiName: apiName, + ); } /// Cancels a request with a given request ID. From f756a17d6ccd34be5273c4f98f216772ad7a4d73 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Fri, 10 Jun 2022 14:34:17 -0700 Subject: [PATCH 13/26] address some comments --- .../lib/src/types/api/api_types.dart | 4 +-- .../graphql_operation.dart} | 14 ++++---- .../lib/src/types/api/rest/http_payload.dart | 34 +++++++++---------- .../src/types/api/rest/rest_operation.dart | 29 ++++++++++++++++ .../example/lib/rest_api_view.dart | 21 ++++++------ .../lib/src/method_channel_api.dart | 9 ++++- 6 files changed, 74 insertions(+), 37 deletions(-) rename packages/amplify_core/lib/src/types/api/{async/legacy_api_operation.dart => graphql/graphql_operation.dart} (64%) create mode 100644 packages/amplify_core/lib/src/types/api/rest/rest_operation.dart diff --git a/packages/amplify_core/lib/src/types/api/api_types.dart b/packages/amplify_core/lib/src/types/api/api_types.dart index c038b0c239..299fd03412 100644 --- a/packages/amplify_core/lib/src/types/api/api_types.dart +++ b/packages/amplify_core/lib/src/types/api/api_types.dart @@ -13,8 +13,6 @@ * permissions and limitations under the License. */ -export 'async/legacy_api_operation.dart'; - // API Authorization export 'auth/api_auth_provider.dart'; export 'auth/api_authorization_type.dart'; @@ -22,6 +20,7 @@ export 'auth/api_authorization_type.dart'; export 'exceptions/api_exception.dart'; export 'graphql/graphql_helpers.dart'; +export 'graphql/graphql_operation.dart'; export 'graphql/graphql_request.dart'; export 'graphql/graphql_request_type.dart'; export 'graphql/graphql_response.dart'; @@ -30,6 +29,7 @@ export 'graphql/graphql_subscription_operation.dart'; export 'rest/http_payload.dart'; export 'rest/rest_exception.dart'; +export 'rest/rest_operation.dart'; export 'rest/rest_options.dart'; export 'types/pagination/paginated_model_type.dart'; diff --git a/packages/amplify_core/lib/src/types/api/async/legacy_api_operation.dart b/packages/amplify_core/lib/src/types/api/graphql/graphql_operation.dart similarity index 64% rename from packages/amplify_core/lib/src/types/api/async/legacy_api_operation.dart rename to packages/amplify_core/lib/src/types/api/graphql/graphql_operation.dart index 41c36eb711..b9f72dbd37 100644 --- a/packages/amplify_core/lib/src/types/api/async/legacy_api_operation.dart +++ b/packages/amplify_core/lib/src/types/api/graphql/graphql_operation.dart @@ -15,10 +15,12 @@ import 'package:async/async.dart'; -/// Eventually this should be deprecated and just use [CancelableOperation]. -/// Until then, this is used to make `.response` available like it was for older -/// [GraphQLOperation] and [RestOperation] classes. -extension LegacyApiOperation on CancelableOperation { - @Deprecated('Use .value instead.') - Future get response => value; +import 'graphql_response.dart'; + +/// Allows callers to synchronously get the unstreamed response with decoded body. +extension GraphQLOperation on CancelableOperation> { + @Deprecated('use .value instead') + Future> get response { + return value; + } } diff --git a/packages/amplify_core/lib/src/types/api/rest/http_payload.dart b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart index ddadc750a7..e9099f6142 100644 --- a/packages/amplify_core/lib/src/types/api/rest/http_payload.dart +++ b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart @@ -21,10 +21,12 @@ import 'package:async/async.dart'; /// An HTTP request's payload. /// {@endtemplate} class HttpPayload extends StreamView> { + String contentType = 'text/plain'; + /// {@macro amplify_core.http_payload} factory HttpPayload([Object? body]) { if (body == null) { - return const HttpPayload.empty(); + return HttpPayload.empty(); } if (body is String) { return HttpPayload.string(body); @@ -36,13 +38,13 @@ class HttpPayload extends StreamView> { return HttpPayload.streaming(body); } if (body is Map) { - return HttpPayload.fields(body); + return HttpPayload.formFields(body); } throw ArgumentError('Invalid HTTP payload type: ${body.runtimeType}'); } /// An empty HTTP body. - const HttpPayload.empty() : super(const Stream.empty()); + HttpPayload.empty() : super(const Stream.empty()); /// A UTF-8 encoded HTTP body. HttpPayload.string(String body, {Encoding encoding = utf8}) @@ -51,26 +53,24 @@ class HttpPayload extends StreamView> { /// A byte buffer HTTP body. HttpPayload.bytes(List body) : super(Stream.value(body)); - /// Form-encodes the body. - HttpPayload.fields(Map body, {Encoding encoding = utf8}) - : super(LazyStream(() => Stream.value( + /// A form-encoded body of `key=value` pairs. + HttpPayload.formFields(Map body, {Encoding encoding = utf8}) + : contentType = 'application/x-www-form-urlencoded', + super(LazyStream(() => Stream.value( encoding.encode(_mapToQuery(body, encoding: encoding))))); /// A streaming HTTP body. - const HttpPayload.streaming(Stream> body) : super(body); + HttpPayload.streaming(Stream> body) : super(body); } /// Converts a [Map] from parameter names to values to a URL query string. /// -/// mapToQuery({"foo": "bar", "baz": "bang"}); +/// _mapToQuery({"foo": "bar", "baz": "bang"}); /// //=> "foo=bar&baz=bang" /// -/// Copied from similar util. https://github.com/dart-lang/http/blob/06649afbb5847dbb0293816ba8348766b116e419/pkgs/http/lib/src/utils.dart#L15 -String _mapToQuery(Map map, {Encoding? encoding}) { - var pairs = >[]; - map.forEach((key, value) => pairs.add([ - Uri.encodeQueryComponent(key, encoding: encoding ?? utf8), - Uri.encodeQueryComponent(value, encoding: encoding ?? utf8) - ])); - return pairs.map((pair) => '${pair[0]}=${pair[1]}').join('&'); -} +/// Similar util at https://github.com/dart-lang/http/blob/06649afbb5847dbb0293816ba8348766b116e419/pkgs/http/lib/src/utils.dart#L15 +String _mapToQuery(Map map, {required Encoding encoding}) => map + .entries + .map((e) => + '${Uri.encodeQueryComponent(e.key, encoding: encoding)}=${Uri.encodeQueryComponent(e.value, encoding: encoding)}') + .join('&'); diff --git a/packages/amplify_core/lib/src/types/api/rest/rest_operation.dart b/packages/amplify_core/lib/src/types/api/rest/rest_operation.dart new file mode 100644 index 0000000000..a24ad39ad2 --- /dev/null +++ b/packages/amplify_core/lib/src/types/api/rest/rest_operation.dart @@ -0,0 +1,29 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import 'package:async/async.dart'; +import 'package:aws_common/aws_common.dart'; + +/// Allows callers to synchronously get unstreamed response with the decoded body. +extension RestOperation on CancelableOperation { + Future get response async { + final value = await this.value; + return AWSHttpResponse( + body: await value.bodyBytes, + statusCode: value.statusCode, + headers: value.headers, + ); + } +} diff --git a/packages/api/amplify_api/example/lib/rest_api_view.dart b/packages/api/amplify_api/example/lib/rest_api_view.dart index 9a30d68e41..8623c1f9f4 100644 --- a/packages/api/amplify_api/example/lib/rest_api_view.dart +++ b/packages/api/amplify_api/example/lib/rest_api_view.dart @@ -44,10 +44,10 @@ class _RestApiViewState extends State { ); _lastRestOperation = restOperation; - final response = await restOperation.value; + final response = await restOperation.response; print('Put SUCCESS'); - print(await response.decodeBody()); + print(response.decodeBody()); } on Exception catch (e) { print('Put FAILED'); print(e); @@ -62,10 +62,10 @@ class _RestApiViewState extends State { ); _lastRestOperation = restOperation; - final response = await restOperation.value; + final response = await restOperation.response; print('Post SUCCESS'); - print(await response.decodeBody()); + print(response.decodeBody()); } on Exception catch (e) { print('Post FAILED'); print(e); @@ -79,11 +79,10 @@ class _RestApiViewState extends State { ); _lastRestOperation = restOperation; - final response = await restOperation.value; + final response = await restOperation.response; print('Get SUCCESS'); - print(response.statusCode); - print(await response.decodeBody()); + print(response.decodeBody()); } on ApiException catch (e) { print('Get FAILED'); print(e.toString()); @@ -96,10 +95,10 @@ class _RestApiViewState extends State { _apiPathController.text, ); _lastRestOperation = restOperation; - final response = await restOperation.value; + final response = await restOperation.response; print('Delete SUCCESS'); - print(await response.decodeBody()); + print(response.decodeBody()); } on Exception catch (e) { print('Delete FAILED'); print(e); @@ -122,7 +121,7 @@ class _RestApiViewState extends State { ); _lastRestOperation = restOperation; - final response = await restOperation.response; + await restOperation.response; print('Head SUCCESS'); } on ApiException catch (e) { @@ -142,7 +141,7 @@ class _RestApiViewState extends State { final response = await restOperation.response; print('Patch SUCCESS'); - print(await response.decodeBody()); + print(response.decodeBody()); } on ApiException catch (e) { print('Patch FAILED'); print(e.toString()); diff --git a/packages/api/amplify_api/lib/src/method_channel_api.dart b/packages/api/amplify_api/lib/src/method_channel_api.dart index 3fa5773faa..60c6c0b6ec 100644 --- a/packages/api/amplify_api/lib/src/method_channel_api.dart +++ b/packages/api/amplify_api/lib/src/method_channel_api.dart @@ -282,12 +282,19 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { }) { // Send Request cancelToken to Native String cancelToken = uuid(); + // Ensure Content-Type header matches payload. + var modifiedHeaders = headers; + final contentType = body?.contentType; + if (contentType != null) { + modifiedHeaders = headers ?? {}; + modifiedHeaders.putIfAbsent(AWSHeaders.contentType, () => contentType); + } final responseFuture = _restResponseHelper( methodName: methodName, path: path, cancelToken: cancelToken, body: body, - headers: headers, + headers: modifiedHeaders, queryParameters: queryParameters, apiName: apiName, ); From 7dfad32f3c1bc28c8e24c4c06b5fcd40648657c9 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Mon, 13 Jun 2022 10:37:20 -0700 Subject: [PATCH 14/26] fix tests --- .../api/amplify_api/lib/src/method_channel_api.dart | 4 ++-- .../test/amplify_rest_api_methods_test.dart | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/api/amplify_api/lib/src/method_channel_api.dart b/packages/api/amplify_api/lib/src/method_channel_api.dart index 60c6c0b6ec..17c11abead 100644 --- a/packages/api/amplify_api/lib/src/method_channel_api.dart +++ b/packages/api/amplify_api/lib/src/method_channel_api.dart @@ -283,10 +283,10 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { // Send Request cancelToken to Native String cancelToken = uuid(); // Ensure Content-Type header matches payload. - var modifiedHeaders = headers; + var modifiedHeaders = headers != null ? Map.of(headers) : null; final contentType = body?.contentType; if (contentType != null) { - modifiedHeaders = headers ?? {}; + modifiedHeaders = modifiedHeaders ?? {}; modifiedHeaders.putIfAbsent(AWSHeaders.contentType, () => contentType); } final responseFuture = _restResponseHelper( diff --git a/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart b/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart index a718e129c5..f839636f1d 100644 --- a/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart +++ b/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart @@ -34,7 +34,11 @@ const queryParameters = { 'queryParameterA': 'queryValueA', 'queryParameterB': 'queryValueB' }; -const headers = {'headerA': 'headerValueA', 'headerB': 'headerValueB'}; +const headers = { + 'headerA': 'headerValueA', + 'headerB': 'headerValueB', + AWSHeaders.contentType: 'text/plain' +}; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -166,7 +170,8 @@ void main() { expect(restOptions['apiName'], 'restapi'); expect(restOptions['path'], '/items'); expect(restOptions['queryParameters'], queryParameters); - expect(restOptions['headers'], headers); + expect(restOptions['headers'][AWSHeaders.contentType], + 'application/x-www-form-urlencoded'); expect(utf8.decode(restOptions['body'] as List), 'foo=bar'); return { 'data': helloResponse, @@ -179,9 +184,8 @@ void main() { final restOperation = api.post( '/items', apiName: 'restapi', - body: HttpPayload.fields({'foo': 'bar'}), + body: HttpPayload.formFields({'foo': 'bar'}), queryParameters: queryParameters, - headers: headers, ); final response = await restOperation.value; From 3f08e796e125aafa459a9c87ba5fed466df5e016 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Mon, 13 Jun 2022 14:06:23 -0700 Subject: [PATCH 15/26] correct merge --- packages/api/amplify_api/lib/src/api_plugin_impl.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart index cf2521fadb..4e0b559713 100644 --- a/packages/api/amplify_api/lib/src/api_plugin_impl.dart +++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart @@ -117,7 +117,7 @@ class AmplifyAPIDart extends AmplifyAPI { final client = getRestClient(apiName: apiName); return _prepareRestResponse(AWSStreamedHttpRequest.delete( uri, - body: body ?? const HttpPayload.empty(), + body: body ?? HttpPayload.empty(), headers: headers, ).send(client)); } @@ -171,7 +171,7 @@ class AmplifyAPIDart extends AmplifyAPI { AWSStreamedHttpRequest.patch( uri, headers: headers, - body: body ?? const HttpPayload.empty(), + body: body ?? HttpPayload.empty(), ).send(client), ); } @@ -190,7 +190,7 @@ class AmplifyAPIDart extends AmplifyAPI { AWSStreamedHttpRequest.post( uri, headers: headers, - body: body ?? const HttpPayload.empty(), + body: body ?? HttpPayload.empty(), ).send(client), ); } @@ -209,7 +209,7 @@ class AmplifyAPIDart extends AmplifyAPI { AWSStreamedHttpRequest.put( uri, headers: headers, - body: body ?? const HttpPayload.empty(), + body: body ?? HttpPayload.empty(), ).send(client), ); } From 72f6e4b5f1a467083e8ead8caae82beec7a77faf Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Wed, 15 Jun 2022 10:05:53 -0700 Subject: [PATCH 16/26] address comments, json constructor --- .../lib/src/types/api/rest/http_payload.dart | 6 ++++ .../api/amplify_api/example/lib/main.dart | 2 +- .../example/lib/rest_api_view.dart | 6 ++-- .../lib/src/method_channel_api.dart | 2 +- .../test/amplify_rest_api_methods_test.dart | 32 +++++++++++++++++++ 5 files changed, 43 insertions(+), 5 deletions(-) diff --git a/packages/amplify_core/lib/src/types/api/rest/http_payload.dart b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart index e9099f6142..1382a7e477 100644 --- a/packages/amplify_core/lib/src/types/api/rest/http_payload.dart +++ b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart @@ -59,6 +59,12 @@ class HttpPayload extends StreamView> { super(LazyStream(() => Stream.value( encoding.encode(_mapToQuery(body, encoding: encoding))))); + /// Encodes body as a JSON string and sets Content-Type to 'application/json' + HttpPayload.json(Map body, {Encoding encoding = utf8}) + : contentType = 'application/json', + super( + LazyStream(() => Stream.value(encoding.encode(json.encode(body))))); + /// A streaming HTTP body. HttpPayload.streaming(Stream> body) : super(body); } diff --git a/packages/api/amplify_api/example/lib/main.dart b/packages/api/amplify_api/example/lib/main.dart index 5c044e7aec..6e5dbf862d 100644 --- a/packages/api/amplify_api/example/lib/main.dart +++ b/packages/api/amplify_api/example/lib/main.dart @@ -44,7 +44,7 @@ class _MyAppState extends State { } void _configureAmplify() async { - Amplify.addPlugins([AmplifyAuthCognito(), AmplifyAPI()]); + await Amplify.addPlugins([AmplifyAuthCognito(), AmplifyAPI()]); try { await Amplify.configure(amplifyconfig); diff --git a/packages/api/amplify_api/example/lib/rest_api_view.dart b/packages/api/amplify_api/example/lib/rest_api_view.dart index 8623c1f9f4..68f8a414f1 100644 --- a/packages/api/amplify_api/example/lib/rest_api_view.dart +++ b/packages/api/amplify_api/example/lib/rest_api_view.dart @@ -40,7 +40,7 @@ class _RestApiViewState extends State { try { final restOperation = Amplify.API.put( _apiPathController.text, - body: HttpPayload.string('{"name":"Mow the lawn"}'), + body: HttpPayload.json({'name': 'Mow the lawn'}), ); _lastRestOperation = restOperation; @@ -58,7 +58,7 @@ class _RestApiViewState extends State { try { final restOperation = Amplify.API.post( _apiPathController.text, - body: HttpPayload.string('{"name":"Mow the lawn"}'), + body: HttpPayload.json({'name': 'Mow the lawn'}), ); _lastRestOperation = restOperation; @@ -134,7 +134,7 @@ class _RestApiViewState extends State { try { final restOperation = Amplify.API.patch( _apiPathController.text, - body: HttpPayload.string('{"name":"Mow the lawn"}'), + body: HttpPayload.json({'name': 'Mow the lawn'}), ); _lastRestOperation = restOperation; diff --git a/packages/api/amplify_api/lib/src/method_channel_api.dart b/packages/api/amplify_api/lib/src/method_channel_api.dart index 17c11abead..95e8f5c17d 100644 --- a/packages/api/amplify_api/lib/src/method_channel_api.dart +++ b/packages/api/amplify_api/lib/src/method_channel_api.dart @@ -335,7 +335,7 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { return AWSStreamedHttpResponse( statusCode: statusCode, headers: headers, - body: Stream.value(rawResponseBody?.toList() ?? [])); + body: Stream.value(rawResponseBody ?? [])); } @override diff --git a/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart b/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart index f839636f1d..5106ada1c2 100644 --- a/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart +++ b/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart @@ -193,6 +193,38 @@ void main() { await _assertResponse(response); }); + test( + 'POST with json-encoded body has property Content-Type and gets proper response', + () async { + apiChannel.setMockMethodCallHandler((MethodCall methodCall) async { + if (methodCall.method == 'post') { + Map restOptions = + methodCall.arguments['restOptions'] as Map; + expect(restOptions['apiName'], 'restapi'); + expect(restOptions['path'], '/items'); + expect(restOptions['queryParameters'], queryParameters); + expect( + restOptions['headers'][AWSHeaders.contentType], 'application/json'); + expect(utf8.decode(restOptions['body'] as List), '{"foo":"bar"}'); + return { + 'data': helloResponse, + 'statusCode': statusOK, + 'headers': {'foo': 'bar'} + }; + } + }); + + final restOperation = api.post( + '/items', + apiName: 'restapi', + body: HttpPayload.json({'foo': 'bar'}), + queryParameters: queryParameters, + ); + + final response = await restOperation.value; + await _assertResponse(response); + }); + test('CANCEL success does not throw error', () async { // Need to reply with PLACEHOLDER to avoid null issues in _formatRestResponse // In actual production code, the methodChannel doesn't respond to the future response From d98302665e49263a930e94b08cd7ef1a7bdc0146 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Wed, 15 Jun 2022 11:14:34 -0700 Subject: [PATCH 17/26] tweak json encoding --- packages/amplify_core/lib/src/types/api/rest/http_payload.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/amplify_core/lib/src/types/api/rest/http_payload.dart b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart index 1382a7e477..eb657d7543 100644 --- a/packages/amplify_core/lib/src/types/api/rest/http_payload.dart +++ b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart @@ -60,7 +60,7 @@ class HttpPayload extends StreamView> { encoding.encode(_mapToQuery(body, encoding: encoding))))); /// Encodes body as a JSON string and sets Content-Type to 'application/json' - HttpPayload.json(Map body, {Encoding encoding = utf8}) + HttpPayload.json(Object body, {Encoding encoding = utf8}) : contentType = 'application/json', super( LazyStream(() => Stream.value(encoding.encode(json.encode(body))))); From e5d4239b31e54376bc01b55d54dccabec56e4aa8 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Wed, 15 Jun 2022 11:35:34 -0800 Subject: [PATCH 18/26] chore!(api): migrate API category type definitions (#1640) --- .../src/category/amplify_api_category.dart | 134 +++++---- .../lib/src/category/amplify_categories.dart | 1 + .../plugin/amplify_api_plugin_interface.dart | 74 +++-- .../lib/src/types/api/api_types.dart | 2 +- .../types/api/exceptions/api_exception.dart | 9 - .../types/api/graphql/graphql_operation.dart | 15 +- .../lib/src/types/api/rest/http_payload.dart | 82 ++++++ .../src/types/api/rest/rest_exception.dart | 14 +- .../src/types/api/rest/rest_operation.dart | 20 +- .../lib/src/types/api/rest/rest_response.dart | 59 ---- packages/api/amplify_api/.gitignore | 40 ++- .../example/lib/graphql_api_view.dart | 3 +- .../api/amplify_api/example/lib/main.dart | 2 +- .../example/lib/rest_api_view.dart | 65 ++--- packages/api/amplify_api/example/pubspec.yaml | 1 + .../lib/src/method_channel_api.dart | 229 +++++++++++---- packages/api/amplify_api/pubspec.yaml | 3 +- .../test/amplify_rest_api_methods_test.dart | 270 ++++++++---------- .../example/lib/main.dart | 13 +- 19 files changed, 611 insertions(+), 425 deletions(-) create mode 100644 packages/amplify_core/lib/src/types/api/rest/http_payload.dart delete mode 100644 packages/amplify_core/lib/src/types/api/rest/rest_response.dart diff --git a/packages/amplify_core/lib/src/category/amplify_api_category.dart b/packages/amplify_core/lib/src/category/amplify_api_category.dart index 99406d31fc..7d9692a725 100644 --- a/packages/amplify_core/lib/src/category/amplify_api_category.dart +++ b/packages/amplify_core/lib/src/category/amplify_api_category.dart @@ -1,5 +1,5 @@ /* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. @@ -21,17 +21,13 @@ class APICategory extends AmplifyCategory { Category get category => Category.api; // ====== GraphQL ======= - GraphQLOperation query({required GraphQLRequest request}) { - return plugins.length == 1 - ? plugins[0].query(request: request) - : throw _pluginNotAddedException('Api'); - } + CancelableOperation> query( + {required GraphQLRequest request}) => + defaultPlugin.query(request: request); - GraphQLOperation mutate({required GraphQLRequest request}) { - return plugins.length == 1 - ? plugins[0].mutate(request: request) - : throw _pluginNotAddedException('Api'); - } + CancelableOperation> mutate( + {required GraphQLRequest request}) => + defaultPlugin.mutate(request: request); /// Subscribes to the given [request] and returns the stream of response events. /// An optional [onEstablished] callback can be used to be alerted when the @@ -42,52 +38,88 @@ class APICategory extends AmplifyCategory { Stream> subscribe( GraphQLRequest request, { void Function()? onEstablished, - }) { - return plugins.length == 1 - ? plugins[0].subscribe(request, onEstablished: onEstablished) - : throw _pluginNotAddedException('Api'); - } + }) => + defaultPlugin.subscribe(request, onEstablished: onEstablished); // ====== RestAPI ====== - void cancelRequest(String cancelToken) { - return plugins.length == 1 - ? plugins[0].cancelRequest(cancelToken) - : throw _pluginNotAddedException('Api'); - } - RestOperation get({required RestOptions restOptions}) { - return plugins.length == 1 - ? plugins[0].get(restOptions: restOptions) - : throw _pluginNotAddedException('Api'); - } + CancelableOperation delete( + String path, { + Map? headers, + HttpPayload? body, + Map? queryParameters, + String? apiName, + }) => + defaultPlugin.delete( + path, + headers: headers, + body: body, + apiName: apiName, + ); - RestOperation put({required RestOptions restOptions}) { - return plugins.length == 1 - ? plugins[0].put(restOptions: restOptions) - : throw _pluginNotAddedException('Api'); - } + CancelableOperation get( + String path, { + Map? headers, + Map? queryParameters, + String? apiName, + }) => + defaultPlugin.get( + path, + headers: headers, + apiName: apiName, + ); - RestOperation post({required RestOptions restOptions}) { - return plugins.length == 1 - ? plugins[0].post(restOptions: restOptions) - : throw _pluginNotAddedException('Api'); - } + CancelableOperation head( + String path, { + Map? headers, + Map? queryParameters, + String? apiName, + }) => + defaultPlugin.head( + path, + headers: headers, + apiName: apiName, + ); - RestOperation delete({required RestOptions restOptions}) { - return plugins.length == 1 - ? plugins[0].delete(restOptions: restOptions) - : throw _pluginNotAddedException('Api'); - } + CancelableOperation patch( + String path, { + Map? headers, + HttpPayload? body, + Map? queryParameters, + String? apiName, + }) => + defaultPlugin.patch( + path, + headers: headers, + body: body, + apiName: apiName, + ); - RestOperation head({required RestOptions restOptions}) { - return plugins.length == 1 - ? plugins[0].head(restOptions: restOptions) - : throw _pluginNotAddedException('Api'); - } + CancelableOperation post( + String path, { + Map? headers, + HttpPayload? body, + Map? queryParameters, + String? apiName, + }) => + defaultPlugin.post( + path, + headers: headers, + body: body, + apiName: apiName, + ); - RestOperation patch({required RestOptions restOptions}) { - return plugins.length == 1 - ? plugins[0].patch(restOptions: restOptions) - : throw _pluginNotAddedException('Api'); - } + CancelableOperation put( + String path, { + Map? headers, + HttpPayload? body, + Map? queryParameters, + String? apiName, + }) => + defaultPlugin.put( + path, + headers: headers, + body: body, + apiName: apiName, + ); } diff --git a/packages/amplify_core/lib/src/category/amplify_categories.dart b/packages/amplify_core/lib/src/category/amplify_categories.dart index e245f77763..e1e121f0d8 100644 --- a/packages/amplify_core/lib/src/category/amplify_categories.dart +++ b/packages/amplify_core/lib/src/category/amplify_categories.dart @@ -18,6 +18,7 @@ library amplify_interface; import 'dart:async'; import 'package:amplify_core/amplify_core.dart'; +import 'package:async/async.dart'; import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; diff --git a/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart b/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart index d318db6e13..5169acb091 100644 --- a/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart +++ b/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart @@ -1,5 +1,5 @@ /* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ */ import 'package:amplify_core/amplify_core.dart'; +import 'package:async/async.dart'; import 'package:meta/meta.dart'; abstract class APIPluginInterface extends AmplifyPluginInterface { @@ -25,11 +26,13 @@ abstract class APIPluginInterface extends AmplifyPluginInterface { ModelProviderInterface? get modelProvider => throw UnimplementedError(); // ====== GraphQL ======= - GraphQLOperation query({required GraphQLRequest request}) { + CancelableOperation> query( + {required GraphQLRequest request}) { throw UnimplementedError('query() has not been implemented.'); } - GraphQLOperation mutate({required GraphQLRequest request}) { + CancelableOperation> mutate( + {required GraphQLRequest request}) { throw UnimplementedError('mutate() has not been implemented.'); } @@ -50,31 +53,64 @@ abstract class APIPluginInterface extends AmplifyPluginInterface { void registerAuthProvider(APIAuthProvider authProvider); // ====== RestAPI ====== - void cancelRequest(String cancelToken) { - throw UnimplementedError('cancelRequest has not been implemented.'); - } - - RestOperation get({required RestOptions restOptions}) { - throw UnimplementedError('get has not been implemented.'); + CancelableOperation delete( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + throw UnimplementedError('delete() has not been implemented'); } - RestOperation put({required RestOptions restOptions}) { - throw UnimplementedError('put has not been implemented.'); + /// Uses Amplify configuration to authorize request to [path] and returns + /// [CancelableOperation] which resolves to standard HTTP + /// [Response](https://pub.dev/documentation/http/latest/http/Response-class.html). + CancelableOperation get( + String path, { + Map? headers, + Map? queryParameters, + String? apiName, + }) { + throw UnimplementedError('get() has not been implemented'); } - RestOperation post({required RestOptions restOptions}) { - throw UnimplementedError('post has not been implemented.'); + CancelableOperation head( + String path, { + Map? headers, + Map? queryParameters, + String? apiName, + }) { + throw UnimplementedError('head() has not been implemented'); } - RestOperation delete({required RestOptions restOptions}) { - throw UnimplementedError('delete has not been implemented.'); + CancelableOperation patch( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + throw UnimplementedError('patch() has not been implemented'); } - RestOperation head({required RestOptions restOptions}) { - throw UnimplementedError('head has not been implemented.'); + CancelableOperation post( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + throw UnimplementedError('post() has not been implemented'); } - RestOperation patch({required RestOptions restOptions}) { - throw UnimplementedError('patch has not been implemented.'); + CancelableOperation put( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + throw UnimplementedError('put() has not been implemented'); } } diff --git a/packages/amplify_core/lib/src/types/api/api_types.dart b/packages/amplify_core/lib/src/types/api/api_types.dart index 3e69a1dc4b..299fd03412 100644 --- a/packages/amplify_core/lib/src/types/api/api_types.dart +++ b/packages/amplify_core/lib/src/types/api/api_types.dart @@ -27,10 +27,10 @@ export 'graphql/graphql_response.dart'; export 'graphql/graphql_response_error.dart'; export 'graphql/graphql_subscription_operation.dart'; +export 'rest/http_payload.dart'; export 'rest/rest_exception.dart'; export 'rest/rest_operation.dart'; export 'rest/rest_options.dart'; -export 'rest/rest_response.dart'; export 'types/pagination/paginated_model_type.dart'; export 'types/pagination/paginated_result.dart'; diff --git a/packages/amplify_core/lib/src/types/api/exceptions/api_exception.dart b/packages/amplify_core/lib/src/types/api/exceptions/api_exception.dart index 9f9d833110..2ec1bf37ac 100644 --- a/packages/amplify_core/lib/src/types/api/exceptions/api_exception.dart +++ b/packages/amplify_core/lib/src/types/api/exceptions/api_exception.dart @@ -19,18 +19,11 @@ import 'package:amplify_core/amplify_core.dart'; /// Exception thrown from the API Category. /// {@endtemplate} class ApiException extends AmplifyException { - /// HTTP status of response, only available if error - @Deprecated( - 'Use RestException instead to retrieve the HTTP response. Existing uses of ' - 'ApiException for handling REST errors can be safely replaced with RestException') - final int? httpStatusCode; - /// {@macro api_exception} const ApiException( String message, { String? recoverySuggestion, String? underlyingException, - this.httpStatusCode, }) : super( message, recoverySuggestion: recoverySuggestion, @@ -40,7 +33,6 @@ class ApiException extends AmplifyException { /// Constructor for down casting an AmplifyException to this exception ApiException._private( AmplifyException exception, - this.httpStatusCode, ) : super( exception.message, recoverySuggestion: exception.recoverySuggestion, @@ -57,7 +49,6 @@ class ApiException extends AmplifyException { } return ApiException._private( AmplifyException.fromMap(serializedException), - statusCode, ); } } diff --git a/packages/amplify_core/lib/src/types/api/graphql/graphql_operation.dart b/packages/amplify_core/lib/src/types/api/graphql/graphql_operation.dart index 94035a8997..b9f72dbd37 100644 --- a/packages/amplify_core/lib/src/types/api/graphql/graphql_operation.dart +++ b/packages/amplify_core/lib/src/types/api/graphql/graphql_operation.dart @@ -1,5 +1,5 @@ /* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. @@ -13,11 +13,14 @@ * permissions and limitations under the License. */ -import 'package:amplify_core/amplify_core.dart'; +import 'package:async/async.dart'; -class GraphQLOperation { - final Function cancel; - final Future> response; +import 'graphql_response.dart'; - const GraphQLOperation({required this.response, required this.cancel}); +/// Allows callers to synchronously get the unstreamed response with decoded body. +extension GraphQLOperation on CancelableOperation> { + @Deprecated('use .value instead') + Future> get response { + return value; + } } diff --git a/packages/amplify_core/lib/src/types/api/rest/http_payload.dart b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart new file mode 100644 index 0000000000..eb657d7543 --- /dev/null +++ b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart @@ -0,0 +1,82 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +import 'dart:async'; +import 'dart:convert'; + +import 'package:async/async.dart'; + +/// {@template amplify_core.http_payload} +/// An HTTP request's payload. +/// {@endtemplate} +class HttpPayload extends StreamView> { + String contentType = 'text/plain'; + + /// {@macro amplify_core.http_payload} + factory HttpPayload([Object? body]) { + if (body == null) { + return HttpPayload.empty(); + } + if (body is String) { + return HttpPayload.string(body); + } + if (body is List) { + return HttpPayload.bytes(body); + } + if (body is Stream>) { + return HttpPayload.streaming(body); + } + if (body is Map) { + return HttpPayload.formFields(body); + } + throw ArgumentError('Invalid HTTP payload type: ${body.runtimeType}'); + } + + /// An empty HTTP body. + HttpPayload.empty() : super(const Stream.empty()); + + /// A UTF-8 encoded HTTP body. + HttpPayload.string(String body, {Encoding encoding = utf8}) + : super(LazyStream(() => Stream.value(encoding.encode(body)))); + + /// A byte buffer HTTP body. + HttpPayload.bytes(List body) : super(Stream.value(body)); + + /// A form-encoded body of `key=value` pairs. + HttpPayload.formFields(Map body, {Encoding encoding = utf8}) + : contentType = 'application/x-www-form-urlencoded', + super(LazyStream(() => Stream.value( + encoding.encode(_mapToQuery(body, encoding: encoding))))); + + /// Encodes body as a JSON string and sets Content-Type to 'application/json' + HttpPayload.json(Object body, {Encoding encoding = utf8}) + : contentType = 'application/json', + super( + LazyStream(() => Stream.value(encoding.encode(json.encode(body))))); + + /// A streaming HTTP body. + HttpPayload.streaming(Stream> body) : super(body); +} + +/// Converts a [Map] from parameter names to values to a URL query string. +/// +/// _mapToQuery({"foo": "bar", "baz": "bang"}); +/// //=> "foo=bar&baz=bang" +/// +/// Similar util at https://github.com/dart-lang/http/blob/06649afbb5847dbb0293816ba8348766b116e419/pkgs/http/lib/src/utils.dart#L15 +String _mapToQuery(Map map, {required Encoding encoding}) => map + .entries + .map((e) => + '${Uri.encodeQueryComponent(e.key, encoding: encoding)}=${Uri.encodeQueryComponent(e.value, encoding: encoding)}') + .join('&'); diff --git a/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart b/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart index fe6a6a8ee5..1f6dc18c2e 100644 --- a/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart +++ b/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart @@ -19,16 +19,8 @@ import 'package:amplify_core/amplify_core.dart'; /// An HTTP error encountered during a REST API call, i.e. for calls returning /// non-2xx status codes. /// {@endtemplate} -class RestException extends ApiException { - /// The HTTP response from the server. - final RestResponse response; - +@Deprecated('BREAKING CHANGE: No longer thrown for non-200 responses.') +abstract class RestException extends ApiException { /// {@macro rest_exception} - RestException(this.response) - : super(response.body, httpStatusCode: response.statusCode); - - @override - String toString() { - return 'RestException{response=$response}'; - } + const RestException() : super('REST exception.'); } diff --git a/packages/amplify_core/lib/src/types/api/rest/rest_operation.dart b/packages/amplify_core/lib/src/types/api/rest/rest_operation.dart index eb84a0ea42..a24ad39ad2 100644 --- a/packages/amplify_core/lib/src/types/api/rest/rest_operation.dart +++ b/packages/amplify_core/lib/src/types/api/rest/rest_operation.dart @@ -1,5 +1,5 @@ /* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. @@ -13,11 +13,17 @@ * permissions and limitations under the License. */ -import 'rest_response.dart'; +import 'package:async/async.dart'; +import 'package:aws_common/aws_common.dart'; -class RestOperation { - final Function cancel; - final Future response; - - const RestOperation({required this.response, required this.cancel}); +/// Allows callers to synchronously get unstreamed response with the decoded body. +extension RestOperation on CancelableOperation { + Future get response async { + final value = await this.value; + return AWSHttpResponse( + body: await value.bodyBytes, + statusCode: value.statusCode, + headers: value.headers, + ); + } } diff --git a/packages/amplify_core/lib/src/types/api/rest/rest_response.dart b/packages/amplify_core/lib/src/types/api/rest/rest_response.dart deleted file mode 100644 index f93a2079e4..0000000000 --- a/packages/amplify_core/lib/src/types/api/rest/rest_response.dart +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0 - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:amplify_core/amplify_core.dart'; -import 'package:meta/meta.dart'; - -/// {@template rest_response} -/// An HTTP response from a REST API call. -/// {@endtemplate} -@immutable -class RestResponse with AWSEquatable { - /// The response status code. - final int statusCode; - - /// The response headers. - /// - /// Will be `null` if unavailable from the platform. - final Map? headers; - - /// The response body bytes. - final Uint8List data; - - /// The decoded body (using UTF-8). - /// - /// For custom processing, use [data] to get the raw body bytes. - late final String body; - - /// {@macro rest_response} - RestResponse({ - required Uint8List? data, - required this.headers, - required this.statusCode, - }) : data = data ?? Uint8List(0) { - body = utf8.decode(this.data, allowMalformed: true); - } - - @override - List get props => [statusCode, headers, data]; - - @override - String toString() { - return 'RestResponse{statusCode=$statusCode, headers=$headers, body=$body}'; - } -} diff --git a/packages/api/amplify_api/.gitignore b/packages/api/amplify_api/.gitignore index e9dc58d3d6..6bb69a50e0 100644 --- a/packages/api/amplify_api/.gitignore +++ b/packages/api/amplify_api/.gitignore @@ -1,7 +1,43 @@ +# See https://dart.dev/guides/libraries/private-files + +# Miscellaneous +*.class +*.log +*.pyc +*.swp .DS_Store -.dart_tool/ +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies .packages +.pub-cache/ .pub/ - build/ + +# Code coverage +coverage/ + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/api/amplify_api/example/lib/graphql_api_view.dart b/packages/api/amplify_api/example/lib/graphql_api_view.dart index 53a218efcd..6644dad380 100644 --- a/packages/api/amplify_api/example/lib/graphql_api_view.dart +++ b/packages/api/amplify_api/example/lib/graphql_api_view.dart @@ -14,6 +14,7 @@ */ import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:async/async.dart'; import 'package:flutter/material.dart'; class GraphQLApiView extends StatefulWidget { @@ -29,7 +30,7 @@ class GraphQLApiView extends StatefulWidget { class _GraphQLApiViewState extends State { String _result = ''; void Function()? _unsubscribe; - late GraphQLOperation _lastOperation; + late CancelableOperation _lastOperation; Future subscribe() async { String graphQLDocument = '''subscription MySubscription { diff --git a/packages/api/amplify_api/example/lib/main.dart b/packages/api/amplify_api/example/lib/main.dart index 5c044e7aec..6e5dbf862d 100644 --- a/packages/api/amplify_api/example/lib/main.dart +++ b/packages/api/amplify_api/example/lib/main.dart @@ -44,7 +44,7 @@ class _MyAppState extends State { } void _configureAmplify() async { - Amplify.addPlugins([AmplifyAuthCognito(), AmplifyAPI()]); + await Amplify.addPlugins([AmplifyAuthCognito(), AmplifyAPI()]); try { await Amplify.configure(amplifyconfig); diff --git a/packages/api/amplify_api/example/lib/rest_api_view.dart b/packages/api/amplify_api/example/lib/rest_api_view.dart index aeca89c97f..68f8a414f1 100644 --- a/packages/api/amplify_api/example/lib/rest_api_view.dart +++ b/packages/api/amplify_api/example/lib/rest_api_view.dart @@ -13,9 +13,8 @@ * permissions and limitations under the License. */ -import 'dart:convert'; - import 'package:amplify_flutter/amplify_flutter.dart'; +import 'package:async/async.dart'; import 'package:flutter/material.dart'; class RestApiView extends StatefulWidget { @@ -27,7 +26,7 @@ class RestApiView extends StatefulWidget { class _RestApiViewState extends State { late TextEditingController _apiPathController; - late RestOperation _lastRestOperation; + late CancelableOperation _lastRestOperation; @override void initState() { @@ -39,18 +38,16 @@ class _RestApiViewState extends State { void onPutPressed() async { try { - RestOperation restOperation = Amplify.API.put( - restOptions: RestOptions( - path: _apiPathController.text, - body: ascii.encode('{"name":"Mow the lawn"}'), - ), + final restOperation = Amplify.API.put( + _apiPathController.text, + body: HttpPayload.json({'name': 'Mow the lawn'}), ); _lastRestOperation = restOperation; - RestResponse response = await restOperation.response; + final response = await restOperation.response; print('Put SUCCESS'); - print(response); + print(response.decodeBody()); } on Exception catch (e) { print('Put FAILED'); print(e); @@ -59,18 +56,16 @@ class _RestApiViewState extends State { void onPostPressed() async { try { - RestOperation restOperation = Amplify.API.post( - restOptions: RestOptions( - path: _apiPathController.text, - body: ascii.encode('{"name":"Mow the lawn"}'), - ), + final restOperation = Amplify.API.post( + _apiPathController.text, + body: HttpPayload.json({'name': 'Mow the lawn'}), ); _lastRestOperation = restOperation; - RestResponse response = await restOperation.response; + final response = await restOperation.response; print('Post SUCCESS'); - print(response); + print(response.decodeBody()); } on Exception catch (e) { print('Post FAILED'); print(e); @@ -79,16 +74,15 @@ class _RestApiViewState extends State { void onGetPressed() async { try { - RestOperation restOperation = Amplify.API.get( - restOptions: RestOptions( - path: _apiPathController.text, - )); + final restOperation = Amplify.API.get( + _apiPathController.text, + ); _lastRestOperation = restOperation; - RestResponse response = await restOperation.response; + final response = await restOperation.response; print('Get SUCCESS'); - print(response); + print(response.decodeBody()); } on ApiException catch (e) { print('Get FAILED'); print(e.toString()); @@ -97,15 +91,14 @@ class _RestApiViewState extends State { void onDeletePressed() async { try { - RestOperation restOperation = Amplify.API.delete( - restOptions: RestOptions(path: _apiPathController.text), + final restOperation = Amplify.API.delete( + _apiPathController.text, ); - _lastRestOperation = restOperation; - RestResponse response = await restOperation.response; + final response = await restOperation.response; print('Delete SUCCESS'); - print(response); + print(response.decodeBody()); } on Exception catch (e) { print('Delete FAILED'); print(e); @@ -123,15 +116,14 @@ class _RestApiViewState extends State { void onHeadPressed() async { try { - RestOperation restOperation = Amplify.API.head( - restOptions: RestOptions(path: _apiPathController.text), + final restOperation = Amplify.API.head( + _apiPathController.text, ); _lastRestOperation = restOperation; - RestResponse response = await restOperation.response; + await restOperation.response; print('Head SUCCESS'); - print(response); } on ApiException catch (e) { print('Head FAILED'); print(e.toString()); @@ -140,15 +132,16 @@ class _RestApiViewState extends State { void onPatchPressed() async { try { - RestOperation restOperation = Amplify.API.patch( - restOptions: RestOptions(path: _apiPathController.text), + final restOperation = Amplify.API.patch( + _apiPathController.text, + body: HttpPayload.json({'name': 'Mow the lawn'}), ); _lastRestOperation = restOperation; - RestResponse response = await restOperation.response; + final response = await restOperation.response; print('Patch SUCCESS'); - print(response); + print(response.decodeBody()); } on ApiException catch (e) { print('Patch FAILED'); print(e.toString()); diff --git a/packages/api/amplify_api/example/pubspec.yaml b/packages/api/amplify_api/example/pubspec.yaml index 4e57ade772..7b017f9370 100644 --- a/packages/api/amplify_api/example/pubspec.yaml +++ b/packages/api/amplify_api/example/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: path: ../../../auth/amplify_auth_cognito amplify_flutter: path: ../../../amplify/amplify_flutter + async: ^2.8.2 aws_common: ^0.1.0 # The following adds the Cupertino Icons font to your application. diff --git a/packages/api/amplify_api/lib/src/method_channel_api.dart b/packages/api/amplify_api/lib/src/method_channel_api.dart index 59deb7fca0..95e8f5c17d 100644 --- a/packages/api/amplify_api/lib/src/method_channel_api.dart +++ b/packages/api/amplify_api/lib/src/method_channel_api.dart @@ -14,13 +14,14 @@ */ import 'dart:async'; +import 'dart:convert'; import 'dart:typed_data'; import 'package:amplify_api/src/graphql/graphql_response_decoder.dart'; import 'package:amplify_api/src/graphql/graphql_subscription_event.dart'; import 'package:amplify_api/src/graphql/graphql_subscription_transformer.dart'; import 'package:amplify_core/amplify_core.dart'; - +import 'package:async/async.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -150,31 +151,19 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { } @override - GraphQLOperation query({required GraphQLRequest request}) { - Future> response = + CancelableOperation> query( + {required GraphQLRequest request}) { + Future> responseFuture = _getMethodChannelResponse(methodName: 'query', request: request); - - //TODO: Cancel implementation will be added along with REST API as it is shared - GraphQLOperation result = GraphQLOperation( - cancel: () => cancelRequest(request.id), - response: response, - ); - - return result; + return CancelableOperation.fromFuture(responseFuture); } @override - GraphQLOperation mutate({required GraphQLRequest request}) { - Future> response = + CancelableOperation> mutate( + {required GraphQLRequest request}) { + Future> responseFuture = _getMethodChannelResponse(methodName: 'mutate', request: request); - - //TODO: Cancel implementation will be added along with REST API as it is shared - GraphQLOperation result = GraphQLOperation( - cancel: () => cancelRequest(request.id), - response: response, - ); - - return result; + return CancelableOperation.fromFuture(responseFuture); } @override @@ -248,21 +237,73 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { } // ====== RestAPI ====== - RestOperation _restFunctionHelper( - {required String methodName, required RestOptions restOptions}) { - // Send Request cancelToken to Native - String cancelToken = UUID.getUUID(); - Future futureResponse = - _callNativeRestMethod(methodName, cancelToken, restOptions); + Future _restResponseHelper({ + required String methodName, + required String path, + required String cancelToken, + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) async { + Uint8List? bodyBytes; + if (body != null) { + final completer = Completer(); + final sink = ByteConversionSink.withCallback( + (bytes) => completer.complete(Uint8List.fromList(bytes)), + ); + body.listen( + sink.add, + onError: completer.completeError, + onDone: sink.close, + cancelOnError: true, + ); + bodyBytes = await completer.future; + } - return RestOperation( - response: futureResponse, - cancel: () => cancelRequest(cancelToken), + final restOptions = RestOptions( + path: path, + body: bodyBytes, + apiName: apiName, + queryParameters: queryParameters, + headers: headers, + ); + return _callNativeRestMethod(methodName, cancelToken, restOptions); + } + + CancelableOperation _restFunctionHelper({ + required String methodName, + required String path, + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + // Send Request cancelToken to Native + String cancelToken = uuid(); + // Ensure Content-Type header matches payload. + var modifiedHeaders = headers != null ? Map.of(headers) : null; + final contentType = body?.contentType; + if (contentType != null) { + modifiedHeaders = modifiedHeaders ?? {}; + modifiedHeaders.putIfAbsent(AWSHeaders.contentType, () => contentType); + } + final responseFuture = _restResponseHelper( + methodName: methodName, + path: path, + cancelToken: cancelToken, + body: body, + headers: modifiedHeaders, + queryParameters: queryParameters, + apiName: apiName, ); + + return CancelableOperation.fromFuture(responseFuture, + onCancel: () => cancelRequest(cancelToken)); } - Future _callNativeRestMethod( + Future _callNativeRestMethod( String methodName, String cancelToken, RestOptions restOptions) async { // Prepare map input Map inputsMap = {}; @@ -284,55 +325,125 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { } } - bool _shouldThrow(int statusCode) { - return statusCode < 200 || statusCode > 299; - } - - RestResponse _formatRestResponse(Map res) { + AWSStreamedHttpResponse _formatRestResponse(Map res) { final statusCode = res['statusCode'] as int; - final headers = res['headers'] as Map?; - final response = RestResponse( - data: res['data'] as Uint8List?, - headers: headers?.cast(), - statusCode: statusCode, - ); - if (_shouldThrow(statusCode)) { - throw RestException(response); - } - return response; + // Make type-safe version of response headers. + final serializedHeaders = res['headers'] as Map?; + final headers = serializedHeaders?.cast(); + final rawResponseBody = res['data'] as Uint8List?; + + return AWSStreamedHttpResponse( + statusCode: statusCode, + headers: headers, + body: Stream.value(rawResponseBody ?? [])); } @override - RestOperation get({required RestOptions restOptions}) { - return _restFunctionHelper(methodName: 'get', restOptions: restOptions); + CancelableOperation get( + String path, { + Map? headers, + Map? queryParameters, + String? apiName, + }) { + return _restFunctionHelper( + methodName: 'get', + path: path, + headers: headers, + queryParameters: queryParameters, + apiName: apiName, + ); } @override - RestOperation put({required RestOptions restOptions}) { - return _restFunctionHelper(methodName: 'put', restOptions: restOptions); + CancelableOperation put( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + return _restFunctionHelper( + methodName: 'put', + path: path, + body: body, + headers: headers, + queryParameters: queryParameters, + apiName: apiName, + ); } @override - RestOperation post({required RestOptions restOptions}) { - return _restFunctionHelper(methodName: 'post', restOptions: restOptions); + CancelableOperation post( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + return _restFunctionHelper( + methodName: 'post', + path: path, + body: body, + headers: headers, + queryParameters: queryParameters, + apiName: apiName, + ); } @override - RestOperation delete({required RestOptions restOptions}) { - return _restFunctionHelper(methodName: 'delete', restOptions: restOptions); + CancelableOperation delete( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + return _restFunctionHelper( + methodName: 'delete', + path: path, + body: body, + headers: headers, + queryParameters: queryParameters, + apiName: apiName, + ); } @override - RestOperation head({required RestOptions restOptions}) { - return _restFunctionHelper(methodName: 'head', restOptions: restOptions); + CancelableOperation head( + String path, { + Map? headers, + Map? queryParameters, + String? apiName, + }) { + return _restFunctionHelper( + methodName: 'head', + path: path, + headers: headers, + queryParameters: queryParameters, + apiName: apiName, + ); } @override - RestOperation patch({required RestOptions restOptions}) { - return _restFunctionHelper(methodName: 'patch', restOptions: restOptions); + CancelableOperation patch( + String path, { + HttpPayload? body, + Map? headers, + Map? queryParameters, + String? apiName, + }) { + return _restFunctionHelper( + methodName: 'patch', + path: path, + body: body, + headers: headers, + queryParameters: queryParameters, + apiName: apiName, + ); } - @override + /// Cancels a request with a given request ID. + @Deprecated('Use .cancel() on CancelableOperation instead.') Future cancelRequest(String cancelToken) async { print('Attempting to cancel Operation $cancelToken'); diff --git a/packages/api/amplify_api/pubspec.yaml b/packages/api/amplify_api/pubspec.yaml index 4305f09b30..869779172e 100644 --- a/packages/api/amplify_api/pubspec.yaml +++ b/packages/api/amplify_api/pubspec.yaml @@ -2,6 +2,7 @@ name: amplify_api description: The Amplify Flutter API category plugin, supporting GraphQL and REST operations. version: 0.5.0 homepage: https://github.com/aws-amplify/amplify-flutter/tree/main/packages/amplify_api +publish_to: none # until finalized environment: sdk: ">=2.17.0 <3.0.0" @@ -12,6 +13,7 @@ dependencies: amplify_api_ios: 0.5.0 amplify_core: 0.5.0 amplify_flutter: 0.5.0 + async: ^2.8.2 aws_common: ^0.1.0 collection: ^1.15.0 flutter: @@ -23,7 +25,6 @@ dev_dependencies: amplify_lints: ^2.0.0 amplify_test: path: ../../amplify_test - async: ^2.6.0 build_runner: ^2.0.0 flutter_test: sdk: flutter diff --git a/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart b/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart index 925c940b6b..5106ada1c2 100644 --- a/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart +++ b/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart @@ -26,9 +26,19 @@ import 'graphql_helpers_test.dart'; const statusOK = 200; const statusBadRequest = 400; - -// Matchers -final throwsRestException = throwsA(isA()); +const mowLawnBody = '{"name": "Mow the lawn"}'; +const hello = 'Hello from lambda!'; +final helloResponse = ascii.encode(hello); +final encodedMowLoanBody = ascii.encode(mowLawnBody); +const queryParameters = { + 'queryParameterA': 'queryValueA', + 'queryParameterB': 'queryValueB' +}; +const headers = { + 'headerA': 'headerValueA', + 'headerB': 'headerValueB', + AWSHeaders.contentType: 'text/plain' +}; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -42,184 +52,177 @@ void main() { await Amplify.addPlugin(api); }); - test('PUT returns proper response.data', () async { - var responseData = Uint8List.fromList( - '{"success": "put call succeed!","url":/items?queryParameterA=queryValueA&queryParameterB=queryValueB","body": {"name": "Mow the lawn"}}' - .codeUnits); - var body = Uint8List.fromList('{"name":"Mow the lawn"}'.codeUnits); - var queryParameters = { - 'queryParameterA': 'queryValueA', - 'queryParameterB': 'queryValueB' - }; - var headers = {'headerA': 'headerValueA', 'headerB': 'headerValueB'}; + Future _assertResponse(AWSStreamedHttpResponse response) async { + final actualResponseBody = await response.decodeBody(); + expect(actualResponseBody, hello); + expect(response.statusCode, statusOK); + } + test('PUT returns proper response.data', () async { apiChannel.setMockMethodCallHandler((MethodCall methodCall) async { if (methodCall.method == 'put') { Map restOptions = methodCall.arguments['restOptions'] as Map; expect(restOptions['apiName'], 'restapi'); expect(restOptions['path'], '/items'); - expect(restOptions['body'], body); + expect(restOptions['body'], encodedMowLoanBody); expect(restOptions['queryParameters'], queryParameters); expect(restOptions['headers'], headers); - - return {'data': responseData, 'statusCode': statusOK}; + return {'data': helloResponse, 'statusCode': statusOK}; } }); - RestOperation restOperation = api.put( - restOptions: RestOptions( - path: '/items', - body: body, - apiName: 'restapi', - queryParameters: queryParameters, - headers: headers, - ), + final restOperation = api.put( + '/items', + body: HttpPayload.string(mowLawnBody), + apiName: 'restapi', + queryParameters: queryParameters, + headers: headers, ); - RestResponse response = await restOperation.response; - - expect(response.data, responseData); + final response = await restOperation.value; + await _assertResponse(response); }); test('POST returns proper response.data', () async { - var responseData = Uint8List.fromList( - '{"success": "post call succeed!","url":"/items?queryParameterA=queryValueA&queryParameterB=queryValueB","body": {"name": "Mow the lawn"}}' - .codeUnits); - var body = Uint8List.fromList('{"name":"Mow the lawn"}'.codeUnits); - var queryParameters = { - 'queryParameterA': 'queryValueA', - 'queryParameterB': 'queryValueB' - }; - var headers = {'headerA': 'headerValueA', 'headerB': 'headerValueB'}; - apiChannel.setMockMethodCallHandler((MethodCall methodCall) async { if (methodCall.method == 'post') { Map restOptions = methodCall.arguments['restOptions'] as Map; expect(restOptions['apiName'], 'restapi'); expect(restOptions['path'], '/items'); - expect(restOptions['body'], body); + expect(restOptions['body'], encodedMowLoanBody); expect(restOptions['queryParameters'], queryParameters); expect(restOptions['headers'], headers); - - return {'data': responseData, 'statusCode': statusOK}; + return {'data': helloResponse, 'statusCode': statusOK}; } }); - RestOperation restOperation = api.post( - restOptions: RestOptions( - path: '/items', - body: body, - apiName: 'restapi', - headers: headers, - queryParameters: queryParameters, - ), + final restOperation = api.post( + '/items', + body: HttpPayload.string(mowLawnBody), + apiName: 'restapi', + queryParameters: queryParameters, + headers: headers, ); - RestResponse response = await restOperation.response; - - expect(response.data, responseData); + final response = await restOperation.value; + await _assertResponse(response); }); test('GET returns proper response.data', () async { - var responseData = Uint8List.fromList( - '{"success":"get call succeed!","url":"/items"}'.codeUnits); - apiChannel.setMockMethodCallHandler((MethodCall methodCall) async { if (methodCall.method == 'get') { Map restOptions = methodCall.arguments['restOptions'] as Map; + expect(restOptions['apiName'], 'restapi'); expect(restOptions['path'], '/items'); - - return {'data': responseData, 'statusCode': statusOK}; + expect(restOptions['queryParameters'], queryParameters); + expect(restOptions['headers'], headers); + return {'data': helloResponse, 'statusCode': statusOK}; } }); - RestOperation restOperation = api.get( - restOptions: const RestOptions( - path: '/items', - )); - - RestResponse response = await restOperation.response; + final restOperation = api.get( + '/items', + apiName: 'restapi', + queryParameters: queryParameters, + headers: headers, + ); - expect(response.data, responseData); + final response = await restOperation.value; + await _assertResponse(response); }); test('DELETE returns proper response.data', () async { - var responseData = Uint8List.fromList( - '{"success":"delete call succeed!","url":"/items"}'.codeUnits); - apiChannel.setMockMethodCallHandler((MethodCall methodCall) async { if (methodCall.method == 'delete') { Map restOptions = methodCall.arguments['restOptions'] as Map; + expect(restOptions['apiName'], 'restapi'); expect(restOptions['path'], '/items'); - - return {'data': responseData, 'statusCode': statusOK}; + expect(restOptions['body'], encodedMowLoanBody); + expect(restOptions['queryParameters'], queryParameters); + expect(restOptions['headers'], headers); + return {'data': helloResponse, 'statusCode': statusOK}; } }); - RestOperation restOperation = api.delete( - restOptions: const RestOptions( - path: '/items', - )); - - RestResponse response = await restOperation.response; + final restOperation = api.delete( + '/items', + body: HttpPayload.string(mowLawnBody), + apiName: 'restapi', + queryParameters: queryParameters, + headers: headers, + ); - expect(response.data, responseData); + final response = await restOperation.value; + await _assertResponse(response); }); - test('GET Status Code Error throws proper error', () async { + test( + 'POST with form-encoded body gets proper response with response headers included', + () async { apiChannel.setMockMethodCallHandler((MethodCall methodCall) async { - if (methodCall.method == 'get') { - throw PlatformException(code: 'ApiException', details: { - 'message': 'AMPLIFY_API_MUTATE_FAILED', - 'recoverySuggestion': 'some insightful suggestion', - 'underlyingException': 'Act of God' - }); + if (methodCall.method == 'post') { + Map restOptions = + methodCall.arguments['restOptions'] as Map; + expect(restOptions['apiName'], 'restapi'); + expect(restOptions['path'], '/items'); + expect(restOptions['queryParameters'], queryParameters); + expect(restOptions['headers'][AWSHeaders.contentType], + 'application/x-www-form-urlencoded'); + expect(utf8.decode(restOptions['body'] as List), 'foo=bar'); + return { + 'data': helloResponse, + 'statusCode': statusOK, + 'headers': {'foo': 'bar'} + }; } }); - try { - RestOperation restOperation = api.get( - restOptions: const RestOptions( - path: '/items', - )); - await restOperation.response; - } on ApiException catch (e) { - expect(e.message, 'AMPLIFY_API_MUTATE_FAILED'); - expect(e.recoverySuggestion, 'some insightful suggestion'); - expect(e.underlyingException, 'Act of God'); - } + final restOperation = api.post( + '/items', + apiName: 'restapi', + body: HttpPayload.formFields({'foo': 'bar'}), + queryParameters: queryParameters, + ); + + final response = await restOperation.value; + expect(response.headers['foo'], 'bar'); + await _assertResponse(response); }); - test('GET exception adds the httpStatusCode to exception if available', + test( + 'POST with json-encoded body has property Content-Type and gets proper response', () async { - const statusCode = 500; - const data = 'Internal server error'; - apiChannel.setMockMethodCallHandler((MethodCall methodCall) async { - if (methodCall.method == 'get') { + if (methodCall.method == 'post') { + Map restOptions = + methodCall.arguments['restOptions'] as Map; + expect(restOptions['apiName'], 'restapi'); + expect(restOptions['path'], '/items'); + expect(restOptions['queryParameters'], queryParameters); + expect( + restOptions['headers'][AWSHeaders.contentType], 'application/json'); + expect(utf8.decode(restOptions['body'] as List), '{"foo":"bar"}'); return { - 'statusCode': statusCode, - 'headers': {}, - 'data': Uint8List.fromList(data.codeUnits), + 'data': helloResponse, + 'statusCode': statusOK, + 'headers': {'foo': 'bar'} }; } }); - try { - RestOperation restOperation = api.get( - restOptions: const RestOptions( - path: '/items', - ), - ); - await restOperation.response; - } on RestException catch (e) { - expect(e.response.statusCode, 500); - expect(e.response.body, data); - } + final restOperation = api.post( + '/items', + apiName: 'restapi', + body: HttpPayload.json({'foo': 'bar'}), + queryParameters: queryParameters, + ); + + final response = await restOperation.value; + await _assertResponse(response); }); test('CANCEL success does not throw error', () async { @@ -237,50 +240,9 @@ void main() { } }); - RestOperation restOperation = api.get( - restOptions: const RestOptions( - path: '/items', - )); + final restOperation = api.get('/items'); //RestResponse response = await restOperation.response; restOperation.cancel(); }); - - group('non-2xx status code', () { - const testBody = 'test'; - const testResponseHeaders = {'key': 'value'}; - - setUpAll(() { - apiChannel.setMockMethodCallHandler((call) async { - return { - 'data': utf8.encode(testBody), - 'statusCode': statusBadRequest, - 'headers': testResponseHeaders, - }; - }); - }); - - test('throws RestException', () async { - final restOp = api.get(restOptions: const RestOptions(path: '/')); - await expectLater(restOp.response, throwsRestException); - }); - - test('has valid RestResponse', () async { - final restOp = api.get(restOptions: const RestOptions(path: '/')); - - RestException restException; - try { - await restOp.response; - fail('RestOperation should throw'); - } on Exception catch (e) { - expect(e, isA()); - restException = e as RestException; - } - - final response = restException.response; - expect(response.statusCode, statusBadRequest); - expect(response.headers, testResponseHeaders); - expect(response.body, testBody); - }); - }); } diff --git a/packages/auth/amplify_auth_cognito/example/lib/main.dart b/packages/auth/amplify_auth_cognito/example/lib/main.dart index 6996c306ac..78b325ebf6 100644 --- a/packages/auth/amplify_auth_cognito/example/lib/main.dart +++ b/packages/auth/amplify_auth_cognito/example/lib/main.dart @@ -12,9 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'dart:convert'; import 'dart:io'; -import 'dart:typed_data'; import 'package:amplify_api/amplify_api.dart'; import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; @@ -151,14 +149,13 @@ class _HomeScreenState extends State { try { final response = await Amplify.API .post( - restOptions: RestOptions( - path: '/hello', - body: utf8.encode(_controller.text) as Uint8List, - ), + '/hello', + body: HttpPayload.string(_controller.text), ) - .response; + .value; + final decodedBody = await response.decodeBody(); setState(() { - _greeting = response.body; + _greeting = decodedBody; }); } on Exception catch (e) { setState(() { From c1fc4f5522f6f16e6d312459cddab081e27869a0 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Tue, 21 Jun 2022 13:04:45 -0700 Subject: [PATCH 19/26] add tests --- .../amplify_flutter/lib/src/hybrid_impl.dart | 1 + .../amplify_api/lib/src/api_plugin_impl.dart | 9 +- .../lib/src/method_channel_api.dart | 10 +- packages/api/amplify_api/lib/src/util.dart | 29 +++++ .../test/amplify_dart_rest_methods_test.dart | 101 ++++++++++++++++++ .../test_data/fake_amplify_configuration.dart | 79 ++++++++++++++ 6 files changed, 217 insertions(+), 12 deletions(-) create mode 100644 packages/api/amplify_api/lib/src/util.dart create mode 100644 packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart create mode 100644 packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart diff --git a/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart b/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart index a8c91af3a0..5eb3f1257e 100644 --- a/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart +++ b/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart @@ -34,6 +34,7 @@ class AmplifyHybridImpl extends AmplifyClassImpl { ); await Future.wait( [ + ...API.plugins, ...Auth.plugins, ].map((p) => p.configure(config: amplifyConfig)), eagerError: true, diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart index 4e0b559713..f5a2732ff5 100644 --- a/packages/api/amplify_api/lib/src/api_plugin_impl.dart +++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart @@ -22,6 +22,7 @@ import 'package:meta/meta.dart'; import 'amplify_api_config.dart'; import 'amplify_authorization_rest_client.dart'; +import 'util.dart'; /// {@template amplify_api.amplify_api_dart} /// The AWS implementation of the Amplify API category. @@ -118,7 +119,7 @@ class AmplifyAPIDart extends AmplifyAPI { return _prepareRestResponse(AWSStreamedHttpRequest.delete( uri, body: body ?? HttpPayload.empty(), - headers: headers, + headers: addContentTypeToHeaders(headers, body), ).send(client)); } @@ -170,7 +171,7 @@ class AmplifyAPIDart extends AmplifyAPI { return _prepareRestResponse( AWSStreamedHttpRequest.patch( uri, - headers: headers, + headers: addContentTypeToHeaders(headers, body), body: body ?? HttpPayload.empty(), ).send(client), ); @@ -189,7 +190,7 @@ class AmplifyAPIDart extends AmplifyAPI { return _prepareRestResponse( AWSStreamedHttpRequest.post( uri, - headers: headers, + headers: addContentTypeToHeaders(headers, body), body: body ?? HttpPayload.empty(), ).send(client), ); @@ -208,7 +209,7 @@ class AmplifyAPIDart extends AmplifyAPI { return _prepareRestResponse( AWSStreamedHttpRequest.put( uri, - headers: headers, + headers: addContentTypeToHeaders(headers, body), body: body ?? HttpPayload.empty(), ).send(client), ); diff --git a/packages/api/amplify_api/lib/src/method_channel_api.dart b/packages/api/amplify_api/lib/src/method_channel_api.dart index 95e8f5c17d..c0285a6305 100644 --- a/packages/api/amplify_api/lib/src/method_channel_api.dart +++ b/packages/api/amplify_api/lib/src/method_channel_api.dart @@ -26,6 +26,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import '../amplify_api.dart'; +import 'util.dart'; part 'auth_token.dart'; @@ -282,19 +283,12 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { }) { // Send Request cancelToken to Native String cancelToken = uuid(); - // Ensure Content-Type header matches payload. - var modifiedHeaders = headers != null ? Map.of(headers) : null; - final contentType = body?.contentType; - if (contentType != null) { - modifiedHeaders = modifiedHeaders ?? {}; - modifiedHeaders.putIfAbsent(AWSHeaders.contentType, () => contentType); - } final responseFuture = _restResponseHelper( methodName: methodName, path: path, cancelToken: cancelToken, body: body, - headers: modifiedHeaders, + headers: addContentTypeToHeaders(headers, body), queryParameters: queryParameters, apiName: apiName, ); diff --git a/packages/api/amplify_api/lib/src/util.dart b/packages/api/amplify_api/lib/src/util.dart new file mode 100644 index 0000000000..392b41a592 --- /dev/null +++ b/packages/api/amplify_api/lib/src/util.dart @@ -0,0 +1,29 @@ +// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:amplify_core/amplify_core.dart'; +import 'package:meta/meta.dart'; + +/// Sets the 'Content-Type' of headers to match the [HttpPayload] body. +@internal +Map? addContentTypeToHeaders( + Map? headers, HttpPayload? body) { + var modifiedHeaders = headers != null ? Map.of(headers) : null; + final contentType = body?.contentType; + if (contentType != null) { + modifiedHeaders = modifiedHeaders ?? {}; + modifiedHeaders.putIfAbsent(AWSHeaders.contentType, () => contentType); + } + return modifiedHeaders; +} diff --git a/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart b/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart new file mode 100644 index 0000000000..0c631ba999 --- /dev/null +++ b/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart @@ -0,0 +1,101 @@ +// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +import 'dart:convert'; + +import 'package:amplify_api/amplify_api.dart'; +import 'package:amplify_api/src/api_plugin_impl.dart'; +import 'package:amplify_core/amplify_core.dart'; +import 'package:async/async.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; + +import 'test_data/fake_amplify_configuration.dart'; + +const _expectedRestResponseBody = '"Hello from Lambda!"'; +const _pathThatShouldFail = 'notHere'; + +class MockAmplifyAPI extends AmplifyAPIDart { + @override + http.Client getRestClient({String? apiName}) => MockClient((request) async { + if (request.body.isNotEmpty) { + expect(request.headers['Content-Type'], 'application/json'); + } + if (request.url.path.contains(_pathThatShouldFail)) { + return http.Response('Not found', 404); + } + return http.Response(_expectedRestResponseBody, 200); + }); +} + +void main() { + late AmplifyAPI api; + + setUpAll(() async { + await Amplify.addPlugin(MockAmplifyAPI()); + await Amplify.configure(amplifyconfig); + }); + group('REST API', () { + Future _verifyRestOperation( + CancelableOperation operation, + ) async { + final response = + await operation.value.timeout(const Duration(seconds: 3)); + final body = await response.decodeBody(); + expect(body, _expectedRestResponseBody); + expect(response.statusCode, 200); + } + + test('get() should get 200', () async { + final operation = Amplify.API.get('items'); + await _verifyRestOperation(operation); + }); + + test('head() should get 200', () async { + final operation = Amplify.API.head('items'); + final response = await operation.value; + expect(response.statusCode, 200); + }); + + test('patch() should get 200', () async { + final operation = Amplify.API + .patch('items', body: HttpPayload.json({'name': 'Mow the lawn'})); + await _verifyRestOperation(operation); + }); + + test('post() should get 200', () async { + final operation = Amplify.API + .post('items', body: HttpPayload.json({'name': 'Mow the lawn'})); + await _verifyRestOperation(operation); + }); + + test('put() should get 200', () async { + final operation = Amplify.API + .put('items', body: HttpPayload.json({'name': 'Mow the lawn'})); + await _verifyRestOperation(operation); + }); + + test('delete() should get 200', () async { + final operation = Amplify.API + .delete('items', body: HttpPayload.json({'name': 'Mow the lawn'})); + await _verifyRestOperation(operation); + }); + + test('canceled request should never resolve', () async { + final operation = Amplify.API.get('items'); + operation.cancel(); + operation.then((p0) => fail('Request should have been cancelled.')); + }); + }); +} diff --git a/packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart b/packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart new file mode 100644 index 0000000000..7b8fd53be0 --- /dev/null +++ b/packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart @@ -0,0 +1,79 @@ +const amplifyconfig = '''{ + "UserAgent": "aws-amplify-cli/2.0", + "Version": "1.0", + "api": { + "plugins": { + "awsAPIPlugin": { + "apiIntegrationTestGraphQL": { + "endpointType": "GraphQL", + "endpoint": "https://abc123.appsync-api.us-east-1.amazonaws.com/graphql", + "region": "us-east-1", + "authorizationType": "API_KEY", + "apiKey": "abc123" + }, + "api123": { + "endpointType": "REST", + "endpoint": "https://abc123.execute-api.us-east-1.amazonaws.com/test", + "region": "us-east-1", + "authorizationType": "AWS_IAM" + } + } + } + }, + "auth": { + "plugins": { + "awsCognitoAuthPlugin": { + "UserAgent": "aws-amplify-cli/0.1.0", + "Version": "0.1.0", + "IdentityManager": { + "Default": {} + }, + "AppSync": { + "Default": { + "ApiUrl": "https://abc123.appsync-api.us-east-1.amazonaws.com/graphql", + "Region": "us-east-1", + "AuthMode": "API_KEY", + "ApiKey": "abc123", + "ClientDatabasePrefix": "apiIntegrationTestGraphQL_API_KEY" + } + }, + "CredentialsProvider": { + "CognitoIdentity": { + "Default": { + "PoolId": "us-east-1:abc123", + "Region": "us-east-1" + } + } + }, + "CognitoUserPool": { + "Default": { + "PoolId": "us-east-1_abc123", + "AppClientId": "abc123", + "Region": "us-east-1" + } + }, + "Auth": { + "Default": { + "authenticationFlowType": "USER_SRP_AUTH", + "socialProviders": [], + "usernameAttributes": [], + "signupAttributes": [ + "EMAIL" + ], + "passwordProtectionSettings": { + "passwordPolicyMinLength": 8, + "passwordPolicyCharacters": [] + }, + "mfaConfiguration": "OFF", + "mfaTypes": [ + "SMS" + ], + "verificationMechanisms": [ + "EMAIL" + ] + } + } + } + } + } +}'''; From 66704de172f4cc2647038263e226d97c93597bea Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Tue, 21 Jun 2022 13:08:57 -0700 Subject: [PATCH 20/26] correct qp types --- .../api/amplify_api/lib/src/api_plugin_impl.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart index f5a2732ff5..0b6d4d355f 100644 --- a/packages/api/amplify_api/lib/src/api_plugin_impl.dart +++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart @@ -111,7 +111,7 @@ class AmplifyAPIDart extends AmplifyAPI { String path, { HttpPayload? body, Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { final uri = _getRestUri(path, apiName, queryParameters); @@ -127,7 +127,7 @@ class AmplifyAPIDart extends AmplifyAPI { CancelableOperation get( String path, { Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { final uri = _getRestUri(path, apiName, queryParameters); @@ -144,7 +144,7 @@ class AmplifyAPIDart extends AmplifyAPI { CancelableOperation head( String path, { Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { final uri = _getRestUri(path, apiName, queryParameters); @@ -163,7 +163,7 @@ class AmplifyAPIDart extends AmplifyAPI { String path, { HttpPayload? body, Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { final uri = _getRestUri(path, apiName, queryParameters); @@ -182,7 +182,7 @@ class AmplifyAPIDart extends AmplifyAPI { String path, { HttpPayload? body, Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { final uri = _getRestUri(path, apiName, queryParameters); @@ -201,7 +201,7 @@ class AmplifyAPIDart extends AmplifyAPI { String path, { HttpPayload? body, Map? headers, - Map? queryParameters, + Map? queryParameters, String? apiName, }) { final uri = _getRestUri(path, apiName, queryParameters); From 8f9c6d86969f2671871d33ddca90e6caa4709f50 Mon Sep 17 00:00:00 2001 From: Elijah Quartey Date: Thu, 23 Jun 2022 11:39:46 -0500 Subject: [PATCH 21/26] chore(api): API Native Bridge for .addPlugin() (#1756) --- packages/api/amplify_api/Makefile | 4 + packages/api/amplify_api/example/pubspec.yaml | 3 +- packages/api/amplify_api/lib/amplify_api.dart | 21 ++-- .../amplify_api/lib/src/api_plugin_impl.dart | 81 ++++++++++++++ .../lib/src/native_api_plugin.dart | 63 +++++++++++ .../pigeons/native_api_plugin.dart | 43 ++++++++ packages/api/amplify_api/pubspec.yaml | 10 +- .../amplify/amplify_api/AmplifyApi.kt | 52 +++++---- .../amplify_api/NativeApiPluginBindings.java | 87 +++++++++++++++ packages/api/amplify_api_android/pubspec.yaml | 3 +- .../ios/Classes/NativeApiPlugin.h | 35 ++++++ .../ios/Classes/NativeApiPlugin.m | 102 ++++++++++++++++++ .../ios/Classes/SwiftAmplifyApiPlugin.swift | 43 ++++---- .../ios/Classes/amplify_api_ios.h | 21 ++++ .../ios/amplify_api_ios.podspec | 14 +++ .../api/amplify_api_ios/ios/module.modulemap | 6 ++ packages/api/amplify_api_ios/pubspec.yaml | 3 +- 17 files changed, 526 insertions(+), 65 deletions(-) create mode 100644 packages/api/amplify_api/Makefile create mode 100644 packages/api/amplify_api/lib/src/api_plugin_impl.dart create mode 100644 packages/api/amplify_api/lib/src/native_api_plugin.dart create mode 100644 packages/api/amplify_api/pigeons/native_api_plugin.dart create mode 100644 packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java create mode 100644 packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h create mode 100644 packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m create mode 100644 packages/api/amplify_api_ios/ios/Classes/amplify_api_ios.h create mode 100644 packages/api/amplify_api_ios/ios/module.modulemap diff --git a/packages/api/amplify_api/Makefile b/packages/api/amplify_api/Makefile new file mode 100644 index 0000000000..f1c3ac38ba --- /dev/null +++ b/packages/api/amplify_api/Makefile @@ -0,0 +1,4 @@ +.PHONY: pigeons +pigeons: + flutter pub run pigeon --input pigeons/native_api_plugin.dart + flutter format --fix lib/src/native_api_plugin.dart diff --git a/packages/api/amplify_api/example/pubspec.yaml b/packages/api/amplify_api/example/pubspec.yaml index 7b017f9370..9a2ddbe654 100644 --- a/packages/api/amplify_api/example/pubspec.yaml +++ b/packages/api/amplify_api/example/pubspec.yaml @@ -32,7 +32,8 @@ dependencies: sdk: flutter dev_dependencies: - amplify_lints: ^2.0.0 + amplify_lints: + path: ../../../amplify_lints amplify_test: path: ../../../amplify_test flutter_driver: diff --git a/packages/api/amplify_api/lib/amplify_api.dart b/packages/api/amplify_api/lib/amplify_api.dart index f0ca3c2c4f..a4db7b1e97 100644 --- a/packages/api/amplify_api/lib/amplify_api.dart +++ b/packages/api/amplify_api/lib/amplify_api.dart @@ -15,9 +15,7 @@ library amplify_api_plugin; -import 'dart:io'; - -import 'package:amplify_api/src/method_channel_api.dart'; +import 'package:amplify_api/src/api_plugin_impl.dart'; import 'package:amplify_core/amplify_core.dart'; import 'package:meta/meta.dart'; @@ -32,18 +30,11 @@ export './model_subscriptions.dart'; /// {@endtemplate} abstract class AmplifyAPI extends APIPluginInterface { /// {@macro amplify_api.amplify_api} - factory AmplifyAPI({ - List authProviders = const [], - ModelProviderInterface? modelProvider, - }) { - if (zIsWeb || Platform.isWindows || Platform.isMacOS || Platform.isLinux) { - throw UnsupportedError('This platform is not supported yet'); - } - return AmplifyAPIMethodChannel( - authProviders: authProviders, - modelProvider: modelProvider, - ); - } + factory AmplifyAPI( + {List authProviders = const [], + ModelProviderInterface? modelProvider}) => + AmplifyAPIDart( + authProviders: authProviders, modelProvider: modelProvider); /// Protected constructor for subclasses. @protected diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart new file mode 100644 index 0000000000..5ac4fc36ff --- /dev/null +++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart @@ -0,0 +1,81 @@ +// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +library amplify_api; + +import 'dart:io'; + +import 'package:amplify_api/amplify_api.dart'; +import 'package:amplify_api/src/native_api_plugin.dart'; +import 'package:amplify_core/amplify_core.dart'; +import 'package:flutter/services.dart'; + +/// {@template amplify_api.amplify_api_dart} +/// The AWS implementation of the Amplify API category. +/// {@endtemplate} +class AmplifyAPIDart extends AmplifyAPI { + late final AWSApiPluginConfig _apiConfig; + + /// The registered [APIAuthProvider] instances. + final Map _authProviders = {}; + + /// {@macro amplify_api.amplify_api_dart} + AmplifyAPIDart({ + List authProviders = const [], + this.modelProvider, + }) : super.protected() { + authProviders.forEach(registerAuthProvider); + } + + @override + Future configure({AmplifyConfig? config}) async { + final apiConfig = config?.api?.awsPlugin; + if (apiConfig == null) { + throw const ApiException('No AWS API config found', + recoverySuggestion: 'Add API from the Amplify CLI. See ' + 'https://docs.amplify.aws/lib/graphqlapi/getting-started/q/platform/flutter/#configure-api'); + } + _apiConfig = apiConfig; + } + + @override + Future addPlugin() async { + if (zIsWeb || !(Platform.isAndroid || Platform.isIOS)) { + return; + } + + final nativeBridge = NativeApiBridge(); + try { + final authProvidersList = + _authProviders.keys.map((key) => key.rawValue).toList(); + await nativeBridge.addPlugin(authProvidersList); + } on PlatformException catch (e) { + if (e.code == 'AmplifyAlreadyConfiguredException') { + throw const AmplifyAlreadyConfiguredException( + AmplifyExceptionMessages.alreadyConfiguredDefaultMessage, + recoverySuggestion: + AmplifyExceptionMessages.alreadyConfiguredDefaultSuggestion); + } + throw AmplifyException.fromMap((e.details as Map).cast()); + } + } + + @override + final ModelProviderInterface? modelProvider; + + @override + void registerAuthProvider(APIAuthProvider authProvider) { + _authProviders[authProvider.type] = authProvider; + } +} diff --git a/packages/api/amplify_api/lib/src/native_api_plugin.dart b/packages/api/amplify_api/lib/src/native_api_plugin.dart new file mode 100644 index 0000000000..e7c5af4d04 --- /dev/null +++ b/packages/api/amplify_api/lib/src/native_api_plugin.dart @@ -0,0 +1,63 @@ +// +// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// A copy of the License is located at +// +// http://aws.amazon.com/apache2.0 +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. +// +// Autogenerated from Pigeon (v3.1.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name +// @dart = 2.12 +import 'dart:async'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; + +import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/services.dart'; + +class _NativeApiBridgeCodec extends StandardMessageCodec { + const _NativeApiBridgeCodec(); +} + +class NativeApiBridge { + /// Constructor for [NativeApiBridge]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + NativeApiBridge({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _NativeApiBridgeCodec(); + + Future addPlugin(List arg_authProvidersList) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.NativeApiBridge.addPlugin', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_authProvidersList]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} diff --git a/packages/api/amplify_api/pigeons/native_api_plugin.dart b/packages/api/amplify_api/pigeons/native_api_plugin.dart new file mode 100644 index 0000000000..a36f7397f9 --- /dev/null +++ b/packages/api/amplify_api/pigeons/native_api_plugin.dart @@ -0,0 +1,43 @@ +// +// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// A copy of the License is located at +// +// http://aws.amazon.com/apache2.0 +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. +// + +// ignore_for_file: avoid_positional_boolean_parameters + +@ConfigurePigeon( + PigeonOptions( + copyrightHeader: '../../../tool/license.txt', + dartOptions: DartOptions(), + dartOut: 'lib/src/native_Api_plugin.dart', + javaOptions: JavaOptions( + className: 'NativeApiPluginBindings', + package: 'com.amazonaws.amplify.amplify_api', + ), + javaOut: + '../amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java', + objcOptions: ObjcOptions( + header: 'NativeApiPlugin.h', + ), + objcHeaderOut: '../amplify_api_ios/ios/Classes/NativeApiPlugin.h', + objcSourceOut: '../amplify_api_ios/ios/Classes/NativeApiPlugin.m', + ), +) +library native_api_plugin; + +import 'package:pigeon/pigeon.dart'; + +@HostApi() +abstract class NativeApiBridge { + void addPlugin(List authProvidersList); +} diff --git a/packages/api/amplify_api/pubspec.yaml b/packages/api/amplify_api/pubspec.yaml index 869779172e..b65a92bfec 100644 --- a/packages/api/amplify_api/pubspec.yaml +++ b/packages/api/amplify_api/pubspec.yaml @@ -21,14 +21,22 @@ dependencies: meta: ^1.7.0 plugin_platform_interface: ^2.0.0 +dependency_overrides: + # TODO(dnys1): Remove when pigeon is bumped + # https://github.com/flutter/flutter/issues/105090 + analyzer: ^3.0.0 + + dev_dependencies: - amplify_lints: ^2.0.0 + amplify_lints: + path: ../../amplify_lints amplify_test: path: ../../amplify_test build_runner: ^2.0.0 flutter_test: sdk: flutter mockito: ^5.0.0 + pigeon: ^3.1.5 # The following section is specific to Flutter. flutter: diff --git a/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt index 02de711722..0205877bf7 100644 --- a/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt +++ b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt @@ -39,7 +39,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers /** AmplifyApiPlugin */ -class AmplifyApi : FlutterPlugin, MethodCallHandler { +class AmplifyApi : FlutterPlugin, MethodCallHandler, NativeApiPluginBindings.NativeApiBridge { private companion object { /** @@ -83,6 +83,11 @@ class AmplifyApi : FlutterPlugin, MethodCallHandler { "com.amazonaws.amplify/api_observe_events" ) eventchannel!!.setStreamHandler(graphqlSubscriptionStreamHandler) + + NativeApiPluginBindings.NativeApiBridge.setup( + flutterPluginBinding.binaryMessenger, + this + ) } @Suppress("UNCHECKED_CAST") @@ -94,27 +99,6 @@ class AmplifyApi : FlutterPlugin, MethodCallHandler { if (methodName == "cancel") { onCancel(result, (call.arguments as String)) return - } else if (methodName == "addPlugin") { - try { - val authProvidersList: List = - (arguments["authProviders"] as List<*>?)?.cast() ?: listOf() - val authProviders = authProvidersList.map { AuthorizationType.valueOf(it) } - if (flutterAuthProviders == null) { - flutterAuthProviders = FlutterAuthProviders(authProviders) - } - flutterAuthProviders!!.setMethodChannel(channel) - Amplify.addPlugin( - AWSApiPlugin - .builder() - .apiAuthProviders(flutterAuthProviders!!.factory) - .build() - ) - logger.info("Added API plugin") - result.success(null) - } catch (e: Exception) { - handleAddPluginException("API", e, result) - } - return } try { @@ -168,5 +152,29 @@ class AmplifyApi : FlutterPlugin, MethodCallHandler { eventchannel = null graphqlSubscriptionStreamHandler?.close() graphqlSubscriptionStreamHandler = null + + NativeApiPluginBindings.NativeApiBridge.setup( + flutterPluginBinding.binaryMessenger, + null, + ) + } + + override fun addPlugin(authProvidersList: MutableList) { + try { + val authProviders = authProvidersList.map { AuthorizationType.valueOf(it) } + if (flutterAuthProviders == null) { + flutterAuthProviders = FlutterAuthProviders(authProviders) + } + flutterAuthProviders!!.setMethodChannel(channel) + Amplify.addPlugin( + AWSApiPlugin + .builder() + .apiAuthProviders(flutterAuthProviders!!.factory) + .build() + ) + logger.info("Added API plugin") + } catch (e: Exception) { + logger.error(e.message) + } } } diff --git a/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java new file mode 100644 index 0000000000..d8d07f4add --- /dev/null +++ b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java @@ -0,0 +1,87 @@ +// +// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// A copy of the License is located at +// +// http://aws.amazon.com/apache2.0 +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. +// +// Autogenerated from Pigeon (v3.1.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +package com.amazonaws.amplify.amplify_api; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StandardMessageCodec; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +/** Generated class from Pigeon. */ +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) +public class NativeApiPluginBindings { + private static class NativeApiBridgeCodec extends StandardMessageCodec { + public static final NativeApiBridgeCodec INSTANCE = new NativeApiBridgeCodec(); + private NativeApiBridgeCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/ + public interface NativeApiBridge { + void addPlugin(@NonNull List authProvidersList); + + /** The codec used by NativeApiBridge. */ + static MessageCodec getCodec() { + return NativeApiBridgeCodec.INSTANCE; + } + + /** Sets up an instance of `NativeApiBridge` to handle messages through the `binaryMessenger`. */ + static void setup(BinaryMessenger binaryMessenger, NativeApiBridge api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeApiBridge.addPlugin", getCodec()); + if (api != null) { + channel.setMessageHandler((message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList)message; + List authProvidersListArg = (List)args.get(0); + if (authProvidersListArg == null) { + throw new NullPointerException("authProvidersListArg unexpectedly null."); + } + api.addPlugin(authProvidersListArg); + wrapped.put("result", null); + } + catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + private static Map wrapError(Throwable exception) { + Map errorMap = new HashMap<>(); + errorMap.put("message", exception.toString()); + errorMap.put("code", exception.getClass().getSimpleName()); + errorMap.put("details", "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + return errorMap; + } +} diff --git a/packages/api/amplify_api_android/pubspec.yaml b/packages/api/amplify_api_android/pubspec.yaml index 186f102aff..920cdc661b 100644 --- a/packages/api/amplify_api_android/pubspec.yaml +++ b/packages/api/amplify_api_android/pubspec.yaml @@ -12,7 +12,8 @@ dependencies: sdk: flutter dev_dependencies: - amplify_lints: ^2.0.0 + amplify_lints: + path: ../../amplify_lints flutter_test: sdk: flutter diff --git a/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h new file mode 100644 index 0000000000..7b3bad24ed --- /dev/null +++ b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h @@ -0,0 +1,35 @@ +// +// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// A copy of the License is located at +// +// http://aws.amazon.com/apache2.0 +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. +// +// Autogenerated from Pigeon (v3.1.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import +@protocol FlutterBinaryMessenger; +@protocol FlutterMessageCodec; +@class FlutterError; +@class FlutterStandardTypedData; + +NS_ASSUME_NONNULL_BEGIN + + +/// The codec used by NativeApiBridge. +NSObject *NativeApiBridgeGetCodec(void); + +@protocol NativeApiBridge +- (void)addPluginAuthProvidersList:(NSArray *)authProvidersList error:(FlutterError *_Nullable *_Nonnull)error; +@end + +extern void NativeApiBridgeSetup(id binaryMessenger, NSObject *_Nullable api); + +NS_ASSUME_NONNULL_END diff --git a/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m new file mode 100644 index 0000000000..c936591be5 --- /dev/null +++ b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m @@ -0,0 +1,102 @@ +// +// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// A copy of the License is located at +// +// http://aws.amazon.com/apache2.0 +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. +// +// Autogenerated from Pigeon (v3.1.5), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import "NativeApiPlugin.h" +#import + +#if !__has_feature(objc_arc) +#error File requires ARC to be enabled. +#endif + +static NSDictionary *wrapResult(id result, FlutterError *error) { + NSDictionary *errorDict = (NSDictionary *)[NSNull null]; + if (error) { + errorDict = @{ + @"code": (error.code ?: [NSNull null]), + @"message": (error.message ?: [NSNull null]), + @"details": (error.details ?: [NSNull null]), + }; + } + return @{ + @"result": (result ?: [NSNull null]), + @"error": errorDict, + }; +} +static id GetNullableObject(NSDictionary* dict, id key) { + id result = dict[key]; + return (result == [NSNull null]) ? nil : result; +} +static id GetNullableObjectAtIndex(NSArray* array, NSInteger key) { + id result = array[key]; + return (result == [NSNull null]) ? nil : result; +} + + + +@interface NativeApiBridgeCodecReader : FlutterStandardReader +@end +@implementation NativeApiBridgeCodecReader +@end + +@interface NativeApiBridgeCodecWriter : FlutterStandardWriter +@end +@implementation NativeApiBridgeCodecWriter +@end + +@interface NativeApiBridgeCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation NativeApiBridgeCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[NativeApiBridgeCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[NativeApiBridgeCodecReader alloc] initWithData:data]; +} +@end + +NSObject *NativeApiBridgeGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + NativeApiBridgeCodecReaderWriter *readerWriter = [[NativeApiBridgeCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + + +void NativeApiBridgeSetup(id binaryMessenger, NSObject *api) { + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.NativeApiBridge.addPlugin" + binaryMessenger:binaryMessenger + codec:NativeApiBridgeGetCodec() ]; + if (api) { + NSCAssert([api respondsToSelector:@selector(addPluginAuthProvidersList:error:)], @"NativeApiBridge api (%@) doesn't respond to @selector(addPluginAuthProvidersList:error:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSArray *arg_authProvidersList = GetNullableObjectAtIndex(args, 0); + FlutterError *error; + [api addPluginAuthProvidersList:arg_authProvidersList error:&error]; + callback(wrapResult(nil, error)); + }]; + } + else { + [channel setMessageHandler:nil]; + } + } +} diff --git a/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift b/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift index 7ad1accd1a..63ce5c373c 100644 --- a/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift +++ b/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift @@ -20,7 +20,7 @@ import AmplifyPlugins import amplify_flutter_ios import AWSPluginsCore -public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin { +public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin, NativeApiBridge { private let bridge: ApiBridge private let graphQLSubscriptionsStreamHandler: GraphQLSubscriptionsStreamHandler static var methodChannel: FlutterMethodChannel! @@ -43,6 +43,7 @@ public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin { let instance = SwiftAmplifyApiPlugin() eventchannel.setStreamHandler(instance.graphQLSubscriptionsStreamHandler) registrar.addMethodCallDelegate(instance, channel: methodChannel) + NativeApiBridgeSetup(registrar.messenger(), instance) } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { @@ -62,33 +63,26 @@ public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin { let arguments = try FlutterApiRequest.getMap(args: callArgs) - if method == "addPlugin"{ - let authProvidersList = arguments["authProviders"] as? [String] ?? [] - let authProviders = authProvidersList.compactMap { - AWSAuthorizationType(rawValue: $0) - } - addPlugin(authProviders: authProviders, result: result) - return - } - try innerHandle(method: method, arguments: arguments, result: result) } catch { print("Failed to parse query arguments with \(error)") FlutterApiErrorHandler.handleApiError(error: APIError(error: error), flutterResult: result) } } - - private func addPlugin(authProviders: [AWSAuthorizationType], result: FlutterResult) { + + public func addPluginAuthProvidersList(_ authProvidersList: [String], error: AutoreleasingUnsafeMutablePointer) { do { + let authProviders = authProvidersList.compactMap { + AWSAuthorizationType(rawValue: $0) + } try Amplify.add( plugin: AWSAPIPlugin( sessionFactory: FlutterURLSessionBehaviorFactory(), apiAuthProviderFactory: FlutterAuthProviders(authProviders))) - result(true) } catch let apiError as APIError { - ErrorUtil.postErrorToFlutterChannel( - result: result, - errorCode: "APIException", + error.pointee = FlutterError( + code: "APIException", + message: apiError.localizedDescription, details: [ "message": apiError.errorDescription, "recoverySuggestion": apiError.recoverySuggestion, @@ -100,20 +94,21 @@ public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin { if case .amplifyAlreadyConfigured = configError { errorCode = "AmplifyAlreadyConfiguredException" } - ErrorUtil.postErrorToFlutterChannel( - result: result, - errorCode: errorCode, + error.pointee = FlutterError( + code: errorCode, + message: configError.localizedDescription, details: [ "message": configError.errorDescription, "recoverySuggestion": configError.recoverySuggestion, "underlyingError": configError.underlyingError?.localizedDescription ?? "" ] ) - } catch { - ErrorUtil.postErrorToFlutterChannel( - result: result, - errorCode: "UNKNOWN", - details: ["message": error.localizedDescription]) + } catch let e { + error.pointee = FlutterError( + code: "UNKNOWN", + message: e.localizedDescription, + details: nil + ) } } diff --git a/packages/api/amplify_api_ios/ios/Classes/amplify_api_ios.h b/packages/api/amplify_api_ios/ios/Classes/amplify_api_ios.h new file mode 100644 index 0000000000..0b890efd4f --- /dev/null +++ b/packages/api/amplify_api_ios/ios/Classes/amplify_api_ios.h @@ -0,0 +1,21 @@ +// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef amplify_api_ios_h +#define amplify_api_ios_h + +#import "NativeApiPlugin.h" +#import "AmplifyApi.h" + +#endif /* amplify_api_ios_h */ diff --git a/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec b/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec index 181063b97c..276c97012b 100644 --- a/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec +++ b/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec @@ -21,6 +21,20 @@ The API module for Amplify Flutter. s.platform = :ios, '11.0' s.swift_version = '5.0' + # Use a custom module map with a manually written umbrella header. + # + # Since we use `package:pigeon` to generate our platform interface + # in ObjC, and since the rest of the module is written in Swift, we + # fall victim to this issue: https://github.com/CocoaPods/CocoaPods/issues/10544 + # + # This is because we have an ObjC -> Swift -> ObjC import cycle: + # ApiPlugin -> SwiftAmplifyApiPlugin -> NativeApiPlugin + # + # The easiest solution to this problem is to create the umbrella + # header which would otherwise be auto-generated by Cocoapods but + # name it what's expected by the Swift compiler (amplify_api_ios.h). + s.module_map = 'module.modulemap' + # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } end diff --git a/packages/api/amplify_api_ios/ios/module.modulemap b/packages/api/amplify_api_ios/ios/module.modulemap new file mode 100644 index 0000000000..acac87c311 --- /dev/null +++ b/packages/api/amplify_api_ios/ios/module.modulemap @@ -0,0 +1,6 @@ +framework module amplify_api_ios { + umbrella header "amplify_api_ios.h" + + export * + module * { export * } +} diff --git a/packages/api/amplify_api_ios/pubspec.yaml b/packages/api/amplify_api_ios/pubspec.yaml index 75c7a121e6..eea5904683 100644 --- a/packages/api/amplify_api_ios/pubspec.yaml +++ b/packages/api/amplify_api_ios/pubspec.yaml @@ -13,7 +13,8 @@ dependencies: sdk: flutter dev_dependencies: - amplify_lints: ^2.0.0 + amplify_lints: + path: ../../amplify_lints flutter_test: sdk: flutter From fb338eb9e83fd8f9ff3a7f8329df83769ecf79e7 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Mon, 27 Jun 2022 13:57:59 -0700 Subject: [PATCH 22/26] address PR comments --- .../lib/src/amplify_api_config.dart | 3 +- .../amplify_authorization_rest_client.dart | 4 +- .../amplify_api/lib/src/api_plugin_impl.dart | 5 +-- packages/api/amplify_api/lib/src/util.dart | 13 +++--- .../test/amplify_dart_rest_methods_test.dart | 2 + packages/api/amplify_api/test/util_test.dart | 42 +++++++++++++++++++ 6 files changed, 57 insertions(+), 12 deletions(-) create mode 100644 packages/api/amplify_api/test/util_test.dart diff --git a/packages/api/amplify_api/lib/src/amplify_api_config.dart b/packages/api/amplify_api/lib/src/amplify_api_config.dart index fff942e19b..960f11bf9b 100644 --- a/packages/api/amplify_api/lib/src/amplify_api_config.dart +++ b/packages/api/amplify_api/lib/src/amplify_api_config.dart @@ -1,4 +1,3 @@ -// // Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). @@ -11,7 +10,6 @@ // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either // express or implied. See the License for the specific language governing // permissions and limitations under the License. -// import 'package:amplify_core/amplify_core.dart'; import 'package:collection/collection.dart'; @@ -34,6 +32,7 @@ class EndpointConfig with AWSEquatable { Uri getUri(String path, Map? queryParameters) { final parsed = Uri.parse(config.endpoint); // Remove leading slashes which are suggested in public documentation. + // https://docs.amplify.aws/lib/restapi/getting-started/q/platform/flutter/#make-a-post-request if (path.startsWith(_slash)) { path = path.substring(1); } diff --git a/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart b/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart index 1ece436cb4..e58885385c 100644 --- a/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart +++ b/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart @@ -15,14 +15,14 @@ import 'dart:async'; import 'package:amplify_core/amplify_core.dart'; -import 'package:aws_common/aws_common.dart'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; /// Implementation of http [http.Client] that authorizes HTTP requests with /// Amplify. @internal -class AmplifyAuthorizationRestClient extends http.BaseClient { +class AmplifyAuthorizationRestClient extends http.BaseClient + implements Closeable { /// Determines how requests with this client are authorized. final AWSApiConfig endpointConfig; final http.Client _baseClient; diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart index 0862962ca6..0926c0a462 100644 --- a/packages/api/amplify_api/lib/src/api_plugin_impl.dart +++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart @@ -176,9 +176,8 @@ class AmplifyAPIDart extends AmplifyAPI { final uri = _getRestUri(path, apiName, queryParameters); final client = getRestClient(apiName: apiName); return _prepareRestResponse( - AWSHttpRequest( - method: AWSHttpMethod.head, - uri: uri, + AWSHttpRequest.head( + uri, headers: headers, ).send(client), ); diff --git a/packages/api/amplify_api/lib/src/util.dart b/packages/api/amplify_api/lib/src/util.dart index 392b41a592..90647ff6a3 100644 --- a/packages/api/amplify_api/lib/src/util.dart +++ b/packages/api/amplify_api/lib/src/util.dart @@ -18,12 +18,15 @@ import 'package:meta/meta.dart'; /// Sets the 'Content-Type' of headers to match the [HttpPayload] body. @internal Map? addContentTypeToHeaders( - Map? headers, HttpPayload? body) { - var modifiedHeaders = headers != null ? Map.of(headers) : null; + Map? headers, + HttpPayload? body, +) { final contentType = body?.contentType; - if (contentType != null) { - modifiedHeaders = modifiedHeaders ?? {}; - modifiedHeaders.putIfAbsent(AWSHeaders.contentType, () => contentType); + if (contentType == null) { + return headers; } + // Create new map to avoid modifying input headers. + final modifiedHeaders = Map.of(headers ?? {}); + modifiedHeaders.putIfAbsent(AWSHeaders.contentType, () => contentType); return modifiedHeaders; } diff --git a/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart b/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart index 0c631ba999..d8c5162377 100644 --- a/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart +++ b/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart @@ -96,6 +96,8 @@ void main() { final operation = Amplify.API.get('items'); operation.cancel(); operation.then((p0) => fail('Request should have been cancelled.')); + await operation.valueOrCancellation(); + expect(operation.isCanceled, isTrue); }); }); } diff --git a/packages/api/amplify_api/test/util_test.dart b/packages/api/amplify_api/test/util_test.dart new file mode 100644 index 0000000000..062b8f7276 --- /dev/null +++ b/packages/api/amplify_api/test/util_test.dart @@ -0,0 +1,42 @@ +// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). +// You may not use this file except in compliance with the License. +// A copy of the License is located at +// +// http://aws.amazon.com/apache2.0 +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +import 'package:amplify_api/src/util.dart'; +import 'package:amplify_core/amplify_core.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('addContentTypeToHeaders', () { + test('adds Content-Type header from payload', () { + final resultHeaders = addContentTypeToHeaders( + null, HttpPayload.json({'name': 'now the lawn'})); + expect(resultHeaders?['Content-Type'], 'application/json'); + }); + + test('no-op when payload null', () { + final inputHeaders = {'foo': 'bar'}; + final resultHeaders = addContentTypeToHeaders(inputHeaders, null); + expect(resultHeaders, inputHeaders); + }); + + test('does not change input headers', () { + final inputHeaders = {'foo': 'bar'}; + final resultHeaders = addContentTypeToHeaders( + inputHeaders, HttpPayload.json({'name': 'now the lawn'})); + expect(resultHeaders?['Content-Type'], 'application/json'); + expect(inputHeaders['Content-Type'], isNull); + }); + }); +} From 6d823db7197ef15502d8fbab656fc39a1c6a8a28 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Mon, 27 Jun 2022 14:09:57 -0700 Subject: [PATCH 23/26] correct some merge mistakes --- packages/api/amplify_api/pubspec.yaml | 6 ------ .../amplify_api_ios/ios/amplify_api_ios.podspec | 14 -------------- 2 files changed, 20 deletions(-) diff --git a/packages/api/amplify_api/pubspec.yaml b/packages/api/amplify_api/pubspec.yaml index faf15b8c2a..00041dcf57 100644 --- a/packages/api/amplify_api/pubspec.yaml +++ b/packages/api/amplify_api/pubspec.yaml @@ -22,12 +22,6 @@ dependencies: meta: ^1.7.0 plugin_platform_interface: ^2.0.0 -dependency_overrides: - # TODO(dnys1): Remove when pigeon is bumped - # https://github.com/flutter/flutter/issues/105090 - analyzer: ^3.0.0 - - dev_dependencies: amplify_lints: path: ../../amplify_lints diff --git a/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec b/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec index 4338bfc7da..f5a6147bff 100644 --- a/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec +++ b/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec @@ -37,20 +37,6 @@ The API module for Amplify Flutter. # name it what's expected by the Swift compiler (amplify_api_ios.h). s.module_map = 'module.modulemap' - # Use a custom module map with a manually written umbrella header. - # - # Since we use `package:pigeon` to generate our platform interface - # in ObjC, and since the rest of the module is written in Swift, we - # fall victim to this issue: https://github.com/CocoaPods/CocoaPods/issues/10544 - # - # This is because we have an ObjC -> Swift -> ObjC import cycle: - # ApiPlugin -> SwiftAmplifyApiPlugin -> NativeApiPlugin - # - # The easiest solution to this problem is to create the umbrella - # header which would otherwise be auto-generated by Cocoapods but - # name it what's expected by the Swift compiler (amplify_api_ios.h). - s.module_map = 'module.modulemap' - # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } end From 24a9926f7e9055a0f2cdebe71706f8e244a34d72 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Mon, 27 Jun 2022 14:11:16 -0700 Subject: [PATCH 24/26] correct another merge error --- packages/api/amplify_api/lib/amplify_api.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/api/amplify_api/lib/amplify_api.dart b/packages/api/amplify_api/lib/amplify_api.dart index b4904b2d4c..a4db7b1e97 100644 --- a/packages/api/amplify_api/lib/amplify_api.dart +++ b/packages/api/amplify_api/lib/amplify_api.dart @@ -19,8 +19,6 @@ import 'package:amplify_api/src/api_plugin_impl.dart'; import 'package:amplify_core/amplify_core.dart'; import 'package:meta/meta.dart'; -import 'src/api_plugin_impl.dart'; - export 'package:amplify_core/src/types/api/api_types.dart'; export './model_mutations.dart'; From 50404c9a23394020a9f9a3ea6ecd195a3cdee5b6 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Mon, 27 Jun 2022 14:20:10 -0700 Subject: [PATCH 25/26] correct merge error --- packages/api/amplify_api/lib/src/method_channel_api.dart | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/api/amplify_api/lib/src/method_channel_api.dart b/packages/api/amplify_api/lib/src/method_channel_api.dart index 3da40070e5..c0285a6305 100644 --- a/packages/api/amplify_api/lib/src/method_channel_api.dart +++ b/packages/api/amplify_api/lib/src/method_channel_api.dart @@ -283,19 +283,12 @@ class AmplifyAPIMethodChannel extends AmplifyAPI { }) { // Send Request cancelToken to Native String cancelToken = uuid(); - // Ensure Content-Type header matches payload. - var modifiedHeaders = headers != null ? Map.of(headers) : null; - final contentType = body?.contentType; - if (contentType != null) { - modifiedHeaders = modifiedHeaders ?? {}; - modifiedHeaders.putIfAbsent(AWSHeaders.contentType, () => contentType); - } final responseFuture = _restResponseHelper( methodName: methodName, path: path, cancelToken: cancelToken, body: body, - headers: modifiedHeaders, + headers: addContentTypeToHeaders(headers, body), queryParameters: queryParameters, apiName: apiName, ); From 467a792e344b11d26b9ac66515da1751956386d2 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Mon, 27 Jun 2022 15:27:56 -0700 Subject: [PATCH 26/26] Update packages/api/amplify_api/lib/src/util.dart Co-authored-by: Dillon Nys <24740863+dnys1@users.noreply.github.com> --- packages/api/amplify_api/lib/src/util.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/amplify_api/lib/src/util.dart b/packages/api/amplify_api/lib/src/util.dart index 90647ff6a3..d91d58ce48 100644 --- a/packages/api/amplify_api/lib/src/util.dart +++ b/packages/api/amplify_api/lib/src/util.dart @@ -25,7 +25,7 @@ Map? addContentTypeToHeaders( if (contentType == null) { return headers; } - // Create new map to avoid modifying input headers. + // Create new map to avoid modifying input headers which may be unmodifiable. final modifiedHeaders = Map.of(headers ?? {}); modifiedHeaders.putIfAbsent(AWSHeaders.contentType, () => contentType); return modifiedHeaders;