// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import 'package:amplify_core/amplify_core.dart'; const _serializedData = 'serializedData'; /// "items", the key name for nested data in AppSync const items = 'items'; class _RelatedFields { _RelatedFields(this.singleFields, this.hasManyFields); final Iterable singleFields; final Iterable hasManyFields; } _RelatedFields _getRelatedFieldsUncached(ModelSchema modelSchema) { final singleFields = modelSchema.fields!.values.where( (field) => field.association?.associationType == ModelAssociationEnum.HasOne || field.association?.associationType == ModelAssociationEnum.BelongsTo || field.type.fieldType == ModelFieldTypeEnum.embedded || field.type.fieldType == ModelFieldTypeEnum.embeddedCollection, ); final hasManyFields = modelSchema.fields!.values.where( (field) => field.association?.associationType == ModelAssociationEnum.HasMany, ); return _RelatedFields(singleFields, hasManyFields); } final _fieldsMemo = {}; // cached to avoid repeat iterations over fields in schema to get related fields _RelatedFields _getRelatedFields(ModelSchema modelSchema) { if (_fieldsMemo[modelSchema] != null) { return _fieldsMemo[modelSchema]!; } final result = _getRelatedFieldsUncached(modelSchema); _fieldsMemo[modelSchema] = result; return _fieldsMemo[modelSchema]!; } // ignore: public_member_api_docs Iterable getBelongsToFieldsFromModelSchema( ModelSchema modelSchema, ) { return _getRelatedFields(modelSchema).singleFields.where( (entry) => entry.association?.associationType == ModelAssociationEnum.BelongsTo, ); } /// Gets the modelSchema from provider that matches the name and validates its fields. ModelSchema getModelSchemaByModelName( String modelName, GraphQLRequestOperation? operation, ) { // ignore: invalid_use_of_protected_member final provider = Amplify.API.defaultPlugin.modelProvider; if (provider == null) { throw const ApiOperationException( 'No modelProvider found', recoverySuggestion: 'Pass in a modelProvider instance while instantiating APIPlugin', ); } // In web, the modelName runtime type conversion will add "$" to returned string. // If ends with "$" on web, strip last character. // TODO(ragingsquirrel3): fix underlying issue with modelName if (zIsWeb && modelName.endsWith(r'$')) { modelName = modelName.substring(0, modelName.length - 1); } final schema = (provider.modelSchemas + provider.customTypeSchemas).firstWhere( (elem) => elem.name == modelName, orElse: () => throw ApiOperationException( 'No schema found for the ModelType provided: $modelName', recoverySuggestion: 'Pass in a valid modelProvider instance while ' 'instantiating APIPlugin or provide a valid ModelType', ), ); if (schema.fields == null) { throw const ApiOperationException( 'Schema found does not have a fields property', recoverySuggestion: 'Pass in a valid modelProvider instance while ' 'instantiating APIPlugin', ); } if (operation == GraphQLRequestOperation.list && schema.pluralName == null) { throw const ApiOperationException( 'No schema name found', recoverySuggestion: 'Pass in a valid modelProvider instance while ' 'instantiating APIPlugin or provide a valid ModelType', ); } return schema; } /// Transform the JSON from AppSync so it matches the fromJson in codegen models. /// 1) Look for a parent in the schema. If that parent exists in the JSON, transform it. /// 2) Look for list of children under [fieldName]["items"] and hoist up so no more ["items"]. Map transformAppSyncJsonToModelJson( Map input, ModelSchema modelSchema, { bool isPaginated = false, }) { input = {...input}; // avoid mutating original // check for list at top-level and transform each entry if (isPaginated && input[items] is List) { final transformedItems = (input[items] as List) .map( (dynamic e) => e != null ? transformAppSyncJsonToModelJson( e as Map, modelSchema, ) : null, ) .toList(); input.update(items, (dynamic value) => transformedItems); return input; } final relatedFields = _getRelatedFields(modelSchema); // transform parents/hasOne recursively for (final parentField in relatedFields.singleFields) { final ofModelName = parentField.type.ofModelName ?? parentField.type.ofCustomTypeName; final inputValue = input[parentField.name]; if ((inputValue is Map || inputValue is List) && ofModelName != null) { final parentSchema = getModelSchemaByModelName(ofModelName, null); input.update(parentField.name, (dynamic parentData) { if (parentData is List) { // only used for embeddedCollection return parentData .map( (dynamic e) => { _serializedData: transformAppSyncJsonToModelJson( e as Map, parentSchema, ), }, ) .toList(); } return { _serializedData: transformAppSyncJsonToModelJson( parentData as Map, parentSchema, ), }; }); } } // transform children recursively for (final childField in relatedFields.hasManyFields) { final ofModelName = childField.type.ofModelName; final inputValue = input[childField.name]; final inputItems = (inputValue is Map) ? inputValue[items] as List? : null; if (inputItems is List && ofModelName != null) { final childSchema = getModelSchemaByModelName(ofModelName, null); final transformedItems = inputItems .map( (dynamic item) => { _serializedData: transformAppSyncJsonToModelJson( item as Map, childSchema, ), }, ) .toList(); input.update(childField.name, (dynamic value) => transformedItems); } } return input; }