diff --git a/packages/amplify_core/lib/src/config/amplify_config.dart b/packages/amplify_core/lib/src/config/amplify_config.dart index 9bf2d9869e..df0ab58bd2 100644 --- a/packages/amplify_core/lib/src/config/amplify_config.dart +++ b/packages/amplify_core/lib/src/config/amplify_config.dart @@ -3,6 +3,7 @@ import 'package:amplify_core/amplify_core.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; part 'amplify_config.g.dart'; @@ -74,12 +75,14 @@ class AmplifyConfig with AWSEquatable, AWSSerializable { ); } + @internal AmplifyOutputs toAmplifyOutputs() { final appSync = auth?.awsPlugin?.appSync; return AmplifyOutputs( version: '1', auth: auth?.toAuthOutputs(), data: api?.toDataOutputs(appSync: appSync), + restApi: api?.toRestApiOutputs(), analytics: analytics?.toAnalyticsOutputs(), notifications: notifications?.toNotificationsOutputs(), storage: storage?.toStorageOutputs(), diff --git a/packages/amplify_core/lib/src/config/amplify_outputs/amplify_outputs.dart b/packages/amplify_core/lib/src/config/amplify_outputs/amplify_outputs.dart index ab16ab242c..72da987de2 100644 --- a/packages/amplify_core/lib/src/config/amplify_outputs/amplify_outputs.dart +++ b/packages/amplify_core/lib/src/config/amplify_outputs/amplify_outputs.dart @@ -6,7 +6,9 @@ import 'package:amplify_core/src/config/amplify_outputs/analytics/analytics_outp import 'package:amplify_core/src/config/amplify_outputs/auth/auth_outputs.dart'; import 'package:amplify_core/src/config/amplify_outputs/data/data_outputs.dart'; import 'package:amplify_core/src/config/amplify_outputs/notifications/notifications_outputs.dart'; +import 'package:amplify_core/src/config/amplify_outputs/rest_api/rest_api_outputs.dart'; import 'package:amplify_core/src/config/amplify_outputs/storage/storage_outputs.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:meta/meta.dart'; part 'amplify_outputs.g.dart'; @@ -26,6 +28,7 @@ class AmplifyOutputs this.analytics, this.auth, this.data, + this.restApi, this.notifications, this.storage, this.custom, @@ -47,7 +50,12 @@ class AmplifyOutputs final AuthOutputs? auth; /// {@macro amplify_core.amplify_outputs.data_outputs} - final DataOutputs? data; + @JsonKey(fromJson: _dataFromJson, toJson: _dataToJson) + final Map? data; + + /// {@macro amplify_core.amplify_outputs.rest_api_outputs} + @internal + final Map? restApi; /// {@macro amplify_core.amplify_outputs.notifications_outputs} final NotificationsOutputs? notifications; @@ -61,8 +69,17 @@ class AmplifyOutputs final Map? custom; @override - List get props => - [schema, version, analytics, auth, data, notifications, storage, custom]; + List get props => [ + schema, + version, + analytics, + auth, + data, + restApi, + notifications, + storage, + custom, + ]; @override String get runtimeTypeName => 'AmplifyOutputs'; @@ -72,3 +89,36 @@ class AmplifyOutputs return _$AmplifyOutputsToJson(this); } } + +/// The name of the API plugin when a Gen 2 config is used. +/// +/// "data" is consistent with the name Gen 2 uses when generating a Gen 1 config. +/// reference: https://github.com/aws-amplify/amplify-backend/blob/4dd9d5a35c378895d1360c15a3b7ad1f09cc7653/packages/client-config/src/client-config-writer/client_config_to_mobile_legacy_converter.ts#L93 +const _dataPluginName = 'data'; + +/// Converts a single data json object to a map of [DataOutputs]. +/// +/// This manual mapping is required since the Amplify Outputs schema only supports +/// a single data object, but Amplify Flutter supports more than 1. +Map? _dataFromJson(Map? object) { + if (object == null) return null; + return { + _dataPluginName: DataOutputs.fromJson(object), + }; +} + +/// Converts a map of [DataOutputs] to a single data json object. +/// +/// This manual mapping is required since the Amplify Outputs schema only supports +/// a single data object, but Amplify Flutter supports more than 1. +Object? _dataToJson(Map? outputs) { + if (outputs == null) return null; + if (outputs.length > 1) { + throw ConfigurationError( + 'Found ${outputs.length} endpoints.' + ' Amplify Outputs does not support multiple GraphQL endpoints.', + ); + } + final data = outputs[_dataPluginName]; + return data?.toJson(); +} diff --git a/packages/amplify_core/lib/src/config/amplify_outputs/amplify_outputs.g.dart b/packages/amplify_core/lib/src/config/amplify_outputs/amplify_outputs.g.dart index 301fd77e27..2688583d6d 100644 --- a/packages/amplify_core/lib/src/config/amplify_outputs/amplify_outputs.g.dart +++ b/packages/amplify_core/lib/src/config/amplify_outputs/amplify_outputs.g.dart @@ -19,9 +19,11 @@ AmplifyOutputs _$AmplifyOutputsFromJson(Map json) => auth: json['auth'] == null ? null : AuthOutputs.fromJson(json['auth'] as Map), - data: json['data'] == null - ? null - : DataOutputs.fromJson(json['data'] as Map), + data: _dataFromJson(json['data'] as Map?), + restApi: (json['rest_api'] as Map?)?.map( + (k, e) => + MapEntry(k, RestApiOutputs.fromJson(e as Map)), + ), notifications: json['notifications'] == null ? null : NotificationsOutputs.fromJson( @@ -45,7 +47,9 @@ Map _$AmplifyOutputsToJson(AmplifyOutputs instance) { val['version'] = instance.version; writeNotNull('analytics', instance.analytics?.toJson()); writeNotNull('auth', instance.auth?.toJson()); - writeNotNull('data', instance.data?.toJson()); + writeNotNull('data', _dataToJson(instance.data)); + writeNotNull( + 'rest_api', instance.restApi?.map((k, e) => MapEntry(k, e.toJson()))); writeNotNull('notifications', instance.notifications?.toJson()); writeNotNull('storage', instance.storage?.toJson()); writeNotNull('custom', instance.custom); diff --git a/packages/amplify_core/lib/src/config/amplify_outputs/rest_api/rest_api_outputs.dart b/packages/amplify_core/lib/src/config/amplify_outputs/rest_api/rest_api_outputs.dart new file mode 100644 index 0000000000..85028f5be1 --- /dev/null +++ b/packages/amplify_core/lib/src/config/amplify_outputs/rest_api/rest_api_outputs.dart @@ -0,0 +1,55 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:amplify_core/amplify_core.dart'; + +part 'rest_api_outputs.g.dart'; + +/// {@template amplify_core.amplify_outputs.rest_api_outputs} +/// An internal representation of Rest API. +/// +/// This class should not be exposed publicly since the Gen 2 schema does not +/// support Rest API. This class exists to support Gen 1 configs that use Rest API. +/// {@endtemplate} +@zAmplifyOutputsSerializable +class RestApiOutputs + with AWSEquatable, AWSSerializable, AWSDebuggable { + /// {@macro amplify_core.amplify_outputs.rest_api_outputs} + const RestApiOutputs({ + required this.awsRegion, + required this.url, + this.apiKey, + required this.authorizationType, + }); + + factory RestApiOutputs.fromJson(Map json) => + _$RestApiOutputsFromJson(json); + + /// The AWS region of Amazon AWS Gateway resources. + final String awsRegion; + + /// The AWS Gateway endpoint URL. + final String url; + + /// The AppSync API Key. + final String? apiKey; + + /// The authorization type. + final APIAuthorizationType authorizationType; + + @override + List get props => [ + awsRegion, + url, + apiKey, + authorizationType, + ]; + + @override + String get runtimeTypeName => 'RestApiOutputs'; + + @override + Object? toJson() { + return _$RestApiOutputsToJson(this); + } +} diff --git a/packages/amplify_core/lib/src/config/amplify_outputs/rest_api/rest_api_outputs.g.dart b/packages/amplify_core/lib/src/config/amplify_outputs/rest_api/rest_api_outputs.g.dart new file mode 100644 index 0000000000..7ab36d0873 --- /dev/null +++ b/packages/amplify_core/lib/src/config/amplify_outputs/rest_api/rest_api_outputs.g.dart @@ -0,0 +1,45 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ignore_for_file: deprecated_member_use_from_same_package + +part of 'rest_api_outputs.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +RestApiOutputs _$RestApiOutputsFromJson(Map json) => + RestApiOutputs( + awsRegion: json['aws_region'] as String, + url: json['url'] as String, + apiKey: json['api_key'] as String?, + authorizationType: $enumDecode( + _$APIAuthorizationTypeEnumMap, json['authorization_type']), + ); + +Map _$RestApiOutputsToJson(RestApiOutputs instance) { + final val = { + 'aws_region': instance.awsRegion, + 'url': instance.url, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('api_key', instance.apiKey); + val['authorization_type'] = + _$APIAuthorizationTypeEnumMap[instance.authorizationType]!; + return val; +} + +const _$APIAuthorizationTypeEnumMap = { + APIAuthorizationType.none: 'NONE', + APIAuthorizationType.apiKey: 'API_KEY', + APIAuthorizationType.iam: 'AWS_IAM', + APIAuthorizationType.oidc: 'OPENID_CONNECT', + APIAuthorizationType.userPools: 'AMAZON_COGNITO_USER_POOLS', + APIAuthorizationType.function: 'AWS_LAMBDA', +}; diff --git a/packages/amplify_core/lib/src/config/analytics/analytics_config.dart b/packages/amplify_core/lib/src/config/analytics/analytics_config.dart index 3efe382393..f8a2e8988c 100644 --- a/packages/amplify_core/lib/src/config/analytics/analytics_config.dart +++ b/packages/amplify_core/lib/src/config/analytics/analytics_config.dart @@ -4,6 +4,7 @@ import 'package:amplify_core/amplify_core.dart'; import 'package:amplify_core/src/config/amplify_outputs/analytics/amazon_pinpoint_outputs.dart'; import 'package:amplify_core/src/config/amplify_outputs/analytics/analytics_outputs.dart'; +import 'package:meta/meta.dart'; export 'pinpoint_config.dart' hide PinpointPluginConfigFactory; @@ -33,6 +34,7 @@ class AnalyticsConfig extends AmplifyPluginConfigMap { @override Map toJson() => _$AnalyticsConfigToJson(this); + @internal AnalyticsOutputs? toAnalyticsOutputs() { final plugin = awsPlugin?.pinpointAnalytics; if (plugin == null) { diff --git a/packages/amplify_core/lib/src/config/api/api_config.dart b/packages/amplify_core/lib/src/config/api/api_config.dart index 214018dbd0..85d6f070bb 100644 --- a/packages/amplify_core/lib/src/config/api/api_config.dart +++ b/packages/amplify_core/lib/src/config/api/api_config.dart @@ -3,6 +3,8 @@ import 'package:amplify_core/amplify_core.dart'; import 'package:amplify_core/src/config/amplify_outputs/data/data_outputs.dart'; +import 'package:amplify_core/src/config/amplify_outputs/rest_api/rest_api_outputs.dart'; +import 'package:meta/meta.dart'; export 'aws_api_config.dart' hide AWSApiPluginConfigFactory; @@ -32,24 +34,59 @@ class ApiConfig extends AmplifyPluginConfigMap { @override Map toJson() => _$ApiConfigToJson(this); - DataOutputs? toDataOutputs({AWSConfigMap? appSync}) { - final plugin = appSync?.default$; + @internal + Map? toDataOutputs({ + AWSConfigMap? appSync, + }) { + final plugins = _getPlugins(EndpointType.graphQL); + if (plugins == null) { + return null; + } + return plugins.map((key, plugin) { + final defaultAuthorizationType = plugin.authorizationType; + final awsRegion = plugin.region; + final url = plugin.endpoint; + final allModes = (appSync?.all.values ?? []); + final authorizationTypes = allModes + .where((plugin) => plugin.apiUrl == url && plugin.region == awsRegion) + .map((config) => config.authMode) + .where((mode) => mode != defaultAuthorizationType) + .toList(); + final data = DataOutputs( + awsRegion: awsRegion, + url: url, + defaultAuthorizationType: defaultAuthorizationType, + apiKey: plugin.apiKey, + authorizationTypes: authorizationTypes, + ); + return MapEntry(key, data); + }); + } + + @internal + Map? toRestApiOutputs() { + final plugins = _getPlugins(EndpointType.rest); + if (plugins == null) { + return null; + } + return plugins.map((key, plugin) { + final rest = RestApiOutputs( + awsRegion: plugin.region, + url: plugin.endpoint, + authorizationType: plugin.authorizationType, + apiKey: plugin.apiKey, + ); + return MapEntry(key, rest); + }); + } + + Map? _getPlugins(EndpointType endpointType) { + final plugin = awsPlugin; if (plugin == null) { return null; } - final region = plugin.region; - final url = plugin.apiUrl; - final apiKey = plugin.apiKey; - final defaultAuthorizationType = plugin.authMode; - final modes = appSync?.all.values.map((config) => config.authMode) ?? []; - final authorizationTypes = - modes.where((mode) => mode != defaultAuthorizationType).toList(); - return DataOutputs( - awsRegion: region, - url: url, - defaultAuthorizationType: defaultAuthorizationType, - apiKey: apiKey, - authorizationTypes: authorizationTypes, - ); + final entries = + plugin.all.entries.where((p) => p.value.endpointType == endpointType); + return Map.fromEntries(entries); } } diff --git a/packages/amplify_core/lib/src/config/auth/auth_config.dart b/packages/amplify_core/lib/src/config/auth/auth_config.dart index 101a725860..0f51ffee36 100644 --- a/packages/amplify_core/lib/src/config/auth/auth_config.dart +++ b/packages/amplify_core/lib/src/config/auth/auth_config.dart @@ -6,6 +6,7 @@ import 'package:amplify_core/src/config/amplify_outputs/auth/auth_outputs.dart'; import 'package:amplify_core/src/config/amplify_outputs/auth/oauth_outputs.dart'; import 'package:amplify_core/src/config/amplify_outputs/auth/oauth_response_type.dart'; import 'package:amplify_core/src/config/amplify_outputs/auth/password_policy.dart'; +import 'package:meta/meta.dart'; export 'cognito_config.dart' hide CognitoPluginConfigFactory; @@ -79,6 +80,7 @@ class AuthConfig extends AmplifyPluginConfigMap { @override AuthConfig copy() => AuthConfig(plugins: Map.of(plugins)); + @internal AuthOutputs? toAuthOutputs() { final plugin = awsPlugin?.auth?.default$; final userPool = awsPlugin?.cognitoUserPool?.default$; diff --git a/packages/amplify_core/lib/src/config/storage/storage_config.dart b/packages/amplify_core/lib/src/config/storage/storage_config.dart index 5d6752c184..b86cda3399 100644 --- a/packages/amplify_core/lib/src/config/storage/storage_config.dart +++ b/packages/amplify_core/lib/src/config/storage/storage_config.dart @@ -3,6 +3,7 @@ import 'package:amplify_core/amplify_core.dart'; import 'package:amplify_core/src/config/amplify_outputs/storage/storage_outputs.dart'; +import 'package:meta/meta.dart'; export 's3_config.dart' hide S3PluginConfigFactory; @@ -32,6 +33,7 @@ class StorageConfig extends AmplifyPluginConfigMap { @override Map toJson() => _$StorageConfigToJson(this); + @internal StorageOutputs? toStorageOutputs() { final plugin = awsPlugin; if (plugin == null) { diff --git a/packages/amplify_core/test/config/amplify_outputs_mapping/amplify_outputs_mapping_test.dart b/packages/amplify_core/test/config/amplify_outputs_mapping/amplify_outputs_mapping_test.dart index 68b4398139..395acd67ac 100644 --- a/packages/amplify_core/test/config/amplify_outputs_mapping/amplify_outputs_mapping_test.dart +++ b/packages/amplify_core/test/config/amplify_outputs_mapping/amplify_outputs_mapping_test.dart @@ -41,6 +41,57 @@ void main() { expect(mappedOutputs.data, null); expect(mappedOutputs.notifications, null); }); + + test('maps config with multiple api plugins', () async { + // hand written config for testing purposes. + const config = ''' + { + "api": { + "plugins": { + "awsAPIPlugin": { + "data1": { + "endpointType": "GraphQL", + "endpoint": "fake-data-url-1", + "region": "us-east-1", + "authorizationType": "AWS_IAM", + "apiKey": "fake-data-api-key" + }, + "data2": { + "endpointType": "GraphQL", + "endpoint": "fake-data-url-2", + "region": "us-east-1", + "authorizationType": "AWS_IAM", + "apiKey": "fake-data-api-key" + }, + "rest1": { + "endpointType": "REST", + "endpoint": "fake-rest-url-1", + "region": "us-east-1", + "authorizationType": "AWS_IAM", + "apiKey": "fake-data-api-key" + }, + "rest2": { + "endpointType": "REST", + "endpoint": "fake-rest-url-2", + "region": "us-east-1", + "authorizationType": "AWS_IAM", + "apiKey": "fake-data-api-key" + } + } + } + } + } + '''; + final configJson = jsonDecode(config) as Map; + final amplifyConfig = AmplifyConfig.fromJson(configJson); + final mappedOutputs = amplifyConfig.toAmplifyOutputs(); + expect(mappedOutputs.data?.length, 2); + expect(mappedOutputs.restApi?.length, 2); + final dataUrls = mappedOutputs.data?.values.map((d) => d.url); + final restUrls = mappedOutputs.restApi?.values.map((d) => d.url); + expect(dataUrls, ['fake-data-url-1', 'fake-data-url-2']); + expect(restUrls, ['fake-rest-url-1', 'fake-rest-url-2']); + }); }); }