Skip to content

Commit

Permalink
feat(api): REST methods in dart with auth mode none (#1783)
Browse files Browse the repository at this point in the history
  • Loading branch information
Travis Sheppard committed Aug 16, 2022
1 parent 4621a66 commit 438c236
Show file tree
Hide file tree
Showing 10 changed files with 554 additions and 9 deletions.
1 change: 1 addition & 0 deletions packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class AmplifyHybridImpl extends AmplifyClassImpl {
);
await Future.wait(
[
...API.plugins,
...Auth.plugins,
].map((p) => p.configure(config: amplifyConfig)),
eagerError: true,
Expand Down
74 changes: 74 additions & 0 deletions packages/api/amplify_api/lib/src/amplify_api_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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<EndpointConfig> {
const EndpointConfig(this.name, this.config);

final String name;
final AWSApiConfig config;

@override
List<Object?> 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<String, dynamic>? 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);
}
// 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);
}
}
Original file line number Diff line number Diff line change
@@ -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: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
implements Closeable {
/// 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<http.StreamedResponse> 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;
}
}
163 changes: 162 additions & 1 deletion packages/api/amplify_api/lib/src/api_plugin_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,36 @@ 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:async/async.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
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.
/// {@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<String, AmplifyAuthorizationRestClient> _clientPool = {};

/// The registered [APIAuthProvider] instances.
final Map<APIAuthorizationType, APIAuthProvider> _authProviders = {};

/// {@macro amplify_api.amplify_api_dart}
AmplifyAPIDart({
List<APIAuthProvider> authProviders = const [],
http.Client? baseHttpClient,
this.modelProvider,
}) : super.protected() {
}) : _baseHttpClient = baseHttpClient,
super.protected() {
authProviders.forEach(registerAuthProvider);
}

Expand Down Expand Up @@ -71,11 +85,158 @@ class AmplifyAPIDart extends AmplifyAPI {
}
}

/// 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<String, dynamic>? 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<T> _makeCancelable<T>(Future<T> responseFuture) {
return CancelableOperation.fromFuture(responseFuture);
}

CancelableOperation<AWSStreamedHttpResponse> _prepareRestResponse(
Future<AWSStreamedHttpResponse> responseFuture) {
return _makeCancelable(responseFuture);
}

@override
final ModelProviderInterface? modelProvider;

@override
void registerAuthProvider(APIAuthProvider authProvider) {
_authProviders[authProvider.type] = authProvider;
}

// ====== REST =======

@override
CancelableOperation<AWSStreamedHttpResponse> delete(
String path, {
HttpPayload? body,
Map<String, String>? headers,
Map<String, String>? queryParameters,
String? apiName,
}) {
final uri = _getRestUri(path, apiName, queryParameters);
final client = getRestClient(apiName: apiName);
return _prepareRestResponse(AWSStreamedHttpRequest.delete(
uri,
body: body ?? HttpPayload.empty(),
headers: addContentTypeToHeaders(headers, body),
).send(client));
}

@override
CancelableOperation<AWSStreamedHttpResponse> get(
String path, {
Map<String, String>? headers,
Map<String, String>? 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<AWSStreamedHttpResponse> head(
String path, {
Map<String, String>? headers,
Map<String, String>? queryParameters,
String? apiName,
}) {
final uri = _getRestUri(path, apiName, queryParameters);
final client = getRestClient(apiName: apiName);
return _prepareRestResponse(
AWSHttpRequest.head(
uri,
headers: headers,
).send(client),
);
}

@override
CancelableOperation<AWSStreamedHttpResponse> patch(
String path, {
HttpPayload? body,
Map<String, String>? headers,
Map<String, String>? queryParameters,
String? apiName,
}) {
final uri = _getRestUri(path, apiName, queryParameters);
final client = getRestClient(apiName: apiName);
return _prepareRestResponse(
AWSStreamedHttpRequest.patch(
uri,
headers: addContentTypeToHeaders(headers, body),
body: body ?? HttpPayload.empty(),
).send(client),
);
}

@override
CancelableOperation<AWSStreamedHttpResponse> post(
String path, {
HttpPayload? body,
Map<String, String>? headers,
Map<String, String>? queryParameters,
String? apiName,
}) {
final uri = _getRestUri(path, apiName, queryParameters);
final client = getRestClient(apiName: apiName);
return _prepareRestResponse(
AWSStreamedHttpRequest.post(
uri,
headers: addContentTypeToHeaders(headers, body),
body: body ?? HttpPayload.empty(),
).send(client),
);
}

@override
CancelableOperation<AWSStreamedHttpResponse> put(
String path, {
HttpPayload? body,
Map<String, String>? headers,
Map<String, String>? queryParameters,
String? apiName,
}) {
final uri = _getRestUri(path, apiName, queryParameters);
final client = getRestClient(apiName: apiName);
return _prepareRestResponse(
AWSStreamedHttpRequest.put(
uri,
headers: addContentTypeToHeaders(headers, body),
body: body ?? HttpPayload.empty(),
).send(client),
);
}
}
10 changes: 2 additions & 8 deletions packages/api/amplify_api/lib/src/method_channel_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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,
);
Expand Down
Loading

0 comments on commit 438c236

Please sign in to comment.