From 7cc9826fca71f03e0fb2f004014b372f9708d57f Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Tue, 10 Oct 2023 18:09:34 -0400 Subject: [PATCH] feat(api): Lazy Loading and Custom Selection Set (#2592) Co-authored-by: Erica Eaton --- .gitignore | 1 + .../api/aws/ApiGraphQLRequestOptions.java | 15 +- .../api/aws/GraphQLRequestHelper.java | 95 ++- .../api/aws/SelectionSet.java | 90 ++- .../api/aws/SelectionSetExtensions.kt | 136 ++++ .../api/graphql/GsonResponseAdapters.java | 4 + .../api/aws/ApiGraphQLRequestOptionsTest.kt | 34 + .../api/aws/SelectionSetTest.java | 37 +- .../appsync/SerializedModelAdapterTest.java | 4 +- .../selection-set-lazy-empty-includes.txt | 9 + .../selection-set-lazy-with-includes.txt | 40 ++ .../serde-for-blog-in-serialized-model.json | 8 + ...serde-for-comment-in-serialized-model.json | 14 + ...serde-for-meeting-in-serialized-model.json | 12 + ...nested-custom-type-se-deserialization.json | 8 + aws-api/build.gradle.kts | 16 + .../GraphQLLazyCreateInstrumentationTest.kt | 154 +++++ .../GraphQLLazyDeleteInstrumentationTest.kt | 117 ++++ .../GraphQLLazyQueryInstrumentationTest.kt | 631 ++++++++++++++++++ ...GraphQLLazySubscribeInstrumentationTest.kt | 350 ++++++++++ .../GraphQLLazyUpdateInstrumentationTest.kt | 235 +++++++ .../api/aws/SubscriptionEndpointTest.java | 4 +- .../generated/model/AmplifyModelProvider.java | 53 ++ .../datastore/generated/model/Blog.java | 186 ++++++ .../datastore/generated/model/BlogPath.java | 22 + .../datastore/generated/model/Comment.java | 218 ++++++ .../generated/model/CommentPath.java | 22 + .../generated/model/HasManyChild.java | 212 ++++++ .../generated/model/HasManyChildPath.java | 22 + .../generated/model/HasOneChild.java | 182 +++++ .../generated/model/HasOneChildPath.java | 14 + .../datastore/generated/model/Parent.java | 196 ++++++ .../datastore/generated/model/ParentPath.java | 30 + .../datastore/generated/model/Post.java | 225 +++++++ .../datastore/generated/model/PostPath.java | 30 + .../datastore/generated/model/Project.java | 242 +++++++ .../generated/model/ProjectPath.java | 22 + .../datastore/generated/model/Team.java | 212 ++++++ .../datastore/generated/model/TeamPath.java | 22 + .../datastore/generated/model/schema.graphql | 61 ++ .../api/aws/AWSApiPlugin.java | 6 +- .../api/aws/AWSApiSchemaRegistry.kt | 42 ++ .../api/aws/AWSGraphQLOperation.kt | 62 ++ .../api/aws/ApiLazyModelReference.kt | 130 ++++ .../api/aws/ApiModelListTypes.kt | 113 ++++ .../com/amplifyframework/api/aws/ApiQuery.kt | 48 ++ .../api/aws/AppSyncGraphQLOperation.java | 11 +- .../api/aws/AppSyncGraphQLRequestFactory.java | 333 --------- .../api/aws/AppSyncGraphQLRequestFactory.kt | 556 +++++++++++++++ .../amplifyframework/api/aws/GsonFactory.java | 63 ++ .../api/aws/GsonGraphQLResponseFactory.java | 56 +- .../api/aws/LazyTypeDeserializers.kt | 148 ++++ .../api/aws/ModelPostProcessingTypeAdapter.kt | 107 +++ .../api/aws/ModelProviderLocator.java | 108 +++ .../aws/MultiAuthAppSyncGraphQLOperation.java | 11 +- .../aws/MultiAuthSubscriptionOperation.java | 12 +- .../api/aws/SubscriptionEndpoint.java | 34 +- .../api/aws/SubscriptionOperation.java | 12 +- .../api/graphql/model/ModelMutation.java | 97 --- .../api/graphql/model/ModelMutation.kt | 190 ++++++ .../api/graphql/model/ModelQuery.java | 145 ---- .../api/graphql/model/ModelQuery.kt | 276 ++++++++ .../api/graphql/model/ModelSubscription.java | 76 --- .../api/graphql/model/ModelSubscription.kt | 144 ++++ .../api/aws/AWSApiPluginTest.java | 4 +- .../api/aws/AWSApiSchemaRegistryTest.kt | 58 ++ .../api/aws/ApiLazyModelListTest.kt | 382 +++++++++++ .../api/aws/ApiLazyModelReferenceTest.kt | 253 +++++++ .../api/aws/ApiLoadedModelListTest.kt | 35 + .../aws/AppSyncGraphQLRequestFactoryTest.java | 96 +++ .../aws/MultiAuthSubscriptionOperationTest.kt | 1 + .../graphql/model/ModelMutationTest.kt | 221 ++++++ .../graphql/model/ModelQueryTest.kt | 244 +++++++ .../graphql/model/ModelSubscriptionTest.kt | 172 +++++ .../resources/lazy_create_no_includes.txt | 1 + .../resources/lazy_create_with_includes.txt | 1 + .../resources/lazy_query_no_includes.json | 1 + .../resources/lazy_query_with_includes.json | 1 + .../auth/cognito/helpers/MFAHelper.kt | 15 + .../auth/TOTPSetupDetailsTest.kt | 15 + .../options/APIOptionsKotlinContractTest.kt | 15 + .../syncengine/ReachabilityMonitorTest.kt | 15 + .../syncengine/TestSchedulerProvider.kt | 15 + .../options/AWSFaceLivenessSessionOptions.kt | 15 + configuration/checkstyle-suppressions.xml | 1 + core/build.gradle.kts | 1 + .../api/graphql/GraphQLOperation.java | 12 +- .../core/NullableConsumer.java | 32 + .../core/model/LoadedModelReferenceImpl.kt | 26 + .../core/model/ModelException.kt | 34 + .../core/model/ModelField.java | 62 ++ .../amplifyframework/core/model/ModelList.kt | 96 +++ .../core/model/ModelPropertyPath.kt | 136 ++++ .../core/model/ModelReference.kt | 62 ++ .../core/model/ModelSchema.java | 13 +- .../core/model/SchemaRegistry.java | 8 +- .../core/model/SchemaRegistryUtils.kt | 65 ++ .../core/model/annotations/HasOne.java | 7 + .../core/model/annotations/ModelConfig.java | 6 + .../model/LoadedModelReferenceImplTest.kt | 42 ++ .../core/model/ModelPathTest.kt | 59 ++ .../core/model/ModelSchemaTest.java | 102 +++ .../core/model/SchemaRegistryUtilsTest.kt | 84 +++ scripts/pull_backend_config_from_s3 | 1 + .../testmodels/lazy/AmplifyModelProvider.java | 53 ++ .../testmodels/lazy/Blog.java | 193 ++++++ .../testmodels/lazy/BlogPath.java | 22 + .../testmodels/lazy/Comment.java | 222 ++++++ .../testmodels/lazy/CommentPath.java | 22 + .../testmodels/lazy/Post.java | 229 +++++++ .../testmodels/lazy/PostPath.java | 30 + .../testmodels/lazy/schema.graphql | 19 + .../testmodels/lazy/LazyTypeTest.kt | 44 ++ .../amplifyframework/testutils/RepeatRule.kt | 4 +- 114 files changed, 9281 insertions(+), 731 deletions(-) rename {aws-api => aws-api-appsync}/src/main/java/com/amplifyframework/api/aws/ApiGraphQLRequestOptions.java (82%) create mode 100644 aws-api-appsync/src/main/java/com/amplifyframework/api/aws/SelectionSetExtensions.kt create mode 100644 aws-api-appsync/src/test/java/com/amplifyframework/api/aws/ApiGraphQLRequestOptionsTest.kt create mode 100644 aws-api-appsync/src/test/resources/selection-set-lazy-empty-includes.txt create mode 100644 aws-api-appsync/src/test/resources/selection-set-lazy-with-includes.txt create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyCreateInstrumentationTest.kt create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyDeleteInstrumentationTest.kt create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyQueryInstrumentationTest.kt create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazySubscribeInstrumentationTest.kt create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyUpdateInstrumentationTest.kt create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/AmplifyModelProvider.java create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Blog.java create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/BlogPath.java create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Comment.java create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/CommentPath.java create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasManyChild.java create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasManyChildPath.java create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasOneChild.java create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasOneChildPath.java create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Parent.java create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/ParentPath.java create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Post.java create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/PostPath.java create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Project.java create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/ProjectPath.java create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Team.java create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/TeamPath.java create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/schema.graphql create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiSchemaRegistry.kt create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/AWSGraphQLOperation.kt create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/ApiQuery.kt delete mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.java create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.kt create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/GsonFactory.java create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/LazyTypeDeserializers.kt create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/ModelPostProcessingTypeAdapter.kt create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/ModelProviderLocator.java delete mode 100644 aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelMutation.java create mode 100644 aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelMutation.kt delete mode 100644 aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelQuery.java create mode 100644 aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelQuery.kt delete mode 100644 aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelSubscription.java create mode 100644 aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelSubscription.kt create mode 100644 aws-api/src/test/java/com/amplifyframework/api/aws/AWSApiSchemaRegistryTest.kt create mode 100644 aws-api/src/test/java/com/amplifyframework/api/aws/ApiLazyModelListTest.kt create mode 100644 aws-api/src/test/java/com/amplifyframework/api/aws/ApiLazyModelReferenceTest.kt create mode 100644 aws-api/src/test/java/com/amplifyframework/api/aws/ApiLoadedModelListTest.kt create mode 100644 aws-api/src/test/java/com/amplifyframework/graphql/model/ModelMutationTest.kt create mode 100644 aws-api/src/test/java/com/amplifyframework/graphql/model/ModelQueryTest.kt create mode 100644 aws-api/src/test/java/com/amplifyframework/graphql/model/ModelSubscriptionTest.kt create mode 100644 aws-api/src/test/resources/lazy_create_no_includes.txt create mode 100644 aws-api/src/test/resources/lazy_create_with_includes.txt create mode 100644 aws-api/src/test/resources/lazy_query_no_includes.json create mode 100644 aws-api/src/test/resources/lazy_query_with_includes.json create mode 100644 core/src/main/java/com/amplifyframework/core/NullableConsumer.java create mode 100644 core/src/main/java/com/amplifyframework/core/model/LoadedModelReferenceImpl.kt create mode 100644 core/src/main/java/com/amplifyframework/core/model/ModelException.kt create mode 100644 core/src/main/java/com/amplifyframework/core/model/ModelList.kt create mode 100644 core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt create mode 100644 core/src/main/java/com/amplifyframework/core/model/ModelReference.kt create mode 100644 core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.kt create mode 100644 core/src/test/java/com/amplifyframework/core/model/LoadedModelReferenceImplTest.kt create mode 100644 core/src/test/java/com/amplifyframework/core/model/ModelPathTest.kt create mode 100644 core/src/test/java/com/amplifyframework/core/model/SchemaRegistryUtilsTest.kt create mode 100644 testmodels/src/main/java/com/amplifyframework/testmodels/lazy/AmplifyModelProvider.java create mode 100644 testmodels/src/main/java/com/amplifyframework/testmodels/lazy/Blog.java create mode 100644 testmodels/src/main/java/com/amplifyframework/testmodels/lazy/BlogPath.java create mode 100644 testmodels/src/main/java/com/amplifyframework/testmodels/lazy/Comment.java create mode 100644 testmodels/src/main/java/com/amplifyframework/testmodels/lazy/CommentPath.java create mode 100644 testmodels/src/main/java/com/amplifyframework/testmodels/lazy/Post.java create mode 100644 testmodels/src/main/java/com/amplifyframework/testmodels/lazy/PostPath.java create mode 100644 testmodels/src/main/java/com/amplifyframework/testmodels/lazy/schema.graphql create mode 100644 testmodels/src/test/java/com/amplifyframework/testmodels/lazy/LazyTypeTest.kt diff --git a/.gitignore b/.gitignore index dd2f83eff2..0672ab652d 100644 --- a/.gitignore +++ b/.gitignore @@ -86,6 +86,7 @@ __pycache__/ **/amplifyconfiguration_v2.json **/credentials.json **/google_client_creds.json +**/amplifyconfiguration*.json # IDE files .idea/** diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiGraphQLRequestOptions.java b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/ApiGraphQLRequestOptions.java similarity index 82% rename from aws-api/src/main/java/com/amplifyframework/api/aws/ApiGraphQLRequestOptions.java rename to aws-api-appsync/src/main/java/com/amplifyframework/api/aws/ApiGraphQLRequestOptions.java index 22cdf9cbbe..c0e6a5a0e1 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiGraphQLRequestOptions.java +++ b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/ApiGraphQLRequestOptions.java @@ -27,6 +27,19 @@ public final class ApiGraphQLRequestOptions implements GraphQLRequestOptions { private static final String ITEMS_KEY = "items"; private static final String NEXT_TOKEN_KEY = "nextToken"; + private static final int DEFAULT_MAX_DEPTH = 2; + + private int maxDepth = DEFAULT_MAX_DEPTH; + + /** + * Public constructor to create ApiGraphQLRequestOptions. + */ + public ApiGraphQLRequestOptions() {} + + ApiGraphQLRequestOptions(int maxDepth) { + this.maxDepth = maxDepth; + } + @NonNull @Override public List paginationFields() { @@ -47,7 +60,7 @@ public String listField() { @Override public int maxDepth() { - return 2; + return maxDepth; } @NonNull diff --git a/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/GraphQLRequestHelper.java b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/GraphQLRequestHelper.java index 98b03ab55c..0a7af538a4 100644 --- a/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/GraphQLRequestHelper.java +++ b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/GraphQLRequestHelper.java @@ -22,10 +22,12 @@ import com.amplifyframework.api.graphql.MutationType; import com.amplifyframework.core.model.AuthRule; import com.amplifyframework.core.model.AuthStrategy; +import com.amplifyframework.core.model.LoadedModelReference; import com.amplifyframework.core.model.Model; import com.amplifyframework.core.model.ModelAssociation; import com.amplifyframework.core.model.ModelField; import com.amplifyframework.core.model.ModelIdentifier; +import com.amplifyframework.core.model.ModelReference; import com.amplifyframework.core.model.ModelSchema; import com.amplifyframework.core.model.SerializedCustomType; import com.amplifyframework.core.model.SerializedModel; @@ -171,7 +173,7 @@ public static Map getDeleteMutationInputMap( @NonNull ModelSchema schema, @NonNull Model instance) throws AmplifyException { final Map input = new HashMap<>(); for (String fieldName : schema.getPrimaryIndexFields()) { - input.put(fieldName, extractFieldValue(fieldName, instance, schema)); + input.put(fieldName, extractFieldValue(fieldName, instance, schema, true)); } return input; } @@ -224,21 +226,30 @@ private static Map extractFieldLevelData( continue; } - Object fieldValue = extractFieldValue(modelField.getName(), instance, schema); + Object fieldValue = extractFieldValue(modelField.getName(), instance, schema, false); + Object underlyingFieldValue = fieldValue; + if (modelField.isModelReference() && fieldValue != null) { + ModelReference modelReference = (ModelReference) fieldValue; + if (modelReference instanceof LoadedModelReference) { + underlyingFieldValue = ((LoadedModelReference) modelReference).getValue(); + } + } if (association == null) { result.put(fieldName, fieldValue); } else if (association.isOwner()) { - if (fieldValue == null && MutationType.CREATE.equals(type)) { + if ((fieldValue == null || + (modelField.isModelReference() && underlyingFieldValue == null)) && + MutationType.CREATE.equals(type)) { // Do not set null values on associations for create mutations. } else if (schema.getVersion() >= 1 && association.getTargetNames() != null && association.getTargetNames().length > 0) { // When target name length is more than 0 there are two scenarios, one is when // there is custom primary key and other is when we have composite primary key. - insertForeignKeyValues(result, modelField, fieldValue, association); + insertForeignKeyValues(result, modelField, fieldValue, underlyingFieldValue, association); } else { String targetName = association.getTargetName(); - result.put(targetName, extractAssociateId(modelField, fieldValue)); + result.put(targetName, extractAssociateId(modelField, fieldValue, underlyingFieldValue)); } } // Ignore if field is associated, but is not a "belongsTo" relationship @@ -250,58 +261,94 @@ private static void insertForeignKeyValues( Map result, ModelField modelField, Object fieldValue, + Object underlyingFieldValue, ModelAssociation association) { if (modelField.isModel() && fieldValue == null) { - // When there is no model field value, set null for removal of values or deassociation. + // When there is no model field value, set null for removal of values or association. for (String key : association.getTargetNames()) { result.put(key, null); } - } else if (modelField.isModel() && fieldValue instanceof Model) { - if (((Model) fieldValue).resolveIdentifier() instanceof ModelIdentifier) { - final ModelIdentifier primaryKey = (ModelIdentifier) ((Model) fieldValue).resolveIdentifier(); - ListIterator targetNames = Arrays.asList(association.getTargetNames()).listIterator(); - Iterator sortedKeys = primaryKey.sortedKeys().listIterator(); + } else if ((modelField.isModel() || modelField.isModelReference()) && underlyingFieldValue instanceof Model) { + if (((Model) underlyingFieldValue).resolveIdentifier() instanceof ModelIdentifier) { + // Here, we are unwrapping our ModelReference to grab our foreign keys. + // If we have a ModelIdentifier, we can pull all the key values, but we don't have + // the key names. We must grab those from the association target names + final ModelIdentifier primaryKey = + (ModelIdentifier) ((Model) underlyingFieldValue).resolveIdentifier(); + ListIterator targetNames = + Arrays.asList(association.getTargetNames()).listIterator(); + Iterator sortedKeys = + primaryKey.sortedKeys().listIterator(); result.put(targetNames.next(), primaryKey.key()); while (targetNames.hasNext()) { result.put(targetNames.next(), sortedKeys.next()); } - } else if ((fieldValue instanceof SerializedModel)) { - SerializedModel serializedModel = ((SerializedModel) fieldValue); + } else if ((underlyingFieldValue instanceof SerializedModel)) { + SerializedModel serializedModel = ((SerializedModel) underlyingFieldValue); ModelSchema serializedSchema = serializedModel.getModelSchema(); if (serializedSchema != null && serializedSchema.getPrimaryIndexFields().size() > 1) { - ListIterator primaryKeyFieldsIterator = serializedSchema.getPrimaryIndexFields() + ListIterator primaryKeyFieldsIterator = + serializedSchema.getPrimaryIndexFields() .listIterator(); for (String targetName : association.getTargetNames()) { result.put(targetName, serializedModel.getSerializedData() .get(primaryKeyFieldsIterator.next())); } } else { - result.put(association.getTargetNames()[0], ((Model) fieldValue).resolveIdentifier().toString()); + // our key was not a ModelIdentifier type, so it must be a singular primary key + result.put( + association.getTargetNames()[0], + ((Model) underlyingFieldValue).resolveIdentifier().toString() + ); } } else { - result.put(association.getTargetNames()[0], ((Model) fieldValue).resolveIdentifier().toString()); + // our key was not a ModelIdentifier type, so it must be a singular primary key + result.put( + association.getTargetNames()[0], + ((Model) underlyingFieldValue).resolveIdentifier().toString() + ); + } + } else if (modelField.isModelReference() && fieldValue instanceof ModelReference) { + // Here we are unwrapping our ModelReference and inserting + Map identifiers = ((ModelReference) fieldValue).getIdentifier(); + if (identifiers.isEmpty()) { + for (String key : association.getTargetNames()) { + result.put(key, null); + } } } } - private static Object extractAssociateId(ModelField modelField, Object fieldValue) { - if (modelField.isModel() && fieldValue instanceof Model) { - return ((Model) fieldValue).resolveIdentifier(); + private static Object extractAssociateId(ModelField modelField, Object fieldValue, Object underlyingFieldValue) { + if ((modelField.isModel() || modelField.isModelReference()) && underlyingFieldValue instanceof Model) { + return ((Model) underlyingFieldValue).resolveIdentifier(); } else if (modelField.isModel() && fieldValue instanceof Map) { return ((Map) fieldValue).get("id"); } else if (modelField.isModel() && fieldValue == null) { // When there is no model field value, set null for removal of values or deassociation. return null; + } else if (modelField.isModelReference() && fieldValue instanceof ModelReference) { + Map identifiers = ((ModelReference) fieldValue).getIdentifier(); + if (identifiers.isEmpty()) { + return null; + } else { + return identifiers.get("id"); + } } else { throw new IllegalStateException("Associated data is not Model or Map."); } } - private static Object extractFieldValue(String fieldName, Model instance, ModelSchema schema) + private static Object extractFieldValue( + String fieldName, + Model instance, + ModelSchema schema, + Boolean extractLazyValue + ) throws AmplifyException { if (instance instanceof SerializedModel) { SerializedModel serializedModel = (SerializedModel) instance; @@ -316,7 +363,13 @@ private static Object extractFieldValue(String fieldName, Model instance, ModelS try { Field privateField = instance.getClass().getDeclaredField(fieldName); privateField.setAccessible(true); - return privateField.get(instance); + Object fieldInstance = privateField.get(instance); + // In some cases, we don't want to return a ModelReference value. If extractLazyValue + // is set, we unwrap the reference to grab to value underneath + if (extractLazyValue && fieldInstance != null && privateField.getType() == LoadedModelReference.class) { + return ((LoadedModelReference) fieldInstance).getValue(); + } + return fieldInstance; } catch (Exception exception) { throw new AmplifyException( "An invalid field was provided. " + fieldName + " is not present in " + schema.getName(), diff --git a/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/SelectionSet.java b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/SelectionSet.java index ec90af6369..64d43efe99 100644 --- a/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/SelectionSet.java +++ b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/SelectionSet.java @@ -17,6 +17,7 @@ import android.text.TextUtils; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.util.ObjectsCompat; import com.amplifyframework.AmplifyException; @@ -29,7 +30,10 @@ import com.amplifyframework.core.model.Model; import com.amplifyframework.core.model.ModelAssociation; import com.amplifyframework.core.model.ModelField; +import com.amplifyframework.core.model.ModelList; +import com.amplifyframework.core.model.ModelReference; import com.amplifyframework.core.model.ModelSchema; +import com.amplifyframework.core.model.PropertyContainerPath; import com.amplifyframework.core.model.SchemaRegistry; import com.amplifyframework.core.model.SerializedModel; import com.amplifyframework.core.model.types.JavaFieldType; @@ -86,6 +90,15 @@ public SelectionSet(String value, @NonNull Set nodes) { this.nodes = Objects.requireNonNull(nodes); } + /** + * Returns node value. + * @return node value + */ + @Nullable + protected String getValue() { + return value; + } + /** * Returns child nodes. * @return child nodes @@ -172,13 +185,20 @@ public static SelectionSet.Builder builder() { * Factory class for creating and serializing a selection set within a GraphQL document. */ static final class Builder { + private String value; private Class modelClass; private Operation operation; private GraphQLRequestOptions requestOptions; private ModelSchema modelSchema; + private List includeRelationships; Builder() { } + Builder value(@Nullable String value) { + this.value = value; + return Builder.this; + } + public Builder modelClass(@NonNull Class modelClass) { this.modelClass = Objects.requireNonNull(modelClass); return Builder.this; @@ -199,6 +219,11 @@ public Builder requestOptions(@NonNull GraphQLRequestOptions requestOptions) { return Builder.this; } + public Builder includeRelationships(@NonNull List relationships) { + this.includeRelationships = relationships; + return Builder.this; + } + /** * Builds the SelectionSet containing all of the fields of the provided model class. * @return selection set @@ -210,10 +235,21 @@ public SelectionSet build() throws AmplifyException { "Provide either a modelClass or a modelSchema to build the selection set"); } Objects.requireNonNull(this.operation); - SelectionSet node = new SelectionSet(null, + SelectionSet node = new SelectionSet(value, SerializedModel.class == modelClass ? getModelFields(modelSchema, requestOptions.maxDepth(), operation) - : getModelFields(modelClass, requestOptions.maxDepth(), operation)); + : getModelFields(modelClass, requestOptions.maxDepth(), operation, false)); + + // Relationships need to be added before wrapping pagination + if (includeRelationships != null) { + for (PropertyContainerPath association : includeRelationships) { + SelectionSet included = SelectionSetUtils.asSelectionSetWithoutRoot(association); + if (included != null) { + SelectionSetUtils.mergeChild(node, included); + } + } + } + if (QueryType.LIST.equals(operation) || QueryType.SYNC.equals(operation)) { node = wrapPagination(node); } @@ -247,13 +283,20 @@ private Set wrapPagination(Set nodes) { * TODO: this is mostly duplicative of {@link #getModelFields(ModelSchema, int, Operation)}. * Long-term, we want to remove this current method and rely only on the ModelSchema-based * version. - * @param clazz Class from which to build selection set - * @param depth Number of children deep to explore - * @return Selection Set + * + * @param clazz Class from which to build selection set + * @param depth Number of children deep to explore + * @param primaryKeyOnly if keys should only be included + * @return SelectionSet for given class * @throws AmplifyException On failure to build selection set */ @SuppressWarnings("unchecked") // Cast to Class - private Set getModelFields(Class clazz, int depth, Operation operation) + private Set getModelFields( + Class clazz, + int depth, + Operation operation, + Boolean primaryKeyOnly + ) throws AmplifyException { if (depth < 0) { return new HashSet<>(); @@ -262,9 +305,12 @@ private Set getModelFields(Class clazz, int depth Set result = new HashSet<>(); ModelSchema schema = ModelSchema.fromModelClass(clazz); - if (depth == 0 - && LeafSerializationBehavior.JUST_ID.equals(requestOptions.leafSerializationBehavior()) - && operation != QueryType.SYNC + if ( + (depth == 0 + && (LeafSerializationBehavior.JUST_ID.equals( + requestOptions.leafSerializationBehavior() + ) || primaryKeyOnly) + && operation != QueryType.SYNC) ) { for (String s : schema.getPrimaryIndexFields()) { result.add(new SelectionSet(s)); @@ -275,17 +321,33 @@ private Set getModelFields(Class clazz, int depth for (Field field : FieldFinder.findModelFieldsIn(clazz)) { String fieldName = field.getName(); if (schema.getAssociations().containsKey(fieldName)) { - if (List.class.isAssignableFrom(field.getType())) { + if (ModelList.class.isAssignableFrom(field.getType())) { + // Default behavior is to not include ModeList to allow for lazy loading + // We do not need to inject any keys since ModelList values are pulled + // from parent information. + continue; + } else if (List.class.isAssignableFrom(field.getType())) { if (depth >= 1) { ParameterizedType listType = (ParameterizedType) field.getGenericType(); Class listTypeClass = (Class) listType.getActualTypeArguments()[0]; - Set fields = wrapPagination(getModelFields(listTypeClass, - depth - 1, - operation)); + Set fields = wrapPagination( + getModelFields( + listTypeClass, + depth - 1, + operation, + false + ) + ); result.add(new SelectionSet(fieldName, fields)); } + } else if (ModelReference.class.isAssignableFrom(field.getType())) { + ParameterizedType pType = (ParameterizedType) field.getGenericType(); + Class modalClass = (Class) pType.getActualTypeArguments()[0]; + Set fields = getModelFields(modalClass, 0, operation, true); + result.add(new SelectionSet(fieldName, fields)); } else if (depth >= 1) { - Set fields = getModelFields((Class) field.getType(), depth - 1, operation); + Class modalClass = (Class) field.getType(); + Set fields = getModelFields(modalClass, depth - 1, operation, false); result.add(new SelectionSet(fieldName, fields)); } } else if (isCustomType(field)) { diff --git a/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/SelectionSetExtensions.kt b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/SelectionSetExtensions.kt new file mode 100644 index 0000000000..b16dbfaf51 --- /dev/null +++ b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/SelectionSetExtensions.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2023 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. + */ + +@file:JvmName("SelectionSetUtils") + +package com.amplifyframework.api.aws + +import com.amplifyframework.api.graphql.QueryType +import com.amplifyframework.core.model.PropertyContainerPath + +/** + * Find a child in the tree matching its `value`. + * + * @param name: the name to match the child node of type `SelectionSetField` + * @return the matched `SelectionSet` or `nil` if there's no child with the specified name. + */ +internal fun SelectionSet.findChildByName(name: String) = nodes.find { it.value == name } + +/** + * Replaces or adds a new child to the selection set tree. When a child node exists + * with a matching `value` property of the `SelectionSet` the node will be replaced + * while retaining its position in the children list. Otherwise the call is + * delegated to `nodes.add()`. + * + * @param selectionSet: the child node to be replaced. + */ +internal fun SelectionSet.replaceChild(selectionSet: SelectionSet) { + this.nodes.removeIf { it.value == selectionSet.value } + this.nodes.add(selectionSet) +} + +/** + * Transforms the entire property path (walking up the tree) into a `SelectionSet`. + */ +internal fun PropertyContainerPath.asSelectionSetWithoutRoot(): SelectionSet? { + // create a lookup to hold info on whether or not the selection set is a collection or not + val isCollectionLookup = mutableListOf() + val selectionSets = nodesInPath(this, false).map { + // always add to lookup list so that indexes match + isCollectionLookup.add(it.getMetadata().isCollection) + getSelectionSet(it) + } + + if (selectionSets.isEmpty()) { + return null + } + + return selectionSets.reduceIndexed { i, acc, selectionSet -> + if (isCollectionLookup[i]) { + selectionSet.nodes.find { it.value == "items" }?.replaceChild(acc) + } else { + selectionSet.replaceChild(acc) + } + selectionSet + } +} + +private fun getSelectionSet(node: PropertyContainerPath): SelectionSet { + val metadata = node.getMetadata() + val name = if (metadata.isCollection) "items" else metadata.name + + var selectionSet = SelectionSet.builder() + .operation(QueryType.GET) + .value(name) + .requestOptions(ApiGraphQLRequestOptions(0)) + .modelClass(node.getModelType()) + .build() + + if (metadata.isCollection) { + selectionSet = SelectionSet(metadata.name, mutableSetOf(selectionSet)) + } + + return selectionSet +} + +private fun shouldProcessNode(node: PropertyContainerPath, includeRoot: Boolean): Boolean { + return includeRoot || node.getMetadata().parent != null +} + +private fun nodesInPath(node: PropertyContainerPath, includeRoot: Boolean): List { + var currentNode: PropertyContainerPath? = node + val path = mutableListOf() + + while (currentNode != null && shouldProcessNode(currentNode, includeRoot)) { + path.add(currentNode) + currentNode = currentNode.getMetadata().parent as? PropertyContainerPath + } + return path +} + +/** + * Merges a subtree into the this `SelectionSet`. The subtree position will be determined + * by the value of the node's `name`. When an existing node is found the algorithm will + * merge its children to ensure no values are lost or incorrectly overwritten. + * + * @param selectionSet the subtree to be merged into the current tree. + * + * @see findChildByName + * @see replaceChild + */ +@JvmName("mergeChild") +internal fun SelectionSet.mergeChild(selectionSet: SelectionSet) { + val name = selectionSet.value ?: "" + val existingField = findChildByName(name) + + if (existingField != null) { + val replaceFields = mutableListOf() + selectionSet.nodes.forEach { child -> + val childName = child.value + if (child.nodes.isNotEmpty() && childName != null) { + if (existingField.findChildByName(childName) != null) { + existingField.mergeChild(child) + } else { + replaceFields.add(child) + } + } else { + replaceFields.add(child) + } + } + replaceFields.forEach(existingField::replaceChild) + } else { + nodes.add(selectionSet) + } +} diff --git a/aws-api-appsync/src/main/java/com/amplifyframework/api/graphql/GsonResponseAdapters.java b/aws-api-appsync/src/main/java/com/amplifyframework/api/graphql/GsonResponseAdapters.java index efbf1bc8df..054c20c72b 100644 --- a/aws-api-appsync/src/main/java/com/amplifyframework/api/graphql/GsonResponseAdapters.java +++ b/aws-api-appsync/src/main/java/com/amplifyframework/api/graphql/GsonResponseAdapters.java @@ -16,6 +16,7 @@ package com.amplifyframework.api.graphql; import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.ModelPage; import com.amplifyframework.datastore.appsync.ModelWithMetadata; import com.amplifyframework.util.GsonObjectConverter; import com.amplifyframework.util.TypeMaker; @@ -110,6 +111,9 @@ private boolean shouldSkipQueryLevel(Type type) { if (Iterable.class.isAssignableFrom((Class) rawType)) { return true; } + if (ModelPage.class.isAssignableFrom((Class) rawType)) { + return true; + } } else { if (Model.class.isAssignableFrom((Class) type)) { return true; diff --git a/aws-api-appsync/src/test/java/com/amplifyframework/api/aws/ApiGraphQLRequestOptionsTest.kt b/aws-api-appsync/src/test/java/com/amplifyframework/api/aws/ApiGraphQLRequestOptionsTest.kt new file mode 100644 index 0000000000..0a1d43b5be --- /dev/null +++ b/aws-api-appsync/src/test/java/com/amplifyframework/api/aws/ApiGraphQLRequestOptionsTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.api.aws + +import org.junit.Assert.assertEquals +import org.junit.Test + +class ApiGraphQLRequestOptionsTest { + @Test + fun testDefaultMaxDepth() { + val options = ApiGraphQLRequestOptions() + assertEquals(2, options.maxDepth()) + } + + @Test + fun testCustomMaxDepth() { + val customDepth = 1 + val options = ApiGraphQLRequestOptions(customDepth) + assertEquals(customDepth, options.maxDepth()) + } +} diff --git a/aws-api-appsync/src/test/java/com/amplifyframework/api/aws/SelectionSetTest.java b/aws-api-appsync/src/test/java/com/amplifyframework/api/aws/SelectionSetTest.java index 3ea11e1cfe..6f9991666d 100644 --- a/aws-api-appsync/src/test/java/com/amplifyframework/api/aws/SelectionSetTest.java +++ b/aws-api-appsync/src/test/java/com/amplifyframework/api/aws/SelectionSetTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2023 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. @@ -28,6 +28,7 @@ import com.amplifyframework.core.model.SchemaRegistry; import com.amplifyframework.core.model.SerializedModel; import com.amplifyframework.testmodels.commentsblog.Post; +import com.amplifyframework.testmodels.lazy.PostPath; import com.amplifyframework.testmodels.ownerauth.OwnerAuth; import com.amplifyframework.testmodels.ownerauth.OwnerAuthExplicit; import com.amplifyframework.testmodels.parenting.Parent; @@ -42,6 +43,7 @@ import java.util.HashMap; import java.util.Map; +import static com.amplifyframework.core.model.ModelPropertyPathKt.includes; import static org.junit.Assert.assertEquals; @RunWith(RobolectricTestRunner.class) @@ -340,4 +342,37 @@ public void nestedSerializedModel() throws AmplifyException { assertEquals(Resources.readAsString("selection-set-post-nested.txt"), selectionSet.toString() + "\n"); } + + /** + * Test that selection set serialization works as expected for lazy types. + * @throws AmplifyException if a ModelSchema can't be derived from Post.class + */ + @Test + public void simpleLazyTypesSerializeToExpectedValue() throws AmplifyException { + PostPath postPath = com.amplifyframework.testmodels.lazy.Post.rootPath; + SelectionSet selectionSet = SelectionSet.builder() + .modelClass(com.amplifyframework.testmodels.lazy.Post.class) + .operation(QueryType.GET) + .requestOptions(new ApiGraphQLRequestOptions(0)) + .includeRelationships( + includes(postPath.getBlog().getPosts(), postPath.getComments().getPost()) + ) + .build(); + assertEquals(Resources.readAsString("selection-set-lazy-with-includes.txt"), selectionSet.toString() + "\n"); + } + + /** + * Test that selection set serialization works as expected for lazy types without includes. + * @throws AmplifyException if a ModelSchema can't be derived from Post.class + */ + @Test + public void simpleLazyTypesSerializeToExpectedValueWithEmptyIncludes() throws AmplifyException { + SelectionSet selectionSet = SelectionSet.builder() + .modelClass(com.amplifyframework.testmodels.lazy.Post.class) + .operation(QueryType.GET) + .requestOptions(new ApiGraphQLRequestOptions(0)) + .includeRelationships(includes()) + .build(); + assertEquals(Resources.readAsString("selection-set-lazy-empty-includes.txt"), selectionSet.toString() + "\n"); + } } diff --git a/aws-api-appsync/src/test/java/com/amplifyframework/datastore/appsync/SerializedModelAdapterTest.java b/aws-api-appsync/src/test/java/com/amplifyframework/datastore/appsync/SerializedModelAdapterTest.java index d4dfd10f3f..0160190334 100644 --- a/aws-api-appsync/src/test/java/com/amplifyframework/datastore/appsync/SerializedModelAdapterTest.java +++ b/aws-api-appsync/src/test/java/com/amplifyframework/datastore/appsync/SerializedModelAdapterTest.java @@ -126,7 +126,7 @@ public void serdeForNestedSerializedModels() throws JSONException, AmplifyExcept String expectedJson = new JSONObject(Resources.readAsString(resourcePath)).toString(2); String actualJson = new JSONObject(gson.toJson(blogAsSerializedModel)).toString(2); - Assert.assertEquals(expectedJson, actualJson); + JSONAssert.assertEquals(expectedJson, actualJson, true); SerializedModel recovered = gson.fromJson(expectedJson, SerializedModel.class); Assert.assertEquals(blogAsSerializedModel, recovered); @@ -182,7 +182,7 @@ public void serdeForNestedCustomTypes() throws JSONException, AmplifyException { String expectedJson = new JSONObject(Resources.readAsString(resourcePath)).toString(2); String actualJson = new JSONObject(gson.toJson(person)).toString(2); - Assert.assertEquals(expectedJson, actualJson); + JSONAssert.assertEquals(expectedJson, actualJson, true); SerializedModel recovered = gson.fromJson(expectedJson, SerializedModel.class); Assert.assertEquals(person, recovered); diff --git a/aws-api-appsync/src/test/resources/selection-set-lazy-empty-includes.txt b/aws-api-appsync/src/test/resources/selection-set-lazy-empty-includes.txt new file mode 100644 index 0000000000..19a234e946 --- /dev/null +++ b/aws-api-appsync/src/test/resources/selection-set-lazy-empty-includes.txt @@ -0,0 +1,9 @@ + { + blog { + id + } + createdAt + id + name + updatedAt +} \ No newline at end of file diff --git a/aws-api-appsync/src/test/resources/selection-set-lazy-with-includes.txt b/aws-api-appsync/src/test/resources/selection-set-lazy-with-includes.txt new file mode 100644 index 0000000000..8f312c37e6 --- /dev/null +++ b/aws-api-appsync/src/test/resources/selection-set-lazy-with-includes.txt @@ -0,0 +1,40 @@ + { + blog { + createdAt + id + name + posts { + items { + blog { + id + } + createdAt + id + name + updatedAt + } + } + updatedAt + } + comments { + items { + createdAt + id + post { + blog { + id + } + createdAt + id + name + updatedAt + } + text + updatedAt + } + } + createdAt + id + name + updatedAt +} diff --git a/aws-api-appsync/src/test/resources/serde-for-blog-in-serialized-model.json b/aws-api-appsync/src/test/resources/serde-for-blog-in-serialized-model.json index b325c2ee4e..f69a236d17 100644 --- a/aws-api-appsync/src/test/resources/serde-for-blog-in-serialized-model.json +++ b/aws-api-appsync/src/test/resources/serde-for-blog-in-serialized-model.json @@ -16,6 +16,8 @@ "isArray": false, "isEnum": false, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [] }, "name": { @@ -28,6 +30,8 @@ "isArray": false, "isEnum": false, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [] }, "owner": { @@ -40,6 +44,8 @@ "isArray": false, "isEnum": false, "isModel": true, + "isModelReference": false, + "isModelList": false, "authRules": [] }, "posts": { @@ -52,6 +58,8 @@ "isArray": true, "isEnum": false, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [] } }, diff --git a/aws-api-appsync/src/test/resources/serde-for-comment-in-serialized-model.json b/aws-api-appsync/src/test/resources/serde-for-comment-in-serialized-model.json index 04e2821634..739b8bcaf9 100644 --- a/aws-api-appsync/src/test/resources/serde-for-comment-in-serialized-model.json +++ b/aws-api-appsync/src/test/resources/serde-for-comment-in-serialized-model.json @@ -42,6 +42,8 @@ "isCustomType": false, "isReadOnly": true, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "createdAt", "isEnum": false, @@ -54,6 +56,8 @@ "isCustomType": false, "isReadOnly": false, "isModel": true, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "post", "isEnum": false, @@ -66,6 +70,8 @@ "isCustomType": false, "isReadOnly": false, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "description", "isEnum": false, @@ -78,6 +84,8 @@ "isCustomType": false, "isReadOnly": false, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "title", "isEnum": false, @@ -90,6 +98,8 @@ "isCustomType": false, "isReadOnly": false, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "content", "isEnum": false, @@ -102,6 +112,8 @@ "isCustomType": false, "isReadOnly": false, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "likes", "isEnum": false, @@ -114,6 +126,8 @@ "isCustomType": false, "isReadOnly": true, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "updatedAt", "isEnum": false, diff --git a/aws-api-appsync/src/test/resources/serde-for-meeting-in-serialized-model.json b/aws-api-appsync/src/test/resources/serde-for-meeting-in-serialized-model.json index 31eb975819..2d7befd644 100644 --- a/aws-api-appsync/src/test/resources/serde-for-meeting-in-serialized-model.json +++ b/aws-api-appsync/src/test/resources/serde-for-meeting-in-serialized-model.json @@ -17,6 +17,8 @@ "isArray": false, "isEnum": false, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [] }, "dateTime": { @@ -29,6 +31,8 @@ "isArray": false, "isEnum": false, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [] }, "id": { @@ -41,6 +45,8 @@ "isArray": false, "isEnum": false, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [] }, "name": { @@ -53,6 +59,8 @@ "isArray": false, "isEnum": false, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [] }, "time": { @@ -65,6 +73,8 @@ "isArray": false, "isEnum": false, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [] }, "timestamp": { @@ -77,6 +87,8 @@ "isArray": false, "isEnum": false, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [] } }, diff --git a/aws-api-appsync/src/test/resources/serialized-model-with-nested-custom-type-se-deserialization.json b/aws-api-appsync/src/test/resources/serialized-model-with-nested-custom-type-se-deserialization.json index 5f6cd5ecc9..102f1adf67 100644 --- a/aws-api-appsync/src/test/resources/serialized-model-with-nested-custom-type-se-deserialization.json +++ b/aws-api-appsync/src/test/resources/serialized-model-with-nested-custom-type-se-deserialization.json @@ -162,6 +162,8 @@ "isCustomType": true, "isReadOnly": false, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "additionalContacts", "isEnum": false, @@ -174,6 +176,8 @@ "isCustomType": true, "isReadOnly": false, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "contact", "isEnum": false, @@ -186,6 +190,8 @@ "isCustomType": false, "isReadOnly": false, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "fullName", "isEnum": false, @@ -198,6 +204,8 @@ "isCustomType": false, "isReadOnly": false, "isModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "id", "isEnum": false, diff --git a/aws-api/build.gradle.kts b/aws-api/build.gradle.kts index 91d42d9c4c..58a7852bdb 100644 --- a/aws-api/build.gradle.kts +++ b/aws-api/build.gradle.kts @@ -23,6 +23,17 @@ apply(from = rootProject.file("configuration/publishing.gradle")) group = properties["POM_GROUP"].toString() +android { + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + testOptions { + execution = "ANDROIDX_TEST_ORCHESTRATOR" + } +} + dependencies { api(project(":core")) api(project(":aws-core")) @@ -44,12 +55,17 @@ dependencies { testImplementation(libs.test.mockwebserver) testImplementation(libs.rxjava) testImplementation(libs.test.robolectric) + testImplementation(libs.test.kotlin.coroutines) androidTestImplementation(project(":testutils")) androidTestImplementation(project(":testmodels")) androidTestImplementation(libs.test.androidx.core) androidTestImplementation(project(":aws-auth-cognito")) + androidTestImplementation(project(":core-kotlin")) androidTestImplementation(libs.test.androidx.runner) androidTestImplementation(libs.test.androidx.junit) androidTestImplementation(libs.rxjava) + androidTestImplementation(libs.test.kotlin.coroutines) + + androidTestUtil(libs.test.androidx.orchestrator) } diff --git a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyCreateInstrumentationTest.kt b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyCreateInstrumentationTest.kt new file mode 100644 index 0000000000..772d6518ac --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyCreateInstrumentationTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.api.aws + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.amplifyframework.api.aws.test.R +import com.amplifyframework.api.graphql.model.ModelMutation +import com.amplifyframework.core.AmplifyConfiguration +import com.amplifyframework.core.model.LazyModelList +import com.amplifyframework.core.model.LazyModelReference +import com.amplifyframework.core.model.LoadedModelList +import com.amplifyframework.core.model.LoadedModelReference +import com.amplifyframework.core.model.PaginationToken +import com.amplifyframework.core.model.includes +import com.amplifyframework.datastore.generated.model.HasManyChild +import com.amplifyframework.datastore.generated.model.HasOneChild +import com.amplifyframework.datastore.generated.model.Parent +import com.amplifyframework.datastore.generated.model.ParentPath +import com.amplifyframework.kotlin.core.Amplify +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.fail +import org.junit.BeforeClass +import org.junit.Test + +class GraphQLLazyCreateInstrumentationTest { + + companion object { + @JvmStatic + @BeforeClass + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + val config = AmplifyConfiguration.fromConfigFile(context, R.raw.amplifyconfigurationlazy) + Amplify.addPlugin(AWSApiPlugin()) + Amplify.configure(config, context) + } + } + + @Test + fun create_with_no_includes() = runTest { + // GIVEN + val hasOneChild = HasOneChild.builder().content("Child1").build() + Amplify.API.mutate(ModelMutation.create(hasOneChild)) + val parent = Parent.builder().parentChildId(hasOneChild.id).build() + val hasManyChild = HasManyChild.builder().content("Child2").parent(parent).build() + val request = ModelMutation.create(parent) + + // WHEN + val responseParent = Amplify.API.mutate(request).data + Amplify.API.mutate(ModelMutation.create(hasManyChild)) + + // THEN + assertEquals(hasOneChild.id, responseParent.parentChildId) + (responseParent.child as? LazyModelReference)?.fetchModel()?.let { + assertEquals(hasOneChild.id, it.id) + assertEquals(hasOneChild.content, it.content) + } ?: fail("Response child was null or not a LazyModelReference") + + val children = responseParent.children as LazyModelList + var children1HasNextPage = true + var children1NextToken: PaginationToken? = null + var hasManyChildren = mutableListOf() + while (children1HasNextPage) { + val page = children.fetchPage(children1NextToken) + children1HasNextPage = page.hasNextPage + children1NextToken = page.nextToken + hasManyChildren.addAll(page.items) + } + assertEquals(1, hasManyChildren.size) + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasOneChild)) + Amplify.API.mutate(ModelMutation.delete(hasManyChild)) + Amplify.API.mutate(ModelMutation.delete(parent)) + } + + @Test + fun create_with_includes() = runTest { + // GIVEN + val hasOneChild = HasOneChild.builder().content("Child1").build() + Amplify.API.mutate(ModelMutation.create(hasOneChild)) + val parent = Parent.builder().parentChildId(hasOneChild.id).build() + val request = ModelMutation.create(parent) { + includes(it.child, it.children) + } + + // WHEN + val responseParent = Amplify.API.mutate(request).data + + // THEN + assertEquals(parent.id, responseParent.id) + assertEquals(hasOneChild.id, responseParent.parentChildId) + (responseParent.child as? LoadedModelReference)?.value?.let { + assertEquals(hasOneChild.id, it.id) + assertEquals(hasOneChild.content, it.content) + } ?: fail("Response child was null or not a LoadedModelReference") + (responseParent.children as? LoadedModelList)?.let { + assertEquals(0, it.items.size) + } ?: fail("Response child was null or not a LoadedModelList") + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasOneChild)) + Amplify.API.mutate(ModelMutation.delete(parent)) + } + + @Test + fun create_with_no_includes_null_optional_relationship() = runTest { + // GIVEN + val hasManyChild = HasManyChild.builder().content("Child1").parent(null).build() + val request = ModelMutation.create(hasManyChild) + // WHEN + val responseChild = Amplify.API.mutate(request).data + + // THEN + assertEquals(hasManyChild.id, responseChild.id) + assertEquals("Child1", responseChild.content) + assertNull((responseChild.parent as LoadedModelReference).value) + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasManyChild)) + } + + @Test + fun create_with_no_includes_missing_optional_relationship() = runTest { + // GIVEN + val hasManyChild = HasManyChild.builder().content("Child1").build() + val request = ModelMutation.create(hasManyChild) + // WHEN + val responseChild = Amplify.API.mutate(request).data + + // THEN + assertEquals(hasManyChild.id, responseChild.id) + assertEquals("Child1", responseChild.content) + assertNull((responseChild.parent as LoadedModelReference).value) + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasManyChild)) + } +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyDeleteInstrumentationTest.kt b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyDeleteInstrumentationTest.kt new file mode 100644 index 0000000000..8b2e156141 --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyDeleteInstrumentationTest.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.api.aws + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.amplifyframework.api.aws.test.R +import com.amplifyframework.api.graphql.model.ModelMutation +import com.amplifyframework.core.AmplifyConfiguration +import com.amplifyframework.core.model.LazyModelList +import com.amplifyframework.core.model.LazyModelReference +import com.amplifyframework.core.model.LoadedModelList +import com.amplifyframework.core.model.LoadedModelReference +import com.amplifyframework.core.model.includes +import com.amplifyframework.datastore.generated.model.HasManyChild +import com.amplifyframework.datastore.generated.model.HasOneChild +import com.amplifyframework.datastore.generated.model.Parent +import com.amplifyframework.datastore.generated.model.ParentPath +import com.amplifyframework.kotlin.core.Amplify +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.fail +import org.junit.BeforeClass +import org.junit.Test + +class GraphQLLazyDeleteInstrumentationTest { + + companion object { + @JvmStatic + @BeforeClass + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + val config = AmplifyConfiguration.fromConfigFile(context, R.raw.amplifyconfigurationlazy) + Amplify.addPlugin(AWSApiPlugin()) + Amplify.configure(config, context) + } + } + + @Test + fun delete_with_no_includes() = runTest { + // GIVEN + val hasOneChild = HasOneChild.builder().content("Child1").build() + Amplify.API.mutate(ModelMutation.create(hasOneChild)) + + val parent = Parent.builder().parentChildId(hasOneChild.id).build() + Amplify.API.mutate(ModelMutation.create(parent)).data + + val hasManyChild = HasManyChild.builder().content("Child2").parent(parent).build() + Amplify.API.mutate(ModelMutation.create(hasManyChild)) + + // WHEN + val request = ModelMutation.delete(parent) + val updatedParent = Amplify.API.mutate(request).data + + // THEN + assertEquals(parent.id, updatedParent.id) + assertEquals(hasOneChild.id, updatedParent.parentChildId) + (updatedParent.child as? LazyModelReference)?.fetchModel()?.let { + assertEquals(hasOneChild.id, it.id) + assertEquals(hasOneChild.content, it.content) + } ?: fail("Response child was null or not a LazyModelReference") + (updatedParent.children as? LazyModelList)?.fetchPage()?.let { + assertEquals(1, it.items.size) + } ?: fail("Response child was null or not a LazyModelList") + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasOneChild)) + Amplify.API.mutate(ModelMutation.delete(hasManyChild)) + } + + @Test + fun delete_with_includes() = runTest { + // GIVEN + val hasOneChild = HasOneChild.builder().content("Child1").build() + Amplify.API.mutate(ModelMutation.create(hasOneChild)) + + val parent = Parent.builder().parentChildId(hasOneChild.id).build() + Amplify.API.mutate(ModelMutation.create(parent)).data + + val hasManyChild = HasManyChild.builder().content("Child2").parent(parent).build() + Amplify.API.mutate(ModelMutation.create(hasManyChild)) + + // WHEN + val request = ModelMutation.delete(parent) { + includes(it.child, it.children) + } + val updatedParent = Amplify.API.mutate(request).data + + // THEN + assertEquals(parent.id, updatedParent.id) + assertEquals(hasOneChild.id, updatedParent.parentChildId) + (updatedParent.child as? LoadedModelReference)?.value?.let { + assertEquals(hasOneChild.id, it.id) + assertEquals(hasOneChild.content, it.content) + } ?: fail("Response child was null or not a LoadedModelReference") + (updatedParent.children as? LoadedModelList)?.let { + assertEquals(1, it.items.size) + } ?: fail("Response child was null or not a LoadedModelList") + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasOneChild)) + Amplify.API.mutate(ModelMutation.delete(hasManyChild)) + } +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyQueryInstrumentationTest.kt b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyQueryInstrumentationTest.kt new file mode 100644 index 0000000000..b45486df8b --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyQueryInstrumentationTest.kt @@ -0,0 +1,631 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.api.aws + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.amplifyframework.api.aws.test.R +import com.amplifyframework.api.graphql.model.ModelQuery +import com.amplifyframework.core.AmplifyConfiguration +import com.amplifyframework.core.model.LazyModelList +import com.amplifyframework.core.model.LazyModelReference +import com.amplifyframework.core.model.LoadedModelList +import com.amplifyframework.core.model.LoadedModelReference +import com.amplifyframework.core.model.PaginationToken +import com.amplifyframework.core.model.includes +import com.amplifyframework.datastore.generated.model.HasManyChild +import com.amplifyframework.datastore.generated.model.HasManyChild.HasManyChildIdentifier +import com.amplifyframework.datastore.generated.model.HasManyChildPath +import com.amplifyframework.datastore.generated.model.Parent +import com.amplifyframework.datastore.generated.model.ParentPath +import com.amplifyframework.datastore.generated.model.Post +import com.amplifyframework.datastore.generated.model.PostPath +import com.amplifyframework.datastore.generated.model.Project +import com.amplifyframework.datastore.generated.model.ProjectPath +import com.amplifyframework.datastore.generated.model.Team +import com.amplifyframework.datastore.generated.model.TeamPath +import com.amplifyframework.kotlin.core.Amplify +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.BeforeClass +import org.junit.Test + +class GraphQLLazyQueryInstrumentationTest { + + companion object { + + const val PARENT1_ID = "GraphQLLazyQueryInstrumentationTest-Parent" + const val PARENT2_ID = "GraphQLLazyQueryInstrumentationTest-Parent2" + const val HAS_ONE_CHILD1_ID = "GraphQLLazyQueryInstrumentationTest-HasOneChild1" + const val HAS_ONE_CHILD2_ID = "GraphQLLazyQueryInstrumentationTest-HasOneChild2" + @JvmStatic + @BeforeClass + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + val config = AmplifyConfiguration.fromConfigFile(context, R.raw.amplifyconfigurationlazy) + Amplify.addPlugin(AWSApiPlugin()) + Amplify.configure(config, context) + } + } + + // run this method once to populate all the data necessary to run the tests +// private suspend fun populate() { +// val hasOneChild = HasOneChild.builder() +// .content("Child1") +// .id("GraphQLLazyQueryInstrumentationTest-HasOneChild1") +// .build() +// Amplify.API.mutate(ModelMutation.create(hasOneChild)) +// +// val parent = Parent.builder().parentChildId(hasOneChild.id).id("GraphQLLazyQueryInstrumentationTest-Parent").build() +// Amplify.API.mutate(ModelMutation.create(parent)) +// +// val hasOneChild2 = HasOneChild.builder() +// .content("Child2") +// .id("GraphQLLazyQueryInstrumentationTest-HasOneChild2") +// .build() +// Amplify.API.mutate(ModelMutation.create(hasOneChild2)) +// +// val parent2 = Parent.builder().parentChildId(hasOneChild2.id).id("GraphQLLazyQueryInstrumentationTest-Parent2").build() +// Amplify.API.mutate(ModelMutation.create(parent2)) +// +// for(i in 0 until 1001) { +// val hasManyChild = HasManyChild.builder() +// .content("Child$i") +// .id("GraphQLLazyQueryInstrumentationTest-HasManyChild$i") +// .parent(parent) +// .build() +// Amplify.API.mutate(ModelMutation.create(hasManyChild)) +// } +// +// val parentNoChildren = Parent.builder().id("GraphQLLazyQueryInstrumentationTest.ParentWithNoChildren").build() +// Amplify.API.mutate(ModelMutation.create(parentNoChildren)) +// +// val hasManyChild = HasManyChild.builder() +// .content("ChildNoParent") +// .id("GraphQLLazyQueryInstrumentationTest.HasManyChildNoParent") +// .build() +// +// Amplify.API.mutate(ModelMutation.create(hasManyChild)) +// +// val project = Project.builder() +// .projectId("GraphQLLazyQueryInstrumentationTest-Parent1") +// .name("Project 1") +// .build() +// val projectFromResponse = Amplify.API.mutate(ModelMutation.create(project)).data +// +// val team = Team.builder() +// .teamId("GraphQLLazyQueryInstrumentationTest-Team1") +// .name("Team 1") +// .project(project) +// .build() +// Amplify.API.mutate(ModelMutation.create(team)) +// +// +// val updateProject = projectFromResponse.copyOfBuilder() +// .projectTeamName("Team 1") +// .projectTeamTeamId("GraphQLLazyQueryInstrumentationTest-Team1") +// .build() +// Amplify.API.mutate(ModelMutation.update(updateProject)) +// +// val blog = Blog.builder() +// .blogId("GraphQLLazyQueryInstrumentationTest-Blog1") +// .name("Blog 1") +// .build() +// val post = Post.builder() +// .postId("GraphQLLazyQueryInstrumentationTest-Post1") +// .title("Post 1") +// .blog(blog) +// .build() +// val comment = Comment.builder() +// .commentId("GraphQLLazyQueryInstrumentationTest-Comment1") +// .content("Comment 1") +// .post(post) +// .build() +// Amplify.API.mutate(ModelMutation.create(blog)) +// Amplify.API.mutate(ModelMutation.create(post)) +// Amplify.API.mutate(ModelMutation.create(comment)) +// } + + @Test + fun query_parent_no_includes() = runTest { + // GIVEN + val request = ModelQuery[Parent::class.java, Parent.ParentIdentifier(PARENT1_ID)] + + // WHEN + val responseParent = Amplify.API.query(request).data + + // THEN + assertEquals(HAS_ONE_CHILD1_ID, responseParent.parentChildId) + (responseParent.child as? LazyModelReference)?.fetchModel()?.let { child -> + assertEquals(HAS_ONE_CHILD1_ID, child.id) + assertEquals("Child1", child.content) + } ?: fail("Response child was null or not a LazyModelReference") + + val children = responseParent.children as LazyModelList + var children1HasNextPage = true + var children1NextToken: PaginationToken? = null + var children1Count = 0 + while (children1HasNextPage) { + val page = children.fetchPage(children1NextToken) + children1HasNextPage = page.hasNextPage + children1NextToken = page.nextToken + children1Count += page.items.size + } + assertEquals(1001, children1Count) + } + + @Test + fun query_parent_with_includes() = runTest { + // GIVEN + val request = ModelQuery.get( + Parent::class.java, Parent.ParentIdentifier(PARENT1_ID) + ) { + includes(it.child, it.children) + } + + // WHEN + val responseParent = Amplify.API.query(request).data + + // THEN + assertEquals(HAS_ONE_CHILD1_ID, responseParent.parentChildId) + (responseParent.child as? LoadedModelReference)?.let { childRef -> + val child = childRef.value!! + assertEquals(HAS_ONE_CHILD1_ID, child.id) + assertEquals("Child1", child.content) + } ?: fail("Response child was null or not a LoadedModelReference") + + val children = responseParent.children as LoadedModelList + assertEquals(100, children.items.size) + } + + @Test + fun query_list_with_no_includes() = runTest { + + val request = ModelQuery.list( + Parent::class.java, + Parent.ID.beginsWith("GraphQLLazyQueryInstrumentationTest-Parent") + ) + + // WHEN + val paginatedResult = Amplify.API.query(request).data + + assertFalse(paginatedResult.hasNextResult()) + + // THEN + val parents = paginatedResult.items.toList() + assertEquals(2, parents.size) + + val parent2 = parents[0] + val parent1 = parents[1] + + assertEquals(HAS_ONE_CHILD1_ID, parent1.parentChildId) + assertEquals(PARENT1_ID, parent1.id) + (parent1.child as? LazyModelReference)?.fetchModel()?.let { child -> + assertEquals(HAS_ONE_CHILD1_ID, child.id) + assertEquals("Child1", child.content) + } ?: fail("Response child was null or not a LazyModelReference") + + val childrenFromParent1 = parent1.children as LazyModelList + var children1HasNextPage = true + var children1NextToken: PaginationToken? = null + var children1Count = 0 + while (children1HasNextPage) { + val page = childrenFromParent1.fetchPage(children1NextToken) + children1HasNextPage = page.hasNextPage + children1NextToken = page.nextToken + children1Count += page.items.size + } + assertEquals(1001, children1Count) + + assertEquals(HAS_ONE_CHILD2_ID, parent2.parentChildId) + assertEquals(PARENT2_ID, parent2.id) + (parent2.child as? LazyModelReference)?.fetchModel()?.let { child -> + assertEquals(HAS_ONE_CHILD2_ID, child.id) + assertEquals("Child2", child.content) + } ?: fail("Response child was null or not a LazyModelReference") + + val childrenFromParent2 = parent2.children as LazyModelList + var children2HasNextPage = true + var children2NextToken: PaginationToken? = null + var children2Count = 0 + while (children2HasNextPage) { + val page = childrenFromParent2.fetchPage(children2NextToken) + children2HasNextPage = page.hasNextPage + children2NextToken = page.nextToken + children2Count += page.items.size + } + assertEquals(0, children2Count) + } + + @Test + fun query_list_with_includes() = runTest { + + val request = ModelQuery.list( + Parent::class.java, + Parent.ID.beginsWith("GraphQLLazyQueryInstrumentationTest-Parent") + ) { + includes(it.child, it.children) + } + + // WHEN + val paginatedResult = Amplify.API.query(request).data + + // THEN + assertFalse(paginatedResult.hasNextResult()) + + val parents = paginatedResult.items.toList() + assertEquals(2, parents.size) + + val parent2 = parents[0] + val parent1 = parents[1] + + assertEquals(HAS_ONE_CHILD1_ID, parent1.parentChildId) + assertEquals(PARENT1_ID, parent1.id) + (parent1.child as? LoadedModelReference)?.let { childRef -> + val child = childRef.value!! + assertEquals(HAS_ONE_CHILD1_ID, child.id) + assertEquals("Child1", child.content) + } ?: fail("Response child was null or not a LoadedModelReference") + + assertEquals(HAS_ONE_CHILD2_ID, parent2.parentChildId) + assertEquals(PARENT2_ID, parent2.id) + (parent2.child as? LoadedModelReference)?.let { childRef -> + val child = childRef.value!! + assertEquals(HAS_ONE_CHILD2_ID, child.id) + assertEquals("Child2", child.content) + } ?: fail("Response child was null or not a LoadedModelReference") + + val children = parent2.children as LoadedModelList + assertEquals(0, children.items.size) + } + + @Test + fun query_parent_with_no_child_with_includes() = runTest { + // GIVEN + val request = ModelQuery.get( + Parent::class.java, Parent.ParentIdentifier("GraphQLLazyQueryInstrumentationTest.ParentWithNoChildren") + ) { + includes(it.child, it.children) + } + + // WHEN + val responseParent = Amplify.API.query(request).data + + // THEN + assertNull(responseParent.parentChildId) + (responseParent.child as? LoadedModelReference)?.let { childRef -> + assertNull(childRef.value) + } ?: fail("Response child was null or not a LoadedModelReference") + + val children = responseParent.children as LoadedModelList + assertEquals(0, children.items.size) + } + + @Test + fun query_parent_with_no_child_no_includes() = runTest { + // GIVEN + val request = ModelQuery[ + Parent::class.java, + Parent.ParentIdentifier("GraphQLLazyQueryInstrumentationTest.ParentWithNoChildren") + ] + + // WHEN + val responseParent = Amplify.API.query(request).data + + // THEN + assertNull(responseParent.parentChildId) + (responseParent.child as? LoadedModelReference)?.let { childRef -> + assertNull(childRef.value) + } ?: fail("Response child was null or not a LoadedModelReference") + + val children = responseParent.children as LazyModelList + assertEquals(0, children.fetchPage().items.size) + } + + @Test + fun query_child_belongsTo_parent_with_no_includes() = runTest { + // GIVEN + val request = ModelQuery[ + HasManyChild::class.java, + HasManyChildIdentifier("GraphQLLazyQueryInstrumentationTest-HasManyChild1") + ] + + // WHEN + val hasManyChild = Amplify.API.query(request).data + + // THEN + (hasManyChild.parent as? LazyModelReference)?.let { parentRef -> + assertEquals(PARENT1_ID, parentRef.fetchModel()!!.id) + } ?: fail("Response child was null or not a LazyModelReference") + } + + @Test + fun query_child_belongsTo_parent_with_includes() = runTest { + // GIVEN + val request = ModelQuery.get( + HasManyChild::class.java, HasManyChildIdentifier("GraphQLLazyQueryInstrumentationTest-HasManyChild1") + ) { + includes(it.parent) + } + + // WHEN + val hasManyChild = Amplify.API.query(request).data + + // THEN + (hasManyChild.parent as? LoadedModelReference)?.let { parentRef -> + assertEquals(PARENT1_ID, parentRef.value!!.id) + } ?: fail("Response child was null or not a LoadedModelReference") + } + + @Test + fun query_child_belongsTo_null_parent_with_no_includes() = runTest { + // GIVEN + val request = ModelQuery[ + HasManyChild::class.java, + HasManyChildIdentifier("GraphQLLazyQueryInstrumentationTest.HasManyChildNoParent") + ] + + // WHEN + val hasManyChild = Amplify.API.query(request).data + + // THEN + (hasManyChild.parent as? LoadedModelReference)?.let { parentRef -> + assertEquals(null, parentRef.value) + } ?: fail("Response child was null or not a LoadedModelReference") + } + + @Test + fun query_list_child_belongsTo_null_parent_with_no_includes() = runTest { + // GIVEN + val request = ModelQuery.list( + HasManyChild::class.java, + HasManyChild.ID.beginsWith("GraphQLLazyQueryInstrumentationTest.HasManyChildNoParent") + ) + + // WHEN + val hasManyChildren = Amplify.API.query(request).data.toList() + + // THEN + assertEquals(1, hasManyChildren.size) + (hasManyChildren[0].parent as? LoadedModelReference)?.let { parentRef -> + assertEquals(null, parentRef.value) + } ?: fail("Response child was null or not a LoadedModelReference") + } + + @Test + fun query_child_belongsTo_null_parent_with_includes() = runTest { + // GIVEN + val request = ModelQuery.list( + HasManyChild::class.java, + HasManyChild.ID.beginsWith("GraphQLLazyQueryInstrumentationTest.HasManyChildNoParent") + ) { + includes(it.parent) + } + + // WHEN + val hasManyChildren = Amplify.API.query(request).data.toList() + + // THEN + assertEquals(1, hasManyChildren.size) + (hasManyChildren[0].parent as? LoadedModelReference)?.let { parentRef -> + assertEquals(null, parentRef.value) + } ?: fail("Response child was null or not a LoadedModelReference") + } + + @Test + fun query_project_and_pull_hasOne_team_no_includes() = runTest { + // GIVEN + val expectedProjectId = "GraphQLLazyQueryInstrumentationTest-Parent1" + val expectedProjectName = "Project 1" + val expectedTeamId = "GraphQLLazyQueryInstrumentationTest-Team1" + val expectedTeamName = "Team 1" + val projectRequest = ModelQuery[ + Project::class.java, + Project.ProjectIdentifier(expectedProjectId, expectedProjectName) + ] + + // WHEN + val projectFromResponse = Amplify.API.query(projectRequest).data + + // THEN + assertEquals(expectedProjectId, projectFromResponse.projectId) + assertEquals(expectedProjectName, projectFromResponse.name) + + (projectFromResponse.team as? LazyModelReference)?.fetchModel()?.let { team -> + assertEquals(expectedTeamId, team.teamId) + assertEquals(expectedTeamName, team.name) + assertTrue(team.project is LazyModelReference) + } ?: fail("Response child was null or not a LazyModelReference") + } + + @Test + fun query_project_and_pull_hasOne_team_includes() = runTest { + // GIVEN + val expectedProjectId = "GraphQLLazyQueryInstrumentationTest-Parent1" + val expectedProjectName = "Project 1" + val expectedTeamId = "GraphQLLazyQueryInstrumentationTest-Team1" + val expectedTeamName = "Team 1" + val projectRequest = ModelQuery.get( + Project::class.java, + Project.ProjectIdentifier(expectedProjectId, expectedProjectName) + ) { + includes(it.team) + } + + // WHEN + val projectFromResponse = Amplify.API.query(projectRequest).data + + // THEN + assertEquals(expectedProjectId, projectFromResponse.projectId) + assertEquals(expectedProjectName, projectFromResponse.name) + + (projectFromResponse.team as? LoadedModelReference)?.value?.let { team -> + assertEquals(expectedTeamId, team.teamId) + assertEquals(expectedTeamName, team.name) + assertTrue(team.project is LazyModelReference) + } ?: fail("Response child was null or not a LoadedModelReference") + } + + @Test + fun query_team_and_pull_belongsTo_project_no_includes() = runTest { + // GIVEN + val expectedProjectId = "GraphQLLazyQueryInstrumentationTest-Parent1" + val expectedProjectName = "Project 1" + val expectedTeamId = "GraphQLLazyQueryInstrumentationTest-Team1" + val expectedTeamName = "Team 1" + val teamRequest = ModelQuery[ + Team::class.java, + Team.TeamIdentifier(expectedTeamId, expectedTeamName) + ] + + // WHEN + val teamFromResponse = Amplify.API.query(teamRequest).data + + // THEN + assertEquals(expectedTeamId, teamFromResponse.teamId) + assertEquals(expectedTeamName, teamFromResponse.name) + + (teamFromResponse.project as? LazyModelReference)?.fetchModel()?.let { project -> + assertEquals(expectedProjectId, project.projectId) + assertEquals(expectedProjectName, project.name) + assertTrue(project.team is LazyModelReference) + } ?: fail("Response child was null or not a LazyModelReference") + } + + @Test + fun query_team_and_pull_belongsTo_project_includes() = runTest { + // GIVEN + val expectedProjectId = "GraphQLLazyQueryInstrumentationTest-Parent1" + val expectedProjectName = "Project 1" + val expectedTeamId = "GraphQLLazyQueryInstrumentationTest-Team1" + val expectedTeamName = "Team 1" + val projectRequest = ModelQuery.get( + Team::class.java, + Team.TeamIdentifier(expectedTeamId, expectedTeamName) + ) { + includes(it.project) + } + + // WHEN + val teamFromResponse = Amplify.API.query(projectRequest).data + + // THEN + assertEquals(expectedTeamId, teamFromResponse.teamId) + assertEquals(expectedTeamName, teamFromResponse.name) + + (teamFromResponse.project as? LoadedModelReference)?.value?.let { project -> + assertEquals(expectedProjectId, project.projectId) + assertEquals(expectedProjectName, project.name) + assertTrue(project.team is LazyModelReference) + } ?: fail("Response child was null or not a LoadedModelReference") + } + + @Test + fun query_with_complex_includes() = runTest { + // GIVEN + val expectedBlogName = "Blog 1" + val expectedPostId = "GraphQLLazyQueryInstrumentationTest-Post1" + val expectedPostTitle = "Post 1" + val expectedCommentConent = "Comment 1" + + // WHEN + val request = ModelQuery.get( + Post::class.java, + Post.PostIdentifier(expectedPostId, expectedPostTitle) + ) { + includes(it.blog.posts.blog.posts.comments, it.comments.post.blog.posts.comments) + } + val post = Amplify.API.query(request).data + + // THEN + + // Scenario 1: it.blog.posts.blog.posts.comments + val l1Blog = (post.blog as LoadedModelReference).value!! + assertEquals(expectedBlogName, l1Blog.name) + val l2Posts = (l1Blog.posts as LoadedModelList).items + assertEquals(1, l2Posts.size) + assertEquals(expectedPostTitle, l2Posts[0].title) + val l3Blog = (l2Posts[0].blog as LoadedModelReference).value!! + assertEquals(expectedBlogName, l3Blog.name) + val l4Posts = (l3Blog.posts as LoadedModelList).items + assertEquals(1, l4Posts.size) + assertEquals(expectedPostTitle, l4Posts[0].title) + val l5Comments = (l4Posts[0].comments as LoadedModelList).items + assertEquals(1, l5Comments.size) + assertEquals(expectedCommentConent, l5Comments[0].content) + + // Scenario 2: it.comments.post.blog.posts.comments + val s2l1Comments = (post.comments as LoadedModelList).items + assertEquals(1, s2l1Comments.size) + assertEquals(expectedCommentConent, s2l1Comments[0].content) + val l2Post = (s2l1Comments[0].post as LoadedModelReference).value!! + assertEquals(expectedPostTitle, l2Post.title) + val s2l3Blog = (l2Posts[0].blog as LoadedModelReference).value!! + assertEquals(expectedBlogName, s2l3Blog.name) + val s2l4Posts = (s2l3Blog.posts as LoadedModelList).items + assertEquals(1, s2l4Posts.size) + assertEquals(expectedPostTitle, s2l4Posts[0].title) + val s2l5Comments = (s2l4Posts[0].comments as LoadedModelList).items + assertEquals(1, s2l5Comments.size) + assertEquals(expectedCommentConent, s2l5Comments[0].content) + } + + @Test + fun query_multiple_lazy_loads_no_includes() = runTest { + // GIVEN + val expectedBlogName = "Blog 1" + val expectedPostId = "GraphQLLazyQueryInstrumentationTest-Post1" + val expectedPostTitle = "Post 1" + val expectedCommentConent = "Comment 1" + + // WHEN + val request = ModelQuery[ + Post::class.java, + Post.PostIdentifier(expectedPostId, expectedPostTitle) + ] + val post = Amplify.API.query(request).data + + // THEN + + // Scenario 1: Start loads from lazy reference of blog + val s1l1Blog = (post.blog as LazyModelReference).fetchModel()!! + assertEquals(expectedBlogName, s1l1Blog.name) + val s1l2Posts = (s1l1Blog.posts as LazyModelList).fetchPage().items + assertEquals(1, s1l2Posts.size) + assertEquals(expectedPostTitle, s1l2Posts[0].title) + val s1l3Blog = (s1l2Posts[0].blog as LazyModelReference).fetchModel()!! + assertEquals(expectedBlogName, s1l3Blog.name) + val s1l3Comments = (s1l2Posts[0].comments as LazyModelList).fetchPage().items + assertEquals(1, s1l3Comments.size) + assertEquals(expectedCommentConent, s1l3Comments[0].content) + + // Scenario 1: Start loads from model list of comments + val s2l1Comments = (post.comments as LazyModelList).fetchPage().items + assertEquals(1, s2l1Comments.size) + assertEquals(expectedCommentConent, s2l1Comments[0].content) + val s2l2Post = (s1l3Comments[0].post as LazyModelReference).fetchModel()!! + assertEquals(expectedPostTitle, s2l2Post.title) + val s2l3Blog = (s2l2Post.blog as LazyModelReference).fetchModel()!! + assertEquals(expectedBlogName, s2l3Blog.name) + val s2l3Comments = (s2l2Post.comments as LazyModelList).fetchPage().items + assertEquals(1, s2l3Comments.size) + assertEquals(expectedCommentConent, s2l3Comments[0].content) + } +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazySubscribeInstrumentationTest.kt b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazySubscribeInstrumentationTest.kt new file mode 100644 index 0000000000..215944002c --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazySubscribeInstrumentationTest.kt @@ -0,0 +1,350 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.api.aws + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.amplifyframework.api.aws.test.R +import com.amplifyframework.api.graphql.model.ModelMutation +import com.amplifyframework.api.graphql.model.ModelSubscription +import com.amplifyframework.core.AmplifyConfiguration +import com.amplifyframework.core.model.LazyModelReference +import com.amplifyframework.core.model.LoadedModelReference +import com.amplifyframework.core.model.includes +import com.amplifyframework.datastore.generated.model.HasOneChild +import com.amplifyframework.datastore.generated.model.Parent +import com.amplifyframework.datastore.generated.model.ParentPath +import com.amplifyframework.kotlin.core.Amplify +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext +import org.junit.Assert.assertEquals +import org.junit.BeforeClass +import org.junit.Test + +class GraphQLLazySubscribeInstrumentationTest { + + companion object { + @JvmStatic + @BeforeClass + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + val config = AmplifyConfiguration.fromConfigFile(context, R.raw.amplifyconfigurationlazy) + Amplify.addPlugin(AWSApiPlugin()) + Amplify.configure(config, context) + } + } + + @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) + @Test + fun subscribe_with_no_includes_create() = runTest { + // GIVEN + val hasOneChild = HasOneChild.builder() + .content("Child1") + .build() + val parent = Parent.builder().parentChildId(hasOneChild.id).build() + + val latch = CountDownLatch(1) + val collectRunningLatch = CountDownLatch(1) + + var capturedParent: Parent? = null + var capturedChild: HasOneChild? = null + val subscription = Amplify.API.subscribe(ModelSubscription.onCreate(Parent::class.java)) + CoroutineScope(Dispatchers.IO).launch { + subscription.collect { + assertEquals(parent.id, it.data.id) + capturedParent = it.data + capturedChild = (it.data.child as LazyModelReference).fetchModel()!! + latch.countDown() + } + collectRunningLatch.countDown() + } + collectRunningLatch.await(1, TimeUnit.SECONDS) + + // WHEN + Amplify.API.mutate(ModelMutation.create(hasOneChild)) + Amplify.API.mutate(ModelMutation.create(parent)) + + // THEN + withContext(this.coroutineContext) { + latch.await(10, TimeUnit.SECONDS) + } + + assertEquals(parent.id, capturedParent!!.id) + assertEquals(hasOneChild.content, capturedChild!!.content) + assertEquals(hasOneChild.id, capturedChild!!.id) + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasOneChild)) + Amplify.API.mutate(ModelMutation.delete(parent)) + } + + @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) + @Test + fun subscribe_with_includes_create() = runTest { + // GIVEN + val hasOneChild = HasOneChild.builder() + .content("Child1") + .build() + val parent = Parent.builder().parentChildId(hasOneChild.id).build() + + val latch = CountDownLatch(1) + val collectRunningLatch = CountDownLatch(1) + + val request = ModelSubscription.onCreate(Parent::class.java) { + includes(it.child) + } + val subscription = Amplify.API.subscribe(request) + + var capturedParent: Parent? = null + var capturedChild: HasOneChild? = null + CoroutineScope(Dispatchers.IO).launch { + subscription.collect { + assertEquals(parent.id, it.data.id) + capturedParent = it.data + capturedChild = (it.data.child as LoadedModelReference).value + latch.countDown() + } + collectRunningLatch.countDown() + } + collectRunningLatch.await(1, TimeUnit.SECONDS) + + // WHEN + Amplify.API.mutate(ModelMutation.create(hasOneChild)) + val createRequest = ModelMutation.create(parent) { + includes(it.child) + } + Amplify.API.mutate(createRequest) + + // THEN + withContext(this.coroutineContext) { + latch.await(10, TimeUnit.SECONDS) + } + + assertEquals(parent.id, capturedParent!!.id) + assertEquals(hasOneChild.content, capturedChild!!.content) + assertEquals(hasOneChild.id, capturedChild!!.id) + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasOneChild)) + Amplify.API.mutate(ModelMutation.delete(parent)) + } + + @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) + @Test + fun subscribe_with_no_includes_update() = runTest { + // GIVEN + val hasOneChild = HasOneChild.builder() + .content("Child1") + .build() + val hasOneChild2 = HasOneChild.builder() + .content("Child2") + .build() + val parent = Parent.builder().parentChildId(hasOneChild.id).build() + + val subscription = Amplify.API.subscribe(ModelSubscription.onUpdate(Parent::class.java)) + + val latch = CountDownLatch(1) + var capturedParent: Parent? = null + var capturedChild: HasOneChild? = null + CoroutineScope(Dispatchers.IO).launch { + subscription.collect { + assertEquals(parent.id, it.data.id) + capturedParent = it.data + capturedChild = (it.data.child as LazyModelReference).fetchModel()!! + latch.countDown() + } + } + + Amplify.API.mutate(ModelMutation.create(hasOneChild)) + Amplify.API.mutate(ModelMutation.create(hasOneChild2)) + val parentFromResponse = Amplify.API.mutate(ModelMutation.create(parent)).data + assertEquals(hasOneChild.id, parentFromResponse.parentChildId) + + // WHEN + val updateParent = parent.copyOfBuilder().parentChildId(hasOneChild2.id).build() + Amplify.API.mutate(ModelMutation.update(updateParent)) + + // THEN + withContext(this.coroutineContext) { + latch.await(10, TimeUnit.SECONDS) + } + + assertEquals(parent.id, capturedParent!!.id) + assertEquals(capturedParent!!.parentChildId, capturedChild!!.id) + assertEquals(hasOneChild2.content, capturedChild!!.content) + assertEquals(hasOneChild2.id, capturedChild!!.id) + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasOneChild)) + Amplify.API.mutate(ModelMutation.delete(hasOneChild2)) + Amplify.API.mutate(ModelMutation.delete(parent)) + } + + @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) + @Test + fun subscribe_with_includes_update() = runTest { + // GIVEN + val hasOneChild = HasOneChild.builder() + .content("Child1") + .build() + val hasOneChild2 = HasOneChild.builder() + .content("Child2") + .build() + val parent = Parent.builder().parentChildId(hasOneChild.id).build() + + val request = ModelSubscription.onUpdate(Parent::class.java) { + includes(it.child) + } + val subscription = Amplify.API.subscribe(request) + + val latch = CountDownLatch(1) + var capturedParent: Parent? = null + var capturedChild: HasOneChild? = null + CoroutineScope(Dispatchers.IO).launch { + + subscription.collect { + assertEquals(parent.id, it.data.id) + capturedParent = it.data + capturedChild = (it.data.child as LoadedModelReference).value + latch.countDown() + } + } + + Amplify.API.mutate(ModelMutation.create(hasOneChild)) + Amplify.API.mutate(ModelMutation.create(hasOneChild2)) + Amplify.API.mutate(ModelMutation.create(parent)) + + // WHEN + val updateParent = parent.copyOfBuilder().parentChildId(hasOneChild2.id).build() + val updateRequest = ModelMutation.update(updateParent) { + includes(it.child) + } + Amplify.API.mutate(updateRequest) + + // THEN + withContext(this.coroutineContext) { + latch.await(10, TimeUnit.SECONDS) + } + + assertEquals(parent.id, capturedParent!!.id) + assertEquals(capturedParent!!.parentChildId, capturedChild!!.id) + assertEquals(hasOneChild2.content, capturedChild!!.content) + assertEquals(hasOneChild2.id, capturedChild!!.id) + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasOneChild)) + Amplify.API.mutate(ModelMutation.delete(hasOneChild2)) + Amplify.API.mutate(ModelMutation.delete(parent)) + } + + @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) + @Test + fun subscribe_with_no_includes_delete() = runTest { + // GIVEN + val hasOneChild = HasOneChild.builder() + .content("Child1") + .build() + val parent = Parent.builder().parentChildId(hasOneChild.id).build() + + val subscription = Amplify.API.subscribe(ModelSubscription.onDelete(Parent::class.java)) + + val latch = CountDownLatch(1) + var capturedParent: Parent? = null + var capturedChild: HasOneChild? = null + CoroutineScope(Dispatchers.IO).launch { + subscription.collect { + assertEquals(parent.id, it.data.id) + capturedParent = it.data + capturedChild = (it.data.child as LazyModelReference).fetchModel()!! + latch.countDown() + } + } + + Amplify.API.mutate(ModelMutation.create(hasOneChild)) + val parentFromResponse = Amplify.API.mutate(ModelMutation.create(parent)).data + assertEquals(hasOneChild.id, parentFromResponse.parentChildId) + + // WHEN + Amplify.API.mutate(ModelMutation.delete(parent)) + + // THEN + withContext(this.coroutineContext) { + latch.await(10, TimeUnit.SECONDS) + } + + assertEquals(parent.id, capturedParent!!.id) + assertEquals(hasOneChild.content, capturedChild!!.content) + assertEquals(hasOneChild.id, capturedChild!!.id) + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasOneChild)) + } + + @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) + @Test + fun subscribe_with_includes_delete() = runTest { + // GIVEN + val hasOneChild = HasOneChild.builder() + .content("Child1") + .build() + val parent = Parent.builder().parentChildId(hasOneChild.id).build() + + val request = ModelSubscription.onDelete(Parent::class.java) { + includes(it.child) + } + val subscription = Amplify.API.subscribe(request) + + val latch = CountDownLatch(1) + var capturedParent: Parent? = null + var capturedChild: HasOneChild? = null + CoroutineScope(Dispatchers.IO).launch { + subscription.collect { + assertEquals(parent.id, it.data.id) + capturedParent = it.data + capturedChild = (it.data.child as LoadedModelReference).value + latch.countDown() + } + } + + Amplify.API.mutate(ModelMutation.create(hasOneChild)) + Amplify.API.mutate(ModelMutation.create(parent)) + + // WHEN + val deleteRequest = ModelMutation.delete(parent) { + includes(it.child) + } + Amplify.API.mutate(deleteRequest) + + // THEN + withContext(this.coroutineContext) { + latch.await(10, TimeUnit.SECONDS) + } + + assertEquals(parent.id, capturedParent!!.id) + assertEquals(hasOneChild.content, capturedChild!!.content) + assertEquals(hasOneChild.id, capturedChild!!.id) + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasOneChild)) + } +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyUpdateInstrumentationTest.kt b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyUpdateInstrumentationTest.kt new file mode 100644 index 0000000000..8b1d6ac878 --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyUpdateInstrumentationTest.kt @@ -0,0 +1,235 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.api.aws + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.amplifyframework.api.aws.test.R +import com.amplifyframework.api.graphql.model.ModelMutation +import com.amplifyframework.core.AmplifyConfiguration +import com.amplifyframework.core.model.LazyModelList +import com.amplifyframework.core.model.LazyModelReference +import com.amplifyframework.core.model.LoadedModelList +import com.amplifyframework.core.model.LoadedModelReference +import com.amplifyframework.core.model.includes +import com.amplifyframework.datastore.generated.model.HasManyChild +import com.amplifyframework.datastore.generated.model.HasManyChildPath +import com.amplifyframework.datastore.generated.model.HasOneChild +import com.amplifyframework.datastore.generated.model.Parent +import com.amplifyframework.datastore.generated.model.ParentPath +import com.amplifyframework.kotlin.core.Amplify +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.fail +import org.junit.BeforeClass +import org.junit.Test + +class GraphQLLazyUpdateInstrumentationTest { + + companion object { + @JvmStatic + @BeforeClass + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + val config = AmplifyConfiguration.fromConfigFile(context, R.raw.amplifyconfigurationlazy) + Amplify.addPlugin(AWSApiPlugin()) + Amplify.configure(config, context) + } + } + + @Test + fun update_with_no_includes() = runTest { + // GIVEN + val hasOneChild = HasOneChild.builder().content("Child1").build() + Amplify.API.mutate(ModelMutation.create(hasOneChild)) + + val hasOneChild2 = HasOneChild.builder().content("Child2").build() + Amplify.API.mutate(ModelMutation.create(hasOneChild2)) + + val parent = Parent.builder().parentChildId(hasOneChild.id).build() + val parentResponse = Amplify.API.mutate(ModelMutation.create(parent)).data + + val hasManyChild = HasManyChild.builder().content("Child2").parent(parent).build() + Amplify.API.mutate(ModelMutation.create(hasManyChild)) + + // WHEN + val newParent = parentResponse.copyOfBuilder().parentChildId(hasOneChild2.id).build() + val request = ModelMutation.update(newParent) + val updatedParent = Amplify.API.mutate(request).data + + // THEN + assertEquals(parent.id, updatedParent.id) + assertEquals(hasOneChild2.id, updatedParent.parentChildId) + (updatedParent.child as? LazyModelReference)?.fetchModel()?.let { + assertEquals(hasOneChild2.id, it.id) + assertEquals(hasOneChild2.content, it.content) + } ?: fail("Response child was null or not a LazyModelReference") + (updatedParent.children as? LazyModelList)?.fetchPage()?.let { + assertEquals(1, it.items.size) + } ?: fail("Response child was null or not a LazyModelList") + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasOneChild)) + Amplify.API.mutate(ModelMutation.delete(hasOneChild2)) + Amplify.API.mutate(ModelMutation.delete(hasManyChild)) + Amplify.API.mutate(ModelMutation.delete(parent)) + } + + @Test + fun update_with_includes() = runTest { + // GIVEN + val hasOneChild = HasOneChild.builder().content("Child1").build() + Amplify.API.mutate(ModelMutation.create(hasOneChild)) + + val hasOneChild2 = HasOneChild.builder().content("Child2").build() + Amplify.API.mutate(ModelMutation.create(hasOneChild2)) + + val parent = Parent.builder().parentChildId(hasOneChild.id).build() + val parentResponse = Amplify.API.mutate(ModelMutation.create(parent)).data + + val hasManyChild = HasManyChild.builder().content("Child2").parent(parent).build() + Amplify.API.mutate(ModelMutation.create(hasManyChild)) + + // WHEN + val newParent = parentResponse.copyOfBuilder().parentChildId(hasOneChild2.id).build() + val request = ModelMutation.update(newParent) { + includes(it.child, it.children) + } + val updatedParent = Amplify.API.mutate(request).data + + // THEN + assertEquals(parent.id, updatedParent.id) + assertEquals(hasOneChild2.id, updatedParent.parentChildId) + (updatedParent.child as? LoadedModelReference)?.value?.let { + assertEquals(hasOneChild2.id, it.id) + assertEquals(hasOneChild2.content, it.content) + } ?: fail("Response child was null or not a LoadedModelReference") + (updatedParent.children as? LoadedModelList)?.let { + assertEquals(1, it.items.size) + } ?: fail("Response child was null or not a LoadedModelList") + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasOneChild)) + Amplify.API.mutate(ModelMutation.delete(hasOneChild2)) + Amplify.API.mutate(ModelMutation.delete(hasManyChild)) + Amplify.API.mutate(ModelMutation.delete(parent)) + } + + @Test + fun update_without_includes_does_not_remove_relationship() = runTest { + // GIVEN + val parent = Parent.builder().build() + Amplify.API.mutate(ModelMutation.create(parent)).data + + val hasManyChild = HasManyChild.builder().content("Child2").parent(parent).build() + Amplify.API.mutate(ModelMutation.create(hasManyChild)) + + // WHEN + val hasManyChildToUpdate = hasManyChild.copyOfBuilder().content("Child2-Updated").build() + val request = ModelMutation.update(hasManyChildToUpdate) + val updatedHasManyChild = Amplify.API.mutate(request).data + + // THEN + assertEquals(hasManyChild.id, updatedHasManyChild.id) + assertEquals("Child2-Updated", updatedHasManyChild.content) + (updatedHasManyChild.parent as? LazyModelReference)?.fetchModel()?.let { + assertEquals(parent.id, it.id) + } ?: fail("Response child was null or not a LazyModelReference") + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasManyChild)) + Amplify.API.mutate(ModelMutation.delete(parent)) + } + + @Test + fun update_with_includes_does_not_remove_relationship() = runTest { + // GIVEN + val parent = Parent.builder().build() + Amplify.API.mutate(ModelMutation.create(parent)).data + + val hasManyChild = HasManyChild.builder().content("Child2").parent(parent).build() + Amplify.API.mutate(ModelMutation.create(hasManyChild)) + + // WHEN + val hasManyChildToUpdate = hasManyChild.copyOfBuilder().content("Child2-Updated").build() + val request = ModelMutation.update(hasManyChildToUpdate) { + includes(it.parent) + } + val updatedHasManyChild = Amplify.API.mutate(request).data + + // THEN + assertEquals(hasManyChild.id, updatedHasManyChild.id) + assertEquals("Child2-Updated", updatedHasManyChild.content) + (updatedHasManyChild.parent as? LoadedModelReference)?.value?.let { + assertEquals(parent.id, it.id) + } ?: fail("Response child was null or not a LoadedModelReference") + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasManyChild)) + Amplify.API.mutate(ModelMutation.delete(parent)) + } + + @Test + fun update_without_includes_explicit_remove_relationship() = runTest { + // GIVEN + val parent = Parent.builder().build() + Amplify.API.mutate(ModelMutation.create(parent)).data + + val hasManyChild = HasManyChild.builder().content("Child2").parent(parent).build() + Amplify.API.mutate(ModelMutation.create(hasManyChild)) + + // WHEN + val hasManyChildToUpdate = hasManyChild.copyOfBuilder().parent(null).content("Child2-Updated").build() + val request = ModelMutation.update(hasManyChildToUpdate) + val updatedHasManyChild = Amplify.API.mutate(request).data + + // THEN + assertEquals(hasManyChild.id, updatedHasManyChild.id) + assertEquals("Child2-Updated", updatedHasManyChild.content) + assertNull((updatedHasManyChild.parent as LoadedModelReference).value) + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasManyChild)) + Amplify.API.mutate(ModelMutation.delete(parent)) + } + + @Test + fun update_with_includes_explicit_remove_relationship() = runTest { + // GIVEN + val parent = Parent.builder().build() + Amplify.API.mutate(ModelMutation.create(parent)).data + + val hasManyChild = HasManyChild.builder().content("Child2").parent(parent).build() + Amplify.API.mutate(ModelMutation.create(hasManyChild)) + + // WHEN + val hasManyChildToUpdate = hasManyChild.copyOfBuilder().parent(null).content("Child2-Updated").build() + val request = ModelMutation.update(hasManyChildToUpdate) { + includes(it.parent) + } + val updatedHasManyChild = Amplify.API.mutate(request).data + + // THEN + assertEquals(hasManyChild.id, updatedHasManyChild.id) + assertEquals("Child2-Updated", updatedHasManyChild.content) + assertNull((updatedHasManyChild.parent as LoadedModelReference).value) + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasManyChild)) + Amplify.API.mutate(ModelMutation.delete(parent)) + } +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/SubscriptionEndpointTest.java b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/SubscriptionEndpointTest.java index 2f50d8e929..d7f79bfd18 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/SubscriptionEndpointTest.java +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/SubscriptionEndpointTest.java @@ -73,7 +73,7 @@ public void setup() throws ApiException, JSONException { final GraphQLResponse.Factory responseFactory = new GsonGraphQLResponseFactory(); final SubscriptionAuthorizer authorizer = new SubscriptionAuthorizer(apiConfiguration); - this.subscriptionEndpoint = new SubscriptionEndpoint(apiConfiguration, null, responseFactory, authorizer); + this.subscriptionEndpoint = new SubscriptionEndpoint(apiConfiguration, null, responseFactory, authorizer, null); this.eventId = RandomString.string(); this.subscriptionIdsForRelease = new HashSet<>(); @@ -156,7 +156,7 @@ public void usesConfiguratorIfPresent() throws ApiException, JSONException { }; this.subscriptionEndpoint = new SubscriptionEndpoint(apiConfiguration, configurator, responseFactory, - authorizer); + authorizer, null); String firstSubscriptionId = subscribeToEventComments(eventId); assertNotNull(firstSubscriptionId); diff --git a/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/AmplifyModelProvider.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/AmplifyModelProvider.java new file mode 100644 index 0000000000..560c3877c3 --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/AmplifyModelProvider.java @@ -0,0 +1,53 @@ +package com.amplifyframework.datastore.generated.model; + +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.ModelProvider; +import com.amplifyframework.util.Immutable; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +/** + * Contains the set of model classes that implement {@link Model} + * interface. + */ + +public final class AmplifyModelProvider implements ModelProvider { + private static final String AMPLIFY_MODEL_VERSION = "8217f08a06a711a8c8d65895bbe74b4f"; + private static AmplifyModelProvider amplifyGeneratedModelInstance; + private AmplifyModelProvider() { + + } + + public static synchronized AmplifyModelProvider getInstance() { + if (amplifyGeneratedModelInstance == null) { + amplifyGeneratedModelInstance = new AmplifyModelProvider(); + } + return amplifyGeneratedModelInstance; + } + + /** + * Get a set of the model classes. + * + * @return a set of the model classes. + */ + @Override + public Set> models() { + final Set> modifiableSet = new HashSet<>( + Arrays.>asList(Parent.class, HasOneChild.class, HasManyChild.class, Project.class, Team.class, Blog.class, Post.class, Comment.class) + ); + + return Immutable.of(modifiableSet); + + } + + /** + * Get the version of the models. + * + * @return the version string of the models. + */ + @Override + public String version() { + return AMPLIFY_MODEL_VERSION; + } +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Blog.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Blog.java new file mode 100644 index 0000000000..be149b38c9 --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Blog.java @@ -0,0 +1,186 @@ +package com.amplifyframework.datastore.generated.model; + +import static com.amplifyframework.core.model.query.predicate.QueryField.field; + +import androidx.core.util.ObjectsCompat; + +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.ModelIdentifier; +import com.amplifyframework.core.model.ModelList; +import com.amplifyframework.core.model.annotations.HasMany; +import com.amplifyframework.core.model.annotations.Index; +import com.amplifyframework.core.model.annotations.ModelConfig; +import com.amplifyframework.core.model.annotations.ModelField; +import com.amplifyframework.core.model.query.predicate.QueryField; +import com.amplifyframework.core.model.temporal.Temporal; + +import java.util.Objects; + +/** This is an auto generated class representing the Blog type in your schema. */ +@SuppressWarnings("all") +@ModelConfig(pluralName = "Blogs", type = Model.Type.USER, version = 1, hasLazySupport = true) +@Index(name = "undefined", fields = {"blogId"}) +public final class Blog implements Model { + public static final BlogPath rootPath = new BlogPath("root", false, null); + public static final QueryField BLOG_ID = field("Blog", "blogId"); + public static final QueryField NAME = field("Blog", "name"); + private final @ModelField(targetType="String", isRequired = true) String blogId; + private final @ModelField(targetType="String", isRequired = true) String name; + private final @ModelField(targetType="Post", isRequired = true) @HasMany(associatedWith = "blog", type = Post.class) ModelList posts = null; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime createdAt; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime updatedAt; + /** @deprecated This API is internal to Amplify and should not be used. */ + @Deprecated + public String resolveIdentifier() { + return blogId; + } + + public String getBlogId() { + return blogId; + } + + public String getName() { + return name; + } + + public ModelList getPosts() { + return posts; + } + + public Temporal.DateTime getCreatedAt() { + return createdAt; + } + + public Temporal.DateTime getUpdatedAt() { + return updatedAt; + } + + private Blog(String blogId, String name) { + this.blogId = blogId; + this.name = name; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if(obj == null || getClass() != obj.getClass()) { + return false; + } else { + Blog blog = (Blog) obj; + return ObjectsCompat.equals(getBlogId(), blog.getBlogId()) && + ObjectsCompat.equals(getName(), blog.getName()) && + ObjectsCompat.equals(getCreatedAt(), blog.getCreatedAt()) && + ObjectsCompat.equals(getUpdatedAt(), blog.getUpdatedAt()); + } + } + + @Override + public int hashCode() { + return new StringBuilder() + .append(getBlogId()) + .append(getName()) + .append(getCreatedAt()) + .append(getUpdatedAt()) + .toString() + .hashCode(); + } + + @Override + public String toString() { + return new StringBuilder() + .append("Blog {") + .append("blogId=" + String.valueOf(getBlogId()) + ", ") + .append("name=" + String.valueOf(getName()) + ", ") + .append("createdAt=" + String.valueOf(getCreatedAt()) + ", ") + .append("updatedAt=" + String.valueOf(getUpdatedAt())) + .append("}") + .toString(); + } + + public static BlogIdStep builder() { + return new Builder(); + } + + public CopyOfBuilder copyOfBuilder() { + return new CopyOfBuilder(blogId, + name); + } + public interface BlogIdStep { + NameStep blogId(String blogId); + } + + + public interface NameStep { + BuildStep name(String name); + } + + + public interface BuildStep { + Blog build(); + } + + + public static class Builder implements BlogIdStep, NameStep, BuildStep { + private String blogId; + private String name; + public Builder() { + + } + + private Builder(String blogId, String name) { + this.blogId = blogId; + this.name = name; + } + + @Override + public Blog build() { + + return new Blog( + blogId, + name); + } + + @Override + public NameStep blogId(String blogId) { + Objects.requireNonNull(blogId); + this.blogId = blogId; + return this; + } + + @Override + public BuildStep name(String name) { + Objects.requireNonNull(name); + this.name = name; + return this; + } + } + + + public final class CopyOfBuilder extends Builder { + private CopyOfBuilder(String blogId, String name) { + super(blogId, name); + Objects.requireNonNull(blogId); + Objects.requireNonNull(name); + } + + @Override + public CopyOfBuilder blogId(String blogId) { + return (CopyOfBuilder) super.blogId(blogId); + } + + @Override + public CopyOfBuilder name(String name) { + return (CopyOfBuilder) super.name(name); + } + } + + + public static class BlogIdentifier extends ModelIdentifier { + private static final long serialVersionUID = 1L; + public BlogIdentifier(String blogId) { + super(blogId); + } + } + +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/BlogPath.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/BlogPath.java new file mode 100644 index 0000000000..82b7e01a25 --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/BlogPath.java @@ -0,0 +1,22 @@ +package com.amplifyframework.datastore.generated.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.amplifyframework.core.model.ModelPath; +import com.amplifyframework.core.model.PropertyPath; + +/** This is an auto generated class representing the ModelPath for the Blog type in your schema. */ +public final class BlogPath extends ModelPath { + private PostPath posts; + BlogPath(@NonNull String name, @NonNull Boolean isCollection, @Nullable PropertyPath parent) { + super(name, isCollection, parent, Blog.class); + } + + public synchronized PostPath getPosts() { + if (posts == null) { + posts = new PostPath("posts", true, this); + } + return posts; + } +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Comment.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Comment.java new file mode 100644 index 0000000000..55d15675bc --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Comment.java @@ -0,0 +1,218 @@ +package com.amplifyframework.datastore.generated.model; + +import static com.amplifyframework.core.model.query.predicate.QueryField.field; + +import androidx.core.util.ObjectsCompat; + +import com.amplifyframework.core.model.LoadedModelReferenceImpl; +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.ModelIdentifier; +import com.amplifyframework.core.model.ModelReference; +import com.amplifyframework.core.model.annotations.BelongsTo; +import com.amplifyframework.core.model.annotations.Index; +import com.amplifyframework.core.model.annotations.ModelConfig; +import com.amplifyframework.core.model.annotations.ModelField; +import com.amplifyframework.core.model.query.predicate.QueryField; +import com.amplifyframework.core.model.temporal.Temporal; + +import java.util.Objects; + +/** This is an auto generated class representing the Comment type in your schema. */ +@SuppressWarnings("all") +@ModelConfig(pluralName = "Comments", type = Model.Type.USER, version = 1, hasLazySupport = true) +@Index(name = "undefined", fields = {"commentId","content"}) +public final class Comment implements Model { + public static final CommentPath rootPath = new CommentPath("root", false, null); + public static final QueryField COMMENT_ID = field("Comment", "commentId"); + public static final QueryField CONTENT = field("Comment", "content"); + public static final QueryField POST = field("Comment", "postCommentsPostId"); + private final @ModelField(targetType="ID", isRequired = true) String commentId; + private final @ModelField(targetType="String", isRequired = true) String content; + private final @ModelField(targetType="Post", isRequired = true) @BelongsTo(targetName = "postCommentsPostId", targetNames = {"postCommentsPostId", "postCommentsTitle"}, type = Post.class) ModelReference post; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime createdAt; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime updatedAt; + private CommentIdentifier commentIdentifier; + /** @deprecated This API is internal to Amplify and should not be used. */ + @Deprecated + public CommentIdentifier resolveIdentifier() { + if (commentIdentifier == null) { + this.commentIdentifier = new CommentIdentifier(commentId, content); + } + return commentIdentifier; + } + + public String getCommentId() { + return commentId; + } + + public String getContent() { + return content; + } + + public ModelReference getPost() { + return post; + } + + public Temporal.DateTime getCreatedAt() { + return createdAt; + } + + public Temporal.DateTime getUpdatedAt() { + return updatedAt; + } + + private Comment(String commentId, String content, ModelReference post) { + this.commentId = commentId; + this.content = content; + this.post = post; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if(obj == null || getClass() != obj.getClass()) { + return false; + } else { + Comment comment = (Comment) obj; + return ObjectsCompat.equals(getCommentId(), comment.getCommentId()) && + ObjectsCompat.equals(getContent(), comment.getContent()) && + ObjectsCompat.equals(getPost(), comment.getPost()) && + ObjectsCompat.equals(getCreatedAt(), comment.getCreatedAt()) && + ObjectsCompat.equals(getUpdatedAt(), comment.getUpdatedAt()); + } + } + + @Override + public int hashCode() { + return new StringBuilder() + .append(getCommentId()) + .append(getContent()) + .append(getPost()) + .append(getCreatedAt()) + .append(getUpdatedAt()) + .toString() + .hashCode(); + } + + @Override + public String toString() { + return new StringBuilder() + .append("Comment {") + .append("commentId=" + String.valueOf(getCommentId()) + ", ") + .append("content=" + String.valueOf(getContent()) + ", ") + .append("post=" + String.valueOf(getPost()) + ", ") + .append("createdAt=" + String.valueOf(getCreatedAt()) + ", ") + .append("updatedAt=" + String.valueOf(getUpdatedAt())) + .append("}") + .toString(); + } + + public static CommentIdStep builder() { + return new Builder(); + } + + public CopyOfBuilder copyOfBuilder() { + return new CopyOfBuilder(commentId, + content, + post); + } + public interface CommentIdStep { + ContentStep commentId(String commentId); + } + + + public interface ContentStep { + PostStep content(String content); + } + + + public interface PostStep { + BuildStep post(Post post); + } + + + public interface BuildStep { + Comment build(); + } + + + public static class Builder implements CommentIdStep, ContentStep, PostStep, BuildStep { + private String commentId; + private String content; + private ModelReference post; + public Builder() { + + } + + private Builder(String commentId, String content, ModelReference post) { + this.commentId = commentId; + this.content = content; + this.post = post; + } + + @Override + public Comment build() { + + return new Comment( + commentId, + content, + post); + } + + @Override + public ContentStep commentId(String commentId) { + Objects.requireNonNull(commentId); + this.commentId = commentId; + return this; + } + + @Override + public PostStep content(String content) { + Objects.requireNonNull(content); + this.content = content; + return this; + } + + @Override + public BuildStep post(Post post) { + Objects.requireNonNull(post); + this.post = new LoadedModelReferenceImpl<>(post); + return this; + } + } + + + public final class CopyOfBuilder extends Builder { + private CopyOfBuilder(String commentId, String content, ModelReference post) { + super(commentId, content, post); + Objects.requireNonNull(commentId); + Objects.requireNonNull(content); + Objects.requireNonNull(post); + } + + @Override + public CopyOfBuilder commentId(String commentId) { + return (CopyOfBuilder) super.commentId(commentId); + } + + @Override + public CopyOfBuilder content(String content) { + return (CopyOfBuilder) super.content(content); + } + + @Override + public CopyOfBuilder post(Post post) { + return (CopyOfBuilder) super.post(post); + } + } + + + public static class CommentIdentifier extends ModelIdentifier { + private static final long serialVersionUID = 1L; + public CommentIdentifier(String commentId, String content) { + super(commentId, content); + } + } + +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/CommentPath.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/CommentPath.java new file mode 100644 index 0000000000..4ae576b534 --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/CommentPath.java @@ -0,0 +1,22 @@ +package com.amplifyframework.datastore.generated.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.amplifyframework.core.model.ModelPath; +import com.amplifyframework.core.model.PropertyPath; + +/** This is an auto generated class representing the ModelPath for the Comment type in your schema. */ +public final class CommentPath extends ModelPath { + private PostPath post; + CommentPath(@NonNull String name, @NonNull Boolean isCollection, @Nullable PropertyPath parent) { + super(name, isCollection, parent, Comment.class); + } + + public synchronized PostPath getPost() { + if (post == null) { + post = new PostPath("post", false, this); + } + return post; + } +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasManyChild.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasManyChild.java new file mode 100644 index 0000000000..a8658d954d --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasManyChild.java @@ -0,0 +1,212 @@ +package com.amplifyframework.datastore.generated.model; + +import static com.amplifyframework.core.model.query.predicate.QueryField.field; + +import androidx.core.util.ObjectsCompat; + +import com.amplifyframework.core.model.LoadedModelReferenceImpl; +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.ModelIdentifier; +import com.amplifyframework.core.model.ModelReference; +import com.amplifyframework.core.model.annotations.BelongsTo; +import com.amplifyframework.core.model.annotations.Index; +import com.amplifyframework.core.model.annotations.ModelConfig; +import com.amplifyframework.core.model.annotations.ModelField; +import com.amplifyframework.core.model.query.predicate.QueryField; +import com.amplifyframework.core.model.temporal.Temporal; + +import java.util.UUID; + +/** This is an auto generated class representing the HasManyChild type in your schema. */ +@SuppressWarnings("all") +@ModelConfig(pluralName = "HasManyChildren", type = Model.Type.USER, version = 1, hasLazySupport = true) +@Index(name = "undefined", fields = {"id"}) +public final class HasManyChild implements Model { + public static final HasManyChildPath rootPath = new HasManyChildPath("root", false, null); + public static final QueryField ID = field("HasManyChild", "id"); + public static final QueryField CONTENT = field("HasManyChild", "content"); + public static final QueryField PARENT = field("HasManyChild", "parentChildrenId"); + private final @ModelField(targetType="ID", isRequired = true) String id; + private final @ModelField(targetType="String") String content; + private final @ModelField(targetType="Parent") @BelongsTo(targetName = "parentChildrenId", targetNames = {"parentChildrenId"}, type = Parent.class) ModelReference parent; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime createdAt; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime updatedAt; + /** @deprecated This API is internal to Amplify and should not be used. */ + @Deprecated + public String resolveIdentifier() { + return id; + } + + public String getId() { + return id; + } + + public String getContent() { + return content; + } + + public ModelReference getParent() { + return parent; + } + + public Temporal.DateTime getCreatedAt() { + return createdAt; + } + + public Temporal.DateTime getUpdatedAt() { + return updatedAt; + } + + private HasManyChild(String id, String content, ModelReference parent) { + this.id = id; + this.content = content; + this.parent = parent; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if(obj == null || getClass() != obj.getClass()) { + return false; + } else { + HasManyChild hasManyChild = (HasManyChild) obj; + return ObjectsCompat.equals(getId(), hasManyChild.getId()) && + ObjectsCompat.equals(getContent(), hasManyChild.getContent()) && + ObjectsCompat.equals(getParent(), hasManyChild.getParent()) && + ObjectsCompat.equals(getCreatedAt(), hasManyChild.getCreatedAt()) && + ObjectsCompat.equals(getUpdatedAt(), hasManyChild.getUpdatedAt()); + } + } + + @Override + public int hashCode() { + return new StringBuilder() + .append(getId()) + .append(getContent()) + .append(getParent()) + .append(getCreatedAt()) + .append(getUpdatedAt()) + .toString() + .hashCode(); + } + + @Override + public String toString() { + return new StringBuilder() + .append("HasManyChild {") + .append("id=" + String.valueOf(getId()) + ", ") + .append("content=" + String.valueOf(getContent()) + ", ") + .append("parent=" + String.valueOf(getParent()) + ", ") + .append("createdAt=" + String.valueOf(getCreatedAt()) + ", ") + .append("updatedAt=" + String.valueOf(getUpdatedAt())) + .append("}") + .toString(); + } + + public static BuildStep builder() { + return new Builder(); + } + + /** + * WARNING: This method should not be used to build an instance of this object for a CREATE mutation. + * This is a convenience method to return an instance of the object with only its ID populated + * to be used in the context of a parameter in a delete mutation or referencing a foreign key + * in a relationship. + * @param id the id of the existing item this instance will represent + * @return an instance of this model with only ID populated + */ + public static HasManyChild justId(String id) { + return new HasManyChild( + id, + null, + null + ); + } + + public CopyOfBuilder copyOfBuilder() { + return new CopyOfBuilder(id, + content, + parent); + } + public interface BuildStep { + HasManyChild build(); + BuildStep id(String id); + BuildStep content(String content); + BuildStep parent(Parent parent); + } + + + public static class Builder implements BuildStep { + private String id; + private String content; + private ModelReference parent; + public Builder() { + + } + + private Builder(String id, String content, ModelReference parent) { + this.id = id; + this.content = content; + this.parent = parent; + } + + @Override + public HasManyChild build() { + String id = this.id != null ? this.id : UUID.randomUUID().toString(); + + return new HasManyChild( + id, + content, + parent); + } + + @Override + public BuildStep content(String content) { + this.content = content; + return this; + } + + @Override + public BuildStep parent(Parent parent) { + this.parent = new LoadedModelReferenceImpl<>(parent); + return this; + } + + /** + * @param id id + * @return Current Builder instance, for fluent method chaining + */ + public BuildStep id(String id) { + this.id = id; + return this; + } + } + + + public final class CopyOfBuilder extends Builder { + private CopyOfBuilder(String id, String content, ModelReference parent) { + super(id, content, parent); + + } + + @Override + public CopyOfBuilder content(String content) { + return (CopyOfBuilder) super.content(content); + } + + @Override + public CopyOfBuilder parent(Parent parent) { + return (CopyOfBuilder) super.parent(parent); + } + } + + + public static class HasManyChildIdentifier extends ModelIdentifier { + private static final long serialVersionUID = 1L; + public HasManyChildIdentifier(String id) { + super(id); + } + } + +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasManyChildPath.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasManyChildPath.java new file mode 100644 index 0000000000..47c32bf11c --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasManyChildPath.java @@ -0,0 +1,22 @@ +package com.amplifyframework.datastore.generated.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.amplifyframework.core.model.ModelPath; +import com.amplifyframework.core.model.PropertyPath; + +/** This is an auto generated class representing the ModelPath for the HasManyChild type in your schema. */ +public final class HasManyChildPath extends ModelPath { + private ParentPath parent; + HasManyChildPath(@NonNull String name, @NonNull Boolean isCollection, @Nullable PropertyPath parent) { + super(name, isCollection, parent, HasManyChild.class); + } + + public synchronized ParentPath getParent() { + if (parent == null) { + parent = new ParentPath("parent", false, this); + } + return parent; + } +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasOneChild.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasOneChild.java new file mode 100644 index 0000000000..8718600f36 --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasOneChild.java @@ -0,0 +1,182 @@ +package com.amplifyframework.datastore.generated.model; + +import static com.amplifyframework.core.model.query.predicate.QueryField.field; + +import androidx.core.util.ObjectsCompat; + +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.ModelIdentifier; +import com.amplifyframework.core.model.annotations.Index; +import com.amplifyframework.core.model.annotations.ModelConfig; +import com.amplifyframework.core.model.annotations.ModelField; +import com.amplifyframework.core.model.query.predicate.QueryField; +import com.amplifyframework.core.model.temporal.Temporal; + +import java.util.UUID; + +/** This is an auto generated class representing the HasOneChild type in your schema. */ +@SuppressWarnings("all") +@ModelConfig(pluralName = "HasOneChildren", type = Model.Type.USER, version = 1, hasLazySupport = true) +@Index(name = "undefined", fields = {"id"}) +public final class HasOneChild implements Model { + public static final HasOneChildPath rootPath = new HasOneChildPath("root", false, null); + public static final QueryField ID = field("HasOneChild", "id"); + public static final QueryField CONTENT = field("HasOneChild", "content"); + private final @ModelField(targetType="ID", isRequired = true) String id; + private final @ModelField(targetType="String") String content; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime createdAt; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime updatedAt; + /** @deprecated This API is internal to Amplify and should not be used. */ + @Deprecated + public String resolveIdentifier() { + return id; + } + + public String getId() { + return id; + } + + public String getContent() { + return content; + } + + public Temporal.DateTime getCreatedAt() { + return createdAt; + } + + public Temporal.DateTime getUpdatedAt() { + return updatedAt; + } + + private HasOneChild(String id, String content) { + this.id = id; + this.content = content; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if(obj == null || getClass() != obj.getClass()) { + return false; + } else { + HasOneChild hasOneChild = (HasOneChild) obj; + return ObjectsCompat.equals(getId(), hasOneChild.getId()) && + ObjectsCompat.equals(getContent(), hasOneChild.getContent()) && + ObjectsCompat.equals(getCreatedAt(), hasOneChild.getCreatedAt()) && + ObjectsCompat.equals(getUpdatedAt(), hasOneChild.getUpdatedAt()); + } + } + + @Override + public int hashCode() { + return new StringBuilder() + .append(getId()) + .append(getContent()) + .append(getCreatedAt()) + .append(getUpdatedAt()) + .toString() + .hashCode(); + } + + @Override + public String toString() { + return new StringBuilder() + .append("HasOneChild {") + .append("id=" + String.valueOf(getId()) + ", ") + .append("content=" + String.valueOf(getContent()) + ", ") + .append("createdAt=" + String.valueOf(getCreatedAt()) + ", ") + .append("updatedAt=" + String.valueOf(getUpdatedAt())) + .append("}") + .toString(); + } + + public static BuildStep builder() { + return new Builder(); + } + + /** + * WARNING: This method should not be used to build an instance of this object for a CREATE mutation. + * This is a convenience method to return an instance of the object with only its ID populated + * to be used in the context of a parameter in a delete mutation or referencing a foreign key + * in a relationship. + * @param id the id of the existing item this instance will represent + * @return an instance of this model with only ID populated + */ + public static HasOneChild justId(String id) { + return new HasOneChild( + id, + null + ); + } + + public CopyOfBuilder copyOfBuilder() { + return new CopyOfBuilder(id, + content); + } + public interface BuildStep { + HasOneChild build(); + BuildStep id(String id); + BuildStep content(String content); + } + + + public static class Builder implements BuildStep { + private String id; + private String content; + public Builder() { + + } + + private Builder(String id, String content) { + this.id = id; + this.content = content; + } + + @Override + public HasOneChild build() { + String id = this.id != null ? this.id : UUID.randomUUID().toString(); + + return new HasOneChild( + id, + content); + } + + @Override + public BuildStep content(String content) { + this.content = content; + return this; + } + + /** + * @param id id + * @return Current Builder instance, for fluent method chaining + */ + public BuildStep id(String id) { + this.id = id; + return this; + } + } + + + public final class CopyOfBuilder extends Builder { + private CopyOfBuilder(String id, String content) { + super(id, content); + + } + + @Override + public CopyOfBuilder content(String content) { + return (CopyOfBuilder) super.content(content); + } + } + + + public static class HasOneChildIdentifier extends ModelIdentifier { + private static final long serialVersionUID = 1L; + public HasOneChildIdentifier(String id) { + super(id); + } + } + +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasOneChildPath.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasOneChildPath.java new file mode 100644 index 0000000000..13c57232eb --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasOneChildPath.java @@ -0,0 +1,14 @@ +package com.amplifyframework.datastore.generated.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.amplifyframework.core.model.ModelPath; +import com.amplifyframework.core.model.PropertyPath; + +/** This is an auto generated class representing the ModelPath for the HasOneChild type in your schema. */ +public final class HasOneChildPath extends ModelPath { + HasOneChildPath(@NonNull String name, @NonNull Boolean isCollection, @Nullable PropertyPath parent) { + super(name, isCollection, parent, HasOneChild.class); + } +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Parent.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Parent.java new file mode 100644 index 0000000000..02b55e0e93 --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Parent.java @@ -0,0 +1,196 @@ +package com.amplifyframework.datastore.generated.model; + +import static com.amplifyframework.core.model.query.predicate.QueryField.field; + +import androidx.core.util.ObjectsCompat; + +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.ModelIdentifier; +import com.amplifyframework.core.model.ModelList; +import com.amplifyframework.core.model.ModelReference; +import com.amplifyframework.core.model.annotations.HasMany; +import com.amplifyframework.core.model.annotations.HasOne; +import com.amplifyframework.core.model.annotations.Index; +import com.amplifyframework.core.model.annotations.ModelConfig; +import com.amplifyframework.core.model.annotations.ModelField; +import com.amplifyframework.core.model.query.predicate.QueryField; +import com.amplifyframework.core.model.temporal.Temporal; + +import java.util.UUID; + +/** This is an auto generated class representing the Parent type in your schema. */ +@SuppressWarnings("all") +@ModelConfig(pluralName = "Parents", type = Model.Type.USER, version = 1, hasLazySupport = true) +@Index(name = "undefined", fields = {"id"}) +public final class Parent implements Model { + public static final ParentPath rootPath = new ParentPath("root", false, null); + public static final QueryField ID = field("Parent", "id"); + public static final QueryField PARENT_CHILD_ID = field("Parent", "parentChildId"); + private final @ModelField(targetType="ID", isRequired = true) String id; + private final @ModelField(targetType="HasOneChild") @HasOne(associatedWith = "id", targetNames = {"parentChildId"}, type = HasOneChild.class) ModelReference child = null; + private final @ModelField(targetType="HasManyChild") @HasMany(associatedWith = "parent", type = HasManyChild.class) ModelList children = null; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime createdAt; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime updatedAt; + private final @ModelField(targetType="ID") String parentChildId; + /** @deprecated This API is internal to Amplify and should not be used. */ + @Deprecated + public String resolveIdentifier() { + return id; + } + + public String getId() { + return id; + } + + public ModelReference getChild() { + return child; + } + + public ModelList getChildren() { + return children; + } + + public Temporal.DateTime getCreatedAt() { + return createdAt; + } + + public Temporal.DateTime getUpdatedAt() { + return updatedAt; + } + + public String getParentChildId() { + return parentChildId; + } + + private Parent(String id, String parentChildId) { + this.id = id; + this.parentChildId = parentChildId; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if(obj == null || getClass() != obj.getClass()) { + return false; + } else { + Parent parent = (Parent) obj; + return ObjectsCompat.equals(getId(), parent.getId()) && + ObjectsCompat.equals(getCreatedAt(), parent.getCreatedAt()) && + ObjectsCompat.equals(getUpdatedAt(), parent.getUpdatedAt()) && + ObjectsCompat.equals(getParentChildId(), parent.getParentChildId()); + } + } + + @Override + public int hashCode() { + return new StringBuilder() + .append(getId()) + .append(getCreatedAt()) + .append(getUpdatedAt()) + .append(getParentChildId()) + .toString() + .hashCode(); + } + + @Override + public String toString() { + return new StringBuilder() + .append("Parent {") + .append("id=" + String.valueOf(getId()) + ", ") + .append("createdAt=" + String.valueOf(getCreatedAt()) + ", ") + .append("updatedAt=" + String.valueOf(getUpdatedAt()) + ", ") + .append("parentChildId=" + String.valueOf(getParentChildId())) + .append("}") + .toString(); + } + + public static BuildStep builder() { + return new Builder(); + } + + /** + * WARNING: This method should not be used to build an instance of this object for a CREATE mutation. + * This is a convenience method to return an instance of the object with only its ID populated + * to be used in the context of a parameter in a delete mutation or referencing a foreign key + * in a relationship. + * @param id the id of the existing item this instance will represent + * @return an instance of this model with only ID populated + */ + public static Parent justId(String id) { + return new Parent( + id, + null + ); + } + + public CopyOfBuilder copyOfBuilder() { + return new CopyOfBuilder(id, + parentChildId); + } + public interface BuildStep { + Parent build(); + BuildStep id(String id); + BuildStep parentChildId(String parentChildId); + } + + + public static class Builder implements BuildStep { + private String id; + private String parentChildId; + public Builder() { + + } + + private Builder(String id, String parentChildId) { + this.id = id; + this.parentChildId = parentChildId; + } + + @Override + public Parent build() { + String id = this.id != null ? this.id : UUID.randomUUID().toString(); + + return new Parent( + id, + parentChildId); + } + + @Override + public BuildStep parentChildId(String parentChildId) { + this.parentChildId = parentChildId; + return this; + } + + /** + * @param id id + * @return Current Builder instance, for fluent method chaining + */ + public BuildStep id(String id) { + this.id = id; + return this; + } + } + + + public final class CopyOfBuilder extends Builder { + private CopyOfBuilder(String id, String parentChildId) { + super(id, parentChildId); + + } + + @Override + public CopyOfBuilder parentChildId(String parentChildId) { + return (CopyOfBuilder) super.parentChildId(parentChildId); + } + } + + + public static class ParentIdentifier extends ModelIdentifier { + private static final long serialVersionUID = 1L; + public ParentIdentifier(String id) { + super(id); + } + } + +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/ParentPath.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/ParentPath.java new file mode 100644 index 0000000000..3137c0d03a --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/ParentPath.java @@ -0,0 +1,30 @@ +package com.amplifyframework.datastore.generated.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.amplifyframework.core.model.ModelPath; +import com.amplifyframework.core.model.PropertyPath; + +/** This is an auto generated class representing the ModelPath for the Parent type in your schema. */ +public final class ParentPath extends ModelPath { + private HasOneChildPath child; + private HasManyChildPath children; + ParentPath(@NonNull String name, @NonNull Boolean isCollection, @Nullable PropertyPath parent) { + super(name, isCollection, parent, Parent.class); + } + + public synchronized HasOneChildPath getChild() { + if (child == null) { + child = new HasOneChildPath("child", false, this); + } + return child; + } + + public synchronized HasManyChildPath getChildren() { + if (children == null) { + children = new HasManyChildPath("children", true, this); + } + return children; + } +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Post.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Post.java new file mode 100644 index 0000000000..5988f6eec8 --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Post.java @@ -0,0 +1,225 @@ +package com.amplifyframework.datastore.generated.model; + +import static com.amplifyframework.core.model.query.predicate.QueryField.field; + +import androidx.core.util.ObjectsCompat; + +import com.amplifyframework.core.model.LoadedModelReferenceImpl; +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.ModelIdentifier; +import com.amplifyframework.core.model.ModelList; +import com.amplifyframework.core.model.ModelReference; +import com.amplifyframework.core.model.annotations.BelongsTo; +import com.amplifyframework.core.model.annotations.HasMany; +import com.amplifyframework.core.model.annotations.Index; +import com.amplifyframework.core.model.annotations.ModelConfig; +import com.amplifyframework.core.model.annotations.ModelField; +import com.amplifyframework.core.model.query.predicate.QueryField; +import com.amplifyframework.core.model.temporal.Temporal; + +import java.util.Objects; + +/** This is an auto generated class representing the Post type in your schema. */ +@SuppressWarnings("all") +@ModelConfig(pluralName = "Posts", type = Model.Type.USER, version = 1, hasLazySupport = true) +@Index(name = "undefined", fields = {"postId","title"}) +public final class Post implements Model { + public static final PostPath rootPath = new PostPath("root", false, null); + public static final QueryField POST_ID = field("Post", "postId"); + public static final QueryField TITLE = field("Post", "title"); + public static final QueryField BLOG = field("Post", "blogPostsBlogId"); + private final @ModelField(targetType="ID", isRequired = true) String postId; + private final @ModelField(targetType="String", isRequired = true) String title; + private final @ModelField(targetType="Blog", isRequired = true) @BelongsTo(targetName = "blogPostsBlogId", targetNames = {"blogPostsBlogId"}, type = Blog.class) ModelReference blog; + private final @ModelField(targetType="Comment", isRequired = true) @HasMany(associatedWith = "post", type = Comment.class) ModelList comments = null; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime createdAt; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime updatedAt; + private PostIdentifier postIdentifier; + /** @deprecated This API is internal to Amplify and should not be used. */ + @Deprecated + public PostIdentifier resolveIdentifier() { + if (postIdentifier == null) { + this.postIdentifier = new PostIdentifier(postId, title); + } + return postIdentifier; + } + + public String getPostId() { + return postId; + } + + public String getTitle() { + return title; + } + + public ModelReference getBlog() { + return blog; + } + + public ModelList getComments() { + return comments; + } + + public Temporal.DateTime getCreatedAt() { + return createdAt; + } + + public Temporal.DateTime getUpdatedAt() { + return updatedAt; + } + + private Post(String postId, String title, ModelReference blog) { + this.postId = postId; + this.title = title; + this.blog = blog; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if(obj == null || getClass() != obj.getClass()) { + return false; + } else { + Post post = (Post) obj; + return ObjectsCompat.equals(getPostId(), post.getPostId()) && + ObjectsCompat.equals(getTitle(), post.getTitle()) && + ObjectsCompat.equals(getBlog(), post.getBlog()) && + ObjectsCompat.equals(getCreatedAt(), post.getCreatedAt()) && + ObjectsCompat.equals(getUpdatedAt(), post.getUpdatedAt()); + } + } + + @Override + public int hashCode() { + return new StringBuilder() + .append(getPostId()) + .append(getTitle()) + .append(getBlog()) + .append(getCreatedAt()) + .append(getUpdatedAt()) + .toString() + .hashCode(); + } + + @Override + public String toString() { + return new StringBuilder() + .append("Post {") + .append("postId=" + String.valueOf(getPostId()) + ", ") + .append("title=" + String.valueOf(getTitle()) + ", ") + .append("blog=" + String.valueOf(getBlog()) + ", ") + .append("createdAt=" + String.valueOf(getCreatedAt()) + ", ") + .append("updatedAt=" + String.valueOf(getUpdatedAt())) + .append("}") + .toString(); + } + + public static PostIdStep builder() { + return new Builder(); + } + + public CopyOfBuilder copyOfBuilder() { + return new CopyOfBuilder(postId, + title, + blog); + } + public interface PostIdStep { + TitleStep postId(String postId); + } + + + public interface TitleStep { + BlogStep title(String title); + } + + + public interface BlogStep { + BuildStep blog(Blog blog); + } + + + public interface BuildStep { + Post build(); + } + + + public static class Builder implements PostIdStep, TitleStep, BlogStep, BuildStep { + private String postId; + private String title; + private ModelReference blog; + public Builder() { + + } + + private Builder(String postId, String title, ModelReference blog) { + this.postId = postId; + this.title = title; + this.blog = blog; + } + + @Override + public Post build() { + + return new Post( + postId, + title, + blog); + } + + @Override + public TitleStep postId(String postId) { + Objects.requireNonNull(postId); + this.postId = postId; + return this; + } + + @Override + public BlogStep title(String title) { + Objects.requireNonNull(title); + this.title = title; + return this; + } + + @Override + public BuildStep blog(Blog blog) { + Objects.requireNonNull(blog); + this.blog = new LoadedModelReferenceImpl<>(blog); + return this; + } + } + + + public final class CopyOfBuilder extends Builder { + private CopyOfBuilder(String postId, String title, ModelReference blog) { + super(postId, title, blog); + Objects.requireNonNull(postId); + Objects.requireNonNull(title); + Objects.requireNonNull(blog); + } + + @Override + public CopyOfBuilder postId(String postId) { + return (CopyOfBuilder) super.postId(postId); + } + + @Override + public CopyOfBuilder title(String title) { + return (CopyOfBuilder) super.title(title); + } + + @Override + public CopyOfBuilder blog(Blog blog) { + return (CopyOfBuilder) super.blog(blog); + } + } + + + public static class PostIdentifier extends ModelIdentifier { + private static final long serialVersionUID = 1L; + public PostIdentifier(String postId, String title) { + super(postId, title); + } + } + +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/PostPath.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/PostPath.java new file mode 100644 index 0000000000..fca3b30a2a --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/PostPath.java @@ -0,0 +1,30 @@ +package com.amplifyframework.datastore.generated.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.amplifyframework.core.model.ModelPath; +import com.amplifyframework.core.model.PropertyPath; + +/** This is an auto generated class representing the ModelPath for the Post type in your schema. */ +public final class PostPath extends ModelPath { + private BlogPath blog; + private CommentPath comments; + PostPath(@NonNull String name, @NonNull Boolean isCollection, @Nullable PropertyPath parent) { + super(name, isCollection, parent, Post.class); + } + + public synchronized BlogPath getBlog() { + if (blog == null) { + blog = new BlogPath("blog", false, this); + } + return blog; + } + + public synchronized CommentPath getComments() { + if (comments == null) { + comments = new CommentPath("comments", true, this); + } + return comments; + } +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Project.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Project.java new file mode 100644 index 0000000000..decaf21668 --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Project.java @@ -0,0 +1,242 @@ +package com.amplifyframework.datastore.generated.model; + +import static com.amplifyframework.core.model.query.predicate.QueryField.field; + +import androidx.core.util.ObjectsCompat; + +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.ModelIdentifier; +import com.amplifyframework.core.model.ModelReference; +import com.amplifyframework.core.model.annotations.HasOne; +import com.amplifyframework.core.model.annotations.Index; +import com.amplifyframework.core.model.annotations.ModelConfig; +import com.amplifyframework.core.model.annotations.ModelField; +import com.amplifyframework.core.model.query.predicate.QueryField; +import com.amplifyframework.core.model.temporal.Temporal; + +import java.util.Objects; + +/** This is an auto generated class representing the Project type in your schema. */ +@SuppressWarnings("all") +@ModelConfig(pluralName = "Projects", type = Model.Type.USER, version = 1, hasLazySupport = true) +@Index(name = "undefined", fields = {"projectId","name"}) +public final class Project implements Model { + public static final ProjectPath rootPath = new ProjectPath("root", false, null); + public static final QueryField PROJECT_ID = field("Project", "projectId"); + public static final QueryField NAME = field("Project", "name"); + public static final QueryField PROJECT_TEAM_TEAM_ID = field("Project", "projectTeamTeamId"); + public static final QueryField PROJECT_TEAM_NAME = field("Project", "projectTeamName"); + private final @ModelField(targetType="ID", isRequired = true) String projectId; + private final @ModelField(targetType="String", isRequired = true) String name; + private final @ModelField(targetType="Team") @HasOne(associatedWith = "project", targetNames = {"projectTeamTeamId", "projectTeamName"}, type = Team.class) ModelReference team = null; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime createdAt; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime updatedAt; + private final @ModelField(targetType="ID") String projectTeamTeamId; + private final @ModelField(targetType="String") String projectTeamName; + private ProjectIdentifier projectIdentifier; + /** @deprecated This API is internal to Amplify and should not be used. */ + @Deprecated + public ProjectIdentifier resolveIdentifier() { + if (projectIdentifier == null) { + this.projectIdentifier = new ProjectIdentifier(projectId, name); + } + return projectIdentifier; + } + + public String getProjectId() { + return projectId; + } + + public String getName() { + return name; + } + + public ModelReference getTeam() { + return team; + } + + public Temporal.DateTime getCreatedAt() { + return createdAt; + } + + public Temporal.DateTime getUpdatedAt() { + return updatedAt; + } + + public String getProjectTeamTeamId() { + return projectTeamTeamId; + } + + public String getProjectTeamName() { + return projectTeamName; + } + + private Project(String projectId, String name, String projectTeamTeamId, String projectTeamName) { + this.projectId = projectId; + this.name = name; + this.projectTeamTeamId = projectTeamTeamId; + this.projectTeamName = projectTeamName; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if(obj == null || getClass() != obj.getClass()) { + return false; + } else { + Project project = (Project) obj; + return ObjectsCompat.equals(getProjectId(), project.getProjectId()) && + ObjectsCompat.equals(getName(), project.getName()) && + ObjectsCompat.equals(getCreatedAt(), project.getCreatedAt()) && + ObjectsCompat.equals(getUpdatedAt(), project.getUpdatedAt()) && + ObjectsCompat.equals(getProjectTeamTeamId(), project.getProjectTeamTeamId()) && + ObjectsCompat.equals(getProjectTeamName(), project.getProjectTeamName()); + } + } + + @Override + public int hashCode() { + return new StringBuilder() + .append(getProjectId()) + .append(getName()) + .append(getCreatedAt()) + .append(getUpdatedAt()) + .append(getProjectTeamTeamId()) + .append(getProjectTeamName()) + .toString() + .hashCode(); + } + + @Override + public String toString() { + return new StringBuilder() + .append("Project {") + .append("projectId=" + String.valueOf(getProjectId()) + ", ") + .append("name=" + String.valueOf(getName()) + ", ") + .append("createdAt=" + String.valueOf(getCreatedAt()) + ", ") + .append("updatedAt=" + String.valueOf(getUpdatedAt()) + ", ") + .append("projectTeamTeamId=" + String.valueOf(getProjectTeamTeamId()) + ", ") + .append("projectTeamName=" + String.valueOf(getProjectTeamName())) + .append("}") + .toString(); + } + + public static ProjectIdStep builder() { + return new Builder(); + } + + public CopyOfBuilder copyOfBuilder() { + return new CopyOfBuilder(projectId, + name, + projectTeamTeamId, + projectTeamName); + } + public interface ProjectIdStep { + NameStep projectId(String projectId); + } + + + public interface NameStep { + BuildStep name(String name); + } + + + public interface BuildStep { + Project build(); + BuildStep projectTeamTeamId(String projectTeamTeamId); + BuildStep projectTeamName(String projectTeamName); + } + + + public static class Builder implements ProjectIdStep, NameStep, BuildStep { + private String projectId; + private String name; + private String projectTeamTeamId; + private String projectTeamName; + public Builder() { + + } + + private Builder(String projectId, String name, String projectTeamTeamId, String projectTeamName) { + this.projectId = projectId; + this.name = name; + this.projectTeamTeamId = projectTeamTeamId; + this.projectTeamName = projectTeamName; + } + + @Override + public Project build() { + + return new Project( + projectId, + name, + projectTeamTeamId, + projectTeamName); + } + + @Override + public NameStep projectId(String projectId) { + Objects.requireNonNull(projectId); + this.projectId = projectId; + return this; + } + + @Override + public BuildStep name(String name) { + Objects.requireNonNull(name); + this.name = name; + return this; + } + + @Override + public BuildStep projectTeamTeamId(String projectTeamTeamId) { + this.projectTeamTeamId = projectTeamTeamId; + return this; + } + + @Override + public BuildStep projectTeamName(String projectTeamName) { + this.projectTeamName = projectTeamName; + return this; + } + } + + + public final class CopyOfBuilder extends Builder { + private CopyOfBuilder(String projectId, String name, String projectTeamTeamId, String projectTeamName) { + super(projectId, name, projectTeamTeamId, projectTeamName); + Objects.requireNonNull(projectId); + Objects.requireNonNull(name); + } + + @Override + public CopyOfBuilder projectId(String projectId) { + return (CopyOfBuilder) super.projectId(projectId); + } + + @Override + public CopyOfBuilder name(String name) { + return (CopyOfBuilder) super.name(name); + } + + @Override + public CopyOfBuilder projectTeamTeamId(String projectTeamTeamId) { + return (CopyOfBuilder) super.projectTeamTeamId(projectTeamTeamId); + } + + @Override + public CopyOfBuilder projectTeamName(String projectTeamName) { + return (CopyOfBuilder) super.projectTeamName(projectTeamName); + } + } + + + public static class ProjectIdentifier extends ModelIdentifier { + private static final long serialVersionUID = 1L; + public ProjectIdentifier(String projectId, String name) { + super(projectId, name); + } + } + +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/ProjectPath.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/ProjectPath.java new file mode 100644 index 0000000000..8c59581fdd --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/ProjectPath.java @@ -0,0 +1,22 @@ +package com.amplifyframework.datastore.generated.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.amplifyframework.core.model.ModelPath; +import com.amplifyframework.core.model.PropertyPath; + +/** This is an auto generated class representing the ModelPath for the Project type in your schema. */ +public final class ProjectPath extends ModelPath { + private TeamPath team; + ProjectPath(@NonNull String name, @NonNull Boolean isCollection, @Nullable PropertyPath parent) { + super(name, isCollection, parent, Project.class); + } + + public synchronized TeamPath getTeam() { + if (team == null) { + team = new TeamPath("team", false, this); + } + return team; + } +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Team.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Team.java new file mode 100644 index 0000000000..dc251ba98c --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Team.java @@ -0,0 +1,212 @@ +package com.amplifyframework.datastore.generated.model; + +import static com.amplifyframework.core.model.query.predicate.QueryField.field; + +import androidx.core.util.ObjectsCompat; + +import com.amplifyframework.core.model.LoadedModelReferenceImpl; +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.ModelIdentifier; +import com.amplifyframework.core.model.ModelReference; +import com.amplifyframework.core.model.annotations.BelongsTo; +import com.amplifyframework.core.model.annotations.Index; +import com.amplifyframework.core.model.annotations.ModelConfig; +import com.amplifyframework.core.model.annotations.ModelField; +import com.amplifyframework.core.model.query.predicate.QueryField; +import com.amplifyframework.core.model.temporal.Temporal; + +import java.util.Objects; + +/** This is an auto generated class representing the Team type in your schema. */ +@SuppressWarnings("all") +@ModelConfig(pluralName = "Teams", type = Model.Type.USER, version = 1, hasLazySupport = true) +@Index(name = "undefined", fields = {"teamId","name"}) +public final class Team implements Model { + public static final TeamPath rootPath = new TeamPath("root", false, null); + public static final QueryField TEAM_ID = field("Team", "teamId"); + public static final QueryField NAME = field("Team", "name"); + public static final QueryField PROJECT = field("Team", "teamProjectProjectId"); + private final @ModelField(targetType="ID", isRequired = true) String teamId; + private final @ModelField(targetType="String", isRequired = true) String name; + private final @ModelField(targetType="Project") @BelongsTo(targetName = "teamProjectProjectId", targetNames = {"teamProjectProjectId", "teamProjectName"}, type = Project.class) ModelReference project; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime createdAt; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime updatedAt; + private TeamIdentifier teamIdentifier; + /** @deprecated This API is internal to Amplify and should not be used. */ + @Deprecated + public TeamIdentifier resolveIdentifier() { + if (teamIdentifier == null) { + this.teamIdentifier = new TeamIdentifier(teamId, name); + } + return teamIdentifier; + } + + public String getTeamId() { + return teamId; + } + + public String getName() { + return name; + } + + public ModelReference getProject() { + return project; + } + + public Temporal.DateTime getCreatedAt() { + return createdAt; + } + + public Temporal.DateTime getUpdatedAt() { + return updatedAt; + } + + private Team(String teamId, String name, ModelReference project) { + this.teamId = teamId; + this.name = name; + this.project = project; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if(obj == null || getClass() != obj.getClass()) { + return false; + } else { + Team team = (Team) obj; + return ObjectsCompat.equals(getTeamId(), team.getTeamId()) && + ObjectsCompat.equals(getName(), team.getName()) && + ObjectsCompat.equals(getProject(), team.getProject()) && + ObjectsCompat.equals(getCreatedAt(), team.getCreatedAt()) && + ObjectsCompat.equals(getUpdatedAt(), team.getUpdatedAt()); + } + } + + @Override + public int hashCode() { + return new StringBuilder() + .append(getTeamId()) + .append(getName()) + .append(getProject()) + .append(getCreatedAt()) + .append(getUpdatedAt()) + .toString() + .hashCode(); + } + + @Override + public String toString() { + return new StringBuilder() + .append("Team {") + .append("teamId=" + String.valueOf(getTeamId()) + ", ") + .append("name=" + String.valueOf(getName()) + ", ") + .append("project=" + String.valueOf(getProject()) + ", ") + .append("createdAt=" + String.valueOf(getCreatedAt()) + ", ") + .append("updatedAt=" + String.valueOf(getUpdatedAt())) + .append("}") + .toString(); + } + + public static TeamIdStep builder() { + return new Builder(); + } + + public CopyOfBuilder copyOfBuilder() { + return new CopyOfBuilder(teamId, + name, + project); + } + public interface TeamIdStep { + NameStep teamId(String teamId); + } + + + public interface NameStep { + BuildStep name(String name); + } + + + public interface BuildStep { + Team build(); + BuildStep project(Project project); + } + + + public static class Builder implements TeamIdStep, NameStep, BuildStep { + private String teamId; + private String name; + private ModelReference project; + public Builder() { + + } + + private Builder(String teamId, String name, ModelReference project) { + this.teamId = teamId; + this.name = name; + this.project = project; + } + + @Override + public Team build() { + + return new Team( + teamId, + name, + project); + } + + @Override + public NameStep teamId(String teamId) { + Objects.requireNonNull(teamId); + this.teamId = teamId; + return this; + } + + @Override + public BuildStep name(String name) { + Objects.requireNonNull(name); + this.name = name; + return this; + } + + @Override + public BuildStep project(Project project) { + this.project = new LoadedModelReferenceImpl<>(project); + return this; + } + } + + + public final class CopyOfBuilder extends Builder { + private CopyOfBuilder(String teamId, String name, ModelReference project) { + super(teamId, name, project); + Objects.requireNonNull(teamId); + Objects.requireNonNull(name); + } + + @Override + public CopyOfBuilder teamId(String teamId) { + return (CopyOfBuilder) super.teamId(teamId); + } + + @Override + public CopyOfBuilder name(String name) { + return (CopyOfBuilder) super.name(name); + } + + @Override + public CopyOfBuilder project(Project project) { + return (CopyOfBuilder) super.project(project); + } + } + + + public static class TeamIdentifier extends ModelIdentifier { + private static final long serialVersionUID = 1L; + public TeamIdentifier(String teamId, String name) { + super(teamId, name); + } + } + +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/TeamPath.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/TeamPath.java new file mode 100644 index 0000000000..9a73fb763b --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/TeamPath.java @@ -0,0 +1,22 @@ +package com.amplifyframework.datastore.generated.model; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.amplifyframework.core.model.ModelPath; +import com.amplifyframework.core.model.PropertyPath; + +/** This is an auto generated class representing the ModelPath for the Team type in your schema. */ +public final class TeamPath extends ModelPath { + private ProjectPath project; + TeamPath(@NonNull String name, @NonNull Boolean isCollection, @Nullable PropertyPath parent) { + super(name, isCollection, parent, Team.class); + } + + public synchronized ProjectPath getProject() { + if (project == null) { + project = new ProjectPath("project", false, this); + } + return project; + } +} diff --git a/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/schema.graphql b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/schema.graphql new file mode 100644 index 0000000000..218fdcdcef --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/schema.graphql @@ -0,0 +1,61 @@ +# This "input" configures a global authorization rule to enable public access to +# all models in this schema. Learn more about authorization rules here: https://docs.amplify.aws/cli/graphql/authorization-rules +input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY! + +input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY! + + +type Parent @model { + id: ID! @primaryKey + child: HasOneChild @hasOne + children: [HasManyChild] @hasMany +} + +type HasOneChild @model { + id: ID! @primaryKey + content: String +} + +type HasManyChild @model { + id: ID! @primaryKey + content: String + parent: Parent @belongsTo +} + +# Start Implicit Bi-directional Has One + +type Project @model { + projectId: ID! @primaryKey(sortKeyFields:["name"]) + name: String! + team: Team @hasOne +} +type Team @model { + teamId: ID! @primaryKey(sortKeyFields:["name"]) + name: String! + project: Project @belongsTo +} + +# End Implicit Bi-directional Has One + +# Start CPK Multiple Use Case + +type Blog @model { + blogId: String! @primaryKey + name: String! + posts: [Post!]! @hasMany +} + +type Post @model { + postId: ID! @primaryKey(sortKeyFields:["title"]) + title: String! + blog: Blog! @belongsTo + comments: [Comment]! @hasMany +} + +type Comment @model { + commentId: ID! @primaryKey(sortKeyFields:["content"]) + content: String! + post: Post! @belongsTo +} + +# End CPK Multiple Use Case \ No newline at end of file diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiPlugin.java b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiPlugin.java index 4c8b71b97e..8d1accfe5b 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiPlugin.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiPlugin.java @@ -180,7 +180,7 @@ public void configure( apiWebsocketUpgradeClientConfigurators.get(apiName); final SubscriptionEndpoint subscriptionEndpoint = new SubscriptionEndpoint(apiConfiguration, websocketUpgradeConfigurator, gqlResponseFactory, - subscriptionAuthorizer); + subscriptionAuthorizer, apiName); clientDetails = new ClientDetails(apiConfiguration, okHttpClientBuilder.build(), subscriptionEndpoint, @@ -611,6 +611,7 @@ private GraphQLOperation buildSubscriptionOperation( // If it gets here, we know that the request is an AppSyncGraphQLRequest because // getAuthModeStrategyType checks for that, so we can safely cast the graphQLRequest. return MultiAuthSubscriptionOperation.builder() + .apiName(apiName) .subscriptionEndpoint(clientDetails.getSubscriptionEndpoint()) .graphQlRequest((AppSyncGraphQLRequest) graphQLRequest) .responseFactory(gqlResponseFactory) @@ -634,6 +635,7 @@ private GraphQLOperation buildSubscriptionOperation( // than passing in the requestDecorator and having to handle that in there. We can always refactor this. GraphQLRequest authDecoratedRequest = requestDecorator.decorate(graphQLRequest, authType); return SubscriptionOperation.builder() + .apiName(apiName) .subscriptionEndpoint(clientDetails.getSubscriptionEndpoint()) .graphQlRequest(authDecoratedRequest) .responseFactory(gqlResponseFactory) @@ -665,6 +667,7 @@ private GraphQLOperation buildAppSyncGraphQLOperation( AuthModeStrategyType authModeStrategyType = getAuthModeStrategyType(graphQLRequest); if (AuthModeStrategyType.MULTIAUTH.equals(authModeStrategyType)) { return MultiAuthAppSyncGraphQLOperation.builder() + .apiName(apiName) .endpoint(clientDetails.getApiConfiguration().getEndpoint()) .client(clientDetails.getOkHttpClient()) .request(graphQLRequest) @@ -677,6 +680,7 @@ private GraphQLOperation buildAppSyncGraphQLOperation( } // Not multiauth, so just return the default operation. return AppSyncGraphQLOperation.builder() + .apiName(apiName) .endpoint(clientDetails.getApiConfiguration().getEndpoint()) .client(clientDetails.getOkHttpClient()) .request(graphQLRequest) diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiSchemaRegistry.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiSchemaRegistry.kt new file mode 100644 index 0000000000..6d83ce0e3d --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiSchemaRegistry.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.api.aws + +import com.amplifyframework.api.ApiException +import com.amplifyframework.core.model.Model +import com.amplifyframework.core.model.ModelSchema + +/** + * This registry is only used for API category and is capable of registering models with lazy support. + * The DataStore schema registry restricts to non-lazy types + */ +internal class AWSApiSchemaRegistry { + private val modelSchemaMap: MutableMap by lazy { + val modelProvider = ModelProviderLocator.locate() + modelProvider.modelSchemas() + } + + fun getModelSchemaForModelClass(classSimpleName: String): ModelSchema { + return modelSchemaMap[classSimpleName] ?: throw ApiException( + "Model type of `$classSimpleName` not found.", + "Please regenerate codegen models and verify the class is found in AmplifyModelProvider." + ) + } + + fun getModelSchemaForModelClass(modelClass: Class): ModelSchema { + return getModelSchemaForModelClass(modelClass.simpleName) + } +} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/AWSGraphQLOperation.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSGraphQLOperation.kt new file mode 100644 index 0000000000..cca6fd46ac --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSGraphQLOperation.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2019 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. + */ +package com.amplifyframework.api.aws + +import com.amplifyframework.AmplifyException +import com.amplifyframework.annotations.InternalAmplifyApi +import com.amplifyframework.api.ApiException +import com.amplifyframework.api.graphql.GraphQLOperation +import com.amplifyframework.api.graphql.GraphQLRequest +import com.amplifyframework.api.graphql.GraphQLResponse + +/** + * A Base AWS GraphQLOperation that also takes an apiName to allow LazyModel support. + * @param The type of data contained in the GraphQLResponse. + * @param graphQLRequest A GraphQL request + * @param responseFactory an implementation of ResponseFactory + * @param apiName to use + */ +@InternalAmplifyApi +abstract class AWSGraphQLOperation( + graphQLRequest: GraphQLRequest, + responseFactory: GraphQLResponse.Factory, + private val apiName: String? +) : GraphQLOperation(graphQLRequest, responseFactory) { + + @Throws(ApiException::class) + override fun wrapResponse(jsonResponse: String): GraphQLResponse { + return buildResponse(jsonResponse) + } + + // This method should be used in place of GraphQLOperation.wrapResponse. In order to pass + // apiName, we had to stop using the default GraphQLResponse.Factory buildResponse method + // as there was no place to inject api name for adding to LazyModel + @Throws(ApiException::class) + private fun buildResponse(jsonResponse: String): GraphQLResponse { + return try { + (responseFactory as? GsonGraphQLResponseFactory)?.buildResponse(request, jsonResponse, apiName) + ?: throw ApiException( + "Amplify encountered an error while deserializing an object. " + + "GraphQLResponse.Factory was not of type GsonGraphQLResponseFactory", + AmplifyException.REPORT_BUG_TO_AWS_SUGGESTION + ) + } catch (cce: ClassCastException) { + throw ApiException( + "Amplify encountered an error while deserializing an object", + AmplifyException.TODO_RECOVERY_SUGGESTION + ) + } + } +} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt new file mode 100644 index 0000000000..bfdecfe049 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.api.aws + +import com.amplifyframework.AmplifyException +import com.amplifyframework.api.ApiCategory +import com.amplifyframework.api.ApiException +import com.amplifyframework.api.graphql.GraphQLRequest +import com.amplifyframework.core.Amplify +import com.amplifyframework.core.Consumer +import com.amplifyframework.core.NullableConsumer +import com.amplifyframework.core.model.LazyModelReference +import com.amplifyframework.core.model.Model +import com.amplifyframework.core.model.ModelSchema +import java.util.concurrent.atomic.AtomicReference +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +internal class ApiLazyModelReference internal constructor( + private val clazz: Class, + private val keyMap: Map, + // API name is important to provide to future query calls. If a custom API name was used for the original call, + // the apiName must be provided to the following lazy call to fetch the value. + private val apiName: String? = null, + private val apiCategory: ApiCategory = Amplify.API +) : LazyModelReference { + private val cachedValue = AtomicReference?>(null) + private val mutex = Mutex() // prevents multiple fetches + private val callbackScope = CoroutineScope(Dispatchers.IO) + + init { + // If we have no keys, we have nothing to loads + if (keyMap.isEmpty()) { + cachedValue.set(LoadedValue(null)) + } + } + + override fun getIdentifier(): Map { + return keyMap + } + + override suspend fun fetchModel(): M? { + val cached = cachedValue.get() + if (cached != null) { + // Quick return if value is already present + return cached.value + } + + return fetchInternal() + } + + override fun fetchModel(onSuccess: NullableConsumer, onError: Consumer) { + val cached = cachedValue.get() + if (cached != null) { + // Quick return if value is already present + onSuccess.accept(cached.value) + } + + callbackScope.launch { + try { + val model = fetchInternal() + onSuccess.accept(model) + } catch (e: AmplifyException) { + onError.accept(e) + } + } + } + + private suspend fun fetchInternal(): M? { + // Use mutex to only allow 1 execution at a time + mutex.withLock { + + // Quick return if value is already present + val cached = cachedValue.get() + if (cached != null) { + return cached.value + } + + return try { + val modelSchema = ModelSchema.fromModelClass(clazz) + val primaryIndexFields = modelSchema.primaryIndexFields + val variables = primaryIndexFields.map { key -> + // Find target field to pull type info + val targetField = requireNotNull(modelSchema.fields[key]) + val requiredSuffix = if (targetField.isRequired) "!" else "" + val targetTypeString = "${targetField.targetType}$requiredSuffix" + val value = requireNotNull(keyMap[key]) + GraphQLRequestVariable(key, value, targetTypeString) + } + + val request: GraphQLRequest = AppSyncGraphQLRequestFactory.buildQueryInternal( + clazz, + null, + *variables.toTypedArray() + ) + + val value = query( + apiCategory, + request, + apiName + ).data + cachedValue.set(LoadedValue(value)) + value + } catch (error: ApiException) { + throw AmplifyException("Error lazy loading the model.", error, error.message ?: "") + } + } + } + + private companion object { + // Wraps the value to determine difference between null/unloaded and null/loaded + private class LoadedValue(val value: M?) + } +} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt new file mode 100644 index 0000000000..e5b0484e96 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.api.aws + +import com.amplifyframework.AmplifyException +import com.amplifyframework.api.ApiCategory +import com.amplifyframework.api.graphql.GraphQLRequest +import com.amplifyframework.core.Amplify +import com.amplifyframework.core.Consumer +import com.amplifyframework.core.model.LazyModelList +import com.amplifyframework.core.model.LoadedModelList +import com.amplifyframework.core.model.Model +import com.amplifyframework.core.model.ModelPage +import com.amplifyframework.core.model.PaginationToken +import com.amplifyframework.core.model.query.predicate.QueryField +import com.amplifyframework.core.model.query.predicate.QueryPredicate +import com.amplifyframework.core.model.query.predicate.QueryPredicates +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +internal class ApiLoadedModelList( + override val items: List +) : LoadedModelList + +internal class ApiModelPage( + override val items: List, + override val nextToken: ApiPaginationToken? +) : ModelPage + +internal class ApiPaginationToken(val nextToken: String) : PaginationToken + +internal class ApiLazyModelList constructor( + private val clazz: Class, + keyMap: Map, + // API name is important to provide to future query calls. If a custom API name was used for the original call, + // the apiName must be provided to the following lazy calls to fetch the lazy list + private val apiName: String?, + private val apiCategory: ApiCategory = Amplify.API +) : LazyModelList { + + private val callbackScope = CoroutineScope(Dispatchers.IO) + private val queryPredicate = createPredicate(clazz, keyMap) + + override suspend fun fetchPage(paginationToken: PaginationToken?): ModelPage { + try { + val response = query(apiCategory, createRequest(paginationToken), apiName) + return response.data + } catch (error: AmplifyException) { + throw createLazyException(error) + } + } + + override fun fetchPage(onSuccess: Consumer>, onError: Consumer) { + callbackScope.launch { + try { + val page = fetchPage() + onSuccess.accept(page) + } catch (e: AmplifyException) { + onError.accept(e) + } + } + } + + override fun fetchPage( + paginationToken: PaginationToken?, + onSuccess: Consumer>, + onError: Consumer + ) { + callbackScope.launch { + try { + val page = fetchPage(paginationToken) + onSuccess.accept(page) + } catch (e: AmplifyException) { + onError.accept(e) + } + } + } + + private fun createRequest(paginationToken: PaginationToken? = null): GraphQLRequest> { + return AppSyncGraphQLRequestFactory.buildModelPageQuery( + clazz, + queryPredicate, + (paginationToken as? ApiPaginationToken)?.nextToken + ) + } + + private fun createLazyException(exception: AmplifyException) = + AmplifyException("Error lazy loading the model list.", exception, exception.message ?: "") + + internal companion object { + fun createPredicate(clazz: Class, keyMap: Map): QueryPredicate { + var queryPredicate = QueryPredicates.all() + keyMap.forEach { + queryPredicate = queryPredicate.and(QueryField.field(clazz.simpleName, it.key).eq(it.value)) + } + return queryPredicate + } + } +} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiQuery.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiQuery.kt new file mode 100644 index 0000000000..c27ca3b4ed --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiQuery.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.api.aws + +import com.amplifyframework.api.ApiCategory +import com.amplifyframework.api.ApiException +import com.amplifyframework.api.graphql.GraphQLRequest +import com.amplifyframework.api.graphql.GraphQLResponse +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +/* + Duplicating the query Kotlin Facade method so we aren't pulling in Kotlin Core + */ +@Throws(ApiException::class) +internal suspend fun query(apiCategory: ApiCategory, request: GraphQLRequest, apiName: String?): + GraphQLResponse { + return suspendCoroutine { continuation -> + if (apiName != null) { + apiCategory.query( + apiName, + request, + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) + } else { + apiCategory.query( + request, + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) + } + } +} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLOperation.java b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLOperation.java index e1cdc9e410..1adc3d18f9 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLOperation.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLOperation.java @@ -23,7 +23,6 @@ import com.amplifyframework.api.ApiException; import com.amplifyframework.api.aws.auth.ApiRequestDecoratorFactory; import com.amplifyframework.api.aws.auth.RequestDecorator; -import com.amplifyframework.api.graphql.GraphQLOperation; import com.amplifyframework.api.graphql.GraphQLRequest; import com.amplifyframework.api.graphql.GraphQLResponse; import com.amplifyframework.core.Amplify; @@ -50,7 +49,7 @@ * this is used for a LIST query vs. a GET query or most mutations. * @param Casted type of GraphQL result data */ -public final class AppSyncGraphQLOperation extends GraphQLOperation { +public final class AppSyncGraphQLOperation extends AWSGraphQLOperation { private static final Logger LOG = Amplify.Logging.logger(CategoryType.API, "amplify:aws-api"); private static final String CONTENT_TYPE = "application/json"; private static final int START_OF_CLIENT_ERROR_CODE = 400; @@ -70,7 +69,7 @@ public final class AppSyncGraphQLOperation extends GraphQLOperation { * @param builder operation builder instance */ private AppSyncGraphQLOperation(@NonNull Builder builder) { - super(builder.request, builder.responseFactory); + super(builder.request, builder.responseFactory, builder.apiName); this.endpoint = Objects.requireNonNull(builder.endpoint); this.client = Objects.requireNonNull(builder.client); this.apiRequestDecoratorFactory = Objects.requireNonNull(builder.apiRequestDecoratorFactory); @@ -177,6 +176,7 @@ static final class Builder { private Consumer> onResponse; private Consumer onFailure; private ExecutorService executorService; + private String apiName; Builder endpoint(@NonNull String endpoint) { this.endpoint = Objects.requireNonNull(endpoint); @@ -218,6 +218,11 @@ Builder executorService(@NonNull ExecutorService executorService) { return this; } + Builder apiName(String apiName) { + this.apiName = apiName; + return this; + } + @SuppressLint("SyntheticAccessor") AppSyncGraphQLOperation build() { return new AppSyncGraphQLOperation<>(this); diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.java b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.java deleted file mode 100644 index 66a63b0108..0000000000 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.java +++ /dev/null @@ -1,333 +0,0 @@ -/* - * Copyright 2019 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. - */ - -package com.amplifyframework.api.aws; - -import androidx.annotation.NonNull; - -import com.amplifyframework.AmplifyException; -import com.amplifyframework.api.graphql.GraphQLRequest; -import com.amplifyframework.api.graphql.MutationType; -import com.amplifyframework.api.graphql.PaginatedResult; -import com.amplifyframework.api.graphql.QueryType; -import com.amplifyframework.api.graphql.SubscriptionType; -import com.amplifyframework.core.model.Model; -import com.amplifyframework.core.model.ModelField; -import com.amplifyframework.core.model.ModelIdentifier; -import com.amplifyframework.core.model.ModelSchema; -import com.amplifyframework.core.model.query.predicate.QueryPredicate; -import com.amplifyframework.core.model.query.predicate.QueryPredicates; -import com.amplifyframework.util.Casing; -import com.amplifyframework.util.TypeMaker; - -import java.io.Serializable; -import java.lang.reflect.Type; -import java.util.List; -import java.util.Objects; - -/** - * Converts provided model or class type into a request container with automatically generated GraphQL documents that - * follow AppSync specifications. - */ -public final class AppSyncGraphQLRequestFactory { - private static final int DEFAULT_QUERY_LIMIT = 1000; - - // This class should not be instantiated - private AppSyncGraphQLRequestFactory() { - } - - /** - * Creates a {@link GraphQLRequest} that represents a query that expects a single value as a result. The request - * will be created with the correct document based on the model schema and variables based on given - * {@code objectId}. - * @param modelClass the model class. - * @param objectId the model identifier. - * @param the response type. - * @param the concrete model type. - * @return a valid {@link GraphQLRequest} instance. - * @throws IllegalStateException when the model schema does not contain the expected information. - */ - public static GraphQLRequest buildQuery( - Class modelClass, - String objectId - ) { - GraphQLRequestVariable variable; - try { - ModelSchema modelSchema = ModelSchema.fromModelClass(modelClass); - String primaryKeyName = modelSchema.getPrimaryKeyName(); - // Find target field to pull type info - ModelField targetField = - Objects.requireNonNull(modelSchema.getFields().get(primaryKeyName)); - String targetTypeString = targetField.getTargetType() + - (targetField.isRequired() ? "!" : ""); - variable = new GraphQLRequestVariable(primaryKeyName, objectId, targetTypeString); - } catch (Exception exception) { - // If we fail to pull primary key name and type, fallback to default id/ID! - variable = new GraphQLRequestVariable("id", objectId, "ID!"); - } - - return buildQuery(modelClass, variable); - } - - /** - * Creates a {@link GraphQLRequest} that represents a query that expects a single value as a result. The request - * will be created with the correct document based on the model schema and variables based on given - * {@code modelIdentifier}. - * @param modelClass the model class. - * @param modelIdentifier the model identifier. - * @param the response type. - * @param the concrete model type. - * @return a valid {@link GraphQLRequest} instance. - * @throws IllegalStateException when the model schema does not contain the expected information. - */ - public static GraphQLRequest buildQuery( - @NonNull Class modelClass, - @NonNull ModelIdentifier modelIdentifier - ) { - GraphQLRequestVariable[] variables; - try { - ModelSchema modelSchema = ModelSchema.fromModelClass(modelClass); - List primaryIndexFields = modelSchema.getPrimaryIndexFields(); - List sortedKeys = modelIdentifier.sortedKeys(); - - variables = new GraphQLRequestVariable[primaryIndexFields.size()]; - - for (int i = 0; i < primaryIndexFields.size(); i++) { - - // Index 0 is primary key, next values are ordered sort keys - String key = primaryIndexFields.get(i); - - // Find target field to pull type info - ModelField targetField = - Objects.requireNonNull(modelSchema.getFields().get(key)); - - // Should create "ID!", "String!", "Float!", etc. - // Appends "!" if required (should always be the case with CPK requirements). - String targetTypeString = targetField.getTargetType() + - (targetField.isRequired() ? "!" : ""); - - // If index 0, value is primary key, else get next unused sort key - Object value = i == 0 ? - modelIdentifier.key().toString() : sortedKeys.get(i - 1); - variables[i] = new GraphQLRequestVariable(key, value, targetTypeString); - } - } catch (AmplifyException exception) { - throw new IllegalStateException( - "Could not generate a schema for the specified class", - exception - ); - } - - return buildQuery(modelClass, variables); - } - - /** - * Creates a {@link GraphQLRequest} that represents a query that expects a single value as a result. The request - * will be created with the correct document based on the model schema and variables. - * @param modelClass the model class. - * @param variables the variables. - * @param the response type. - * @param the concrete model type. - * @return a valid {@link GraphQLRequest} instance. - * @throws IllegalStateException when the model schema does not contain the expected information. - */ - private static GraphQLRequest buildQuery( - Class modelClass, - GraphQLRequestVariable... variables - ) { - try { - AppSyncGraphQLRequest.Builder builder = AppSyncGraphQLRequest.builder() - .modelClass(modelClass) - .operation(QueryType.GET) - .requestOptions(new ApiGraphQLRequestOptions()) - .responseType(modelClass); - - for (GraphQLRequestVariable v : variables) { - builder.variable(v.getKey(), v.getType(), v.getValue()); - } - return builder.build(); - } catch (AmplifyException exception) { - throw new IllegalStateException( - "Could not generate a schema for the specified class", - exception - ); - } - } - - /** - * Creates a {@link GraphQLRequest} that represents a query that expects multiple values as a result. The request - * will be created with the correct document based on the model schema and variables for filtering based on the - * given predicate. - * @param modelClass the model class. - * @param predicate the model predicate. - * @param the response type. - * @param the concrete model type. - * @return a valid {@link GraphQLRequest} instance. - * @throws IllegalStateException when the model schema does not contain the expected information. - */ - public static GraphQLRequest buildQuery( - Class modelClass, - QueryPredicate predicate - ) { - Type dataType = TypeMaker.getParameterizedType(PaginatedResult.class, modelClass); - return buildQuery(modelClass, predicate, DEFAULT_QUERY_LIMIT, dataType); - } - - /** - * Creates a {@link GraphQLRequest} that represents a query that expects multiple values as a result within a - * certain range (i.e. paginated). - *

- * The request will be created with the correct document based on the model schema and variables for filtering based - * on the given predicate and pagination. - * @param modelClass the model class. - * @param predicate the predicate for filtering. - * @param limit the page size/limit. - * @param the response type. - * @param the concrete model type. - * @return a valid {@link GraphQLRequest} instance. - */ - public static GraphQLRequest buildPaginatedResultQuery( - Class modelClass, - QueryPredicate predicate, - int limit - ) { - Type responseType = TypeMaker.getParameterizedType(PaginatedResult.class, modelClass); - return buildQuery(modelClass, predicate, limit, responseType); - } - - static GraphQLRequest buildQuery( - Class modelClass, - QueryPredicate predicate, - int limit, - Type responseType - ) { - try { - String modelName = ModelSchema.fromModelClass(modelClass).getName(); - AppSyncGraphQLRequest.Builder builder = AppSyncGraphQLRequest.builder() - .modelClass(modelClass) - .operation(QueryType.LIST) - .requestOptions(new ApiGraphQLRequestOptions()) - .responseType(responseType); - - if (!QueryPredicates.all().equals(predicate)) { - String filterType = "Model" + Casing.capitalizeFirst(modelName) + "FilterInput"; - builder.variable( - "filter", - filterType, - GraphQLRequestHelper.parsePredicate(predicate) - ); - } - - builder.variable("limit", "Int", limit); - return builder.build(); - } catch (AmplifyException exception) { - throw new IllegalStateException( - "Could not generate a schema for the specified class", - exception - ); - } - } - - /** - * Creates a {@link GraphQLRequest} that represents a mutation of a given type. - * @param model the model instance. - * @param predicate the model predicate. - * @param type the mutation type. - * @param the response type. - * @param the concrete model type. - * @return a valid {@link GraphQLRequest} instance. - * @throws IllegalStateException when the model schema does not contain the expected information. - */ - public static GraphQLRequest buildMutation( - T model, - QueryPredicate predicate, - MutationType type - ) { - try { - Class modelClass = model.getClass(); - ModelSchema schema = ModelSchema.fromModelClass(modelClass); - String graphQlTypeName = schema.getName(); - - AppSyncGraphQLRequest.Builder builder = AppSyncGraphQLRequest.builder() - .operation(type) - .modelClass(modelClass) - .requestOptions(new ApiGraphQLRequestOptions()) - .responseType(modelClass); - - String inputType = - Casing.capitalize(type.toString()) + - Casing.capitalizeFirst(graphQlTypeName) + - "Input!"; // CreateTodoInput - - if (MutationType.DELETE.equals(type)) { - builder.variable( - "input", - inputType, - GraphQLRequestHelper.getDeleteMutationInputMap(schema, model) - ); - } else { - builder.variable( - "input", - inputType, - GraphQLRequestHelper.getMapOfFieldNameAndValues(schema, model, type) - ); - } - - if (!QueryPredicates.all().equals(predicate)) { - String conditionType = - "Model" + - Casing.capitalizeFirst(graphQlTypeName) + - "ConditionInput"; - builder.variable( - "condition", conditionType, GraphQLRequestHelper.parsePredicate(predicate)); - } - - return builder.build(); - } catch (AmplifyException exception) { - throw new IllegalStateException( - "Could not generate a schema for the specified class", - exception - ); - } - } - - /** - * Creates a {@link GraphQLRequest} that represents a subscription of a given type. - * @param modelClass the model type. - * @param subscriptionType the subscription type. - * @param the response type. - * @param the concrete model type. - * @return a valid {@link GraphQLRequest} instance. - * @throws IllegalStateException when the model schema does not contain the expected information. - */ - public static GraphQLRequest buildSubscription( - Class modelClass, - SubscriptionType subscriptionType - ) { - try { - return AppSyncGraphQLRequest.builder() - .modelClass(modelClass) - .operation(subscriptionType) - .requestOptions(new ApiGraphQLRequestOptions()) - .responseType(modelClass) - .build(); - } catch (AmplifyException exception) { - throw new IllegalStateException( - "Failed to build GraphQLRequest", - exception - ); - } - } -} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.kt new file mode 100644 index 0000000000..b5faa3b7ed --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.kt @@ -0,0 +1,556 @@ +/* + * Copyright 2019 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. + */ +package com.amplifyframework.api.aws + +import com.amplifyframework.AmplifyException +import com.amplifyframework.api.graphql.GraphQLRequest +import com.amplifyframework.api.graphql.MutationType +import com.amplifyframework.api.graphql.Operation +import com.amplifyframework.api.graphql.PaginatedResult +import com.amplifyframework.api.graphql.QueryType +import com.amplifyframework.api.graphql.SubscriptionType +import com.amplifyframework.core.model.Model +import com.amplifyframework.core.model.ModelIdentifier +import com.amplifyframework.core.model.ModelPath +import com.amplifyframework.core.model.ModelSchema +import com.amplifyframework.core.model.PropertyContainerPath +import com.amplifyframework.core.model.query.predicate.QueryPredicate +import com.amplifyframework.core.model.query.predicate.QueryPredicates +import com.amplifyframework.util.Casing +import com.amplifyframework.util.TypeMaker +import java.lang.reflect.Type + +/** + * Converts provided model or class type into a request container with automatically generated GraphQL documents that + * follow AppSync specifications. + */ +object AppSyncGraphQLRequestFactory { + private const val DEFAULT_QUERY_LIMIT = 1000 + + /** + * Creates a [GraphQLRequest] that represents a query that expects a single value as a result. The request + * will be created with the correct document based on the model schema and variables based on given + * `objectId`. + * @param modelClass the model class. + * @param objectId the model identifier. + * @param the response type. + * @param the concrete model type. + * @return a valid [GraphQLRequest] instance. + * @throws IllegalStateException when the model schema does not contain the expected information. + */ + @JvmStatic + fun buildQuery( + modelClass: Class, + objectId: String, + ): GraphQLRequest { + val variable: GraphQLRequestVariable = try { + val modelSchema = ModelSchema.fromModelClass(modelClass) + val primaryKeyName = modelSchema.primaryKeyName + // Find target field to pull type info + val targetField = requireNotNull(modelSchema.fields[primaryKeyName]) + val requiredSuffix = if (targetField.isRequired) "!" else "" + val targetTypeString = "${targetField.targetType}$requiredSuffix" + GraphQLRequestVariable(primaryKeyName, objectId, targetTypeString) + } catch (exception: Exception) { + // If we fail to pull primary key name and type, fallback to default id/ID! + GraphQLRequestVariable("id", objectId, "ID!") + } + return buildQueryInternal(modelClass, null, variable) + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects a single value as a result. The request + * will be created with the correct document based on the model schema and variables based on given + * `objectId`. + * @param modelClass the model class. + * @param objectId the model identifier. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the response type. + * @param the concrete model type. + * @param

the concrete model path for the M model type + * @return a valid [GraphQLRequest] instance. + * @throws IllegalStateException when the model schema does not contain the expected information. + */ + @JvmStatic + fun > buildQuery( + modelClass: Class, + objectId: String, + includes: ((P) -> List) + ): GraphQLRequest { + val variable: GraphQLRequestVariable = try { + val modelSchema = ModelSchema.fromModelClass(modelClass) + val primaryKeyName = modelSchema.primaryKeyName + // Find target field to pull type info + val targetField = requireNotNull(modelSchema.fields[primaryKeyName]) + val requiredSuffix = if (targetField.isRequired) "!" else "" + val targetTypeString = "${targetField.targetType}$requiredSuffix" + GraphQLRequestVariable(primaryKeyName, objectId, targetTypeString) + } catch (exception: Exception) { + // If we fail to pull primary key name and type, fallback to default id/ID! + GraphQLRequestVariable("id", objectId, "ID!") + } + return buildQueryInternal(modelClass, includes, variable) + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects a single value as a result. The request + * will be created with the correct document based on the model schema and variables based on given + * `modelIdentifier`. + * @param modelClass the model class. + * @param modelIdentifier the model identifier. + * @param the response type. + * @param the concrete model type. + * @return a valid [GraphQLRequest] instance. + * @throws IllegalStateException when the model schema does not contain the expected information. + */ + @JvmStatic + fun buildQuery( + modelClass: Class, + modelIdentifier: ModelIdentifier, + ): GraphQLRequest { + try { + val modelSchema = ModelSchema.fromModelClass(modelClass) + val primaryIndexFields = modelSchema.primaryIndexFields + val sortedKeys = modelIdentifier.sortedKeys() + val variables = primaryIndexFields.mapIndexed { i, key -> + // Find target field to pull type info + val targetField = requireNotNull(modelSchema.fields[key]) + val requiredSuffix = if (targetField.isRequired) "!" else "" + val targetTypeString = "${targetField.targetType}$requiredSuffix" + + // If index 0, value is primary key, else get next unused sort key + val value = if (i == 0) { + modelIdentifier.key().toString() + } else { + sortedKeys[i - 1] + } + + GraphQLRequestVariable(key, value, targetTypeString) + } + return buildQueryInternal(modelClass, null, *variables.toTypedArray()) + } catch (exception: AmplifyException) { + throw IllegalStateException( + "Could not generate a schema for the specified class", + exception + ) + } + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects a single value as a result. The request + * will be created with the correct document based on the model schema and variables based on given + * `modelIdentifier`. + * @param modelClass the model class. + * @param modelIdentifier the model identifier. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the response type. + * @param the concrete model type. + * @param

the concrete model path for the M model type + * @return a valid [GraphQLRequest] instance. + * @throws IllegalStateException when the model schema does not contain the expected information. + */ + @JvmStatic + fun > buildQuery( + modelClass: Class, + modelIdentifier: ModelIdentifier, + includes: ((P) -> List) + ): GraphQLRequest { + try { + val modelSchema = ModelSchema.fromModelClass(modelClass) + val primaryIndexFields = modelSchema.primaryIndexFields + val sortedKeys = modelIdentifier.sortedKeys() + val variables = primaryIndexFields.mapIndexed { i, key -> + // Find target field to pull type info + val targetField = requireNotNull(modelSchema.fields[key]) + val requiredSuffix = if (targetField.isRequired) "!" else "" + val targetTypeString = "${targetField.targetType}$requiredSuffix" + + // If index 0, value is primary key, else get next unused sort key + val value = if (i == 0) { + modelIdentifier.key().toString() + } else { + sortedKeys[i - 1] + } + + GraphQLRequestVariable(key, value, targetTypeString) + } + return buildQueryInternal(modelClass, includes, *variables.toTypedArray()) + } catch (exception: AmplifyException) { + throw IllegalStateException( + "Could not generate a schema for the specified class", + exception + ) + } + } + + internal fun > buildQueryInternal( + modelClass: Class, + includes: ((P) -> List)?, + vararg variables: GraphQLRequestVariable + ): GraphQLRequest { + return try { + val builder = AppSyncGraphQLRequest.builder() + .modelClass(modelClass) + .operation(QueryType.GET) + .requestOptions(ApiGraphQLRequestOptions()) + .responseType(modelClass) + for ((key, value, type) in variables) { + builder.variable(key, type, value) + } + + val customSelectionSet = includes?.let { createApiSelectionSet(modelClass, QueryType.GET, it) } + customSelectionSet?.let { builder.selectionSet(it) } + + builder.build() + } catch (exception: AmplifyException) { + throw IllegalStateException( + "Could not generate a schema for the specified class", + exception + ) + } + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects multiple values as a result. The request + * will be created with the correct document based on the model schema and variables for filtering based on the + * given predicate. + * @param modelClass the model class. + * @param predicate the model predicate. + * @param the response type. + * @param the concrete model type. + * @return a valid [GraphQLRequest] instance. + * @throws IllegalStateException when the model schema does not contain the expected information. + */ + @JvmStatic + fun buildQuery( + modelClass: Class, + predicate: QueryPredicate + ): GraphQLRequest { + val dataType = TypeMaker.getParameterizedType(PaginatedResult::class.java, modelClass) + return buildListQueryInternal(modelClass, predicate, DEFAULT_QUERY_LIMIT, dataType, null) + } + + internal fun buildModelPageQuery( + modelClass: Class, + predicate: QueryPredicate, + pageToken: String? + ): GraphQLRequest { + val dataType = TypeMaker.getParameterizedType(ApiModelPage::class.java, modelClass) + return buildListQueryInternal(modelClass, predicate, DEFAULT_QUERY_LIMIT, dataType, null, pageToken) + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects multiple values as a result. The request + * will be created with the correct document based on the model schema and variables for filtering based on the + * given predicate. + * @param modelClass the model class. + * @param predicate the model predicate. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the response type. + * @param the concrete model type. + * @param

the concrete model path for the M model type + * @return a valid [GraphQLRequest] instance. + * @throws IllegalStateException when the model schema does not contain the expected information. + */ + @JvmStatic + fun > buildQuery( + modelClass: Class, + predicate: QueryPredicate, + includes: ((P) -> List), + ): GraphQLRequest { + val dataType = TypeMaker.getParameterizedType(PaginatedResult::class.java, modelClass) + return buildListQueryInternal(modelClass, predicate, DEFAULT_QUERY_LIMIT, dataType, includes) + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects multiple values as a result within a + * certain range (i.e. paginated). + * + * + * The request will be created with the correct document based on the model schema and variables for filtering based + * on the given predicate and pagination. + * @param modelClass the model class. + * @param predicate the predicate for filtering. + * @param limit the page size/limit. + * @param the response type. + * @param the concrete model type. + * @return a valid [GraphQLRequest] instance. + */ + @JvmStatic + fun buildPaginatedResultQuery( + modelClass: Class, + predicate: QueryPredicate, + limit: Int + ): GraphQLRequest { + val responseType = TypeMaker.getParameterizedType(PaginatedResult::class.java, modelClass) + return buildListQueryInternal(modelClass, predicate, limit, responseType, null) + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects multiple values as a result within a + * certain range (i.e. paginated). + * + * + * The request will be created with the correct document based on the model schema and variables for filtering based + * on the given predicate and pagination. + * @param modelClass the model class. + * @param predicate the predicate for filtering. + * @param limit the page size/limit. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the response type. + * @param the concrete model type. + * @param

the concrete model path for the M model type + * @return a valid [GraphQLRequest] instance. + */ + @JvmStatic + fun > buildPaginatedResultQuery( + modelClass: Class, + predicate: QueryPredicate, + limit: Int, + includes: ((P) -> List), + ): GraphQLRequest { + val responseType = TypeMaker.getParameterizedType(PaginatedResult::class.java, modelClass) + return buildListQueryInternal(modelClass, predicate, limit, responseType, includes) + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects multiple values as a result within a + * certain range (i.e. paginated). + * + * + * The request will be created with the correct document based on the model schema and variables for filtering based + * on the given predicate and pagination. + * @param modelClass the model class. + * @param predicate the predicate for filtering. + * @param limit the page size/limit. + * @param responseType the response type + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the response type. + * @param the concrete model type. + * @param

the concrete model path for the M model type + * @return a valid [GraphQLRequest] instance. + */ + private fun > buildListQueryInternal( + modelClass: Class, + predicate: QueryPredicate, + limit: Int, + responseType: Type, + includes: ((P) -> List)?, + pageToken: String? = null + ): GraphQLRequest { + return try { + val modelName = ModelSchema.fromModelClass( + modelClass + ).name + val builder = AppSyncGraphQLRequest.builder() + .modelClass(modelClass) + .operation(QueryType.LIST) + .requestOptions(ApiGraphQLRequestOptions()) + .responseType(responseType) + if (QueryPredicates.all() != predicate) { + val filterType = "Model" + Casing.capitalizeFirst(modelName) + "FilterInput" + builder.variable( + "filter", + filterType, + GraphQLRequestHelper.parsePredicate(predicate) + ) + } + builder.variable("limit", "Int", limit) + + if (pageToken != null) { + builder.variable("nextToken", "String", pageToken) + } + + val customSelectionSet = includes?.let { createApiSelectionSet(modelClass, QueryType.LIST, it) } + customSelectionSet?.let { builder.selectionSet(it) } + + builder.build() + } catch (exception: AmplifyException) { + throw IllegalStateException( + "Could not generate a schema for the specified class", + exception + ) + } + } + + /** + * Creates a [GraphQLRequest] that represents a mutation of a given type. + * @param model the model instance. + * @param predicate the model predicate. + * @param type the mutation type. + * @param the response type. + * @param the concrete model type. + * @return a valid [GraphQLRequest] instance. + * @throws IllegalStateException when the model schema does not contain the expected information. + */ + @JvmStatic + fun buildMutation( + model: T, + predicate: QueryPredicate, + type: MutationType + ): GraphQLRequest { + return buildMutationInternal(model, predicate, type, null) + } + + /** + * Creates a [GraphQLRequest] that represents a mutation of a given type. + * @param model the model instance. + * @param predicate the model predicate. + * @param type the mutation type. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the response type. + * @param the concrete model type. + * @param

the concrete model path for the M model type + * @return a valid [GraphQLRequest] instance. + * @throws IllegalStateException when the model schema does not contain the expected information. + */ + @JvmStatic + fun > buildMutation( + model: T, + predicate: QueryPredicate, + type: MutationType, + includes: ((P) -> List) + ): GraphQLRequest { + return buildMutationInternal(model, predicate, type, includes) + } + + private fun > buildMutationInternal( + model: T, + predicate: QueryPredicate, + type: MutationType, + includes: ((P) -> List)? + ): GraphQLRequest { + return try { + val modelClass: Class = model.javaClass + val schema = ModelSchema.fromModelClass(modelClass) + val graphQlTypeName = schema.name + val builder = AppSyncGraphQLRequest.builder() + .operation(type) + .modelClass(modelClass) + .requestOptions(ApiGraphQLRequestOptions()) + .responseType(modelClass) + val inputType = Casing.capitalize(type.toString()) + + Casing.capitalizeFirst(graphQlTypeName) + + "Input!" // CreateTodoInput + if (MutationType.DELETE == type) { + builder.variable( + "input", + inputType, + GraphQLRequestHelper.getDeleteMutationInputMap(schema, model) + ) + } else { + builder.variable( + "input", + inputType, + GraphQLRequestHelper.getMapOfFieldNameAndValues(schema, model, type) + ) + } + if (QueryPredicates.all() != predicate) { + val conditionType = "Model" + + Casing.capitalizeFirst(graphQlTypeName) + + "ConditionInput" + builder.variable( + "condition", conditionType, GraphQLRequestHelper.parsePredicate(predicate) + ) + } + + val customSelectionSet = includes?.let { createApiSelectionSet(modelClass, type, it) } + customSelectionSet?.let { builder.selectionSet(it) } + + builder.build() + } catch (exception: AmplifyException) { + throw IllegalStateException( + "Could not generate a schema for the specified class", + exception + ) + } + } + + /** + * Creates a [GraphQLRequest] that represents a subscription of a given type. + * @param modelClass the model type. + * @param subscriptionType the subscription type. + * @param the response type. + * @param the concrete model type. + * @return a valid [GraphQLRequest] instance. + * @throws IllegalStateException when the model schema does not contain the expected information. + */ + @JvmStatic + fun buildSubscription( + modelClass: Class, + subscriptionType: SubscriptionType + ): GraphQLRequest { + return buildSubscriptionInternal(modelClass, subscriptionType, null) + } + + /** + * Creates a [GraphQLRequest] that represents a subscription of a given type. + * @param modelClass the model type. + * @param subscriptionType the subscription type. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the response type. + * @param the concrete model type. + * @param

the concrete model path for the M model type + * @return a valid [GraphQLRequest] instance. + * @throws IllegalStateException when the model schema does not contain the expected information. + */ + @JvmStatic + fun > buildSubscription( + modelClass: Class, + subscriptionType: SubscriptionType, + includes: ((P) -> List) + ): GraphQLRequest { + return buildSubscriptionInternal(modelClass, subscriptionType, includes) + } + + private fun > buildSubscriptionInternal( + modelClass: Class, + subscriptionType: SubscriptionType, + includes: ((P) -> List)? + ): GraphQLRequest { + return try { + val builder = AppSyncGraphQLRequest.builder() + .modelClass(modelClass) + .operation(subscriptionType) + .requestOptions(ApiGraphQLRequestOptions()) + .responseType(modelClass) + + val customSelectionSet = includes?.let { createApiSelectionSet(modelClass, subscriptionType, it) } + customSelectionSet?.let { builder.selectionSet(it) } + + builder.build() + } catch (exception: AmplifyException) { + throw IllegalStateException( + "Failed to build GraphQLRequest", + exception + ) + } + } + + private fun > createApiSelectionSet( + modelClass: Class, + operationType: Operation, + includes: ((P) -> List) + ): SelectionSet { + includes(ModelPath.getRootPath(modelClass)).let { relationships -> + return SelectionSet.builder() + .modelClass(modelClass) + .operation(operationType) + .requestOptions(ApiGraphQLRequestOptions()) + .includeRelationships(relationships) + .build() + } + } +} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/GsonFactory.java b/aws-api/src/main/java/com/amplifyframework/api/aws/GsonFactory.java new file mode 100644 index 0000000000..e286d27046 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/GsonFactory.java @@ -0,0 +1,63 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.api.aws; + +import com.amplifyframework.api.graphql.GsonResponseAdapters; +import com.amplifyframework.core.model.query.predicate.GsonPredicateAdapters; +import com.amplifyframework.core.model.temporal.GsonTemporalAdapters; +import com.amplifyframework.core.model.types.GsonJavaTypeAdapters; +import com.amplifyframework.datastore.appsync.ModelWithMetadataAdapter; +import com.amplifyframework.datastore.appsync.SerializedCustomTypeAdapter; +import com.amplifyframework.datastore.appsync.SerializedModelAdapter; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * Creates a {@link Gson} instance which may be used around the API plugin. + */ +final class GsonFactory { + private static Gson gson = null; + + private GsonFactory() {} + + /** + * Obtains a singleton instance of {@link Gson}, configured with adapters sufficient + * to serialize and deserialize all types the API plugin will encounter. + * @return A configured Gson instance. + */ + public static synchronized Gson instance() { + if (gson == null) { + gson = create(); + } + return gson; + } + + private static Gson create() { + GsonBuilder builder = new GsonBuilder(); + GsonTemporalAdapters.register(builder); + GsonJavaTypeAdapters.register(builder); + GsonPredicateAdapters.register(builder); + GsonResponseAdapters.register(builder); + ModelWithMetadataAdapter.register(builder); + SerializedModelAdapter.register(builder); + SerializedCustomTypeAdapter.register(builder); + ModelListDeserializer.register(builder); + ModelPageDeserializer.register(builder); + builder.serializeNulls(); + return builder.create(); + } +} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactory.java b/aws-api/src/main/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactory.java index 1857d346f4..103938549f 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactory.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactory.java @@ -15,6 +15,8 @@ package com.amplifyframework.api.aws; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.amplifyframework.AmplifyException; @@ -22,8 +24,9 @@ import com.amplifyframework.api.graphql.GraphQLRequest; import com.amplifyframework.api.graphql.GraphQLResponse; import com.amplifyframework.api.graphql.PaginatedResult; +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.ModelReference; import com.amplifyframework.util.Empty; -import com.amplifyframework.util.GsonFactory; import com.amplifyframework.util.TypeMaker; import com.google.gson.Gson; @@ -45,6 +48,8 @@ final class GsonGraphQLResponseFactory implements GraphQLResponse.Factory { private final Gson gson; + private final AWSApiSchemaRegistry schemaRegistry = new AWSApiSchemaRegistry(); + GsonGraphQLResponseFactory() { this(GsonFactory.instance()); } @@ -54,26 +59,42 @@ final class GsonGraphQLResponseFactory implements GraphQLResponse.Factory { this.gson = gson; } - @Override - public GraphQLResponse buildResponse(GraphQLRequest request, String responseJson) - throws ApiException { - + public GraphQLResponse buildResponse( + @NonNull GraphQLRequest request, + @Nullable String responseJson, + @Nullable String apiName + ) throws ApiException { // On empty strings, Gson returns null instead of throwing JsonSyntaxException. See: // https://github.com/google/gson/issues/457 // https://github.com/google/gson/issues/1697 if (Empty.check(responseJson)) { throw new ApiException( - "Amplify encountered an error while deserializing an object.", - new JsonParseException("Empty response."), - AmplifyException.TODO_RECOVERY_SUGGESTION + "Amplify encountered an error while deserializing an object.", + new JsonParseException("Empty response."), + AmplifyException.TODO_RECOVERY_SUGGESTION ); } - Type responseType = TypeMaker.getParameterizedType(GraphQLResponse.class, request.getResponseType()); + Type responseType = TypeMaker.getParameterizedType( + GraphQLResponse.class, + request.getResponseType() + ); try { Gson responseGson = gson.newBuilder() - .registerTypeHierarchyAdapter(Iterable.class, new IterableDeserializer<>(request)) - .create(); + .registerTypeHierarchyAdapter( + Iterable.class, + new IterableDeserializer<>(request) + ) + .registerTypeAdapter( + ModelReference.class, + new ModelReferenceDeserializer(apiName, schemaRegistry) + ) + .registerTypeAdapterFactory( + // register Model post processing to inject lazy types for fields that + // were missing from json response + new ModelPostProcessingTypeAdapter(apiName, schemaRegistry) + ) + .create(); return responseGson.fromJson(responseJson, responseType); } catch (JsonParseException jsonParseException) { throw new ApiException( @@ -84,6 +105,17 @@ public GraphQLResponse buildResponse(GraphQLRequest request, String re } } + + // Do not use this method. Instead opt for overload with apiName API name is important for + // lazy model types because the apiName must be passed into the response builder in order + // to construct the lazy model reference types. + @Deprecated + @Override + public GraphQLResponse buildResponse(GraphQLRequest request, String responseJson) + throws ApiException { + return buildResponse(request, responseJson, null); + } + static final class IterableDeserializer implements JsonDeserializer> { private static final String ITEMS_KEY = "items"; private static final String NEXT_TOKEN_KEY = "nextToken"; @@ -168,7 +200,7 @@ private Iterable toList(JsonArray jsonArray, Type type, JsonDeserializat private PaginatedResult buildPaginatedResult(Iterable items, JsonElement nextTokenElement) { GraphQLRequest> requestForNextPage = null; - if (nextTokenElement.isJsonPrimitive()) { + if (nextTokenElement != null && nextTokenElement.isJsonPrimitive()) { String nextToken = nextTokenElement.getAsJsonPrimitive().getAsString(); try { if (request instanceof AppSyncGraphQLRequest) { diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/LazyTypeDeserializers.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyTypeDeserializers.kt new file mode 100644 index 0000000000..2736ad254f --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyTypeDeserializers.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2023 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. + */ +package com.amplifyframework.api.aws + +import com.amplifyframework.core.model.LoadedModelReferenceImpl +import com.amplifyframework.core.model.Model +import com.amplifyframework.core.model.ModelList +import com.amplifyframework.core.model.ModelPage +import com.amplifyframework.core.model.ModelReference +import com.google.gson.GsonBuilder +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +const val ITEMS_KEY = "items" +const val NEXT_TOKEN_KEY = "nextToken" + +internal class ModelReferenceDeserializer( + val apiName: String?, + private val schemaRegistry: AWSApiSchemaRegistry +) : + JsonDeserializer> { + @Throws(JsonParseException::class) + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext + ): ModelReference { + val pType = typeOfT as? ParameterizedType + ?: throw JsonParseException("Expected a parameterized type during list deserialization.") + val type = pType.actualTypeArguments[0] as Class + + val jsonObject = getJsonObject(json) + + val predicateKeyMap = schemaRegistry + .getModelSchemaForModelClass(type) + .primaryIndexFields + .associateWith { jsonObject[it] } + + if (jsonObject.size() > predicateKeyMap.size) { + try { + val preloadedValue = context.deserialize(json, type) + return LoadedModelReferenceImpl(preloadedValue) + } catch (e: Exception) { + // fallback to create lazy + } + } + return ApiLazyModelReference(type, predicateKeyMap, apiName) + } +} + +internal class ModelListDeserializer : JsonDeserializer> { + @Throws(JsonParseException::class) + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext + ): ModelList { + val items = deserializeItems(json, typeOfT, context) + return ApiLoadedModelList(items) + } + + companion object { + @JvmStatic + fun register(builder: GsonBuilder) { + builder.registerTypeAdapter(ModelList::class.java, ModelListDeserializer()) + } + } +} + +internal class ModelPageDeserializer : JsonDeserializer> { + @Throws(JsonParseException::class) + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext + ): ModelPage { + val items = deserializeItems(json, typeOfT, context) + val nextToken = deserializeNextToken(json) + return ApiModelPage(items, nextToken) + } + + companion object { + @JvmStatic + fun register(builder: GsonBuilder) { + builder.registerTypeHierarchyAdapter(ModelPage::class.java, ModelPageDeserializer()) + } + } +} + +@Throws(JsonParseException::class) +private fun getJsonObject(json: JsonElement): JsonObject { + return json as? JsonObject ?: throw JsonParseException( + "Got a JSON value that was not an object " + + "Unable to deserialize the response" + ) +} + +@Throws(JsonParseException::class) +private fun deserializeItems( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext +): List { + val pType = typeOfT as? ParameterizedType + ?: throw JsonParseException("Expected a parameterized type during list deserialization.") + val type = pType.actualTypeArguments[0] + + val jsonObject = getJsonObject(json) + + val itemsJsonArray = if (jsonObject.has(ITEMS_KEY) && jsonObject.get(ITEMS_KEY).isJsonArray) { + jsonObject.getAsJsonArray(ITEMS_KEY) + } else { + throw JsonParseException( + "Got JSON from an API call which was supposed to go with a List " + + "but is in the form of an object rather than an array. " + + "It also is not in the standard format of having an items " + + "property with the actual array of data so we do not know how " + + "to deserialize it." + ) + } + + return itemsJsonArray.map { + context.deserialize(it.asJsonObject, type) + } +} +@Throws(JsonParseException::class) +private fun deserializeNextToken(json: JsonElement): ApiPaginationToken? { + return getJsonObject(json).get(NEXT_TOKEN_KEY) + ?.let { if (it.isJsonPrimitive) it.asString else null } + ?.let { ApiPaginationToken(it) } +} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelPostProcessingTypeAdapter.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelPostProcessingTypeAdapter.kt new file mode 100644 index 0000000000..56cf7303b3 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelPostProcessingTypeAdapter.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.api.aws + +import com.amplifyframework.core.model.LoadedModelReferenceImpl +import com.amplifyframework.core.model.Model +import com.amplifyframework.core.model.ModelIdentifier +import com.amplifyframework.core.model.ModelSchema +import com.google.gson.Gson +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import java.io.IOException +import java.io.Serializable + +/** + * This class is used to inject values into lazy model/list reference types when the fields were not included + * in the json response. + * + * If a ModelReference is not included in the response json, it means the reference value is null. + * If a ModelList is not included in the response json, it means that the list must be lazily loaded. + * We must create the ModelList type, injecting required values such as query keys, api name. + */ +internal class ModelPostProcessingTypeAdapter( + private val apiName: String?, + private val schemaRegistry: AWSApiSchemaRegistry +) : TypeAdapterFactory { + override fun create(gson: Gson, type: TypeToken): TypeAdapter { + val delegate = gson.getDelegateAdapter(this, type) + + return object : TypeAdapter() { + @Throws(IOException::class) + override fun write(out: JsonWriter, value: M) { + delegate.write(out, value) + } + + @Throws(IOException::class) + override fun read(`in`: JsonReader): M { + val obj = delegate.read(`in`) + (obj as? Model)?.let { injectLazyValues(it) } + return obj + } + + fun injectLazyValues(parent: Model) { + val parentType = parent.javaClass.simpleName + val parentModelSchema = ModelSchema.fromModelClass(parent.javaClass) + + parentModelSchema.fields.filter { it.value.isModelList || it.value.isModelReference }.map { fieldMap -> + val fieldToUpdate = parent.javaClass.getDeclaredField(fieldMap.key) + fieldToUpdate.isAccessible = true + if (fieldToUpdate.get(parent) == null) { + val lazyField = fieldMap.value + + when { + fieldMap.value.isModelReference -> { + val modelReference = LoadedModelReferenceImpl(null) + fieldToUpdate.set(parent, modelReference) + } + fieldMap.value.isModelList -> { + val lazyFieldModelSchema = schemaRegistry + .getModelSchemaForModelClass(lazyField.targetType) + + val lazyFieldTargetNames = lazyFieldModelSchema + .associations + .values + .first { it.associatedType == parentType } + .targetNames + + val parentIdentifiers = parent.getSortedIdentifiers() + + val queryKeys = lazyFieldTargetNames.mapIndexed { idx, name -> + name to parentIdentifiers[idx] + }.toMap() + + val modelList = ApiLazyModelList(lazyFieldModelSchema.modelClass, queryKeys, apiName) + + fieldToUpdate.set(parent, modelList) + } + } + } + } + } + } + } +} + +private fun Model.getSortedIdentifiers(): List { + return when (val identifier = resolveIdentifier()) { + is ModelIdentifier<*> -> { listOf(identifier.key()) + identifier.sortedKeys() } + else -> listOf(identifier.toString()) + } +} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelProviderLocator.java b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelProviderLocator.java new file mode 100644 index 0000000000..3a82690003 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelProviderLocator.java @@ -0,0 +1,108 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.api.aws; + +import androidx.annotation.NonNull; + +import com.amplifyframework.api.ApiException; +import com.amplifyframework.core.model.ModelProvider; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Objects; + +/** + * This utility class will inspect the JVM's Class loader to find a class named + * "com.amplifyframework.api.generated.model.AmplifyModelProvider". This is the package + * and class name that the Amplify CLI Code Generator tool uses when creating an {@link ModelProvider} + * instance to be consumed inside of the user's consuming application. This class, by contract, + * has public-access static method named `getInstance()` which returns ModelProvider. + * + * There is no compile-time guarantee that this class actually exists, or that it has that method, + * or that we can use that method. But in the "happy path," this simplifies the amount of complexity + * needed to begin using the API category. This utility class is used from + * {@link com.amplifyframework.api.aws.AWSApiPlugin#initialize}. + */ +final class ModelProviderLocator { + private static final String DEFAULT_MODEL_PROVIDER_CLASS_NAME = + "com.amplifyframework.datastore.generated.model.AmplifyModelProvider"; + private static final String GET_INSTANCE_ACCESSOR_METHOD_NAME = "getInstance"; + + private ModelProviderLocator() {} + + /** + * Locate the code-generated model provider. + * @return The code-generated model provider, if found + * @throws ApiException If unable to find the code-generated model provider + */ + static ModelProvider locate() throws ApiException { + return locate(DEFAULT_MODEL_PROVIDER_CLASS_NAME); + } + + @SuppressWarnings({"SameParameterValue", "unchecked"}) + static ModelProvider locate(@NonNull String modelProviderClassName) throws ApiException { + Objects.requireNonNull(modelProviderClassName); + final Class modelProviderClass; + try { + //noinspection unchecked It's very unlikely that someone cooked up a different type at this FQCN. + modelProviderClass = (Class) Class.forName(modelProviderClassName); + } catch (ClassNotFoundException modelProviderClassNotFoundError) { + throw new ApiException( + "Failed to find code-generated model provider.", modelProviderClassNotFoundError, + "Validate that " + modelProviderClassName + " is built into your project." + ); + } + if (!ModelProvider.class.isAssignableFrom(modelProviderClass)) { + throw new ApiException( + "Located class as " + modelProviderClass.getName() + ", but it does not implement " + + ModelProvider.class.getName() + ".", + "Validate that " + modelProviderClass.getName() + " has not been modified since the time " + + "it was code-generated." + ); + } + final Method getInstanceMethod; + try { + getInstanceMethod = modelProviderClass.getDeclaredMethod(GET_INSTANCE_ACCESSOR_METHOD_NAME); + } catch (NoSuchMethodException noGetInstanceMethodError) { + throw new ApiException( + "Found a code-generated model provider = " + modelProviderClass.getName() + ", however " + + "it had no static method named getInstance()!", + noGetInstanceMethodError, + "Validate that " + modelProviderClass.getName() + " has not been modified since the time " + + "it was code-generated." + ); + } + final ModelProvider locatedModelProvider; + try { + locatedModelProvider = (ModelProvider) getInstanceMethod.invoke(null); + } catch (IllegalAccessException getInstanceIsNotAccessibleError) { + throw new ApiException( + "Tried to call " + modelProviderClass.getName() + GET_INSTANCE_ACCESSOR_METHOD_NAME + ", but " + + "this method did not have public access.", getInstanceIsNotAccessibleError, + "Validate that " + modelProviderClass.getName() + " has not been modified since the time " + + "it was code-generated." + ); + } catch (InvocationTargetException wrappedExceptionFromGetInstance) { + throw new ApiException( + "An exception was thrown from " + modelProviderClass.getName() + GET_INSTANCE_ACCESSOR_METHOD_NAME + + " while invoking via reflection.", wrappedExceptionFromGetInstance, + "This is not expected to occur. Contact AWS." + ); + } + + return locatedModelProvider; + } +} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/MultiAuthAppSyncGraphQLOperation.java b/aws-api/src/main/java/com/amplifyframework/api/aws/MultiAuthAppSyncGraphQLOperation.java index fca6d6af6e..188281f492 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/MultiAuthAppSyncGraphQLOperation.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/MultiAuthAppSyncGraphQLOperation.java @@ -23,7 +23,6 @@ import com.amplifyframework.api.ApiException.ApiAuthException; import com.amplifyframework.api.aws.auth.ApiRequestDecoratorFactory; import com.amplifyframework.api.aws.auth.RequestDecorator; -import com.amplifyframework.api.graphql.GraphQLOperation; import com.amplifyframework.api.graphql.GraphQLRequest; import com.amplifyframework.api.graphql.GraphQLResponse; import com.amplifyframework.core.Amplify; @@ -53,7 +52,7 @@ * this is used for a LIST query vs. a GET query or most mutations. * @param Casted type of GraphQL result data */ -public final class MultiAuthAppSyncGraphQLOperation extends GraphQLOperation { +public final class MultiAuthAppSyncGraphQLOperation extends AWSGraphQLOperation { private static final Logger LOG = Amplify.Logging.logger(CategoryType.API, "amplify:aws-api"); private static final String CONTENT_TYPE = "application/json"; @@ -72,7 +71,7 @@ public final class MultiAuthAppSyncGraphQLOperation extends GraphQLOperation< * @param builder An instance of the {@link Builder} object. */ private MultiAuthAppSyncGraphQLOperation(Builder builder) { - super(builder.request, builder.responseFactory); + super(builder.request, builder.responseFactory, builder.apiName); this.apiRequestDecoratorFactory = builder.apiRequestDecoratorFactory; this.endpoint = builder.endpoint; this.client = builder.client; @@ -205,6 +204,7 @@ static final class Builder { private Consumer> onResponse; private Consumer onFailure; private ExecutorService executorService; + private String apiName; Builder endpoint(@NonNull String endpoint) { this.endpoint = Objects.requireNonNull(endpoint); @@ -246,6 +246,11 @@ Builder executorService(ExecutorService executorService) { return this; } + Builder apiName(String apiName) { + this.apiName = apiName; + return this; + } + @SuppressLint("SyntheticAccessor") MultiAuthAppSyncGraphQLOperation build() { return new MultiAuthAppSyncGraphQLOperation<>(this); diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/MultiAuthSubscriptionOperation.java b/aws-api/src/main/java/com/amplifyframework/api/aws/MultiAuthSubscriptionOperation.java index cbe2ade2c1..9ee37b4625 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/MultiAuthSubscriptionOperation.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/MultiAuthSubscriptionOperation.java @@ -21,7 +21,6 @@ import com.amplifyframework.api.ApiException; import com.amplifyframework.api.ApiException.ApiAuthException; import com.amplifyframework.api.aws.auth.AuthRuleRequestDecorator; -import com.amplifyframework.api.graphql.GraphQLOperation; import com.amplifyframework.api.graphql.GraphQLRequest; import com.amplifyframework.api.graphql.GraphQLResponse; import com.amplifyframework.core.Action; @@ -38,7 +37,7 @@ import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; -final class MultiAuthSubscriptionOperation extends GraphQLOperation { +final class MultiAuthSubscriptionOperation extends AWSGraphQLOperation { private static final Logger LOG = Amplify.Logging.logger(CategoryType.API, "amplify:aws-api"); private final SubscriptionEndpoint subscriptionEndpoint; @@ -54,7 +53,7 @@ final class MultiAuthSubscriptionOperation extends GraphQLOperation { private Future subscriptionFuture; private MultiAuthSubscriptionOperation(Builder builder) { - super(builder.graphQlRequest, builder.responseFactory); + super(builder.graphQlRequest, builder.responseFactory, builder.apiName); this.subscriptionEndpoint = builder.subscriptionEndpoint; this.onSubscriptionStart = builder.onSubscriptionStart; this.onNextItem = builder.onNextItem; @@ -205,6 +204,7 @@ static final class Builder { private Consumer onSubscriptionError; private Action onSubscriptionComplete; private AuthRuleRequestDecorator requestDecorator; + private String apiName; @NonNull public Builder subscriptionEndpoint(@NonNull SubscriptionEndpoint subscriptionEndpoint) { @@ -259,6 +259,12 @@ public Builder requestDecorator(AuthRuleRequestDecorator requestDecorator) { return this; } + @NonNull + public Builder apiName(String apiName) { + this.apiName = apiName; + return this; + } + @NonNull public MultiAuthSubscriptionOperation build() { return new MultiAuthSubscriptionOperation<>(this); diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/SubscriptionEndpoint.java b/aws-api/src/main/java/com/amplifyframework/api/aws/SubscriptionEndpoint.java index 79592508cb..218186ce73 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/SubscriptionEndpoint.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/SubscriptionEndpoint.java @@ -76,12 +76,14 @@ final class SubscriptionEndpoint { private final Object webSocketLock = new Object(); private WebSocket webSocket; private AmplifyWebSocketListener webSocketListener; + private String apiName; SubscriptionEndpoint( @NonNull ApiConfiguration apiConfiguration, @Nullable OkHttpConfigurator configurator, @NonNull GraphQLResponse.Factory responseFactory, - @NonNull SubscriptionAuthorizer authorizer + @NonNull SubscriptionAuthorizer authorizer, + @Nullable String apiName ) { this.apiConfiguration = Objects.requireNonNull(apiConfiguration); this.subscriptions = new ConcurrentHashMap<>(); @@ -89,6 +91,7 @@ final class SubscriptionEndpoint { this.authorizer = Objects.requireNonNull(authorizer); this.timeoutWatchdog = new TimeoutWatchdog(); this.pendingSubscriptionIds = Collections.synchronizedSet(new HashSet<>()); + this.apiName = apiName; OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder() .retryOnConnectionFailure(true); @@ -197,7 +200,7 @@ synchronized void requestSubscription( Subscription subscription = new Subscription<>( onNextItem, onSubscriptionError, onSubscriptionComplete, - responseFactory, request.getResponseType(), request + responseFactory, request.getResponseType(), request, apiName ); subscriptions.put(subscriptionId, subscription); if (subscription.awaitSubscriptionReady()) { @@ -372,6 +375,7 @@ static final class Subscription { private final CountDownLatch subscriptionReadyAcknowledgment; private final CountDownLatch subscriptionCompletionAcknowledgement; private boolean failed; + private String apiName; Subscription( Consumer> onNextItem, @@ -379,13 +383,16 @@ static final class Subscription { Action onSubscriptionComplete, GraphQLResponse.Factory responseFactory, Type responseType, - GraphQLRequest request) { + GraphQLRequest request, + String apiName + ) { this.onNextItem = onNextItem; this.onSubscriptionError = onSubscriptionError; this.onSubscriptionComplete = onSubscriptionComplete; this.responseFactory = responseFactory; this.responseType = responseType; this.request = request; + this.apiName = apiName; this.subscriptionReadyAcknowledgment = new CountDownLatch(1); this.subscriptionCompletionAcknowledgement = new CountDownLatch(1); this.failed = false; @@ -444,9 +451,28 @@ void awaitSubscriptionCompleted() { } } + // This method should be used in place of GraphQLResponse.Factory buildResponse. + // We need to use this method to pass apiName for LazyModel + private GraphQLResponse buildResponse(String jsonResponse) throws ApiException { + if (!(responseFactory instanceof GsonGraphQLResponseFactory)) { + throw new ApiException( + "Amplify encountered an error while deserializing an object. " + + "GraphQLResponse.Factory was not of type GsonGraphQLResponseFactory", + AmplifyException.REPORT_BUG_TO_AWS_SUGGESTION); + } + + try { + return ((GsonGraphQLResponseFactory) responseFactory) + .buildResponse(request, jsonResponse, apiName); + } catch (ClassCastException cce) { + throw new ApiException("Amplify encountered an error while deserializing an object", + AmplifyException.TODO_RECOVERY_SUGGESTION); + } + } + void dispatchNextMessage(String message) { try { - onNextItem.accept(responseFactory.buildResponse(request, message)); + onNextItem.accept(buildResponse(message)); } catch (ApiException exception) { dispatchError(exception); } diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/SubscriptionOperation.java b/aws-api/src/main/java/com/amplifyframework/api/aws/SubscriptionOperation.java index 9095fc0008..5a6aa59aca 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/SubscriptionOperation.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/SubscriptionOperation.java @@ -18,7 +18,6 @@ import androidx.annotation.NonNull; import com.amplifyframework.api.ApiException; -import com.amplifyframework.api.graphql.GraphQLOperation; import com.amplifyframework.api.graphql.GraphQLRequest; import com.amplifyframework.api.graphql.GraphQLResponse; import com.amplifyframework.core.Action; @@ -32,7 +31,7 @@ import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; -final class SubscriptionOperation extends GraphQLOperation { +final class SubscriptionOperation extends AWSGraphQLOperation { private static final Logger LOG = Amplify.Logging.logger(CategoryType.API, "amplify:aws-api"); private final SubscriptionEndpoint subscriptionEndpoint; @@ -48,7 +47,7 @@ final class SubscriptionOperation extends GraphQLOperation { private Future subscriptionFuture; private SubscriptionOperation(Builder builder) { - super(builder.graphQlRequest, builder.responseFactory); + super(builder.graphQlRequest, builder.responseFactory, builder.apiName); this.subscriptionEndpoint = builder.subscriptionEndpoint; this.onSubscriptionStart = builder.onSubscriptionStart; this.onNextItem = builder.onNextItem; @@ -121,6 +120,7 @@ static final class Builder { private Consumer onSubscriptionError; private Action onSubscriptionComplete; private AuthorizationType authorizationType; + private String apiName; @NonNull public Builder subscriptionEndpoint(@NonNull SubscriptionEndpoint subscriptionEndpoint) { @@ -176,6 +176,12 @@ public Builder authorizationType(AuthorizationType authorizationType) { return this; } + @NonNull + public Builder apiName(String apiName) { + this.apiName = apiName; + return this; + } + @NonNull public SubscriptionOperation build() { return new SubscriptionOperation<>(this); diff --git a/aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelMutation.java b/aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelMutation.java deleted file mode 100644 index e36a2b8659..0000000000 --- a/aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelMutation.java +++ /dev/null @@ -1,97 +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. - */ - -package com.amplifyframework.api.graphql.model; - -import androidx.annotation.NonNull; - -import com.amplifyframework.api.aws.AppSyncGraphQLRequestFactory; -import com.amplifyframework.api.graphql.GraphQLRequest; -import com.amplifyframework.api.graphql.MutationType; -import com.amplifyframework.core.model.Model; -import com.amplifyframework.core.model.query.predicate.QueryPredicate; -import com.amplifyframework.core.model.query.predicate.QueryPredicates; - -/** - * Helper class that provides methods to create {@link GraphQLRequest} from {@link Model}. - */ -public final class ModelMutation { - private ModelMutation() {} - - /** - * Creates a {@link GraphQLRequest} that represents a create mutation for a given {@code model} instance. - * @param model the model instance populated with values. - * @param the model concrete type. - * @return a valid {@code GraphQLRequest} instance. - * @see MutationType#CREATE - */ - public static GraphQLRequest create(@NonNull M model) { - return AppSyncGraphQLRequestFactory.buildMutation(model, QueryPredicates.all(), MutationType.CREATE); - } - - /** - * Creates a {@link GraphQLRequest} that represents an update mutation for a given {@code model} instance. - * @param model the model instance populated with values. - * @param predicate a predicate passed as the condition to apply the mutation. - * @param the model concrete type. - * @return a valid {@code GraphQLRequest} instance. - * @see MutationType#UPDATE - */ - public static GraphQLRequest update( - @NonNull M model, - @NonNull QueryPredicate predicate - ) { - return AppSyncGraphQLRequestFactory.buildMutation(model, predicate, MutationType.UPDATE); - } - - /** - * Creates a {@link GraphQLRequest} that represents an update mutation for a given {@code model} instance. - * @param model the model instance populated with values. - * @param the model concrete type. - * @return a valid {@code GraphQLRequest} instance. - * @see MutationType#UPDATE - * @see #update(Model, QueryPredicate) - */ - public static GraphQLRequest update(@NonNull M model) { - return AppSyncGraphQLRequestFactory.buildMutation(model, QueryPredicates.all(), MutationType.UPDATE); - } - - /** - * Creates a {@link GraphQLRequest} that represents a delete mutation for a given {@code model} instance. - * @param model the model instance populated with values. - * @param predicate a predicate passed as the condition to apply the mutation. - * @param the model concrete type. - * @return a valid {@code GraphQLRequest} instance. - * @see MutationType#DELETE - */ - public static GraphQLRequest delete( - @NonNull M model, - @NonNull QueryPredicate predicate - ) { - return AppSyncGraphQLRequestFactory.buildMutation(model, predicate, MutationType.DELETE); - } - - /** - * Creates a {@link GraphQLRequest} that represents a delete mutation for a given {@code model} instance. - * @param model the model instance populated with values. - * @param the model concrete type. - * @return a valid {@code GraphQLRequest} instance. - * @see MutationType#DELETE - * @see #delete(Model, QueryPredicate) - */ - public static GraphQLRequest delete(@NonNull M model) { - return AppSyncGraphQLRequestFactory.buildMutation(model, QueryPredicates.all(), MutationType.DELETE); - } -} diff --git a/aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelMutation.kt b/aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelMutation.kt new file mode 100644 index 0000000000..6c5c47ab9a --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelMutation.kt @@ -0,0 +1,190 @@ +/* + * 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. + */ +package com.amplifyframework.api.graphql.model + +import com.amplifyframework.api.aws.AppSyncGraphQLRequestFactory.buildMutation +import com.amplifyframework.api.graphql.GraphQLRequest +import com.amplifyframework.api.graphql.MutationType +import com.amplifyframework.core.model.Model +import com.amplifyframework.core.model.ModelPath +import com.amplifyframework.core.model.PropertyContainerPath +import com.amplifyframework.core.model.query.predicate.QueryPredicate +import com.amplifyframework.core.model.query.predicate.QueryPredicates + +/** + * Helper class that provides methods to create [GraphQLRequest] from [Model]. + */ +object ModelMutation { + /** + * Creates a [GraphQLRequest] that represents a create mutation for a given `model` instance. + * @param model the model instance populated with values. + * @param the model concrete type. + * @return a valid `GraphQLRequest` instance. + * @see MutationType.CREATE + */ + @JvmStatic + fun create(model: M): GraphQLRequest { + return buildMutation(model, QueryPredicates.all(), MutationType.CREATE) + } + + /** + * Creates a [GraphQLRequest] that represents a create mutation for a given `model` instance. + * @param model the model instance populated with values. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the model concrete type. + * @param

the concrete model path for the M model type + * @return a valid `GraphQLRequest` instance. + * @see MutationType.CREATE + */ + @JvmStatic + fun > create( + model: M, + includes: ((P) -> List) + ): GraphQLRequest { + return buildMutation(model, QueryPredicates.all(), MutationType.CREATE, includes) + } + + /** + * Creates a [GraphQLRequest] that represents an update mutation for a given `model` instance. + * @param model the model instance populated with values. + * @param predicate a predicate passed as the condition to apply the mutation. + * @param the model concrete type. + * @return a valid `GraphQLRequest` instance. + * @see MutationType.UPDATE + */ + @JvmStatic + fun update( + model: M, + predicate: QueryPredicate + ): GraphQLRequest { + return buildMutation(model, predicate, MutationType.UPDATE) + } + + /** + * Creates a [GraphQLRequest] that represents an update mutation for a given `model` instance. + * @param model the model instance populated with values. + * @param predicate a predicate passed as the condition to apply the mutation. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the model concrete type. + * @param

the concrete model path for the M model type + * @return a valid `GraphQLRequest` instance. + * @see MutationType.UPDATE + */ + @JvmStatic + fun > update( + model: M, + predicate: QueryPredicate, + includes: ((P) -> List) + ): GraphQLRequest { + return buildMutation(model, predicate, MutationType.UPDATE, includes) + } + + /** + * Creates a [GraphQLRequest] that represents an update mutation for a given `model` instance. + * @param model the model instance populated with values. + * @param the model concrete type. + * @return a valid `GraphQLRequest` instance. + * @see MutationType.UPDATE + * + * @see .update + */ + @JvmStatic + fun update(model: M): GraphQLRequest { + return buildMutation(model, QueryPredicates.all(), MutationType.UPDATE) + } + + /** + * Creates a [GraphQLRequest] that represents an update mutation for a given `model` instance. + * @param model the model instance populated with values. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the model concrete type. + * @param

the concrete model path for the M model type + * @return a valid `GraphQLRequest` instance. + * @see MutationType.UPDATE + */ + @JvmStatic + fun > update( + model: M, + includes: ((P) -> List) + ): GraphQLRequest { + return buildMutation(model, QueryPredicates.all(), MutationType.UPDATE, includes) + } + + /** + * Creates a [GraphQLRequest] that represents a delete mutation for a given `model` instance. + * @param model the model instance populated with values. + * @param predicate a predicate passed as the condition to apply the mutation. + * @param the model concrete type. + * @return a valid `GraphQLRequest` instance. + * @see MutationType.DELETE + */ + @JvmStatic + fun delete( + model: M, + predicate: QueryPredicate + ): GraphQLRequest { + return buildMutation(model, predicate, MutationType.DELETE) + } + + /** + * Creates a [GraphQLRequest] that represents a delete mutation for a given `model` instance. + * @param model the model instance populated with values. + * @param predicate a predicate passed as the condition to apply the mutation. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the model concrete type. + * @param

the concrete model path for the M model type + * @return a valid `GraphQLRequest` instance. + * @see MutationType.DELETE + */ + @JvmStatic + fun > delete( + model: M, + predicate: QueryPredicate, + includes: ((P) -> List) + ): GraphQLRequest { + return buildMutation(model, predicate, MutationType.DELETE, includes) + } + + /** + * Creates a [GraphQLRequest] that represents a delete mutation for a given `model` instance. + * @param model the model instance populated with values. + * @param the model concrete type. + * @return a valid `GraphQLRequest` instance. + * @see MutationType.DELETE + * + * @see .delete + */ + @JvmStatic + fun delete(model: M): GraphQLRequest { + return buildMutation(model, QueryPredicates.all(), MutationType.DELETE) + } + + /** + * Creates a [GraphQLRequest] that represents a delete mutation for a given `model` instance. + * @param model the model instance populated with values. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the model concrete type. + * @param

the concrete model path for the M model type + * @return a valid `GraphQLRequest` instance. + * @see MutationType.DELETE + */ + @JvmStatic + fun > delete( + model: M, + includes: ((P) -> List) + ): GraphQLRequest { + return buildMutation(model, QueryPredicates.all(), MutationType.DELETE, includes) + } +} diff --git a/aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelQuery.java b/aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelQuery.java deleted file mode 100644 index 399be1faea..0000000000 --- a/aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelQuery.java +++ /dev/null @@ -1,145 +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. - */ - -package com.amplifyframework.api.graphql.model; - -import androidx.annotation.NonNull; - -import com.amplifyframework.api.aws.AppSyncGraphQLRequestFactory; -import com.amplifyframework.api.graphql.GraphQLRequest; -import com.amplifyframework.api.graphql.PaginatedResult; -import com.amplifyframework.core.model.Model; -import com.amplifyframework.core.model.ModelIdentifier; -import com.amplifyframework.core.model.query.predicate.QueryPredicate; -import com.amplifyframework.core.model.query.predicate.QueryPredicates; - -import java.util.Objects; - -/** - * Helper class that provides methods to create {@link GraphQLRequest} for queries - * from {@link Model} and {@link QueryPredicate}. - */ -public final class ModelQuery { - - /** This class should not be instantiated. */ - private ModelQuery() {} - - /** - * Creates a {@link GraphQLRequest} that represents a query that expects a single value as a result. - * The request will be created with the correct correct document based on the model schema and - * variables based on given {@code modelId}. - * @param modelType the model class. - * @param modelId the model identifier. - * @param the concrete model type. - * @return a valid {@link GraphQLRequest} instance. - */ - public static GraphQLRequest get( - @NonNull Class modelType, - @NonNull String modelId - ) { - return AppSyncGraphQLRequestFactory.buildQuery(modelType, modelId); - } - - /** - * Creates a {@link GraphQLRequest} that represents a query that expects a single value as a result. - * The request will be created with the correct document based on the model schema and - * variables based on given {@code modelIdentifier}. - * @param modelType the model class. - * @param modelIdentifier the model identifier. - * @param the concrete model type. - * @return a valid {@link GraphQLRequest} instance. - */ - public static GraphQLRequest get( - @NonNull Class modelType, - @NonNull ModelIdentifier modelIdentifier - ) { - return AppSyncGraphQLRequestFactory.buildQuery(modelType, modelIdentifier); - } - - /** - * Creates a {@link GraphQLRequest} that represents a query that expects multiple values as a result. - * The request will be created with the correct document based on the model schema and variables - * for filtering based on the given predicate. - * @param modelType the model class. - * @param predicate the predicate for filtering. - * @param the concrete model type. - * @return a valid {@link GraphQLRequest} instance. - */ - public static GraphQLRequest> list( - @NonNull Class modelType, - @NonNull QueryPredicate predicate - ) { - Objects.requireNonNull(modelType); - Objects.requireNonNull(predicate); - return AppSyncGraphQLRequestFactory.buildQuery(modelType, predicate); - } - - /** - * Creates a {@link GraphQLRequest} that represents a query that expects multiple values as a result. - * The request will be created with the correct document based on the model schema. - * @param modelType the model class. - * @param the concrete model type. - * @return a valid {@link GraphQLRequest} instance. - * @see #list(Class, QueryPredicate) - */ - public static GraphQLRequest> list(@NonNull Class modelType) { - return list(modelType, QueryPredicates.all()); - } - - /** - * Creates a {@link GraphQLRequest} that represents a query that expects multiple values as a result - * within a certain range (i.e. paginated). - * - * The request will be created with the correct document based on the model schema and variables - * for filtering based on the given predicate and pagination. - * - * @param modelType the model class. - * @param predicate the predicate for filtering. - * @param pagination the pagination settings. - * @param the concrete model type. - * @return a valid {@link GraphQLRequest} instance. - * @see ModelPagination#firstPage() - */ - public static GraphQLRequest> list( - @NonNull Class modelType, - @NonNull QueryPredicate predicate, - @NonNull ModelPagination pagination - ) { - return AppSyncGraphQLRequestFactory.buildPaginatedResultQuery( - modelType, predicate, pagination.getLimit()); - } - - /** - * Creates a {@link GraphQLRequest} that represents a query that expects multiple values as a result - * within a certain range (i.e. paginated). - * - * The request will be created with the correct document based on the model schema and variables - * for pagination based on the given {@link ModelPagination}. - * - * @param modelType the model class. - * @param pagination the pagination settings. - * @param the concrete model type. - * @return a valid {@link GraphQLRequest} instance. - * @see ModelPagination#firstPage() - */ - public static GraphQLRequest> list( - @NonNull Class modelType, - @NonNull ModelPagination pagination - ) { - return AppSyncGraphQLRequestFactory.buildPaginatedResultQuery( - modelType, QueryPredicates.all(), pagination.getLimit()); - } - -} diff --git a/aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelQuery.kt b/aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelQuery.kt new file mode 100644 index 0000000000..fc51cf39ee --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelQuery.kt @@ -0,0 +1,276 @@ +/* + * 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. + */ +package com.amplifyframework.api.graphql.model + +import com.amplifyframework.api.aws.AppSyncGraphQLRequestFactory +import com.amplifyframework.api.graphql.GraphQLRequest +import com.amplifyframework.api.graphql.PaginatedResult +import com.amplifyframework.core.model.Model +import com.amplifyframework.core.model.ModelIdentifier +import com.amplifyframework.core.model.ModelPath +import com.amplifyframework.core.model.PropertyContainerPath +import com.amplifyframework.core.model.query.predicate.QueryPredicate +import com.amplifyframework.core.model.query.predicate.QueryPredicates + +/** + * Helper class that provides methods to create [GraphQLRequest] for queries + * from [Model] and [QueryPredicate]. + */ +object ModelQuery { + + /** + * Creates a [GraphQLRequest] that represents a query that expects a single value as a result. + * The request will be created with the correct correct document based on the model schema and + * variables based on given `modelId`. + * @param modelType the model class. + * @param modelId the model identifier. + * @param the concrete model type. + * @return a valid [GraphQLRequest] instance. + */ + @JvmStatic + operator fun get( + modelType: Class, + modelId: String, + ): GraphQLRequest { + return AppSyncGraphQLRequestFactory.buildQuery(modelType, modelId) + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects a single value as a result. + * The request will be created with the correct correct document based on the model schema and + * variables based on given `modelId`. + * @param modelType the model class. + * @param modelId the model identifier. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the concrete model type. + * @param

the concrete model path for the M model type + * @return a valid [GraphQLRequest] instance. + */ + @JvmStatic + operator fun > get( + modelType: Class, + modelId: String, + includes: ((P) -> List) + ): GraphQLRequest { + return AppSyncGraphQLRequestFactory.buildQuery(modelType, modelId, includes) + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects a single value as a result. + * The request will be created with the correct document based on the model schema and + * variables based on given `modelIdentifier`. + * @param modelType the model class. + * @param modelIdentifier the model identifier. + * @param the concrete model type. + * @return a valid [GraphQLRequest] instance. + */ + @JvmStatic + operator fun get( + modelType: Class, + modelIdentifier: ModelIdentifier + ): GraphQLRequest { + return AppSyncGraphQLRequestFactory.buildQuery(modelType, modelIdentifier) + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects a single value as a result. + * The request will be created with the correct document based on the model schema and + * variables based on given `modelIdentifier`. + * @param modelType the model class. + * @param modelIdentifier the model identifier. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the concrete model type. + * @param

the concrete model path for the M model type + * @return a valid [GraphQLRequest] instance. + */ + @JvmStatic + operator fun > get( + modelType: Class, + modelIdentifier: ModelIdentifier, + includes: ((P) -> List) + ): GraphQLRequest { + return AppSyncGraphQLRequestFactory.buildQuery(modelType, modelIdentifier, includes) + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects multiple values as a result. + * The request will be created with the correct document based on the model schema and variables + * for filtering based on the given predicate. + * @param modelType the model class. + * @param predicate the predicate for filtering. + * @param the concrete model type. + * @return a valid [GraphQLRequest] instance. + */ + @JvmStatic + fun list( + modelType: Class, + predicate: QueryPredicate + ): GraphQLRequest> { + return AppSyncGraphQLRequestFactory.buildQuery(modelType, predicate) + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects multiple values as a result. + * The request will be created with the correct document based on the model schema and variables + * for filtering based on the given predicate. + * @param modelType the model class. + * @param predicate the predicate for filtering. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the concrete model type. + * @param

the concrete model path for the M model type + * @return a valid [GraphQLRequest] instance. + */ + @JvmStatic + fun > list( + modelType: Class, + predicate: QueryPredicate, + includes: ((P) -> List) + ): GraphQLRequest> { + return AppSyncGraphQLRequestFactory.buildQuery(modelType, predicate, includes) + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects multiple values as a result. + * The request will be created with the correct document based on the model schema. + * @param modelType the model class. + * @param the concrete model type. + * @return a valid [GraphQLRequest] instance. + * @see .list + */ + @JvmStatic + fun list(modelType: Class): GraphQLRequest> { + return list(modelType, QueryPredicates.all()) + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects multiple values as a result. + * The request will be created with the correct document based on the model schema. + * @param modelType the model class. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the concrete model type. + * @param

the concrete model path for the M model type + * @return a valid [GraphQLRequest] instance. + * @see .list + */ + @JvmStatic + fun > list( + modelType: Class, + includes: ((P) -> List) + ): GraphQLRequest> { + return list(modelType, QueryPredicates.all(), includes) + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects multiple values as a result + * within a certain range (i.e. paginated). + * + * The request will be created with the correct document based on the model schema and variables + * for filtering based on the given predicate and pagination. + * + * @param modelType the model class. + * @param predicate the predicate for filtering. + * @param pagination the pagination settings. + * @param the concrete model type. + * @return a valid [GraphQLRequest] instance. + * @see ModelPagination.firstPage + */ + @JvmStatic + fun list( + modelType: Class, + predicate: QueryPredicate, + pagination: ModelPagination + ): GraphQLRequest> { + return AppSyncGraphQLRequestFactory.buildPaginatedResultQuery( + modelType, predicate, pagination.limit + ) + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects multiple values as a result + * within a certain range (i.e. paginated). + * + * The request will be created with the correct document based on the model schema and variables + * for filtering based on the given predicate and pagination. + * + * @param modelType the model class. + * @param predicate the predicate for filtering. + * @param pagination the pagination settings. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the concrete model type. + * @param

the concrete model path for the M model type + * @return a valid [GraphQLRequest] instance. + * @see ModelPagination.firstPage + */ + @JvmStatic + fun > list( + modelType: Class, + predicate: QueryPredicate, + pagination: ModelPagination, + includes: ((P) -> List) + ): GraphQLRequest> { + return AppSyncGraphQLRequestFactory.buildPaginatedResultQuery( + modelType, predicate, pagination.limit, includes + ) + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects multiple values as a result + * within a certain range (i.e. paginated). + * + * The request will be created with the correct document based on the model schema and variables + * for pagination based on the given [ModelPagination]. + * + * @param modelType the model class. + * @param pagination the pagination settings. + * @param the concrete model type. + * @return a valid [GraphQLRequest] instance. + * @see ModelPagination.firstPage + */ + @JvmStatic + fun list( + modelType: Class, + pagination: ModelPagination + ): GraphQLRequest> { + return AppSyncGraphQLRequestFactory.buildPaginatedResultQuery( + modelType, QueryPredicates.all(), pagination.limit + ) + } + + /** + * Creates a [GraphQLRequest] that represents a query that expects multiple values as a result + * within a certain range (i.e. paginated). + * + * The request will be created with the correct document based on the model schema and variables + * for pagination based on the given [ModelPagination]. + * + * @param modelType the model class. + * @param pagination the pagination settings. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the concrete model type. + * @param

the concrete model path for the M model type + * @return a valid [GraphQLRequest] instance. + * @see ModelPagination.firstPage + */ + @JvmStatic + fun > list( + modelType: Class, + pagination: ModelPagination, + includes: ((P) -> List) + ): GraphQLRequest> { + return AppSyncGraphQLRequestFactory.buildPaginatedResultQuery( + modelType, QueryPredicates.all(), pagination.limit, includes + ) + } +} diff --git a/aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelSubscription.java b/aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelSubscription.java deleted file mode 100644 index a42ae6914b..0000000000 --- a/aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelSubscription.java +++ /dev/null @@ -1,76 +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. - */ - -package com.amplifyframework.api.graphql.model; - -import com.amplifyframework.api.aws.AppSyncGraphQLRequestFactory; -import com.amplifyframework.api.graphql.GraphQLRequest; -import com.amplifyframework.api.graphql.SubscriptionType; -import com.amplifyframework.core.model.Model; - -/** - * Helper class that provides methods to create {@link GraphQLRequest} that represents - * subscriptions from {@link Model}. - */ -public final class ModelSubscription { - - /** This class should not be instantiated. */ - private ModelSubscription() {} - - /** - * Builds a subscriptions request of a given {@code type} for a {@code modelType}. - * @param modelType the model class. - * @param type the subscription type. - * @param the concrete type of the model. - * @return a valid {@link GraphQLRequest} instance. - */ - public static GraphQLRequest of(Class modelType, SubscriptionType type) { - return AppSyncGraphQLRequestFactory.buildSubscription(modelType, type); - } - - /** - * Creates a subscription request of type {@link SubscriptionType#ON_CREATE}. - * @param modelType the model class. - * @param the concrete type of the model. - * @return a valid {@link GraphQLRequest} instance. - * @see #of(Class, SubscriptionType) - */ - public static GraphQLRequest onCreate(Class modelType) { - return of(modelType, SubscriptionType.ON_CREATE); - } - - /** - * Creates a subscription request of type {@link SubscriptionType#ON_DELETE}. - * @param modelType the model class. - * @param the concrete type of the model. - * @return a valid {@link GraphQLRequest} instance. - * @see #of(Class, SubscriptionType) - */ - public static GraphQLRequest onDelete(Class modelType) { - return of(modelType, SubscriptionType.ON_DELETE); - } - - /** - * Creates a subscription request of type {@link SubscriptionType#ON_UPDATE}. - * @param modelType the model class. - * @param the concrete type of the model. - * @return a valid {@link GraphQLRequest} instance. - * @see #of(Class, SubscriptionType) - */ - public static GraphQLRequest onUpdate(Class modelType) { - return of(modelType, SubscriptionType.ON_UPDATE); - } - -} diff --git a/aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelSubscription.kt b/aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelSubscription.kt new file mode 100644 index 0000000000..c5d883c0e7 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelSubscription.kt @@ -0,0 +1,144 @@ +/* + * 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. + */ +package com.amplifyframework.api.graphql.model + +import com.amplifyframework.api.aws.AppSyncGraphQLRequestFactory.buildSubscription +import com.amplifyframework.api.graphql.GraphQLRequest +import com.amplifyframework.api.graphql.SubscriptionType +import com.amplifyframework.core.model.Model +import com.amplifyframework.core.model.ModelPath +import com.amplifyframework.core.model.PropertyContainerPath + +/** + * Helper class that provides methods to create [GraphQLRequest] that represents + * subscriptions from [Model]. + */ +object ModelSubscription { + /** + * Builds a subscriptions request of a given `type` for a `modelType`. + * @param modelType the model class. + * @param type the subscription type. + * @param the concrete type of the model. + * @return a valid [GraphQLRequest] instance. + */ + fun of( + modelType: Class, + type: SubscriptionType, + ): GraphQLRequest { + return buildSubscription(modelType, type) + } + + /** + * Builds a subscriptions request of a given `type` for a `modelType`. + * @param modelType the model class. + * @param type the subscription type. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the concrete type of the model. + * @param

the concrete model path for the M model type + * @return a valid [GraphQLRequest] instance. + */ + fun > of( + modelType: Class, + type: SubscriptionType, + includes: ((P) -> List) + ): GraphQLRequest { + return buildSubscription(modelType, type, includes) + } + + /** + * Creates a subscription request of type [SubscriptionType.ON_CREATE]. + * @param modelType the model class. + * @param the concrete type of the model. + * @return a valid [GraphQLRequest] instance. + * @see .of + */ + @JvmStatic + fun onCreate(modelType: Class): GraphQLRequest { + return of(modelType, SubscriptionType.ON_CREATE) + } + + /** + * Creates a subscription request of type [SubscriptionType.ON_CREATE]. + * @param modelType the model class. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the concrete type of the model. + * @param

the concrete model path for the M model type + * @return a valid [GraphQLRequest] instance. + * @see .of + */ + @JvmStatic + fun > onCreate( + modelType: Class, + includes: ((P) -> List) + ): GraphQLRequest { + return of(modelType, SubscriptionType.ON_CREATE, includes) + } + + /** + * Creates a subscription request of type [SubscriptionType.ON_DELETE]. + * @param modelType the model class. + * @param the concrete type of the model. + * @return a valid [GraphQLRequest] instance. + * @see .of + */ + fun onDelete(modelType: Class): GraphQLRequest { + return of(modelType, SubscriptionType.ON_DELETE) + } + + /** + * Creates a subscription request of type [SubscriptionType.ON_DELETE]. + * @param modelType the model class. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the concrete type of the model. + * @param

the concrete model path for the M model type + * @return a valid [GraphQLRequest] instance. + * @see .of + */ + @JvmStatic + fun > onDelete( + modelType: Class, + includes: ((P) -> List) + ): GraphQLRequest { + return of(modelType, SubscriptionType.ON_DELETE, includes) + } + + /** + * Creates a subscription request of type [SubscriptionType.ON_UPDATE]. + * @param modelType the model class. + * @param the concrete type of the model. + * @return a valid [GraphQLRequest] instance. + * @see .of + */ + fun onUpdate(modelType: Class): GraphQLRequest { + return of(modelType, SubscriptionType.ON_UPDATE) + } + + /** + * Creates a subscription request of type [SubscriptionType.ON_UPDATE]. + * @param modelType the model class. + * @param includes lambda returning list of relationships that should be included in the selection set + * @param the concrete type of the model. + * @param

the concrete model path for the M model type + * @return a valid [GraphQLRequest] instance. + * @see .of + */ + @JvmStatic + fun > onUpdate( + modelType: Class, + includes: ((P) -> List) + ): GraphQLRequest { + return of(modelType, SubscriptionType.ON_UPDATE, includes) + } +} diff --git a/aws-api/src/test/java/com/amplifyframework/api/aws/AWSApiPluginTest.java b/aws-api/src/test/java/com/amplifyframework/api/aws/AWSApiPluginTest.java index 1a96dd378d..c1fed48e5e 100644 --- a/aws-api/src/test/java/com/amplifyframework/api/aws/AWSApiPluginTest.java +++ b/aws-api/src/test/java/com/amplifyframework/api/aws/AWSApiPluginTest.java @@ -84,12 +84,12 @@ public final class AWSApiPluginTest { /** * Sets up the test. - * @throws ApiException On failure to configure plugin * @throws IOException On failure to start web server * @throws JSONException On failure to arrange configuration JSON + * @throws AmplifyException On failure to create request */ @Before - public void setup() throws ApiException, IOException, JSONException { + public void setup() throws AmplifyException, IOException, JSONException { webServer = new MockWebServer(); webServer.start(8080); baseUrl = webServer.url("/"); diff --git a/aws-api/src/test/java/com/amplifyframework/api/aws/AWSApiSchemaRegistryTest.kt b/aws-api/src/test/java/com/amplifyframework/api/aws/AWSApiSchemaRegistryTest.kt new file mode 100644 index 0000000000..76624dce43 --- /dev/null +++ b/aws-api/src/test/java/com/amplifyframework/api/aws/AWSApiSchemaRegistryTest.kt @@ -0,0 +1,58 @@ +/* + * + * * Copyright 2023 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. + * + */ + +package com.amplifyframework.api.aws + +import com.amplifyframework.api.ApiException +import com.amplifyframework.testmodels.lazy.AmplifyModelProvider +import com.amplifyframework.testmodels.lazy.Comment +import com.amplifyframework.testmodels.lazy.Post +import io.mockk.clearStaticMockk +import io.mockk.every +import io.mockk.mockkStatic +import org.junit.After +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Test + +class AWSApiSchemaRegistryTest { + + private val schemaRegistry = AWSApiSchemaRegistry() + + @Before + fun setUp() { + mockkStatic(ModelProviderLocator::class) + every { ModelProviderLocator.locate() } returns AmplifyModelProvider.getInstance() + } + + @After + fun tearDown() { + clearStaticMockk(ModelProviderLocator::class) + } + + @Test + fun models_are_registered() { + assertNotNull(schemaRegistry.getModelSchemaForModelClass(Post::class.java)) + assertNotNull(schemaRegistry.getModelSchemaForModelClass("Post")) + assertNotNull(schemaRegistry.getModelSchemaForModelClass(Comment::class.java)) + } + + @Test(expected = ApiException::class) + fun models_not_provided_throw() { + assertNotNull(schemaRegistry.getModelSchemaForModelClass(Todo::class.java)) + } +} diff --git a/aws-api/src/test/java/com/amplifyframework/api/aws/ApiLazyModelListTest.kt b/aws-api/src/test/java/com/amplifyframework/api/aws/ApiLazyModelListTest.kt new file mode 100644 index 0000000000..88a80ea022 --- /dev/null +++ b/aws-api/src/test/java/com/amplifyframework/api/aws/ApiLazyModelListTest.kt @@ -0,0 +1,382 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.api.aws + +import com.amplifyframework.AmplifyException +import com.amplifyframework.api.ApiCategory +import com.amplifyframework.api.ApiException +import com.amplifyframework.api.graphql.GraphQLRequest +import com.amplifyframework.api.graphql.GraphQLResponse +import com.amplifyframework.core.Consumer +import com.amplifyframework.core.model.ModelPage +import com.amplifyframework.testmodels.lazy.Blog +import com.amplifyframework.testmodels.lazy.Post +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ApiLazyModelListTest { + + private val apiCategory = mockk() + private val expectedApiName = "myApi" + private val expectedQuery = "query ListPosts(\$filter: ModelPostFilterInput, \$limit: Int) {\n" + + " listPosts(filter: \$filter, limit: \$limit) {\n" + + " items {\n" + + " blog {\n" + + " id\n" + + " }\n" + + " createdAt\n" + + " id\n" + + " name\n" + + " updatedAt\n" + + " }\n" + + " nextToken\n" + + " }\n" + + "}\n" + private val expectedContent = "{\"query\": \"query ListPosts(\$filter: ModelPostFilterInput, \$limit: Int) " + + "{\\n listPosts(filter: \$filter, limit: \$limit) {\\n items {\\n blog {\\n id\\n }" + + "\\n createdAt\\n id\\n name\\n updatedAt\\n }\\n nextToken\\n }\\n}\\n\", " + + "\"variables\": {\"filter\":{\"blogPostsId\":{\"eq\":\"b1\"}},\"limit\":1000}}" + private val expectedVariables = "{filter={blogPostsId={eq=b1}}, limit=1000}" + + private val expectedNextToken = ApiPaginationToken("456") + private val expectedContentWithToken = "{\"query\": \"query ListPosts(\$filter: ModelPostFilterInput, " + + "\$limit: Int, \$nextToken: String) {\\n listPosts(filter: \$filter, limit: \$limit, " + + "nextToken: \$nextToken) {\\n items {\\n blog {\\n id\\n }\\n " + + "createdAt\\n id\\n name\\n updatedAt\\n }\\n nextToken\\n }\\n}\\n\", \"" + + "variables\": {\"filter\":{\"blogPostsId\":{\"eq\":\"b1\"}},\"limit\":1000,\"nextToken\":\"123\"}}" + + private val expectedQueryWithToken = + "query ListPosts(\$filter: ModelPostFilterInput, \$limit: Int, \$nextToken: String) {\n" + + " listPosts(filter: \$filter, limit: \$limit, nextToken: \$nextToken) {\n" + + " items {\n" + + " blog {\n" + + " id\n" + + " }\n" + + " createdAt\n" + + " id\n" + + " name\n" + + " updatedAt\n" + + " }\n" + + " nextToken\n" + + " }\n" + + "}\n" + private val expectedVariablesWithToken = "{filter={blogPostsId={eq=b1}}, limit=1000, nextToken=123}" + + private val items = listOf( + Post.builder().name("p1").blog(Blog.justId("b1")).build(), + Post.builder().name("p2").blog(Blog.justId("b1")).build(), + ) + + @Test + fun fetch_with_provided_api_success() = runTest { + val lazyPostList = ApiLazyModelList( + Post::class.java, + mapOf(Pair("blogPostsId", "b1")), + expectedApiName, + apiCategory + ) + val requestSlot = slot>() + + every { + apiCategory.query(any(), capture(requestSlot), any(), any()) + } answers { + thirdArg>>>().accept( + GraphQLResponse(ApiModelPage(items, null), null) + ) + mockk() + } + + val page = lazyPostList.fetchPage() + + assertEquals(items, page.items) + assertFalse(page.hasNextPage) + assertNull(page.nextToken) + assertEquals(expectedContent, requestSlot.captured.content) + assertEquals(expectedQuery, requestSlot.captured.query) + assertEquals(expectedVariables, requestSlot.captured.variables.toString()) + } + + @Test + fun fetch_with_no_provided_api_success() = runTest { + val lazyPostList = ApiLazyModelList( + Post::class.java, + mapOf(Pair("blogPostsId", "b1")), + null, + apiCategory + ) + val requestSlot = slot>() + + every { + apiCategory.query(capture(requestSlot), any(), any()) + } answers { + secondArg>>>().accept( + GraphQLResponse(ApiModelPage(items, null), null) + ) + mockk() + } + + val page = lazyPostList.fetchPage() + + assertEquals(items, page.items) + assertFalse(page.hasNextPage) + assertNull(page.nextToken) + assertEquals(expectedContent, requestSlot.captured.content) + assertEquals(expectedQuery, requestSlot.captured.query) + assertEquals(expectedVariables, requestSlot.captured.variables.toString()) + } + + @Test + fun fetch_with_provided_api_and_token_success() = runTest { + val lazyPostList = ApiLazyModelList( + Post::class.java, + mapOf(Pair("blogPostsId", "b1")), + expectedApiName, + apiCategory + ) + val expectedToken = ApiPaginationToken("456") + val requestSlot = slot>() + every { + apiCategory.query(any(), capture(requestSlot), any(), any()) + } answers { + thirdArg>>>().accept( + GraphQLResponse(ApiModelPage(items, expectedToken), null) + ) + mockk() + } + + val page = lazyPostList.fetchPage(ApiPaginationToken("123")) + + assertEquals(items, page.items) + assertTrue(page.hasNextPage) + assertNotNull(page.nextToken) + assertEquals(expectedToken, page.nextToken) + assertEquals(expectedContentWithToken, requestSlot.captured.content) + assertEquals(expectedQueryWithToken, requestSlot.captured.query) + assertEquals(expectedVariablesWithToken, requestSlot.captured.variables.toString()) + } + + @Test + fun fetch_with_provided_api_failure() = runTest { + val lazyPostList = ApiLazyModelList( + Post::class.java, + mapOf(Pair("blogPostsId", "b1")), + expectedApiName, + apiCategory + ) + val requestSlot = slot>() + val apiException = ApiException("fail", "fail") + val expectedException = AmplifyException("Error lazy loading the model list.", apiException, "fail") + + every { + apiCategory.query(any(), capture(requestSlot), any(), any()) + } answers { + lastArg>().accept(apiException) + mockk() + } + + var page: ModelPage? = null + var capturedException: AmplifyException? = null + try { + page = lazyPostList.fetchPage() + } catch (e: AmplifyException) { + capturedException = e + } + + assertNull(page) + assertEquals(expectedException, capturedException) + } + + @Test + fun fetch_by_callback_with_provided_api_success() = runTest { + val lazyPostList = ApiLazyModelList( + Post::class.java, + mapOf(Pair("blogPostsId", "b1")), + expectedApiName, + apiCategory + ) + val requestSlot = slot>() + val latch = CountDownLatch(1) + + every { + apiCategory.query(any(), capture(requestSlot), any(), any()) + } answers { + thirdArg>>>().accept( + GraphQLResponse(ApiModelPage(items, null), null) + ) + mockk() + } + + var page: ModelPage? = null + var capturedException: AmplifyException? = null + lazyPostList.fetchPage( + onSuccess = { + page = it + latch.countDown() + }, + onError = { + capturedException = it + latch.countDown() + } + ) + + latch.await(2, TimeUnit.SECONDS) + assertNotNull(page) + assertNull(capturedException) + + assertEquals(items, page!!.items) + assertFalse(page!!.hasNextPage) + assertNull(page!!.nextToken) + assertEquals(expectedContent, requestSlot.captured.content) + assertEquals(expectedQuery, requestSlot.captured.query) + assertEquals(expectedVariables, requestSlot.captured.variables.toString()) + } + + @Test + fun fetch_by_callback_with_token_provided_api_success() = runTest { + val lazyPostList = ApiLazyModelList( + Post::class.java, + mapOf(Pair("blogPostsId", "b1")), + expectedApiName, + apiCategory + ) + + val requestSlot = slot>() + val latch = CountDownLatch(1) + + every { + apiCategory.query(any(), capture(requestSlot), any(), any()) + } answers { + thirdArg>>>().accept( + GraphQLResponse(ApiModelPage(items, expectedNextToken), null) + ) + mockk() + } + + var page: ModelPage? = null + var capturedException: AmplifyException? = null + lazyPostList.fetchPage( + ApiPaginationToken("123"), + onSuccess = { + page = it + latch.countDown() + }, + onError = { + capturedException = it + latch.countDown() + } + ) + + latch.await(2, TimeUnit.SECONDS) + assertNotNull(page) + assertNull(capturedException) + + assertEquals(items, page!!.items) + assertTrue(page!!.hasNextPage) + assertEquals(expectedNextToken, page!!.nextToken) + assertEquals(expectedContentWithToken, requestSlot.captured.content) + assertEquals(expectedQueryWithToken, requestSlot.captured.query) + assertEquals(expectedVariablesWithToken, requestSlot.captured.variables.toString()) + } + + @Test + fun fetch_by_callback_with_provided_api_failure() = runTest { + val lazyPostList = ApiLazyModelList( + Post::class.java, + mapOf(Pair("blogPostsId", "b1")), + expectedApiName, + apiCategory + ) + val requestSlot = slot>() + val apiException = ApiException("fail", "fail") + val expectedException = AmplifyException("Error lazy loading the model list.", apiException, "fail") + val latch = CountDownLatch(1) + + every { + apiCategory.query(any(), capture(requestSlot), any(), any()) + } answers { + lastArg>().accept(apiException) + mockk() + } + + var page: ModelPage? = null + var capturedException: AmplifyException? = null + lazyPostList.fetchPage( + onSuccess = { + page = it + latch.countDown() + }, + onError = { + capturedException = it + latch.countDown() + } + ) + + latch.await(2, TimeUnit.SECONDS) + assertNull(page) + assertEquals(expectedException, capturedException) + } + + @Test + fun fetch_by_callback_with_no_provided_api_failure() = runTest { + val lazyPostList = ApiLazyModelList( + Post::class.java, + mapOf(Pair("blogPostsId", "b1")), + null, + apiCategory + ) + val requestSlot = slot>() + val apiException = ApiException("fail", "fail") + val expectedException = AmplifyException("Error lazy loading the model list.", apiException, "fail") + val latch = CountDownLatch(1) + + every { + apiCategory.query(capture(requestSlot), any(), any()) + } answers { + lastArg>().accept(apiException) + mockk() + } + + var page: ModelPage? = null + var capturedException: AmplifyException? = null + lazyPostList.fetchPage( + onSuccess = { + page = it + latch.countDown() + }, + onError = { + capturedException = it + latch.countDown() + } + ) + + latch.await(2, TimeUnit.SECONDS) + assertNull(page) + assertEquals(expectedException, capturedException) + } +} diff --git a/aws-api/src/test/java/com/amplifyframework/api/aws/ApiLazyModelReferenceTest.kt b/aws-api/src/test/java/com/amplifyframework/api/aws/ApiLazyModelReferenceTest.kt new file mode 100644 index 0000000000..a177d2ef49 --- /dev/null +++ b/aws-api/src/test/java/com/amplifyframework/api/aws/ApiLazyModelReferenceTest.kt @@ -0,0 +1,253 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.api.aws + +import com.amplifyframework.AmplifyException +import com.amplifyframework.api.ApiCategory +import com.amplifyframework.api.ApiException +import com.amplifyframework.api.graphql.GraphQLRequest +import com.amplifyframework.api.graphql.GraphQLResponse +import com.amplifyframework.core.Consumer +import com.amplifyframework.testmodels.lazy.Blog +import com.amplifyframework.testmodels.lazy.Post +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ApiLazyModelReferenceTest { + + private val apiCategory = mockk() + + private val expectedQuery = "query GetPost(\$id: ID!) {\n" + + " getPost(id: \$id) {\n" + + " blog {\n" + + " id\n" + + " }\n" + + " createdAt\n" + + " id\n" + + " name\n" + + " updatedAt\n" + + " }\n" + + "}\n" + private val expectedContent = "{\"query\": \"query GetPost(\$id: ID!) {\\n getPost(id: \$id) {\\n " + + "blog {\\n id\\n }\\n createdAt\\n id\\n name\\n updatedAt\\n }" + + "\\n}\\n\", \"variables\": {\"id\":\"p1\"}}" + private val expectedVariables = mapOf(Pair("id", "p1")) + private val expectedPost = Post.builder().name("My Post").blog(Blog.justId("b1")).build() + + @Test + fun fetch_with_provided_api_success_uses_cache_after() = runTest { + // GIVEN + val expectedApi = "myApi" + val requestSlot = slot>() + + every { + apiCategory.query(any(), capture(requestSlot), any>>(), any()) + } answers { + thirdArg>>().accept(GraphQLResponse(expectedPost, null)) + mockk() + } + val postReference = ApiLazyModelReference(Post::class.java, expectedVariables, expectedApi, apiCategory) + + // WHEN + val fetchedPost1 = postReference.fetchModel() + val fetchedPost2 = postReference.fetchModel() + + // THEN + verify(exactly = 1) { apiCategory.query(expectedApi, any(), any>>(), any()) } + assertEquals(expectedPost, fetchedPost1) + assertEquals(fetchedPost1, fetchedPost2) + assertEquals(expectedQuery, requestSlot.captured.query) + assertEquals(expectedContent, requestSlot.captured.content) + assertEquals(expectedVariables, requestSlot.captured.variables) + } + + @Test + fun fetch_default_api_success_uses_cache_after() = runTest { + // GIVEN + val requestSlot = slot>() + + every { apiCategory.query(capture(requestSlot), any>>(), any()) } answers { + secondArg>>().accept(GraphQLResponse(expectedPost, null)) + mockk() + } + val postReference = ApiLazyModelReference(Post::class.java, mapOf(Pair("id", "p1")), apiCategory = apiCategory) + + // WHEN + val fetchedPost1 = postReference.fetchModel() + val fetchedPost2 = postReference.fetchModel() + + // THEN + verify(exactly = 1) { apiCategory.query(any(), any>>(), any()) } + assertEquals(expectedPost, fetchedPost1) + assertEquals(fetchedPost1, fetchedPost2) + assertEquals(expectedQuery, requestSlot.captured.query) + assertEquals(expectedContent, requestSlot.captured.content) + assertEquals(expectedVariables, requestSlot.captured.variables) + } + + @Test + fun fetch_failure_tries_again() = runTest { + val expectedPost = Post.builder().name("My Post").blog(Blog.justId("b1")).build() + val expectedApi = "myApi" + val apiException = ApiException("fail", "fail") + val expectedException = AmplifyException("Error lazy loading the model.", apiException, "fail") + val postReference = ApiLazyModelReference(Post::class.java, mapOf(Pair("id", "p1")), expectedApi, apiCategory) + + // fail first time + every { apiCategory.query(any(), any(), any>>(), any()) } answers { + lastArg>().accept(apiException) + mockk() + } + + var fetchedPost1: Post? = null + var capturedException: AmplifyException? = null + try { + fetchedPost1 = postReference.fetchModel() + } catch (e: AmplifyException) { + capturedException = e + } + + assertNull(fetchedPost1) + assertEquals(expectedException, capturedException) + + // success second time + every { apiCategory.query(any(), any(), any>>(), any()) } answers { + thirdArg>>().accept(GraphQLResponse(expectedPost, null)) + mockk() + } + + val fetchedPost2 = postReference.fetchModel() + + verify(exactly = 2) { apiCategory.query(expectedApi, any(), any>>(), any()) } + assertNull(fetchedPost1) + assertEquals(expectedPost, fetchedPost2) + } + + @Test + fun empty_map_returns_null_model() = runTest { + // GIVEN + val postReference = ApiLazyModelReference(Post::class.java, emptyMap(), apiCategory = apiCategory) + + // WHEN + val fetchedPost1 = postReference.fetchModel() + + // THEN + verify(exactly = 0) { apiCategory.query(any(), any>>(), any()) } + assertNull(fetchedPost1) + } + + @Test + fun fetch_with_callbacks_with_provided_api_success_uses_cache_after() = runTest { + // GIVEN + val expectedApi = "myApi" + val requestSlot = slot>() + + every { + apiCategory.query(any(), capture(requestSlot), any>>(), any()) + } answers { + thirdArg>>().accept(GraphQLResponse(expectedPost, null)) + mockk() + } + val postReference = ApiLazyModelReference(Post::class.java, expectedVariables, expectedApi, apiCategory) + var fetchedPost1: Post? = null + var fetchedPost2: Post? = null + + // WHEN + var latch = CountDownLatch(1) + postReference.fetchModel({ fetchedPost1 = it; latch.countDown() }, {}) + latch.await(2, TimeUnit.SECONDS) + latch = CountDownLatch(1) + postReference.fetchModel({ fetchedPost2 = it; latch.countDown() }, {}) + latch.await(2, TimeUnit.SECONDS) + + // THEN + verify(exactly = 1) { apiCategory.query(expectedApi, any(), any>>(), any()) } + assertEquals(expectedPost, fetchedPost1) + assertEquals(fetchedPost1, fetchedPost2) + assertEquals(expectedQuery, requestSlot.captured.query) + assertEquals(expectedContent, requestSlot.captured.content) + assertEquals(expectedVariables, requestSlot.captured.variables) + } + + @Test + fun fetch_with_callbacks_failure_tries_again() = runTest { + val expectedPost = Post.builder().name("My Post").blog(Blog.justId("b1")).build() + val expectedApi = "myApi" + val apiException = ApiException("fail", "fail") + val expectedException = AmplifyException("Error lazy loading the model.", apiException, "fail") + val postReference = ApiLazyModelReference(Post::class.java, mapOf(Pair("id", "p1")), expectedApi, apiCategory) + var latch = CountDownLatch(1) + var fetchedPost1: Post? = null + var fetchedPost2: Post? = null + var capturedException1: AmplifyException? = null + var capturedException2: AmplifyException? = null + + // fail first time + every { apiCategory.query(any(), any(), any>>(), any()) } answers { + lastArg>().accept(apiException) + mockk() + } + + postReference.fetchModel( + { + fetchedPost1 = it + latch.countDown() + }, + { + capturedException1 = it + latch.countDown() + } + ) + + latch.await(2, TimeUnit.SECONDS) + latch = CountDownLatch(1) + + // success second time + every { apiCategory.query(any(), any(), any>>(), any()) } answers { + thirdArg>>().accept(GraphQLResponse(expectedPost, null)) + mockk() + } + + postReference.fetchModel( + { + fetchedPost2 = it + latch.countDown() + }, + { + capturedException2 = it + latch.countDown() + } + ) + + latch.await(2, TimeUnit.SECONDS) + verify(exactly = 2) { apiCategory.query(expectedApi, any(), any>>(), any()) } + assertNull(fetchedPost1) + assertEquals(expectedException, capturedException1) + assertNull(capturedException2) + assertEquals(expectedPost, fetchedPost2) + } +} diff --git a/aws-api/src/test/java/com/amplifyframework/api/aws/ApiLoadedModelListTest.kt b/aws-api/src/test/java/com/amplifyframework/api/aws/ApiLoadedModelListTest.kt new file mode 100644 index 0000000000..7faf4194f5 --- /dev/null +++ b/aws-api/src/test/java/com/amplifyframework/api/aws/ApiLoadedModelListTest.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.api.aws + +import com.amplifyframework.testmodels.lazy.Blog +import org.junit.Assert.assertEquals +import org.junit.Test + +class ApiLoadedModelListTest { + + @Test + fun loaded_list_provides_items() { + val expectedItems = listOf( + Blog.builder().name("b1").build(), + Blog.builder().name("b2").build() + ) + + val loadedList = ApiLoadedModelList(expectedItems) + + assertEquals(expectedItems, loadedList.items) + } +} diff --git a/aws-api/src/test/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactoryTest.java b/aws-api/src/test/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactoryTest.java index 5f4eb65bb8..d567a49693 100644 --- a/aws-api/src/test/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactoryTest.java +++ b/aws-api/src/test/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactoryTest.java @@ -32,6 +32,9 @@ import com.amplifyframework.datastore.DataStoreException; import com.amplifyframework.testmodels.ecommerce.Item; import com.amplifyframework.testmodels.ecommerce.Status; +import com.amplifyframework.testmodels.lazy.Blog; +import com.amplifyframework.testmodels.lazy.Post; +import com.amplifyframework.testmodels.lazy.PostPath; import com.amplifyframework.testmodels.meeting.Meeting; import com.amplifyframework.testmodels.personcar.MaritalStatus; import com.amplifyframework.testmodels.personcar.Person; @@ -49,6 +52,7 @@ import java.util.Map; import java.util.concurrent.TimeUnit; +import static com.amplifyframework.core.model.ModelPropertyPathKt.includes; import static org.junit.Assert.assertEquals; /** @@ -126,6 +130,98 @@ public void buildQueryFromClassAndPredicate() throws JSONException { ); } + /** + * Validate construction of a GraphQL create mutation for a lazy model. + * @throws JSONException from JSONAssert.assertEquals + */ + @Test + public void buildCreateMutationWithLazyModel() throws JSONException { + // Act: generate query + Blog blog = Blog.builder().name("My Blog").id("b1").build(); + Post post = Post.builder().name("My Post").blog(blog).id("p1").build(); + + GraphQLRequest request = + AppSyncGraphQLRequestFactory.buildMutation( + post, + QueryPredicates.all(), + MutationType.CREATE + ); + + // Validate request is expected request + JSONAssert.assertEquals( + Resources.readAsString("lazy_create_no_includes.txt"), + request.getContent(), + true + ); + } + + /** + * Validate construction of a GraphQL create mutation for a lazy model with includes. + * @throws JSONException from JSONAssert.assertEquals + */ + @Test + public void buildCreateMutationWithLazyModelAndIncludes() throws JSONException { + // Act: generate query + Blog blog = Blog.builder().name("My Blog").id("b1").build(); + Post post = Post.builder().name("My Post").blog(blog).id("p1").build(); + + GraphQLRequest request = + AppSyncGraphQLRequestFactory.buildMutation( + post, + QueryPredicates.all(), + MutationType.CREATE, + ((path) -> includes(path.getBlog(), path.getComments())) + ); + + // Validate request is expected request + JSONAssert.assertEquals( + Resources.readAsString("lazy_create_with_includes.txt"), + request.getContent(), + true + ); + } + + /** + * Validate construction of a GraphQL query for a lazy model. + * @throws JSONException from JSONAssert.assertEquals + */ + @Test + public void buildQueryFromLazyModel() throws JSONException { + // Act: generate query + GraphQLRequest request = + AppSyncGraphQLRequestFactory.buildQuery(Post.class, "p1"); + + // Validate request is expected request + JSONAssert.assertEquals( + Resources.readAsString("lazy_query_no_includes.json"), + request.getContent(), + true + ); + } + + /** + * Validate construction of a GraphQL query for a lazy model with includes. + * @throws JSONException from JSONAssert.assertEquals + */ + @Test + public void buildQueryFromLazyModelWithIncludes() throws JSONException { + // Act: generate query + GraphQLRequest request = + AppSyncGraphQLRequestFactory.buildQuery( + Post.class, + "p1", + ((path) -> includes(path.getBlog(), path.getComments())) + ); + + // Validate request is expected request + JSONAssert.assertEquals( + Resources.readAsString("lazy_query_with_includes.json"), + request.getContent(), + true + ); + } + + /** * Validates construction of a delete mutation query from a Person instance, a predicate. * @throws JSONException from JSONAssert.assertEquals diff --git a/aws-api/src/test/java/com/amplifyframework/api/aws/MultiAuthSubscriptionOperationTest.kt b/aws-api/src/test/java/com/amplifyframework/api/aws/MultiAuthSubscriptionOperationTest.kt index 382ba99a33..a1739f194c 100644 --- a/aws-api/src/test/java/com/amplifyframework/api/aws/MultiAuthSubscriptionOperationTest.kt +++ b/aws-api/src/test/java/com/amplifyframework/api/aws/MultiAuthSubscriptionOperationTest.kt @@ -211,6 +211,7 @@ class MultiAuthSubscriptionOperationTest { .executorService(executorService) .requestDecorator(requestDecorator) .graphQlRequest(graphQlRequest) + .responseFactory(mockk()) .build() return operation } diff --git a/aws-api/src/test/java/com/amplifyframework/graphql/model/ModelMutationTest.kt b/aws-api/src/test/java/com/amplifyframework/graphql/model/ModelMutationTest.kt new file mode 100644 index 0000000000..e0e36b6dbd --- /dev/null +++ b/aws-api/src/test/java/com/amplifyframework/graphql/model/ModelMutationTest.kt @@ -0,0 +1,221 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.graphql.model + +import com.amplifyframework.api.aws.AppSyncGraphQLRequestFactory +import com.amplifyframework.api.graphql.GraphQLRequest +import com.amplifyframework.api.graphql.MutationType +import com.amplifyframework.api.graphql.model.ModelMutation +import com.amplifyframework.core.model.includes +import com.amplifyframework.core.model.query.predicate.QueryPredicates +import com.amplifyframework.testmodels.lazy.Blog +import com.amplifyframework.testmodels.lazy.Post +import com.amplifyframework.testmodels.lazy.PostPath +import org.junit.Assert.assertEquals +import org.junit.Test + +class ModelMutationTest { + + @Test + fun create() { + val expectedClass = Post.builder().name("Post").blog(Blog.builder().name("Blog").build()).build() + val expectedType = MutationType.CREATE + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory + .buildMutation( + expectedClass, + QueryPredicates.all(), + expectedType + ) + + val actualRequest = ModelMutation.create(expectedClass) + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun create_with_includes() { + val expectedClass = Post.builder().name("Post").blog(Blog.builder().name("Blog").build()).build() + val expectedType = MutationType.CREATE + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory + .buildMutation( + expectedClass, + QueryPredicates.all(), + expectedType + ) { + includes(it.comments, it.blog) + } + + val actualRequest = ModelMutation.create(expectedClass) { + includes(it.comments, it.blog) + } + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun delete() { + val expectedClass = Post.builder().name("Post").blog(Blog.builder().name("Blog").build()).build() + val expectedType = MutationType.DELETE + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory + .buildMutation( + expectedClass, + QueryPredicates.all(), + expectedType + ) + + val actualRequest = ModelMutation.delete(expectedClass) + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun delete_with_predicate() { + val expectedClass = Post.builder().name("Post").blog(Blog.builder().name("Blog").build()).build() + val expectedType = MutationType.DELETE + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory + .buildMutation( + expectedClass, + QueryPredicates.all(), + expectedType + ) + + val actualRequest = ModelMutation.delete(expectedClass, QueryPredicates.all()) + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun delete_with_includes() { + val expectedClass = Post.builder().name("Post").blog(Blog.builder().name("Blog").build()).build() + val expectedType = MutationType.DELETE + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory + .buildMutation( + expectedClass, + QueryPredicates.all(), + expectedType + ) { + includes(it.comments, it.blog) + } + + val actualRequest = ModelMutation.delete(expectedClass) { + includes(it.comments, it.blog) + } + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun delete_with_predicate_with_includes() { + val expectedClass = Post.builder().name("Post").blog(Blog.builder().name("Blog").build()).build() + val expectedType = MutationType.DELETE + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory + .buildMutation( + expectedClass, + QueryPredicates.all(), + expectedType + ) { + includes(it.comments, it.blog) + } + + val actualRequest = ModelMutation.delete(expectedClass, QueryPredicates.all()) { + includes(it.comments, it.blog) + } + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun update() { + val expectedClass = Post.builder().name("Post").blog(Blog.builder().name("Blog").build()).build() + val expectedType = MutationType.UPDATE + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory + .buildMutation( + expectedClass, + QueryPredicates.all(), + expectedType + ) + + val actualRequest = ModelMutation.update(expectedClass) + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun update_with_predicate() { + val expectedClass = Post.builder().name("Post").blog(Blog.builder().name("Blog").build()).build() + val expectedType = MutationType.UPDATE + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory + .buildMutation( + expectedClass, + QueryPredicates.all(), + expectedType + ) + + val actualRequest = ModelMutation.update(expectedClass, QueryPredicates.all()) + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun update_with_includes() { + val expectedClass = Post.builder().name("Post").blog(Blog.builder().name("Blog").build()).build() + val expectedType = MutationType.UPDATE + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory + .buildMutation( + expectedClass, + QueryPredicates.all(), + expectedType + ) { + includes(it.comments, it.blog) + } + + val actualRequest = ModelMutation.update(expectedClass) { + includes(it.comments, it.blog) + } + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun update_with_includes_with_predicate() { + val expectedClass = Post.builder().name("Post").blog(Blog.builder().name("Blog").build()).build() + val expectedType = MutationType.UPDATE + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory + .buildMutation( + expectedClass, + QueryPredicates.all(), + expectedType + ) { + includes(it.comments, it.blog) + } + + val actualRequest = ModelMutation.update(expectedClass, QueryPredicates.all()) { + includes(it.comments, it.blog) + } + + assertEquals(expectedRequest, actualRequest) + } +} diff --git a/aws-api/src/test/java/com/amplifyframework/graphql/model/ModelQueryTest.kt b/aws-api/src/test/java/com/amplifyframework/graphql/model/ModelQueryTest.kt new file mode 100644 index 0000000000..e9150a2b61 --- /dev/null +++ b/aws-api/src/test/java/com/amplifyframework/graphql/model/ModelQueryTest.kt @@ -0,0 +1,244 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.graphql.model + +import com.amplifyframework.api.aws.AppSyncGraphQLRequestFactory +import com.amplifyframework.api.graphql.GraphQLRequest +import com.amplifyframework.api.graphql.PaginatedResult +import com.amplifyframework.api.graphql.model.ModelPagination +import com.amplifyframework.api.graphql.model.ModelQuery +import com.amplifyframework.core.model.includes +import com.amplifyframework.core.model.query.predicate.QueryPredicates +import com.amplifyframework.testmodels.lazy.Post +import com.amplifyframework.testmodels.lazy.PostPath +import org.junit.Assert.assertEquals +import org.junit.Test + +class ModelQueryTest { + + @Test + fun get_string_id() { + val expectedClass = Post::class.java + val expectedId = "p1" + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory.buildQuery( + expectedClass, + expectedId + ) + + val actualRequest = ModelQuery[expectedClass, expectedId] + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun get_string_id_passes_includes() { + val expectedClass = Post::class.java + val expectedId = "p1" + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory.buildQuery( + expectedClass, + expectedId + ) { + includes(it.comments, it.blog) + } + + val actualRequest = ModelQuery.get(expectedClass, expectedId) { + includes(it.comments, it.blog) + } + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun get_model_identifier() { + val expectedClass = Post::class.java + val expectedId = Post.PostIdentifier("p1") + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory.buildQuery( + expectedClass, + expectedId + ) + + val actualRequest = ModelQuery[expectedClass, expectedId] + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun get_model_identifier_passes_includes() { + val expectedClass = Post::class.java + val expectedId = Post.PostIdentifier("p1") + + val expectedRequest = AppSyncGraphQLRequestFactory.buildQuery( + expectedClass, + expectedId + ) { + includes(it.comments, it.blog) + } + + val actualRequest = ModelQuery.get(expectedClass, expectedId) { + includes(it.comments, it.blog) + } + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun list_with_default_query_predicate() { + val expectedClass = Post::class.java + + val expectedRequest = AppSyncGraphQLRequestFactory.buildQuery, Post>( + expectedClass, + QueryPredicates.all() + ) + + val actualRequest = ModelQuery.list(expectedClass) { + includes(it.comments, it.blog) + } + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun list_with_query_predicate() { + val expectedClass = Post::class.java + + val expectedRequest = AppSyncGraphQLRequestFactory.buildQuery, Post>( + expectedClass, + QueryPredicates.all() + ) + + val actualRequest = ModelQuery.list(expectedClass, QueryPredicates.all()) { + includes(it.comments, it.blog) + } + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun list_with_default_query_predicate_passes_includes() { + val expectedClass = Post::class.java + + val expectedRequest = AppSyncGraphQLRequestFactory.buildQuery, Post, PostPath>( + expectedClass, + QueryPredicates.all() + ) { + includes(it.comments, it.blog) + } + + val actualRequest = ModelQuery.list(expectedClass) { + includes(it.comments, it.blog) + } + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun list_with_query_predicate_passes_includes() { + val expectedClass = Post::class.java + + val expectedRequest = AppSyncGraphQLRequestFactory.buildQuery, Post, PostPath>( + expectedClass, + QueryPredicates.all() + ) { + includes(it.comments, it.blog) + } + + val actualRequest = ModelQuery.list(expectedClass, QueryPredicates.all()) { + includes(it.comments, it.blog) + } + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun list_with_query_predicate_and_pagination() { + val expectedClass = Post::class.java + + val expectedRequest = AppSyncGraphQLRequestFactory.buildPaginatedResultQuery, Post>( + expectedClass, + QueryPredicates.all(), + 10 + ) + + val actualRequest = ModelQuery.list( + expectedClass, + QueryPredicates.all(), + ModelPagination.limit(10) + ) + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun list_with_query_predicate_and_pagination_and_includes() { + val expectedClass = Post::class.java + + val expectedRequest = AppSyncGraphQLRequestFactory + .buildPaginatedResultQuery, Post, PostPath>( + expectedClass, + QueryPredicates.all(), + 10 + ) { + includes(it.comments, it.blog) + } + + val actualRequest = ModelQuery.list( + expectedClass, + QueryPredicates.all(), + ModelPagination.limit(10) + ) { + includes(it.comments, it.blog) + } + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun list_with_model_pagination() { + val expectedClass = Post::class.java + val expectedPagination = ModelPagination.limit(10) + + val expectedRequest = AppSyncGraphQLRequestFactory + .buildPaginatedResultQuery, Post>( + expectedClass, QueryPredicates.all(), 10 + ) + + val actualRequest = ModelQuery.list(expectedClass, expectedPagination) + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun list_with_model_pagination_passes_includes() { + val expectedClass = Post::class.java + val expectedPagination = ModelPagination.limit(10) + + val expectedRequest = AppSyncGraphQLRequestFactory + .buildPaginatedResultQuery, Post, PostPath>( + expectedClass, QueryPredicates.all(), 10 + ) { + includes(it.comments, it.blog) + } + + val actualRequest = ModelQuery.list(expectedClass, expectedPagination) { + includes(it.comments, it.blog) + } + + assertEquals(expectedRequest, actualRequest) + } +} diff --git a/aws-api/src/test/java/com/amplifyframework/graphql/model/ModelSubscriptionTest.kt b/aws-api/src/test/java/com/amplifyframework/graphql/model/ModelSubscriptionTest.kt new file mode 100644 index 0000000000..4e3a00f0ee --- /dev/null +++ b/aws-api/src/test/java/com/amplifyframework/graphql/model/ModelSubscriptionTest.kt @@ -0,0 +1,172 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.graphql.model + +import com.amplifyframework.api.aws.AppSyncGraphQLRequestFactory +import com.amplifyframework.api.graphql.GraphQLRequest +import com.amplifyframework.api.graphql.SubscriptionType +import com.amplifyframework.api.graphql.model.ModelSubscription +import com.amplifyframework.core.model.includes +import com.amplifyframework.testmodels.lazy.Post +import com.amplifyframework.testmodels.lazy.PostPath +import org.junit.Assert.assertEquals +import org.junit.Test + +class ModelSubscriptionTest { + + @Test + fun of() { + val expectedClass = Post::class.java + val expectedType = SubscriptionType.ON_CREATE + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory.buildSubscription( + expectedClass, + expectedType + ) + + val actualRequest = ModelSubscription.of(expectedClass, expectedType) + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun of_with_includes() { + val expectedClass = Post::class.java + val expectedType = SubscriptionType.ON_CREATE + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory + .buildSubscription( + expectedClass, + expectedType + ) { + includes(it.comments, it.blog) + } + + val actualRequest = ModelSubscription.of(expectedClass, expectedType) { + includes(it.comments, it.blog) + } + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun create() { + val expectedClass = Post::class.java + val expectedType = SubscriptionType.ON_CREATE + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory + .buildSubscription( + expectedClass, + expectedType + ) + + val actualRequest = ModelSubscription.onCreate(expectedClass) + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun create_with_includes() { + val expectedClass = Post::class.java + val expectedType = SubscriptionType.ON_CREATE + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory + .buildSubscription( + expectedClass, + expectedType + ) { + includes(it.comments, it.blog) + } + + val actualRequest = ModelSubscription.onCreate(expectedClass) { + includes(it.comments, it.blog) + } + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun delete() { + val expectedClass = Post::class.java + val expectedType = SubscriptionType.ON_DELETE + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory + .buildSubscription( + expectedClass, + expectedType + ) + + val actualRequest = ModelSubscription.onDelete(expectedClass) + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun delete_with_includes() { + val expectedClass = Post::class.java + val expectedType = SubscriptionType.ON_DELETE + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory + .buildSubscription( + expectedClass, + expectedType + ) { + includes(it.comments, it.blog) + } + + val actualRequest = ModelSubscription.onDelete(expectedClass) { + includes(it.comments, it.blog) + } + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun update() { + val expectedClass = Post::class.java + val expectedType = SubscriptionType.ON_UPDATE + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory + .buildSubscription( + expectedClass, + expectedType + ) + + val actualRequest = ModelSubscription.onUpdate(expectedClass) + + assertEquals(expectedRequest, actualRequest) + } + + @Test + fun update_with_includes() { + val expectedClass = Post::class.java + val expectedType = SubscriptionType.ON_UPDATE + + val expectedRequest: GraphQLRequest = AppSyncGraphQLRequestFactory + .buildSubscription( + expectedClass, + expectedType + ) { + includes(it.comments, it.blog) + } + + val actualRequest = ModelSubscription.onUpdate(expectedClass) { + includes(it.comments, it.blog) + } + + assertEquals(expectedRequest, actualRequest) + } +} diff --git a/aws-api/src/test/resources/lazy_create_no_includes.txt b/aws-api/src/test/resources/lazy_create_no_includes.txt new file mode 100644 index 0000000000..ca38e45ad5 --- /dev/null +++ b/aws-api/src/test/resources/lazy_create_no_includes.txt @@ -0,0 +1 @@ +{"query": "mutation CreatePost($input: CreatePostInput!) {\n createPost(input: $input) {\n blog {\n id\n }\n createdAt\n id\n name\n updatedAt\n }\n}\n", "variables": {"input":{"blogPostsId":"b1","name":"My Post","id":"p1"}}} \ No newline at end of file diff --git a/aws-api/src/test/resources/lazy_create_with_includes.txt b/aws-api/src/test/resources/lazy_create_with_includes.txt new file mode 100644 index 0000000000..e03262e6c6 --- /dev/null +++ b/aws-api/src/test/resources/lazy_create_with_includes.txt @@ -0,0 +1 @@ +{"query": "mutation CreatePost($input: CreatePostInput!) {\n createPost(input: $input) {\n blog {\n createdAt\n id\n name\n updatedAt\n }\n comments {\n items {\n createdAt\n id\n post {\n id\n }\n text\n updatedAt\n }\n }\n createdAt\n id\n name\n updatedAt\n }\n}\n", "variables": {"input":{"blogPostsId":"b1","name":"My Post","id":"p1"}}} \ No newline at end of file diff --git a/aws-api/src/test/resources/lazy_query_no_includes.json b/aws-api/src/test/resources/lazy_query_no_includes.json new file mode 100644 index 0000000000..1db860ca83 --- /dev/null +++ b/aws-api/src/test/resources/lazy_query_no_includes.json @@ -0,0 +1 @@ +{"query": "query GetPost($id: ID!) {\n getPost(id: $id) {\n blog {\n id\n }\n createdAt\n id\n name\n updatedAt\n }\n}\n", "variables": {"id":"p1"}} \ No newline at end of file diff --git a/aws-api/src/test/resources/lazy_query_with_includes.json b/aws-api/src/test/resources/lazy_query_with_includes.json new file mode 100644 index 0000000000..3f67515c64 --- /dev/null +++ b/aws-api/src/test/resources/lazy_query_with_includes.json @@ -0,0 +1 @@ +{"query": "query GetPost($id: ID!) {\n getPost(id: $id) {\n blog {\n createdAt\n id\n name\n updatedAt\n }\n comments {\n items {\n createdAt\n id\n post {\n id\n }\n text\n updatedAt\n }\n }\n createdAt\n id\n name\n updatedAt\n }\n}\n", "variables": {"id":"p1"}} \ No newline at end of file diff --git a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/MFAHelper.kt b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/MFAHelper.kt index 2b6e29a56e..219b43e370 100644 --- a/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/MFAHelper.kt +++ b/aws-auth-cognito/src/main/java/com/amplifyframework/auth/cognito/helpers/MFAHelper.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 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. + */ + package com.amplifyframework.auth.cognito.helpers import com.amplifyframework.auth.MFAType diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/TOTPSetupDetailsTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/TOTPSetupDetailsTest.kt index 11b2197955..c12e6ec33b 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/TOTPSetupDetailsTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/TOTPSetupDetailsTest.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 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. + */ + package com.amplifyframework.auth import org.junit.Assert.assertEquals diff --git a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/options/APIOptionsKotlinContractTest.kt b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/options/APIOptionsKotlinContractTest.kt index 179ceb94f5..14eba8f493 100644 --- a/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/options/APIOptionsKotlinContractTest.kt +++ b/aws-auth-cognito/src/test/java/com/amplifyframework/auth/cognito/options/APIOptionsKotlinContractTest.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 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. + */ + package com.amplifyframework.auth.cognito.options import com.amplifyframework.auth.AuthUserAttribute diff --git a/aws-datastore/src/test/java/com/amplifyframework/datastore/syncengine/ReachabilityMonitorTest.kt b/aws-datastore/src/test/java/com/amplifyframework/datastore/syncengine/ReachabilityMonitorTest.kt index bc0bd81f2f..cd078f0dc7 100644 --- a/aws-datastore/src/test/java/com/amplifyframework/datastore/syncengine/ReachabilityMonitorTest.kt +++ b/aws-datastore/src/test/java/com/amplifyframework/datastore/syncengine/ReachabilityMonitorTest.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 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. + */ + package com.amplifyframework.datastore.syncengine import android.content.Context diff --git a/aws-datastore/src/test/java/com/amplifyframework/datastore/syncengine/TestSchedulerProvider.kt b/aws-datastore/src/test/java/com/amplifyframework/datastore/syncengine/TestSchedulerProvider.kt index a1c3464288..b98c1410ef 100644 --- a/aws-datastore/src/test/java/com/amplifyframework/datastore/syncengine/TestSchedulerProvider.kt +++ b/aws-datastore/src/test/java/com/amplifyframework/datastore/syncengine/TestSchedulerProvider.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 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. + */ + package com.amplifyframework.datastore.syncengine import io.reactivex.rxjava3.schedulers.TestScheduler diff --git a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/options/AWSFaceLivenessSessionOptions.kt b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/options/AWSFaceLivenessSessionOptions.kt index 54b947fc36..736ca25ae9 100644 --- a/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/options/AWSFaceLivenessSessionOptions.kt +++ b/aws-predictions/src/main/java/com/amplifyframework/predictions/aws/options/AWSFaceLivenessSessionOptions.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2023 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. + */ + package com.amplifyframework.predictions.aws.options import com.amplifyframework.annotations.InternalAmplifyApi diff --git a/configuration/checkstyle-suppressions.xml b/configuration/checkstyle-suppressions.xml index 0bdb79b604..b1ab57bd43 100644 --- a/configuration/checkstyle-suppressions.xml +++ b/configuration/checkstyle-suppressions.xml @@ -33,5 +33,6 @@ + diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 2305a8a305..c749fdd5a9 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -47,6 +47,7 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.test.mockito.core) testImplementation(libs.test.mockito.inline) + testImplementation(libs.test.mockk) testImplementation(libs.test.robolectric) testImplementation(libs.rxjava) testImplementation(libs.test.androidx.core) diff --git a/core/src/main/java/com/amplifyframework/api/graphql/GraphQLOperation.java b/core/src/main/java/com/amplifyframework/api/graphql/GraphQLOperation.java index 2dab61c8c5..c299199005 100644 --- a/core/src/main/java/com/amplifyframework/api/graphql/GraphQLOperation.java +++ b/core/src/main/java/com/amplifyframework/api/graphql/GraphQLOperation.java @@ -26,6 +26,8 @@ * @param The type of data contained in the GraphQLResponse. */ public abstract class GraphQLOperation extends ApiOperation> { + + // responseFactory used to parse responses private final GraphQLResponse.Factory responseFactory; /** @@ -48,7 +50,7 @@ public GraphQLOperation( * @return wrapped response object * @throws ApiException If the class provided mismatches the data */ - protected final GraphQLResponse wrapResponse(String jsonResponse) throws ApiException { + protected GraphQLResponse wrapResponse(String jsonResponse) throws ApiException { try { return responseFactory.buildResponse(getRequest(), jsonResponse); } catch (ClassCastException cce) { @@ -56,4 +58,12 @@ protected final GraphQLResponse wrapResponse(String jsonResponse) throws ApiE AmplifyException.TODO_RECOVERY_SUGGESTION); } } + + /** + * Provides the GraphQLResponse.Factory for extending methods. + * @return responseFactory provided + */ + protected final GraphQLResponse.Factory getResponseFactory() { + return responseFactory; + } } diff --git a/core/src/main/java/com/amplifyframework/core/NullableConsumer.java b/core/src/main/java/com/amplifyframework/core/NullableConsumer.java new file mode 100644 index 0000000000..ec752c2eaf --- /dev/null +++ b/core/src/main/java/com/amplifyframework/core/NullableConsumer.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.core; + +import androidx.annotation.Nullable; + +/** + * A consumer of a nullable value type. + * @param Type of thing being consumed + */ +@SuppressWarnings("EmptyMethod") // Lint looks for class impl, not lambda (as almost all uses are) +public interface NullableConsumer { + + /** + * Accept a value. + * @param value A value + */ + void accept(@Nullable T value); +} diff --git a/core/src/main/java/com/amplifyframework/core/model/LoadedModelReferenceImpl.kt b/core/src/main/java/com/amplifyframework/core/model/LoadedModelReferenceImpl.kt new file mode 100644 index 0000000000..d86fafb875 --- /dev/null +++ b/core/src/main/java/com/amplifyframework/core/model/LoadedModelReferenceImpl.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.core.model + +import com.amplifyframework.annotations.InternalAmplifyApi + +@InternalAmplifyApi +class LoadedModelReferenceImpl( + override val value: M? = null +) : LoadedModelReference { + + override fun getIdentifier() = emptyMap() +} diff --git a/core/src/main/java/com/amplifyframework/core/model/ModelException.kt b/core/src/main/java/com/amplifyframework/core/model/ModelException.kt new file mode 100644 index 0000000000..130d53803d --- /dev/null +++ b/core/src/main/java/com/amplifyframework/core/model/ModelException.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.core.model + +import com.amplifyframework.AmplifyException + +sealed class ModelException( + message: String, + recoverySuggestion: String, + cause: Exception? = null +) : AmplifyException(message, cause, recoverySuggestion) { + + class PropertyPathNotFound( + val modelName: String, + cause: Exception? = null + ) : ModelException( + "The root property path for the model $modelName could not be found", + "Check if the model types were generated with the latest Amplify CLI and try again", + cause + ) +} diff --git a/core/src/main/java/com/amplifyframework/core/model/ModelField.java b/core/src/main/java/com/amplifyframework/core/model/ModelField.java index 284852ef6b..44fb34a0cf 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelField.java +++ b/core/src/main/java/com/amplifyframework/core/model/ModelField.java @@ -54,6 +54,12 @@ public final class ModelField { // True if the field is an instance of model. private final boolean isModel; + // True if the field is an instance of ModelReference. + private final boolean isModelReference; + + // True if the field is an instance of ModelList. + private final boolean isModelList; + // True if the field is an instance of CustomType private final boolean isCustomType; @@ -72,6 +78,8 @@ private ModelField(@NonNull ModelFieldBuilder builder) { this.isArray = builder.isArray; this.isEnum = builder.isEnum; this.isModel = builder.isModel; + this.isModelReference = builder.isModelReference; + this.isModelList = builder.isModelList; this.isCustomType = builder.isCustomType; this.authRules = builder.authRules; } @@ -154,6 +162,24 @@ public boolean isModel() { return isModel; } + /** + * Returns true if the field's target type is ModelReference. + * + * @return True if the field's target type is ModelReference. + */ + public boolean isModelReference() { + return isModelReference; + } + + /** + * Returns true if the field's target type is ModelList. + * + * @return True if the field's target type is ModelList. + */ + public boolean isModelList() { + return isModelList; + } + /** * Returns true if the field's target type is CustomType. * @@ -198,6 +224,12 @@ public boolean equals(Object thatObject) { if (isModel != that.isModel) { return false; } + if (isModelReference != that.isModelReference) { + return false; + } + if (isModelList != that.isModelList) { + return false; + } if (isCustomType != that.isCustomType) { return false; } @@ -220,6 +252,8 @@ public int hashCode() { result = 31 * result + (isArray ? 1 : 0); result = 31 * result + (isEnum ? 1 : 0); result = 31 * result + (isModel ? 1 : 0); + result = 31 * result + (isModelReference ? 1 : 0); + result = 31 * result + (isModelList ? 1 : 0); result = 31 * result + (isCustomType ? 1 : 0); return result; } @@ -235,6 +269,8 @@ public String toString() { ", isArray=" + isArray + ", isEnum=" + isEnum + ", isModel=" + isModel + + ", isModelReference=" + isModelReference + + ", isModelList=" + isModelList + ", isCustomType=" + isCustomType + '}'; } @@ -269,6 +305,12 @@ public static class ModelFieldBuilder { // True if the field's target type is Model. private boolean isModel = false; + // True if the field's target type is a ModelReference type. + private boolean isModelReference = false; + + // True if the field's target type is a ModelList type. + private boolean isModelList = false; + // True if the field's target type is CustomType. private boolean isCustomType = false; @@ -365,6 +407,26 @@ public ModelFieldBuilder isModel(boolean isModel) { return this; } + /** + * Sets a flag indicating whether or not the field's target type is a ModelReference. + * @param isModelReference flag indicating if the field is a ModelReference type + * @return the builder object + */ + public ModelFieldBuilder isModelReference(boolean isModelReference) { + this.isModelReference = isModelReference; + return this; + } + + /** + * Sets a flag indicating whether or not the field's type is a ModelList type. + * @param isModelList flag indicating if the field is a ModelList type + * @return the builder object + */ + public ModelFieldBuilder isModelList(boolean isModelList) { + this.isModelList = isModelList; + return this; + } + /** * Sets a flag indicating whether or not the field's target type is a Model. * @param isCustomType flag indicating if the field is a model diff --git a/core/src/main/java/com/amplifyframework/core/model/ModelList.kt b/core/src/main/java/com/amplifyframework/core/model/ModelList.kt new file mode 100644 index 0000000000..457faf1b8f --- /dev/null +++ b/core/src/main/java/com/amplifyframework/core/model/ModelList.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.core.model + +import com.amplifyframework.AmplifyException +import com.amplifyframework.core.Consumer +import kotlin.jvm.Throws + +/** + * The base wrapper class for providing a list of models. + */ +sealed interface ModelList + +/** + * A wrapped list of preloaded models that were included in the selection set. + */ +interface LoadedModelList : ModelList { + + /** The list of preloaded models. */ + val items: List +} + +/** + * A wrapped list of models that must be fetched. + */ +interface LazyModelList : ModelList { + + /** + * Loads the next page of models. + * + * @throws AmplifyException when loading the page fails. + * @param paginationToken the pagination token to use during load. + * @return the next page of models. + */ + @JvmSynthetic + @Throws(AmplifyException::class) + suspend fun fetchPage(paginationToken: PaginationToken? = null): ModelPage + + /** + * Loads the next page of models. + * + * @param onSuccess called upon successfully loading the next page of models. + * @param onError called when loading the page fails. + */ + fun fetchPage( + onSuccess: Consumer>, + onError: Consumer + ) + + /** + * Loads the next page of models. + * + * @param paginationToken the pagination token to use during load. + * @param onSuccess called upon successfully loading the next page of models. + * @param onError called when loading the page fails. + */ + fun fetchPage( + paginationToken: PaginationToken?, + onSuccess: Consumer>, + onError: Consumer + ) +} + +/** + * Token providing information on the next page to load. + */ +interface PaginationToken + +/** + * A page of loaded models. + */ +interface ModelPage { + + /** The list of loaded models. */ + val items: List + + /** The token that can be used to load the next page. */ + val nextToken: PaginationToken? + + /** Whether the next page is available. */ + val hasNextPage: Boolean + get() = nextToken != null +} diff --git a/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt b/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt new file mode 100644 index 0000000000..92c22a9869 --- /dev/null +++ b/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt @@ -0,0 +1,136 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.core.model + +import com.amplifyframework.annotations.InternalAmplifyApi + +/** + * Represents a property of a `Model`. PropertyPath is a way of representing the + * structure of a model with static typing, so developers can reference model + * properties in queries and other functionality that require them. + */ +interface PropertyPath { + + /** + * Access the property metadata. + * + * **Implementation note:** this function is in place over an implicit accessor over + * a property named `metadata` in order to avoid name conflict with the actual property + * names that will get generate from the `Model`. + * + * @return the property metadata, that contains the name and a reference to its parent. + */ + fun getMetadata(): PropertyPathMetadata +} + +/** + * Runtime information about a property. Its `name` and `parent` property reference, + * as well as whether the property represents a collection of the type or not. + */ +data class PropertyPathMetadata internal constructor( + /** + * Name of node path + */ + val name: String, + + /** + * Whether or not the path is a collection + */ + val isCollection: Boolean = false, + + /** + * Parent node path, if any + */ + val parent: PropertyPath? = null +) + +/** + * This interface is used to mark a property path as being a container + * for other properties. + * + * @see ModelPath for a more concrete representation of a property container + */ +interface PropertyContainerPath : PropertyPath { + + /** + * Returns the model type of the property container. + */ + fun getModelType(): Class +} + +/** + * Represents the `Model` structure itself, a container of property references. + */ +open class ModelPath protected constructor( + private val name: String, + private val isCollection: Boolean = false, + private val parent: PropertyPath? = null, + private val modelType: Class +) : PropertyContainerPath { + + @InternalAmplifyApi + override fun getMetadata() = PropertyPathMetadata( + name = name, + isCollection = isCollection, + parent = parent + ) + + @InternalAmplifyApi + override fun getModelType(): Class = modelType as Class + + companion object { + + /** + * Attempts to get a reference to the root property path of a given model + * of type `M`. This uses reflection to allow models created before the + * `PropertyPath` type was added to continue working without disruption + * of the development workflow. + * + * @return the `P : ModelPath` + * @throws ModelException.PropertyPathNotFound in case the path could not be read or found. + */ + @Throws(ModelException.PropertyPathNotFound::class) + @InternalAmplifyApi + fun > getRootPath(clazz: Class): P { + val field = try { + clazz.getDeclaredField("rootPath") + } catch (e: NoSuchFieldException) { + throw ModelException.PropertyPathNotFound(clazz.simpleName) + } + field.isAccessible = true + val path = field.get(null) as? P + return path ?: throw ModelException.PropertyPathNotFound(clazz.simpleName) + } + } +} + +/** + * Function used to define which relationships are included in the selection set + * in an idiomatic manner. It's a simple delegation to `listOf` with the main + * goal of improved code readability. + * + * Example: + * + * ```kotlin + * ModelQuery.get(Post::class.java, "id") { postPath -> + * includes(postPath.comments) + * } + * ``` + * + * @param relationships the relationships that should be included + * @return the passed associations as an array + */ +fun includes(vararg relationships: PropertyContainerPath) = listOf(*relationships) diff --git a/core/src/main/java/com/amplifyframework/core/model/ModelReference.kt b/core/src/main/java/com/amplifyframework/core/model/ModelReference.kt new file mode 100644 index 0000000000..bb50d74f57 --- /dev/null +++ b/core/src/main/java/com/amplifyframework/core/model/ModelReference.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.core.model + +import com.amplifyframework.AmplifyException +import com.amplifyframework.annotations.InternalAmplifyApi +import com.amplifyframework.core.Consumer +import com.amplifyframework.core.NullableConsumer + +/** + * Base interface for a class holding a Model type + */ +sealed interface ModelReference { + @InternalAmplifyApi + fun getIdentifier(): Map +} + +/** + * A reference holder that holds an in-memory model + */ +interface LoadedModelReference : ModelReference { + + /** The loaded model value */ + val value: M? +} + +/** + * A reference holder that allows lazily loading a model + */ +interface LazyModelReference : ModelReference { + + /** + * Load the model instance represented by this LazyModelReference. + * + * @throws AmplifyException If loading the model fails. + * @return The lazily loaded model or null if no such model exists. + */ + @JvmSynthetic + @Throws(AmplifyException::class) + suspend fun fetchModel(): M? + + /** + * Load the model instance represented by this LazyModelReference. + * + * @param onSuccess Called upon successfully loading the model. + * @param onError Called when loading the model fails. + */ + fun fetchModel(onSuccess: NullableConsumer, onError: Consumer) +} diff --git a/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java b/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java index 6ceb595039..d966fd26ff 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java +++ b/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java @@ -32,6 +32,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -210,7 +211,14 @@ private static ModelField createModelField(Field field) { field.getAnnotation(com.amplifyframework.core.model.annotations.ModelField.class); if (annotation != null) { final String fieldName = field.getName(); - final Class fieldType = field.getType(); + final Class fieldType; + if (field.getType() == ModelReference.class && field.getGenericType() + instanceof ParameterizedType) { + ParameterizedType pType = (ParameterizedType) field.getGenericType(); + fieldType = (Class) pType.getActualTypeArguments()[0]; + } else { + fieldType = field.getType(); + } final String targetType = annotation.targetType(); final List authRules = new ArrayList<>(); for (com.amplifyframework.core.model.annotations.AuthRule ruleAnnotation : annotation.authRules()) { @@ -225,6 +233,8 @@ private static ModelField createModelField(Field field) { .isArray(Collection.class.isAssignableFrom(field.getType())) .isEnum(Enum.class.isAssignableFrom(field.getType())) .isModel(Model.class.isAssignableFrom(field.getType())) + .isModelReference(ModelReference.class.isAssignableFrom(field.getType())) + .isModelList(ModelList.class.isAssignableFrom(field.getType())) .authRules(authRules) .build(); } @@ -247,6 +257,7 @@ private static ModelAssociation createModelAssociation(Field field) { HasOne association = Objects.requireNonNull(field.getAnnotation(HasOne.class)); return ModelAssociation.builder() .name(HasOne.class.getSimpleName()) + .targetNames(association.targetNames()) .associatedName(association.associatedWith()) .associatedType(association.type().getSimpleName()) .build(); diff --git a/core/src/main/java/com/amplifyframework/core/model/SchemaRegistry.java b/core/src/main/java/com/amplifyframework/core/model/SchemaRegistry.java index 5bde1046d2..8c56ff0415 100644 --- a/core/src/main/java/com/amplifyframework/core/model/SchemaRegistry.java +++ b/core/src/main/java/com/amplifyframework/core/model/SchemaRegistry.java @@ -48,7 +48,7 @@ public synchronized void register(@NonNull Set> models) t for (Class modelClass : models) { final String modelClassName = modelClass.getSimpleName(); final ModelSchema modelSchema = ModelSchema.fromModelClass(modelClass); - modelSchemaMap.put(modelClassName, modelSchema); + SchemaRegistryUtils.registerSchema(modelSchemaMap, modelClassName, modelSchema); } } @@ -57,7 +57,7 @@ public synchronized void register(@NonNull Set> models) t * @param modelSchemas the map that contains mapping of ModelName to ModelSchema. */ public synchronized void register(@NonNull Map modelSchemas) { - modelSchemaMap.putAll(modelSchemas); + SchemaRegistryUtils.registerSchemas(modelSchemaMap, modelSchemas); } /** @@ -69,7 +69,7 @@ public synchronized void register(@NonNull Map modelSchemas public synchronized void register( @NonNull Map modelSchemas, @NonNull Map customTypeSchemas) { - modelSchemaMap.putAll(modelSchemas); + SchemaRegistryUtils.registerSchemas(modelSchemaMap, modelSchemas); customTypeSchemaMap.putAll(customTypeSchemas); } @@ -79,7 +79,7 @@ public synchronized void register( * @param modelSchema schema of the model to be registered. */ public synchronized void register(@NonNull String modelName, @NonNull ModelSchema modelSchema) { - modelSchemaMap.put(modelName, modelSchema); + SchemaRegistryUtils.registerSchema(modelSchemaMap, modelName, modelSchema); } /** diff --git a/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.kt b/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.kt new file mode 100644 index 0000000000..2b6c8f0afd --- /dev/null +++ b/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.core.model + +import com.amplifyframework.core.model.annotations.ModelConfig +import com.amplifyframework.datastore.DataStoreException.IrRecoverableException +import java.lang.NullPointerException + +internal object SchemaRegistryUtils { + + /** + * Registers the ModelSchema's while filtering out unsupported lazy types + */ + @JvmStatic + fun registerSchemas( + modelSchemaMap: MutableMap, + modelSchemas: Map? = null, + ) { + modelSchemas?.forEach { (name, schema) -> + registerSchema(modelSchemaMap, name, schema) + } + } + + /** + * Registers the ModelSchema while filtering out unsupported lazy types + */ + @JvmStatic + fun registerSchema( + modelSchemaMap: MutableMap, + modelName: String, + modelSchema: ModelSchema + ) { + + try { + if (modelSchema.modelClass.getAnnotation(ModelConfig::class.java)?.hasLazySupport == true) { + throw IrRecoverableException( + "Unsupported model type. Lazy model types are not yet supported on DataStore.", + "Regenerate models with generatemodelsforlazyloadandcustomselectionset=false." + ) + } + } catch (npe: NullPointerException) { + /* + modelSchema.modelClass could throw if modelClass was not set from builder. + This is likely not a valid scenario, as modelClass should be required, but + we have a number of test cases that don't provide one. Since the builder is public and + modelClass isn't a mandatory builder param, we add this block for additional safety. + */ + } + + modelSchemaMap[modelName] = modelSchema + } +} diff --git a/core/src/main/java/com/amplifyframework/core/model/annotations/HasOne.java b/core/src/main/java/com/amplifyframework/core/model/annotations/HasOne.java index 5b2ae729fe..c00322f11c 100644 --- a/core/src/main/java/com/amplifyframework/core/model/annotations/HasOne.java +++ b/core/src/main/java/com/amplifyframework/core/model/annotations/HasOne.java @@ -49,4 +49,11 @@ * @return the name of the corresponding field in the other model. */ String associatedWith(); + + /** + * Returns the target names of foreign key when there is a primary key and at least one sort key. + * These are the names that will be used to store foreign key. + * @return the target names of foreign key. + */ + String[] targetNames() default {}; } diff --git a/core/src/main/java/com/amplifyframework/core/model/annotations/ModelConfig.java b/core/src/main/java/com/amplifyframework/core/model/annotations/ModelConfig.java index fdb5830a17..5b8fc3ed49 100644 --- a/core/src/main/java/com/amplifyframework/core/model/annotations/ModelConfig.java +++ b/core/src/main/java/com/amplifyframework/core/model/annotations/ModelConfig.java @@ -71,4 +71,10 @@ * @return Version of Model. */ int version() default 0; + + /** + * Specifies if a Model supports fields with lazy types. + * @return true if model support fields with lazy types. + */ + boolean hasLazySupport() default false; } diff --git a/core/src/test/java/com/amplifyframework/core/model/LoadedModelReferenceImplTest.kt b/core/src/test/java/com/amplifyframework/core/model/LoadedModelReferenceImplTest.kt new file mode 100644 index 0000000000..80eec0db0c --- /dev/null +++ b/core/src/test/java/com/amplifyframework/core/model/LoadedModelReferenceImplTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.core.model + +import com.amplifyframework.testmodels.lazy.Comment +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class LoadedModelReferenceImplTest { + + @Test + fun model_reference_provides_value() { + val expectedComment = Comment.builder().text("Hello").post(mockk()).build() + val loadedModelReference = LoadedModelReferenceImpl(expectedComment) + + assertEquals(expectedComment, loadedModelReference.value) + assertEquals(0, loadedModelReference.getIdentifier().size) + } + + @Test + fun null_reference_provides_null_value() { + val loadedModelReference = LoadedModelReferenceImpl(null) + + assertNull(loadedModelReference.value) + assertEquals(0, loadedModelReference.getIdentifier().size) + } +} diff --git a/core/src/test/java/com/amplifyframework/core/model/ModelPathTest.kt b/core/src/test/java/com/amplifyframework/core/model/ModelPathTest.kt new file mode 100644 index 0000000000..8c5b8ad44c --- /dev/null +++ b/core/src/test/java/com/amplifyframework/core/model/ModelPathTest.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.core.model + +import com.amplifyframework.testmodels.lazy.Post +import com.amplifyframework.testmodels.todo.Todo +import org.junit.Assert.assertEquals +import org.junit.Test + +class ModelPathTest { + + @Test + fun get_path_directly_from_model() { + val expectedMetadata = PropertyPathMetadata("root", false, null) + + val postPath = Post.rootPath + + assertEquals(expectedMetadata, postPath.getMetadata()) + assertEquals(Post::class.java, postPath.getModelType()) + } + + @Test + fun get_path_from_model_path() { + val expectedMetadata = PropertyPathMetadata("root", false, null) + + val actualPath = ModelPath.getRootPath(Post::class.java) + + assertEquals(Post::class.java, actualPath.getModelType()) + assertEquals(expectedMetadata, actualPath.getMetadata()) + } + + @Test + fun includes_provides_list_of_relationships() { + val postPath = Post.rootPath + val expectedRelationships = listOf(postPath.blog, postPath.comments) + + val acutalRelationships = includes(postPath.blog, postPath.comments) + + assertEquals(expectedRelationships, acutalRelationships) + } + + @Test(expected = ModelException.PropertyPathNotFound::class) + fun get_root_path_fails_on_non_lazy_supported_model() { + ModelPath.getRootPath(Todo::class.java) + } +} diff --git a/core/src/test/java/com/amplifyframework/core/model/ModelSchemaTest.java b/core/src/test/java/com/amplifyframework/core/model/ModelSchemaTest.java index 7dd9964102..afaf6ebb16 100644 --- a/core/src/test/java/com/amplifyframework/core/model/ModelSchemaTest.java +++ b/core/src/test/java/com/amplifyframework/core/model/ModelSchemaTest.java @@ -20,6 +20,8 @@ import com.amplifyframework.testmodels.commentsblog.BlogOwner; import com.amplifyframework.testmodels.ecommerce.Item; import com.amplifyframework.testmodels.ecommerce.Order; +import com.amplifyframework.testmodels.lazy.Blog; +import com.amplifyframework.testmodels.lazy.Post; import com.amplifyframework.testmodels.personcar.MaritalStatus; import com.amplifyframework.testmodels.personcar.Person; @@ -33,7 +35,9 @@ import java.util.Set; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; /** * Tests the {@link ModelSchema}. @@ -122,6 +126,104 @@ public void modelSchemaIsGeneratedForPersonModel() throws AmplifyException { assertSame(actualModelSchema, modelSchemaSet.iterator().next()); } + /** + * The factory {@link ModelSchema#fromModelClass(Class)} will produce + * an {@link ModelSchema} that meets our expectations for the {@link Post} model. + * @throws AmplifyException from model schema parsing + */ + @Test + public void modelSchemaAllowsLazyTypes() throws AmplifyException { + Map expectedFields = new HashMap<>(); + + expectedFields.put("id", ModelField.builder() + .targetType("ID") + .name("id") + .javaClassForValue(String.class) + .isRequired(true) + .build()); + expectedFields.put("name", ModelField.builder() + .targetType("String") + .name("name") + .javaClassForValue(String.class) + .isRequired(true) + .build()); + expectedFields.put("createdAt", ModelField.builder() + .targetType("AWSDateTime") + .name("createdAt") + .javaClassForValue(Temporal.DateTime.class) + .isReadOnly(true) + .build()); + expectedFields.put("updatedAt", ModelField.builder() + .targetType("AWSDateTime") + .name("updatedAt") + .javaClassForValue(Temporal.DateTime.class) + .isReadOnly(true) + .build()); + expectedFields.put("blog", ModelField.builder() + .targetType("Blog") + .name("blog") + .javaClassForValue(Blog.class) + .isRequired(true) + .isModelReference(true) + .isModelList(false) + .build()); + expectedFields.put("comments", ModelField.builder() + .targetType("Comment") + .name("comments") + .javaClassForValue(ModelList.class) + .isRequired(false) + .isModelReference(false) + .isModelList(true) + .build()); + + Map expectedAssociations = new HashMap<>(); + + expectedAssociations.put("blog", ModelAssociation.builder() + .name("BelongsTo") + .targetName("blogPostsId") + .associatedName("blog") + .associatedType("Blog") + .build()); + expectedAssociations.put("comments", ModelAssociation.builder() + .name("HasMany") + .targetName(null) + .associatedName("post") + .associatedType("Comment") + .build()); + + ModelSchema expectedModelSchema = ModelSchema.builder() + .fields(expectedFields) + .name("Post") + .modelClass(Post.class) + .pluralName("Posts") + .associations(expectedAssociations) + .version(1) + .build(); + ModelSchema actualModelSchema = ModelSchema.fromModelClass(Post.class); + assertEquals(expectedModelSchema, actualModelSchema); + + // Sneaking in a cheeky lil' hashCode() test here, while we have two equals() + // ModelSchema in scope.... + Set modelSchemaSet = new HashSet<>(); + modelSchemaSet.add(actualModelSchema); + modelSchemaSet.add(expectedModelSchema); + assertEquals(1, modelSchemaSet.size()); + + // The object reference is the first one that was put into map + // (actualModelSchema was first call). + // The call to add expectedModelSchema was a no-op since hashCode() + // showed that the object was already in the collection. + assertSame(actualModelSchema, modelSchemaSet.iterator().next()); + + // Double check lazy field reference values are correct + ModelField blogField = actualModelSchema.getFields().get("blog"); + assertTrue(blogField.isModelReference()); + assertFalse(blogField.isModelList()); + ModelField commentsField = actualModelSchema.getFields().get("comments"); + assertFalse(commentsField.isModelReference()); + assertTrue(commentsField.isModelList()); + } + /** * A model with no @Index annotations should return the default primary index fields. ["id"] * @throws AmplifyException from model schema parsing diff --git a/core/src/test/java/com/amplifyframework/core/model/SchemaRegistryUtilsTest.kt b/core/src/test/java/com/amplifyframework/core/model/SchemaRegistryUtilsTest.kt new file mode 100644 index 0000000000..815467f1aa --- /dev/null +++ b/core/src/test/java/com/amplifyframework/core/model/SchemaRegistryUtilsTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.core.model + +import com.amplifyframework.datastore.DataStoreException.IrRecoverableException +import com.amplifyframework.testmodels.lazy.Post +import com.amplifyframework.testmodels.phonecall.Phone +import com.amplifyframework.testmodels.todo.Todo +import org.junit.Assert.assertEquals +import org.junit.Test + +class SchemaRegistryUtilsTest { + + @Test(expected = IrRecoverableException::class) + fun throws_if_has_lazy_detected() { + SchemaRegistryUtils.registerSchema( + mutableMapOf(), + "Post", + ModelSchema.fromModelClass(Post::class.java) + ) + } + + @Test + fun test_register() { + val schemaMap = mutableMapOf() + val expectedKey = "Todo" + val expectedValue = ModelSchema.fromModelClass(Todo::class.java) + + SchemaRegistryUtils.registerSchema( + schemaMap, + expectedKey, + expectedValue + ) + + assertEquals(1, schemaMap.size) + assertEquals(expectedValue, schemaMap[expectedKey]) + } + + @Test + fun test_registers() { + val schemaMap = mutableMapOf() + val expectedKey = "Todo" + val expectedValue = ModelSchema.fromModelClass(Todo::class.java) + + SchemaRegistryUtils.registerSchemas( + schemaMap, + mapOf((expectedKey to expectedValue), ("TodoOwner" to ModelSchema.fromModelClass(Phone::class.java))) + ) + + assertEquals(2, schemaMap.size) + assertEquals(expectedValue, schemaMap[expectedKey]) + } + + @Test + fun test_empty_schemas() { + val schemaMap = mutableMapOf() + + SchemaRegistryUtils.registerSchemas(schemaMap) + + assertEquals(0, schemaMap.size) + } + + @Test + fun test_schema_missing_class_catches_exception_and_continues() { + val schemaMap = mutableMapOf() + + SchemaRegistryUtils.registerSchema(schemaMap, "Empty", ModelSchema.builder().name("Empty").build()) + + assertEquals(1, schemaMap.size) + } +} diff --git a/scripts/pull_backend_config_from_s3 b/scripts/pull_backend_config_from_s3 index e95fbcb103..9a15b5f148 100755 --- a/scripts/pull_backend_config_from_s3 +++ b/scripts/pull_backend_config_from_s3 @@ -21,6 +21,7 @@ readonly config_files=( "aws-api/src/androidTest/res/raw/amplifyconfiguration.json" "aws-api/src/androidTest/res/raw/awsconfiguration.json" "aws-api/src/androidTest/res/raw/credentials.json" + "aws-api/src/androidTest/res/raw/amplifyconfigurationlazy.json" # DataStore "aws-datastore/src/androidTest/res/raw/amplifyconfiguration.json" diff --git a/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/AmplifyModelProvider.java b/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/AmplifyModelProvider.java new file mode 100644 index 0000000000..1169c882bc --- /dev/null +++ b/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/AmplifyModelProvider.java @@ -0,0 +1,53 @@ +package com.amplifyframework.testmodels.lazy; + +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.ModelProvider; +import com.amplifyframework.util.Immutable; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +/** + * Contains the set of model classes that implement {@link Model} + * interface. + */ + +public final class AmplifyModelProvider implements ModelProvider { + private static final String AMPLIFY_MODEL_VERSION = "176d4f44374548002bd7e9d2f0f960a7"; + private static AmplifyModelProvider amplifyGeneratedModelInstance; + private AmplifyModelProvider() { + + } + + public static synchronized AmplifyModelProvider getInstance() { + if (amplifyGeneratedModelInstance == null) { + amplifyGeneratedModelInstance = new AmplifyModelProvider(); + } + return amplifyGeneratedModelInstance; + } + + /** + * Get a set of the model classes. + * + * @return a set of the model classes. + */ + @Override + public Set> models() { + final Set> modifiableSet = new HashSet<>( + Arrays.>asList(Blog.class, Post.class, Comment.class) + ); + + return Immutable.of(modifiableSet); + + } + + /** + * Get the version of the models. + * + * @return the version string of the models. + */ + @Override + public String version() { + return AMPLIFY_MODEL_VERSION; + } +} diff --git a/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/Blog.java b/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/Blog.java new file mode 100644 index 0000000000..3125d85071 --- /dev/null +++ b/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/Blog.java @@ -0,0 +1,193 @@ +package com.amplifyframework.testmodels.lazy; + +import static com.amplifyframework.core.model.query.predicate.QueryField.field; + +import androidx.core.util.ObjectsCompat; + +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.ModelIdentifier; +import com.amplifyframework.core.model.ModelList; +import com.amplifyframework.core.model.annotations.HasMany; +import com.amplifyframework.core.model.annotations.ModelConfig; +import com.amplifyframework.core.model.annotations.ModelField; +import com.amplifyframework.core.model.query.predicate.QueryField; +import com.amplifyframework.core.model.temporal.Temporal; + +import java.util.Objects; +import java.util.UUID; + +/** This is an auto generated class representing the Blog type in your schema. */ +@SuppressWarnings("all") +@ModelConfig(pluralName = "Blogs", type = Model.Type.USER, version = 1, hasLazySupport = true) +public final class Blog implements Model { + public static final BlogPath rootPath = new BlogPath("root", false, null); + public static final QueryField ID = field("Blog", "id"); + public static final QueryField NAME = field("Blog", "name"); + private final @ModelField(targetType="ID", isRequired = true) String id; + private final @ModelField(targetType="String", isRequired = true) String name; + private final @ModelField(targetType="Post", isRequired = true) @HasMany(associatedWith = "blog", type = Post.class) ModelList posts = null; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime createdAt; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime updatedAt; + /** @deprecated This API is internal to Amplify and should not be used. */ + @Deprecated + public String resolveIdentifier() { + return id; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public ModelList getPosts() { + return posts; + } + + public Temporal.DateTime getCreatedAt() { + return createdAt; + } + + public Temporal.DateTime getUpdatedAt() { + return updatedAt; + } + + private Blog(String id, String name) { + this.id = id; + this.name = name; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if(obj == null || getClass() != obj.getClass()) { + return false; + } else { + Blog blog = (Blog) obj; + return ObjectsCompat.equals(getId(), blog.getId()) && + ObjectsCompat.equals(getName(), blog.getName()) && + ObjectsCompat.equals(getCreatedAt(), blog.getCreatedAt()) && + ObjectsCompat.equals(getUpdatedAt(), blog.getUpdatedAt()); + } + } + + @Override + public int hashCode() { + return new StringBuilder() + .append(getId()) + .append(getName()) + .append(getCreatedAt()) + .append(getUpdatedAt()) + .toString() + .hashCode(); + } + + @Override + public String toString() { + return new StringBuilder() + .append("Blog {") + .append("id=" + String.valueOf(getId()) + ", ") + .append("name=" + String.valueOf(getName()) + ", ") + .append("createdAt=" + String.valueOf(getCreatedAt()) + ", ") + .append("updatedAt=" + String.valueOf(getUpdatedAt())) + .append("}") + .toString(); + } + + public static NameStep builder() { + return new Builder(); + } + + /** + * WARNING: This method should not be used to build an instance of this object for a CREATE mutation. + * This is a convenience method to return an instance of the object with only its ID populated + * to be used in the context of a parameter in a delete mutation or referencing a foreign key + * in a relationship. + * @param id the id of the existing item this instance will represent + * @return an instance of this model with only ID populated + */ + public static Blog justId(String id) { + return new Blog( + id, + null + ); + } + + public CopyOfBuilder copyOfBuilder() { + return new CopyOfBuilder(id, + name); + } + public interface NameStep { + BuildStep name(String name); + } + + + public interface BuildStep { + Blog build(); + BuildStep id(String id); + } + + + public static class Builder implements NameStep, BuildStep { + private String id; + private String name; + public Builder() { + + } + + private Builder(String id, String name) { + this.id = id; + this.name = name; + } + + @Override + public Blog build() { + String id = this.id != null ? this.id : UUID.randomUUID().toString(); + + return new Blog( + id, + name); + } + + @Override + public BuildStep name(String name) { + Objects.requireNonNull(name); + this.name = name; + return this; + } + + /** + * @param id id + * @return Current Builder instance, for fluent method chaining + */ + public BuildStep id(String id) { + this.id = id; + return this; + } + } + + + public final class CopyOfBuilder extends Builder { + private CopyOfBuilder(String id, String name) { + super(id, name); + Objects.requireNonNull(name); + } + + @Override + public CopyOfBuilder name(String name) { + return (CopyOfBuilder) super.name(name); + } + } + + + public static class BlogIdentifier extends ModelIdentifier { + private static final long serialVersionUID = 1L; + public BlogIdentifier(String id) { + super(id); + } + } + +} diff --git a/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/BlogPath.java b/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/BlogPath.java new file mode 100644 index 0000000000..d2c83b4b79 --- /dev/null +++ b/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/BlogPath.java @@ -0,0 +1,22 @@ +package com.amplifyframework.testmodels.lazy; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.amplifyframework.core.model.ModelPath; +import com.amplifyframework.core.model.PropertyPath; + +/** This is an auto generated class representing the ModelPath for the Blog type in your schema. */ +public final class BlogPath extends ModelPath { + private PostPath posts; + BlogPath(@NonNull String name, @NonNull Boolean isCollection, @Nullable PropertyPath parent) { + super(name, isCollection, parent, Blog.class); + } + + public synchronized PostPath getPosts() { + if (posts == null) { + posts = new PostPath("posts", true, this); + } + return posts; + } +} diff --git a/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/Comment.java b/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/Comment.java new file mode 100644 index 0000000000..1ccf55ba8c --- /dev/null +++ b/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/Comment.java @@ -0,0 +1,222 @@ +package com.amplifyframework.testmodels.lazy; + +import static com.amplifyframework.core.model.query.predicate.QueryField.field; + +import androidx.core.util.ObjectsCompat; + +import com.amplifyframework.core.model.LoadedModelReferenceImpl; +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.ModelIdentifier; +import com.amplifyframework.core.model.ModelReference; +import com.amplifyframework.core.model.annotations.BelongsTo; +import com.amplifyframework.core.model.annotations.ModelConfig; +import com.amplifyframework.core.model.annotations.ModelField; +import com.amplifyframework.core.model.query.predicate.QueryField; +import com.amplifyframework.core.model.temporal.Temporal; + +import java.util.Objects; +import java.util.UUID; + +/** This is an auto generated class representing the Comment type in your schema. */ +@SuppressWarnings("all") +@ModelConfig(pluralName = "Comments", type = Model.Type.USER, version = 1, hasLazySupport = true) +public final class Comment implements Model { + public static final CommentPath rootPath = new CommentPath("root", false, null); + public static final QueryField ID = field("Comment", "id"); + public static final QueryField TEXT = field("Comment", "text"); + public static final QueryField POST = field("Comment", "postCommentsId"); + private final @ModelField(targetType="ID", isRequired = true) String id; + private final @ModelField(targetType="String", isRequired = true) String text; + private final @ModelField(targetType="Post", isRequired = true) @BelongsTo(targetName = "postCommentsId", targetNames = {"postCommentsId"}, type = Post.class) ModelReference post; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime createdAt; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime updatedAt; + /** @deprecated This API is internal to Amplify and should not be used. */ + @Deprecated + public String resolveIdentifier() { + return id; + } + + public String getId() { + return id; + } + + public String getText() { + return text; + } + + public ModelReference getPost() { + return post; + } + + public Temporal.DateTime getCreatedAt() { + return createdAt; + } + + public Temporal.DateTime getUpdatedAt() { + return updatedAt; + } + + private Comment(String id, String text, ModelReference post) { + this.id = id; + this.text = text; + this.post = post; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if(obj == null || getClass() != obj.getClass()) { + return false; + } else { + Comment comment = (Comment) obj; + return ObjectsCompat.equals(getId(), comment.getId()) && + ObjectsCompat.equals(getText(), comment.getText()) && + ObjectsCompat.equals(getPost(), comment.getPost()) && + ObjectsCompat.equals(getCreatedAt(), comment.getCreatedAt()) && + ObjectsCompat.equals(getUpdatedAt(), comment.getUpdatedAt()); + } + } + + @Override + public int hashCode() { + return new StringBuilder() + .append(getId()) + .append(getText()) + .append(getPost()) + .append(getCreatedAt()) + .append(getUpdatedAt()) + .toString() + .hashCode(); + } + + @Override + public String toString() { + return new StringBuilder() + .append("Comment {") + .append("id=" + String.valueOf(getId()) + ", ") + .append("text=" + String.valueOf(getText()) + ", ") + .append("post=" + String.valueOf(getPost()) + ", ") + .append("createdAt=" + String.valueOf(getCreatedAt()) + ", ") + .append("updatedAt=" + String.valueOf(getUpdatedAt())) + .append("}") + .toString(); + } + + public static TextStep builder() { + return new Builder(); + } + + /** + * WARNING: This method should not be used to build an instance of this object for a CREATE mutation. + * This is a convenience method to return an instance of the object with only its ID populated + * to be used in the context of a parameter in a delete mutation or referencing a foreign key + * in a relationship. + * @param id the id of the existing item this instance will represent + * @return an instance of this model with only ID populated + */ + public static Comment justId(String id) { + return new Comment( + id, + null, + null + ); + } + + public CopyOfBuilder copyOfBuilder() { + return new CopyOfBuilder(id, + text, + post); + } + public interface TextStep { + PostStep text(String text); + } + + + public interface PostStep { + BuildStep post(Post post); + } + + + public interface BuildStep { + Comment build(); + BuildStep id(String id); + } + + + public static class Builder implements TextStep, PostStep, BuildStep { + private String id; + private String text; + private ModelReference post; + public Builder() { + + } + + private Builder(String id, String text, ModelReference post) { + this.id = id; + this.text = text; + this.post = post; + } + + @Override + public Comment build() { + String id = this.id != null ? this.id : UUID.randomUUID().toString(); + + return new Comment( + id, + text, + post); + } + + @Override + public PostStep text(String text) { + Objects.requireNonNull(text); + this.text = text; + return this; + } + + @Override + public BuildStep post(Post post) { + Objects.requireNonNull(post); + this.post = new LoadedModelReferenceImpl<>(post); + return this; + } + + /** + * @param id id + * @return Current Builder instance, for fluent method chaining + */ + public BuildStep id(String id) { + this.id = id; + return this; + } + } + + + public final class CopyOfBuilder extends Builder { + private CopyOfBuilder(String id, String text, ModelReference post) { + super(id, text, post); + Objects.requireNonNull(text); + Objects.requireNonNull(post); + } + + @Override + public CopyOfBuilder text(String text) { + return (CopyOfBuilder) super.text(text); + } + + @Override + public CopyOfBuilder post(Post post) { + return (CopyOfBuilder) super.post(post); + } + } + + + public static class CommentIdentifier extends ModelIdentifier { + private static final long serialVersionUID = 1L; + public CommentIdentifier(String id) { + super(id); + } + } + +} diff --git a/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/CommentPath.java b/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/CommentPath.java new file mode 100644 index 0000000000..14bf004eb9 --- /dev/null +++ b/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/CommentPath.java @@ -0,0 +1,22 @@ +package com.amplifyframework.testmodels.lazy; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.amplifyframework.core.model.ModelPath; +import com.amplifyframework.core.model.PropertyPath; + +/** This is an auto generated class representing the ModelPath for the Comment type in your schema. */ +public final class CommentPath extends ModelPath { + private PostPath post; + CommentPath(@NonNull String name, @NonNull Boolean isCollection, @Nullable PropertyPath parent) { + super(name, isCollection, parent, Comment.class); + } + + public synchronized PostPath getPost() { + if (post == null) { + post = new PostPath("post", false, this); + } + return post; + } +} diff --git a/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/Post.java b/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/Post.java new file mode 100644 index 0000000000..7c41d68cff --- /dev/null +++ b/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/Post.java @@ -0,0 +1,229 @@ +package com.amplifyframework.testmodels.lazy; + +import static com.amplifyframework.core.model.query.predicate.QueryField.field; + +import androidx.core.util.ObjectsCompat; + +import com.amplifyframework.core.model.LoadedModelReferenceImpl; +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.ModelIdentifier; +import com.amplifyframework.core.model.ModelList; +import com.amplifyframework.core.model.ModelReference; +import com.amplifyframework.core.model.annotations.BelongsTo; +import com.amplifyframework.core.model.annotations.HasMany; +import com.amplifyframework.core.model.annotations.ModelConfig; +import com.amplifyframework.core.model.annotations.ModelField; +import com.amplifyframework.core.model.query.predicate.QueryField; +import com.amplifyframework.core.model.temporal.Temporal; + +import java.util.Objects; +import java.util.UUID; + +/** This is an auto generated class representing the Post type in your schema. */ +@SuppressWarnings("all") +@ModelConfig(pluralName = "Posts", type = Model.Type.USER, version = 1, hasLazySupport = true) +public final class Post implements Model { + public static final PostPath rootPath = new PostPath("root", false, null); + public static final QueryField ID = field("Post", "id"); + public static final QueryField NAME = field("Post", "name"); + public static final QueryField BLOG = field("Post", "blogPostsId"); + private final @ModelField(targetType="ID", isRequired = true) String id; + private final @ModelField(targetType="String", isRequired = true) String name; + private final @ModelField(targetType="Blog", isRequired = true) @BelongsTo(targetName = "blogPostsId", targetNames = {"blogPostsId"}, type = Blog.class) ModelReference blog; + private final @ModelField(targetType="Comment") @HasMany(associatedWith = "post", type = Comment.class) ModelList comments = null; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime createdAt; + private @ModelField(targetType="AWSDateTime", isReadOnly = true) Temporal.DateTime updatedAt; + /** @deprecated This API is internal to Amplify and should not be used. */ + @Deprecated + public String resolveIdentifier() { + return id; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public ModelReference getBlog() { + return blog; + } + + public ModelList getComments() { + return comments; + } + + public Temporal.DateTime getCreatedAt() { + return createdAt; + } + + public Temporal.DateTime getUpdatedAt() { + return updatedAt; + } + + private Post(String id, String name, ModelReference blog) { + this.id = id; + this.name = name; + this.blog = blog; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if(obj == null || getClass() != obj.getClass()) { + return false; + } else { + Post post = (Post) obj; + return ObjectsCompat.equals(getId(), post.getId()) && + ObjectsCompat.equals(getName(), post.getName()) && + ObjectsCompat.equals(getBlog(), post.getBlog()) && + ObjectsCompat.equals(getCreatedAt(), post.getCreatedAt()) && + ObjectsCompat.equals(getUpdatedAt(), post.getUpdatedAt()); + } + } + + @Override + public int hashCode() { + return new StringBuilder() + .append(getId()) + .append(getName()) + .append(getBlog()) + .append(getCreatedAt()) + .append(getUpdatedAt()) + .toString() + .hashCode(); + } + + @Override + public String toString() { + return new StringBuilder() + .append("Post {") + .append("id=" + String.valueOf(getId()) + ", ") + .append("name=" + String.valueOf(getName()) + ", ") + .append("blog=" + String.valueOf(getBlog()) + ", ") + .append("createdAt=" + String.valueOf(getCreatedAt()) + ", ") + .append("updatedAt=" + String.valueOf(getUpdatedAt())) + .append("}") + .toString(); + } + + public static NameStep builder() { + return new Builder(); + } + + /** + * WARNING: This method should not be used to build an instance of this object for a CREATE mutation. + * This is a convenience method to return an instance of the object with only its ID populated + * to be used in the context of a parameter in a delete mutation or referencing a foreign key + * in a relationship. + * @param id the id of the existing item this instance will represent + * @return an instance of this model with only ID populated + */ + public static Post justId(String id) { + return new Post( + id, + null, + null + ); + } + + public CopyOfBuilder copyOfBuilder() { + return new CopyOfBuilder(id, + name, + blog); + } + public interface NameStep { + BlogStep name(String name); + } + + + public interface BlogStep { + BuildStep blog(Blog blog); + } + + + public interface BuildStep { + Post build(); + BuildStep id(String id); + } + + + public static class Builder implements NameStep, BlogStep, BuildStep { + private String id; + private String name; + private ModelReference blog; + public Builder() { + + } + + private Builder(String id, String name, ModelReference blog) { + this.id = id; + this.name = name; + this.blog = blog; + } + + @Override + public Post build() { + String id = this.id != null ? this.id : UUID.randomUUID().toString(); + + return new Post( + id, + name, + blog); + } + + @Override + public BlogStep name(String name) { + Objects.requireNonNull(name); + this.name = name; + return this; + } + + @Override + public BuildStep blog(Blog blog) { + Objects.requireNonNull(blog); + this.blog = new LoadedModelReferenceImpl<>(blog); + return this; + } + + /** + * @param id id + * @return Current Builder instance, for fluent method chaining + */ + public BuildStep id(String id) { + this.id = id; + return this; + } + } + + + public final class CopyOfBuilder extends Builder { + private CopyOfBuilder(String id, String name, ModelReference blog) { + super(id, name, blog); + Objects.requireNonNull(name); + Objects.requireNonNull(blog); + } + + @Override + public CopyOfBuilder name(String name) { + return (CopyOfBuilder) super.name(name); + } + + @Override + public CopyOfBuilder blog(Blog blog) { + return (CopyOfBuilder) super.blog(blog); + } + } + + + public static class PostIdentifier extends ModelIdentifier { + private static final long serialVersionUID = 1L; + public PostIdentifier(String id) { + super(id); + } + } + +} diff --git a/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/PostPath.java b/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/PostPath.java new file mode 100644 index 0000000000..3f16b9acda --- /dev/null +++ b/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/PostPath.java @@ -0,0 +1,30 @@ +package com.amplifyframework.testmodels.lazy; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.amplifyframework.core.model.ModelPath; +import com.amplifyframework.core.model.PropertyPath; + +/** This is an auto generated class representing the ModelPath for the Post type in your schema. */ +public final class PostPath extends ModelPath { + private BlogPath blog; + private CommentPath comments; + PostPath(@NonNull String name, @NonNull Boolean isCollection, @Nullable PropertyPath parent) { + super(name, isCollection, parent, Post.class); + } + + public synchronized BlogPath getBlog() { + if (blog == null) { + blog = new BlogPath("blog", false, this); + } + return blog; + } + + public synchronized CommentPath getComments() { + if (comments == null) { + comments = new CommentPath("comments", true, this); + } + return comments; + } +} diff --git a/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/schema.graphql b/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/schema.graphql new file mode 100644 index 0000000000..e4817e6dc5 --- /dev/null +++ b/testmodels/src/main/java/com/amplifyframework/testmodels/lazy/schema.graphql @@ -0,0 +1,19 @@ +# This "input" configures a global authorization rule to enable public access to +# all models in this schema. Learn more about authorization rules here: https://docs.amplify.aws/cli/graphql/authorization-rules +input AMPLIFY { globalAuthRule: AuthRule = { allow: public } } # FOR TESTING ONLY! + +type Blog @model { + name: String! + posts: [Post!]! @hasMany +} + +type Post @model { + name: String! + blog: Blog! @belongsTo + comments: [Comment] @hasMany +} + +type Comment @model { + text: String! + post: Post! @belongsTo +} \ No newline at end of file diff --git a/testmodels/src/test/java/com/amplifyframework/testmodels/lazy/LazyTypeTest.kt b/testmodels/src/test/java/com/amplifyframework/testmodels/lazy/LazyTypeTest.kt new file mode 100644 index 0000000000..2d7a0d2987 --- /dev/null +++ b/testmodels/src/test/java/com/amplifyframework/testmodels/lazy/LazyTypeTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2023 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. + */ + +package com.amplifyframework.testmodels.lazy + +import com.amplifyframework.core.model.ModelSchema +import com.amplifyframework.core.model.annotations.ModelConfig +import com.amplifyframework.testmodels.todo.Todo +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class LazyTypeTest { + + @Test + fun check_lazy_support() { + assertTrue( + ModelSchema.fromModelClass(Post::class.java) + .modelClass.getAnnotation(ModelConfig::class.java) + ?.hasLazySupport ?: false + ) + } + + @Test + fun check_older_model_no_lazy_support() { + assertFalse( + ModelSchema.fromModelClass(Todo::class.java) + .modelClass.getAnnotation(ModelConfig::class.java) + ?.hasLazySupport ?: true + ) + } +} diff --git a/testutils/src/main/java/com/amplifyframework/testutils/RepeatRule.kt b/testutils/src/main/java/com/amplifyframework/testutils/RepeatRule.kt index 482f95b438..222fdccedc 100644 --- a/testutils/src/main/java/com/amplifyframework/testutils/RepeatRule.kt +++ b/testutils/src/main/java/com/amplifyframework/testutils/RepeatRule.kt @@ -1,5 +1,3 @@ -package com.amplifyframework.testutils - /* * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. * @@ -15,6 +13,8 @@ package com.amplifyframework.testutils * permissions and limitations under the License. */ +package com.amplifyframework.testutils + import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement