From 476f5b864cc4719a7ab79928a24a89207fa2d050 Mon Sep 17 00:00:00 2001 From: Erica Eaton Date: Thu, 1 Jun 2023 13:01:24 -0700 Subject: [PATCH 001/100] add lazy loading for api --- aws-api-appsync/build.gradle.kts | 3 + .../api/aws/SelectionSet.java | 44 +++++-- .../core/model/types/JavaFieldType.java | 3 + aws-api/build.gradle.kts | 4 + .../api/aws/AWSApiPlugin.java | 17 +++ .../api/aws/AppSyncGraphQLRequestFactory.java | 7 +- .../api/aws/AppSyncLazyListModel.kt | 78 ++++++++++++ .../api/aws/AppSyncLazyModel.kt | 84 +++++++++++++ .../api/aws/AppSyncLazyQueryPredicate.kt | 56 +++++++++ .../amplifyframework/api/aws/GsonFactory.java | 64 ++++++++++ .../api/aws/GsonGraphQLResponseFactory.java | 1 - .../api/aws/LazyModelAdapter.java | 69 +++++++++++ .../api/aws/LazyModelListAdapter.java | 75 ++++++++++++ .../api/aws/ModelProviderLocatorAppSync.java | 111 ++++++++++++++++++ core/build.gradle.kts | 3 + .../com/amplifyframework/api/ApiPlugin.java | 7 -- .../amplifyframework/core/model/ILazyList.kt | 26 ++++ .../core/model/InMemoryLazyModel.kt | 57 +++++++++ .../amplifyframework/core/model/LazyList.kt | 36 ++++++ .../amplifyframework/core/model/LazyModel.kt | 34 ++++++ .../core/model/ModelField.java | 78 ++++++++++++ .../core/model/ModelSchema.java | 26 +++- .../core/model/SchemaHelper.kt | 44 +++++++ .../core/model/annotations/HasMany.java | 7 ++ .../core/model/annotations/HasOne.java | 7 ++ .../amplifyframework/util/FieldFinder.java | 9 ++ 26 files changed, 928 insertions(+), 22 deletions(-) create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyListModel.kt create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyModel.kt create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyQueryPredicate.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/LazyModelAdapter.java create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelListAdapter.java create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/ModelProviderLocatorAppSync.java create mode 100644 core/src/main/java/com/amplifyframework/core/model/ILazyList.kt create mode 100644 core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt create mode 100644 core/src/main/java/com/amplifyframework/core/model/LazyList.kt create mode 100644 core/src/main/java/com/amplifyframework/core/model/LazyModel.kt create mode 100644 core/src/main/java/com/amplifyframework/core/model/SchemaHelper.kt diff --git a/aws-api-appsync/build.gradle.kts b/aws-api-appsync/build.gradle.kts index 0a0da660e8..5f35f56fe5 100644 --- a/aws-api-appsync/build.gradle.kts +++ b/aws-api-appsync/build.gradle.kts @@ -30,6 +30,9 @@ dependencies { implementation(dependency.androidx.annotation) implementation(dependency.androidx.core) implementation(dependency.gson) + implementation(dependency.kotlin.stdlib) + implementation(dependency.kotlin.coroutines) + implementation(dependency.androidx.core.ktx) testImplementation(testDependency.junit) testImplementation(testDependency.robolectric) 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..9b81219922 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 @@ -16,6 +16,8 @@ package com.amplifyframework.api.aws; import android.text.TextUtils; +import android.util.Log; + import androidx.annotation.NonNull; import androidx.core.util.ObjectsCompat; @@ -26,6 +28,8 @@ import com.amplifyframework.core.model.AuthStrategy; import com.amplifyframework.core.model.CustomTypeField; import com.amplifyframework.core.model.CustomTypeSchema; +import com.amplifyframework.core.model.LazyList; +import com.amplifyframework.core.model.LazyModel; import com.amplifyframework.core.model.Model; import com.amplifyframework.core.model.ModelAssociation; import com.amplifyframework.core.model.ModelField; @@ -97,7 +101,6 @@ public Set getNodes() { /** * Generate the String value of the SelectionSet used in the GraphQL query document, with no margin. - * * Sample return value: * items { * foo @@ -213,7 +216,7 @@ public SelectionSet build() throws AmplifyException { SelectionSet node = new SelectionSet(null, SerializedModel.class == modelClass ? getModelFields(modelSchema, requestOptions.maxDepth(), operation) - : getModelFields(modelClass, requestOptions.maxDepth(), operation)); + : getModelFields(modelClass, requestOptions.maxDepth(), operation, false)); if (QueryType.LIST.equals(operation) || QueryType.SYNC.equals(operation)) { node = wrapPagination(node); } @@ -247,13 +250,15 @@ 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 + * + * @param clazz Class from which to build selection set + * @param depth Number of children deep to explore + * @param primaryKeyOnly * @return Selection Set * @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 +267,10 @@ 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,18 +281,29 @@ 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 (LazyList.class.isAssignableFrom(field.getType())) { + 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)); + operation, false)); result.add(new SelectionSet(fieldName, fields)); } } else if (depth >= 1) { - Set fields = getModelFields((Class) field.getType(), depth - 1, operation); - result.add(new SelectionSet(fieldName, fields)); + Class modalClass; + if (LazyModel.class.isAssignableFrom(field.getType())) { + ParameterizedType pType = (ParameterizedType) field.getGenericType(); + modalClass = (Class) pType.getActualTypeArguments()[0]; + Set fields = getModelFields(modalClass, 0, operation, true); + result.add(new SelectionSet(fieldName, fields)); + } else { + modalClass = (Class) field.getType(); + Set fields = getModelFields(modalClass, depth - 1, operation, false); + result.add(new SelectionSet(fieldName, fields)); + } } } else if (isCustomType(field)) { result.add(new SelectionSet(fieldName, getNestedCustomTypeFields(getClassForField(field)))); @@ -303,6 +320,9 @@ private Set getModelFields(Class clazz, int depth for (String fieldName : requestOptions.modelMetaFields()) { result.add(new SelectionSet(fieldName)); } + + Log.i("MetadataFields", "for schema:" + schema.getName() + " operation: " + operation + + " fields: " + requestOptions.modelMetaFields()); return result; } diff --git a/aws-api-appsync/src/main/java/com/amplifyframework/core/model/types/JavaFieldType.java b/aws-api-appsync/src/main/java/com/amplifyframework/core/model/types/JavaFieldType.java index 72bf36c7d1..b2ba46d6b8 100644 --- a/aws-api-appsync/src/main/java/com/amplifyframework/core/model/types/JavaFieldType.java +++ b/aws-api-appsync/src/main/java/com/amplifyframework/core/model/types/JavaFieldType.java @@ -18,6 +18,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.amplifyframework.core.model.LazyModel; import com.amplifyframework.core.model.Model; import com.amplifyframework.core.model.temporal.Temporal; @@ -86,6 +87,8 @@ public enum JavaFieldType { */ MODEL(Model.class), + LAZY_MODEL(LazyModel.class), + /** * Represents any custom type (objects that are not models). */ diff --git a/aws-api/build.gradle.kts b/aws-api/build.gradle.kts index 72799ee46e..419aca0e22 100644 --- a/aws-api/build.gradle.kts +++ b/aws-api/build.gradle.kts @@ -25,12 +25,16 @@ group = properties["POM_GROUP"].toString() dependencies { api(project(":core")) + implementation(project(":core-kotlin")) api(project(":aws-core")) implementation(project(":aws-api-appsync")) implementation(dependency.androidx.appcompat) implementation(dependency.aws.signing) implementation(dependency.gson) + implementation(dependency.kotlin.stdlib) + implementation(dependency.kotlin.coroutines) + implementation(dependency.androidx.core.ktx) implementation(dependency.okhttp) testImplementation(project(":testutils")) 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 ae0ea2518f..4a6ae0a85f 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 @@ -16,11 +16,15 @@ package com.amplifyframework.api.aws; import android.content.Context; +import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.annotation.WorkerThread; import androidx.core.util.ObjectsCompat; +import com.amplifyframework.AmplifyException; import com.amplifyframework.api.ApiException; import com.amplifyframework.api.ApiPlugin; import com.amplifyframework.api.aws.auth.ApiRequestDecoratorFactory; @@ -40,6 +44,8 @@ import com.amplifyframework.core.Action; import com.amplifyframework.core.Amplify; import com.amplifyframework.core.Consumer; +import com.amplifyframework.core.model.ModelProvider; +import com.amplifyframework.core.model.SchemaRegistry; import com.amplifyframework.hub.HubChannel; import com.amplifyframework.util.Immutable; import com.amplifyframework.util.UserAgent; @@ -125,6 +131,16 @@ public String getPluginKey() { return "awsAPIPlugin"; } + @WorkerThread + @Override + public void initialize(@NonNull Context context) throws AmplifyException { + SchemaRegistry schemaRegistry = SchemaRegistry.instance(); + if (schemaRegistry.getModelSchemaMap().isEmpty()) { + ModelProvider modelProvider = ModelProviderLocatorAppSync.locate(); + schemaRegistry.register(modelProvider.modelSchemas(), modelProvider.customTypeSchemas()); + } + } + @Override public void configure( JSONObject pluginConfiguration, @@ -235,6 +251,7 @@ public GraphQLOperation query( try { final GraphQLOperation operation = buildAppSyncGraphQLOperation(apiName, graphQLRequest, onResponse, onFailure); + Log.d("AWSApiPlugin","query: " + operation); operation.start(); return operation; } catch (ApiException exception) { 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 index 7b892fc374..10d05c4f19 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.java @@ -25,6 +25,7 @@ import com.amplifyframework.api.graphql.SubscriptionType; import com.amplifyframework.core.model.AuthRule; import com.amplifyframework.core.model.AuthStrategy; +import com.amplifyframework.core.model.LazyModel; import com.amplifyframework.core.model.Model; import com.amplifyframework.core.model.ModelAssociation; import com.amplifyframework.core.model.ModelField; @@ -405,7 +406,11 @@ 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); + if (fieldInstance != null && privateField.getType() == LazyModel.class) { + return ((LazyModel) 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/src/main/java/com/amplifyframework/api/aws/AppSyncLazyListModel.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyListModel.kt new file mode 100644 index 0000000000..af9b894fe4 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyListModel.kt @@ -0,0 +1,78 @@ +/* + * 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.ApiException +import com.amplifyframework.api.graphql.GraphQLResponse +import com.amplifyframework.api.graphql.PaginatedResult +import com.amplifyframework.core.Consumer +import com.amplifyframework.core.model.LazyList +import com.amplifyframework.core.model.Model +import com.amplifyframework.kotlin.core.Amplify +import com.amplifyframework.core.Amplify as coreAmplify + +class AppSyncLazyListModel( + private val clazz: Class, + private val keyMap: List>, + private val predicate: AppSyncLazyQueryPredicate +) : LazyList() { + + private var value: MutableList = mutableListOf() + private var paginatedResult: PaginatedResult? = null + + override fun getItems(): List { + return value + } + + override suspend fun getNextPage(): List { + val request = if (paginatedResult != null) { + paginatedResult!!.requestForNextResult + } else { + AppSyncGraphQLRequestFactory.buildQuery, M>( + clazz, + predicate.createListPredicate(clazz, keyMap) + ) + } + paginatedResult = Amplify.API.query(request).data + val nextPageOfItems = paginatedResult!!.items.toList() + value.addAll(nextPageOfItems) + return nextPageOfItems + } + + override fun getNextPage(onSuccess: Consumer>, onFailure: Consumer) { + val onQuerySuccess = Consumer>> { + paginatedResult = it.data + val nextPageOfItems = paginatedResult!!.items.toList() + value.addAll(nextPageOfItems) + onSuccess.accept(nextPageOfItems) + } + val onApiFailure = Consumer { onFailure.accept(it) } + val request = if (paginatedResult != null) { + paginatedResult!!.requestForNextResult + } else { + AppSyncGraphQLRequestFactory.buildQuery, M>( + clazz, + predicate.createListPredicate(clazz, keyMap) + ) + } + coreAmplify.API.query(request, onQuerySuccess, onApiFailure) + } + + override fun hasNextPage(): Boolean { + return paginatedResult?.hasNextResult() ?: true + } +} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyModel.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyModel.kt new file mode 100644 index 0000000000..58b3ad633d --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyModel.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.api.aws + +import android.util.Log +import com.amplifyframework.AmplifyException +import com.amplifyframework.api.ApiException +import com.amplifyframework.api.graphql.GraphQLResponse +import com.amplifyframework.api.graphql.PaginatedResult +import com.amplifyframework.core.Consumer +import com.amplifyframework.core.model.LazyModel +import com.amplifyframework.core.model.Model +import com.amplifyframework.kotlin.core.Amplify +import com.amplifyframework.core.Amplify as coreAmplify + +class AppSyncLazyModel( + private val clazz: Class, + private val keyMap: Map, + private val predicate: AppSyncLazyQueryPredicate +) : LazyModel() { + + private var value: M? = null + + override fun getValue(): M? { + return value + } + + override suspend fun get(): M? { + value?.let { return value } + val queryPredicate = predicate.createPredicate(clazz, keyMap) + try { + val resultIterator = Amplify.API.query( + AppSyncGraphQLRequestFactory.buildQuery, M>( + clazz, + queryPredicate + ) + ).data.items.iterator() + value = if (resultIterator.hasNext()) { + resultIterator.next() + } else { + null + } + } catch (error: ApiException) { + Log.e("MyAmplifyApp", "Query failure", error) + } + return value + } + + override fun get(onSuccess: Consumer, onFailure: Consumer) { + value?.let { modelValue -> + onSuccess.accept(modelValue) + return + } + val onQuerySuccess = Consumer>> { + val resultIterator = it.data.items.iterator() + value = if (resultIterator.hasNext()) { + resultIterator.next() + } else { + null + } + // TODO : remove !! and allow null value to be passed in + onSuccess.accept(value!!) + } + val onApiFailure = Consumer { onFailure.accept(it) } + coreAmplify.API.query( + AppSyncGraphQLRequestFactory.buildQuery(clazz, predicate.createPredicate(clazz, keyMap)), + onQuerySuccess, + onApiFailure + ) + } +} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyQueryPredicate.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyQueryPredicate.kt new file mode 100644 index 0000000000..b92e29bbfb --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyQueryPredicate.kt @@ -0,0 +1,56 @@ +/* + * 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.Model +import com.amplifyframework.core.model.query.predicate.QueryField +import com.amplifyframework.core.model.query.predicate.QueryPredicate +import com.amplifyframework.core.model.query.predicate.QueryPredicates + +class AppSyncLazyQueryPredicate { + 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 + } + + fun createListPredicate(clazz: Class, keyMap: List>): QueryPredicate { + var queryPredicate = QueryPredicates.all() + var firstItem = true + keyMap.forEach { keyMapEntry -> + var firstKeyEntry = true + var itemPredicate = QueryPredicates.all() + keyMapEntry.forEach { predicateKey -> + val currentPredicate = QueryField.field(clazz.simpleName, predicateKey.key).eq(predicateKey.value) + if (firstKeyEntry) { + itemPredicate = currentPredicate + firstKeyEntry = false + } else { + itemPredicate = itemPredicate.and(currentPredicate) + } + } + if (firstItem) { + queryPredicate = itemPredicate + firstItem = false + } else { + queryPredicate = queryPredicate.or(itemPredicate) + } + } + return queryPredicate + } +} 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..daa1104f10 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/GsonFactory.java @@ -0,0 +1,64 @@ +/* + * 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.LazyList; +import com.amplifyframework.core.model.LazyModel; +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. + */ +public 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); + builder.registerTypeAdapter(LazyModel.class, new LazyModelAdapter<>()); + builder.registerTypeAdapter(LazyList.class, new LazyModelListAdapter<>()); + 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..2b4b9d72cb 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 @@ -23,7 +23,6 @@ import com.amplifyframework.api.graphql.GraphQLResponse; import com.amplifyframework.api.graphql.PaginatedResult; import com.amplifyframework.util.Empty; -import com.amplifyframework.util.GsonFactory; import com.amplifyframework.util.TypeMaker; import com.google.gson.Gson; diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.java b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.java new file mode 100644 index 0000000000..dd59536675 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.java @@ -0,0 +1,69 @@ +/* + * 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.util.Log; + + +import com.amplifyframework.core.model.LazyModel; +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.ModelSchema; +import com.amplifyframework.core.model.SchemaRegistry; +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 com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class LazyModelAdapter implements JsonDeserializer>, + JsonSerializer> { + + @SuppressWarnings("unchecked") + @Override + public LazyModel deserialize(JsonElement json, Type typeOfT, + JsonDeserializationContext context) throws JsonParseException { + ParameterizedType pType = (ParameterizedType) typeOfT; + Class type = (Class) pType.getActualTypeArguments()[0]; + + Log.d("LazyModelAdapter", "json: "+ json + " typeOfT " + typeOfT + + " typeOfT type name" + type + " context " + + context); + Map predicateKeyMap = new HashMap<>(); + Iterator primaryKeysIterator = SchemaRegistry.instance() + .getModelSchemaForModelClass(type) + .getPrimaryIndexFields().iterator(); + JsonObject jsonObject = (JsonObject) json; + while (primaryKeysIterator.hasNext()){ + String primaryKey = primaryKeysIterator.next(); + predicateKeyMap.put(primaryKey, jsonObject.get(primaryKey)); + } + return new AppSyncLazyModel<>(type, predicateKeyMap, new AppSyncLazyQueryPredicate<>()); + } + + @Override + public JsonElement serialize(LazyModel src, Type typeOfSrc, + JsonSerializationContext context) { + return null; + } +} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelListAdapter.java b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelListAdapter.java new file mode 100644 index 0000000000..58cf196027 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelListAdapter.java @@ -0,0 +1,75 @@ +/* + * 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.util.Log; + +import com.amplifyframework.core.model.LazyList; +import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.SchemaRegistry; +import com.google.gson.JsonArray; +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 com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class LazyModelListAdapter implements JsonDeserializer>, + JsonSerializer> { + + private static final String ITEMS_KEY = "items"; + + @SuppressWarnings("unchecked") + @Override + public LazyList deserialize(JsonElement json, Type typeOfT, + JsonDeserializationContext context) throws JsonParseException { + ParameterizedType pType = (ParameterizedType) typeOfT; + Class type = (Class) pType.getActualTypeArguments()[0]; + + Log.d("LazyModelAdapter", "json: "+ json + " typeOfT " + typeOfT + + " typeOfT type name" + type + " context " + + context); + List> predicateKeyList = new ArrayList<>(); + List primaryKeys = SchemaRegistry.instance() + .getModelSchemaForModelClass(type) + .getPrimaryIndexFields(); + JsonObject jsonObject = (JsonObject) json; + JsonArray items = jsonObject.getAsJsonArray(ITEMS_KEY); + for (JsonElement jsonItem : items) { + Map predicateKeyMap = new HashMap<>(); + for (String primaryKey : primaryKeys) { + predicateKeyMap.put(primaryKey, jsonItem.getAsJsonObject().get(primaryKey)); + } + predicateKeyList.add(predicateKeyMap); + } + return new AppSyncLazyListModel<>(type, predicateKeyList, new AppSyncLazyQueryPredicate<>()); + } + + @Override + public JsonElement serialize(LazyList src, Type typeOfSrc, + JsonSerializationContext context) { + return null; + } +} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelProviderLocatorAppSync.java b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelProviderLocatorAppSync.java new file mode 100644 index 0000000000..3112bf5d08 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelProviderLocatorAppSync.java @@ -0,0 +1,111 @@ +/* + * 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.annotation.NonNull; + +import com.amplifyframework.core.model.ModelProvider; +import com.amplifyframework.datastore.DataStoreException; + +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.datastore.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 DataStore. This utility class is used from + * {@link com.amplifyframework.api.aws.AWSApiPlugin#initialize}. + */ +// TODO : this class also exists in DataStore, try to have only one version of this class +public final class ModelProviderLocatorAppSync { + 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 ModelProviderLocatorAppSync() {} + + /** + * Locate the code-generated model provider. + * @return The code-generated model provider, if found + * @throws DataStoreException If unable to find the code-generated model provider + */ + public static ModelProvider locate() throws DataStoreException { + return locate(DEFAULT_MODEL_PROVIDER_CLASS_NAME); + } + + @SuppressWarnings({"SameParameterValue", "unchecked"}) + static ModelProvider locate(@NonNull String modelProviderClassName) throws DataStoreException { + 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 DataStoreException( + "Failed to find code-generated model provider.", modelProviderClassNotFoundError, + "Validate that " + modelProviderClassName + " is built into your project." + ); + } + if (!ModelProvider.class.isAssignableFrom(modelProviderClass)) { + throw new DataStoreException( + "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 DataStoreException( + "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 DataStoreException( + "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 DataStoreException( + "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/core/build.gradle.kts b/core/build.gradle.kts index b91595dfba..9e5cf3d5e1 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -37,6 +37,9 @@ dependencies { implementation(dependency.androidx.nav.ui) implementation(dependency.androidx.security) implementation(dependency.kotlin.serializationJson) + implementation(dependency.kotlin.stdlib) + implementation(dependency.kotlin.coroutines) + implementation(dependency.androidx.core.ktx) api(project(":common-core")) diff --git a/core/src/main/java/com/amplifyframework/api/ApiPlugin.java b/core/src/main/java/com/amplifyframework/api/ApiPlugin.java index 6f32bd158b..a584cebd3e 100644 --- a/core/src/main/java/com/amplifyframework/api/ApiPlugin.java +++ b/core/src/main/java/com/amplifyframework/api/ApiPlugin.java @@ -15,11 +15,8 @@ package com.amplifyframework.api; -import android.content.Context; import androidx.annotation.NonNull; -import androidx.annotation.WorkerThread; -import com.amplifyframework.AmplifyException; import com.amplifyframework.core.category.CategoryType; import com.amplifyframework.core.plugin.Plugin; @@ -37,9 +34,5 @@ public abstract class ApiPlugin implements ApiCategoryBehavior, Plugin { public final CategoryType getCategoryType() { return CategoryType.API; } - - @WorkerThread - @Override - public void initialize(@NonNull Context context) throws AmplifyException {} } diff --git a/core/src/main/java/com/amplifyframework/core/model/ILazyList.kt b/core/src/main/java/com/amplifyframework/core/model/ILazyList.kt new file mode 100644 index 0000000000..928be91324 --- /dev/null +++ b/core/src/main/java/com/amplifyframework/core/model/ILazyList.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 + +public interface ILazyList { + val value: M + + suspend fun get(): List? + + suspend fun require(): List { + return get() ?: throw DataIntegrityException("Required model could not be found") + } +} diff --git a/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt b/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt new file mode 100644 index 0000000000..ed785972a7 --- /dev/null +++ b/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt @@ -0,0 +1,57 @@ +/* + * 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 + +class InMemoryLazyModel(model: M? = null) : LazyModel () { + + private var value: M? = model + + override fun getValue(): M? { + return value + } + + override suspend fun get(): M? { + return value + } + + override fun get(onSuccess: Consumer, onFailure: Consumer) { + if (value != null) { + onSuccess.accept(value!!) + } + } +} + +class InMemoryLazyList(modelList: List = emptyList()) : LazyList() { + private var value: List = modelList + override fun getItems(): List { + return value + } + + override suspend fun getNextPage(): List { + return emptyList() + } + + override fun getNextPage(onSuccess: Consumer>, onFailure: Consumer) { + onSuccess.accept(emptyList()) + } + + override fun hasNextPage(): Boolean { + return false + } +} diff --git a/core/src/main/java/com/amplifyframework/core/model/LazyList.kt b/core/src/main/java/com/amplifyframework/core/model/LazyList.kt new file mode 100644 index 0000000000..96f60e333a --- /dev/null +++ b/core/src/main/java/com/amplifyframework/core/model/LazyList.kt @@ -0,0 +1,36 @@ +/* + * 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.api.ApiException +import com.amplifyframework.core.Consumer + +abstract class LazyList { + abstract fun getItems(): List + + @Throws(ApiException::class) + abstract suspend fun getNextPage(): List + + suspend fun require(): List { + return getNextPage() ?: throw DataIntegrityException("Required model could not be found") + } + + abstract fun getNextPage(onSuccess: Consumer>, + onFailure: Consumer) + + abstract fun hasNextPage(): Boolean +} diff --git a/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt b/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt new file mode 100644 index 0000000000..c7ec5bf7ca --- /dev/null +++ b/core/src/main/java/com/amplifyframework/core/model/LazyModel.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 +import com.amplifyframework.core.Consumer + +abstract class LazyModel { + abstract fun getValue(): M? + + abstract suspend fun get(): M? + + suspend fun require(): M { + return get() ?: throw DataIntegrityException("Required model could not be found") + } + + abstract fun get(onSuccess: Consumer, + onFailure: Consumer) +} + +class DataIntegrityException(s: String) : Throwable() 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..ea25571cb5 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelField.java +++ b/core/src/main/java/com/amplifyframework/core/model/ModelField.java @@ -38,6 +38,10 @@ public final class ModelField { // field in the GraphQL target. private final String targetType; + // The names of foreign key fields in the target. For example: type of the + // field in the GraphQL target. + private final String[] targetNames; + // If the field can be modified private final boolean isReadOnly; @@ -54,6 +58,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 lazy model. + private final boolean isLazyModel; + + // True if the field is an instance of lazy list. + private final boolean isLazyList; + // True if the field is an instance of CustomType private final boolean isCustomType; @@ -72,8 +82,11 @@ private ModelField(@NonNull ModelFieldBuilder builder) { this.isArray = builder.isArray; this.isEnum = builder.isEnum; this.isModel = builder.isModel; + this.isLazyList = builder.isLazyList; + this.isLazyModel = builder.isLazyModel; this.isCustomType = builder.isCustomType; this.authRules = builder.authRules; + this.targetNames = builder.targetNames; } /** @@ -109,6 +122,14 @@ public String getTargetType() { return targetType; } + /** + * Returns the foreign key fields on the associated model. + * @return The data targetType of the field. + */ + public String[] getTargetNames() { + return targetNames; + } + /** * Returns true if the field is read only. * @return true if the field is read only. @@ -154,6 +175,24 @@ public boolean isModel() { return isModel; } + /** + * Returns true if the field's target type is Model. + * + * @return True if the field's target type is Model. + */ + public boolean isLazyModel() { + return isLazyModel; + } + + /** + * Returns true if the field's target type is Model. + * + * @return True if the field's target type is Model. + */ + public boolean isLazyList() { + return isLazyList; + } + /** * Returns true if the field's target type is CustomType. * @@ -253,6 +292,9 @@ public static class ModelFieldBuilder { // The data targetType of the field. private String targetType; + // The data targetNames of the field. + private String[] targetNames; + // If the field can be modified. private boolean isReadOnly = false; @@ -269,6 +311,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 lazy Model. + private boolean isLazyModel = false; + + // True if the field's target type is Lazy list. + private boolean isLazyList = false; + // True if the field's target type is CustomType. private boolean isCustomType = false; @@ -313,6 +361,16 @@ public ModelFieldBuilder targetType(String targetType) { return this; } + /** + * Set the data targetType of the field. + * @param targetNames The name of the foreign key fields. + * @return the builder object + */ + public ModelFieldBuilder targetNames(String[] targetNames) { + this.targetNames = targetNames; + return this; + } + /** * Set the flag indicating if the field can be modified. * @param isReadOnly if the field can be modified. @@ -365,6 +423,26 @@ public ModelFieldBuilder isModel(boolean isModel) { return this; } + /** + * Sets a flag indicating whether or not the field's target type is a Model. + * @param isLazyModel flag indicating if the field is a model + * @return the builder object + */ + public ModelFieldBuilder isLazyModel(boolean isLazyModel) { + this.isLazyModel = isLazyModel; + return this; + } + + /** + * Sets a flag indicating whether or not the field's target type is a Model. + * @param isLazyList flag indicating if the field is a model + * @return the builder object + */ + public ModelFieldBuilder isLazyList(boolean isLazyList) { + this.isLazyList = isLazyList; + 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/ModelSchema.java b/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java index 6ceb595039..3c2e7a4711 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; @@ -208,9 +209,17 @@ public boolean hasModelLevelRules() { private static ModelField createModelField(Field field) { com.amplifyframework.core.model.annotations.ModelField annotation = field.getAnnotation(com.amplifyframework.core.model.annotations.ModelField.class); + String[] targetNames; if (annotation != null) { final String fieldName = field.getName(); - final Class fieldType = field.getType(); + final Class fieldType; + if (field.getType() == LazyModel.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,12 +234,25 @@ private static ModelField createModelField(Field field) { .isArray(Collection.class.isAssignableFrom(field.getType())) .isEnum(Enum.class.isAssignableFrom(field.getType())) .isModel(Model.class.isAssignableFrom(field.getType())) + .isLazyModel(LazyModel.class.isAssignableFrom(field.getType())) + .isLazyList(LazyList.class.isAssignableFrom(field.getType())) .authRules(authRules) .build(); } return null; } + public static Class findSuperClassParameterType(Object instance) { + Class subClass = instance.getClass(); + while (subClass != subClass.getSuperclass()) { + // instance.getClass() is no subclass of classOfInterest or instance is a direct instance of classOfInterest + subClass = subClass.getSuperclass(); + if (subClass == null) throw new IllegalArgumentException(); + } + ParameterizedType parameterizedType = (ParameterizedType) subClass.getGenericSuperclass(); + return (Class) parameterizedType.getActualTypeArguments()[0]; + } + // Utility method to extract association metadata from a field private static ModelAssociation createModelAssociation(Field field) { if (field.isAnnotationPresent(BelongsTo.class)) { @@ -248,6 +270,7 @@ private static ModelAssociation createModelAssociation(Field field) { return ModelAssociation.builder() .name(HasOne.class.getSimpleName()) .associatedName(association.associatedWith()) + .targetNames(association.targetNames()) .associatedType(association.type().getSimpleName()) .build(); } @@ -255,6 +278,7 @@ private static ModelAssociation createModelAssociation(Field field) { HasMany association = Objects.requireNonNull(field.getAnnotation(HasMany.class)); return ModelAssociation.builder() .name(HasMany.class.getSimpleName()) + .targetNames(association.targetNames()) .associatedName(association.associatedWith()) .associatedType(association.type().getSimpleName()) .build(); diff --git a/core/src/main/java/com/amplifyframework/core/model/SchemaHelper.kt b/core/src/main/java/com/amplifyframework/core/model/SchemaHelper.kt new file mode 100644 index 0000000000..cb266665ea --- /dev/null +++ b/core/src/main/java/com/amplifyframework/core/model/SchemaHelper.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.core.model + +class SchemaHelper { + + fun getAssociatedKeys(schema: ModelSchema, modelField: ModelField): Map> { + val associatedFieldsMap = HashMap>() + val associatedFields = ArrayList() + val association = schema.getAssociations().get(modelField.name) + association?.run { + getForeignKeyFields(schema, associatedFields) + if (this.isOwner) { + associatedFieldsMap.put(schema.name, associatedFields) + } else { + associatedFieldsMap.put(associatedName, associatedFields) + } + } + return associatedFieldsMap + } + + private fun ModelAssociation.getForeignKeyFields( + schema: ModelSchema, + associatedFields: ArrayList) = if (schema.version >= 1 && !this.targetNames.isNullOrEmpty()) { + // When target names 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. + associatedFields.addAll(targetNames.toList()) + } else { + associatedFields.add(targetName) + } +} diff --git a/core/src/main/java/com/amplifyframework/core/model/annotations/HasMany.java b/core/src/main/java/com/amplifyframework/core/model/annotations/HasMany.java index b148e3cdf4..b850eb9f20 100644 --- a/core/src/main/java/com/amplifyframework/core/model/annotations/HasMany.java +++ b/core/src/main/java/com/amplifyframework/core/model/annotations/HasMany.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/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/util/FieldFinder.java b/core/src/main/java/com/amplifyframework/util/FieldFinder.java index 3f04b60607..bfc622f7e5 100644 --- a/core/src/main/java/com/amplifyframework/util/FieldFinder.java +++ b/core/src/main/java/com/amplifyframework/util/FieldFinder.java @@ -18,11 +18,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.amplifyframework.core.model.LazyModel; import com.amplifyframework.core.model.SerializedModel; import com.amplifyframework.core.model.annotations.ModelField; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -54,6 +56,13 @@ public static List findModelFieldsIn(@NonNull Class clazz) { while (fieldContainerClazz != null) { for (Field field : fieldContainerClazz.getDeclaredFields()) { if (field.isAnnotationPresent(ModelField.class)) { + if (field.getType() == LazyModel.class){ + if (field.getGenericType() instanceof ParameterizedType) { + ParameterizedType pType = (ParameterizedType)field.getGenericType() ; + } else { + System.out.println("Type: " + field.getType()); + } + } fields.add(field); } } From 84601638fd6b3cb884cbba3123244c09cb6c64b0 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Mon, 5 Jun 2023 11:57:27 -0400 Subject: [PATCH 002/100] Allow passing ModelIdentifier for querying based on CPK --- .../api/aws/AppSyncGraphQLRequestFactory.java | 92 +++++++++++++++++-- .../api/aws/GraphQLRequestVariable.kt | 6 ++ .../api/graphql/model/ModelQuery.java | 17 ++++ 3 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/GraphQLRequestVariable.kt 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 index 7b892fc374..f9f98827e8 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.java @@ -28,6 +28,7 @@ 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.ModelSchema; import com.amplifyframework.core.model.query.predicate.BeginsWithQueryOperator; import com.amplifyframework.core.model.query.predicate.BetweenQueryOperator; @@ -47,6 +48,7 @@ import com.amplifyframework.util.Casing; import com.amplifyframework.util.TypeMaker; +import java.io.Serializable; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.util.ArrayList; @@ -56,6 +58,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; /** * Converts provided model or class type into a request container with automatically generated GraphQL documents that @@ -83,18 +86,89 @@ public static GraphQLRequest buildQuery( Class modelClass, String objectId ) { + return buildQuery(modelClass, new GraphQLRequestVariable("id", objectId, "ID!")); + } + + /** + * 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( + Class modelClass, + ModelIdentifier modelIdentifier + ) { + GraphQLRequestVariable variables[]; try { - return AppSyncGraphQLRequest.builder() - .modelClass(modelClass) - .operation(QueryType.GET) - .requestOptions(new ApiGraphQLRequestOptions()) - .responseType(modelClass) - .variable("id", "ID!", objectId) - .build(); + 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!". Appends "!" if required + 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 + "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 ); } } diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/GraphQLRequestVariable.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/GraphQLRequestVariable.kt new file mode 100644 index 0000000000..da9111f305 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/GraphQLRequestVariable.kt @@ -0,0 +1,6 @@ +package com.amplifyframework.api.aws + +/** + * Holds information needed for a single variable for model querying + */ +internal data class GraphQLRequestVariable(val key: String, val value: Object, val type: String) 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 index fa84ddda86..8145bb08aa 100644 --- 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 @@ -21,6 +21,7 @@ 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; @@ -51,6 +52,22 @@ public static GraphQLRequest get( 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 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 From 017ac0a5be75d7f1fbfc9007f54d02ba9fd1f658 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Tue, 6 Jun 2023 15:27:52 -0400 Subject: [PATCH 003/100] Share RequestFactory work between API and DataStore --- .../api/aws/GraphQLRequestHelper.java | 314 ++++++++++++++++++ .../api/aws/AppSyncGraphQLRequestFactory.java | 190 +---------- .../appsync/AppSyncRequestFactory.java | 305 +---------------- .../appsync/AppSyncRequestFactoryTest.java | 9 +- 4 files changed, 326 insertions(+), 492 deletions(-) create mode 100644 aws-api-appsync/src/main/java/com/amplifyframework/api/aws/GraphQLRequestHelper.java 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 new file mode 100644 index 0000000000..d8377975a9 --- /dev/null +++ b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/GraphQLRequestHelper.java @@ -0,0 +1,314 @@ +package com.amplifyframework.api.aws; + +import androidx.annotation.NonNull; + +import com.amplifyframework.AmplifyException; +import com.amplifyframework.core.model.AuthRule; +import com.amplifyframework.core.model.AuthStrategy; +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.ModelSchema; +import com.amplifyframework.core.model.SerializedCustomType; +import com.amplifyframework.core.model.SerializedModel; +import com.amplifyframework.core.model.query.predicate.BeginsWithQueryOperator; +import com.amplifyframework.core.model.query.predicate.BetweenQueryOperator; +import com.amplifyframework.core.model.query.predicate.ContainsQueryOperator; +import com.amplifyframework.core.model.query.predicate.EqualQueryOperator; +import com.amplifyframework.core.model.query.predicate.GreaterOrEqualQueryOperator; +import com.amplifyframework.core.model.query.predicate.GreaterThanQueryOperator; +import com.amplifyframework.core.model.query.predicate.LessOrEqualQueryOperator; +import com.amplifyframework.core.model.query.predicate.LessThanQueryOperator; +import com.amplifyframework.core.model.query.predicate.NotContainsQueryOperator; +import com.amplifyframework.core.model.query.predicate.NotEqualQueryOperator; +import com.amplifyframework.core.model.query.predicate.QueryOperator; +import com.amplifyframework.core.model.query.predicate.QueryPredicate; +import com.amplifyframework.core.model.query.predicate.QueryPredicateGroup; +import com.amplifyframework.core.model.query.predicate.QueryPredicateOperation; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Locale; +import java.util.Map; + +public class GraphQLRequestHelper { + + private static String appSyncOpType(QueryOperator.Type type) throws AmplifyException { + switch (type) { + case NOT_EQUAL: + return "ne"; + case EQUAL: + return "eq"; + case LESS_OR_EQUAL: + return "le"; + case LESS_THAN: + return "lt"; + case GREATER_OR_EQUAL: + return "ge"; + case GREATER_THAN: + return "gt"; + case CONTAINS: + return "contains"; + case BETWEEN: + return "between"; + case BEGINS_WITH: + return "beginsWith"; + default: + throw new AmplifyException( + "Tried to parse an unsupported QueryOperator type", + "Check if a new QueryOperator.Type enum has been created which is not supported " + + "in the AppSyncGraphQLRequestFactory." + ); + } + } + + private static Object appSyncOpValue(QueryOperator qOp) throws AmplifyException{ + switch (qOp.type()) { + case NOT_EQUAL: + return ((NotEqualQueryOperator) qOp).value(); + case EQUAL: + return ((EqualQueryOperator) qOp).value(); + case LESS_OR_EQUAL: + return ((LessOrEqualQueryOperator) qOp).value(); + case LESS_THAN: + return ((LessThanQueryOperator) qOp).value(); + case GREATER_OR_EQUAL: + return ((GreaterOrEqualQueryOperator) qOp).value(); + case GREATER_THAN: + return ((GreaterThanQueryOperator) qOp).value(); + case CONTAINS: + return ((ContainsQueryOperator) qOp).value(); + case NOT_CONTAINS: + return ((NotContainsQueryOperator) qOp).value(); + case BETWEEN: + BetweenQueryOperator betweenOp = (BetweenQueryOperator) qOp; + return Arrays.asList(betweenOp.start(), betweenOp.end()); + case BEGINS_WITH: + return ((BeginsWithQueryOperator) qOp).value(); + default: + throw new AmplifyException( + "Tried to parse an unsupported QueryOperator type", + "Check if a new QueryOperator.Type enum has been created which is not supported " + + "in the AppSyncGraphQLRequestFactory." + ); + } + } + + public static Map parsePredicate(QueryPredicate queryPredicate) throws AmplifyException { + if (queryPredicate instanceof QueryPredicateOperation) { + QueryPredicateOperation qpo = (QueryPredicateOperation) queryPredicate; + QueryOperator op = qpo.operator(); + return Collections.singletonMap( + qpo.field(), + Collections.singletonMap(appSyncOpType(op.type()), appSyncOpValue(op)) + ); + } else if (queryPredicate instanceof QueryPredicateGroup) { + QueryPredicateGroup qpg = (QueryPredicateGroup) queryPredicate; + + if (QueryPredicateGroup.Type.NOT.equals(qpg.type())) { + try { + return Collections.singletonMap("not", parsePredicate(qpg.predicates().get(0))); + } catch (IndexOutOfBoundsException exception) { + throw new IllegalStateException( + "Predicate group of type NOT must include a value to negate.", + exception + ); + } + } else { + List> predicates = new ArrayList<>(); + + for (QueryPredicate predicate : qpg.predicates()) { + predicates.add(parsePredicate(predicate)); + } + + return Collections.singletonMap(qpg.type().toString().toLowerCase(Locale.getDefault()), predicates); + } + } else { + throw new AmplifyException( + "Tried to parse an unsupported QueryPredicate", + "Try changing to one of the supported values: QueryPredicateOperation, QueryPredicateGroup." + ); + } + } + + 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)); + } + return input; + } + + public static Map getMapOfFieldNameAndValues( + @NonNull ModelSchema schema, @NonNull Model instance) throws AmplifyException { + boolean isSerializedModel = instance instanceof SerializedModel; + boolean hasMatchingModelName = instance.getClass().getSimpleName().equals(schema.getName()); + if (!(hasMatchingModelName || isSerializedModel)) { + throw new AmplifyException( + "The object provided is not an instance of " + schema.getName() + ".", + "Please provide an instance of " + schema.getName() + " that matches the schema type." + ); + } + + Map result = new HashMap<>(extractFieldLevelData(schema, instance)); + + /* + * If the owner field exists on the model, and the value is null, it should be omitted when performing a + * mutation because the AppSync server will automatically populate it using the authentication token provided + * in the request header. The logic below filters out the owner field if null for this scenario. + */ + for (AuthRule authRule : schema.getAuthRules()) { + if (AuthStrategy.OWNER.equals(authRule.getAuthStrategy())) { + String ownerField = authRule.getOwnerFieldOrDefault(); + if (result.containsKey(ownerField) && result.get(ownerField) == null) { + result.remove(ownerField); + } + } + } + + return result; + } + + public static Map extractFieldLevelData( + ModelSchema schema, Model instance) throws AmplifyException { + final Map result = new HashMap<>(); + for (ModelField modelField : schema.getFields().values()) { + if (modelField.isReadOnly()) { + // Skip read only fields, since they should not be included on the input object. + continue; + } + String fieldName = modelField.getName(); + final ModelAssociation association = schema.getAssociations().get(fieldName); + if (instance instanceof SerializedModel + && !((SerializedModel) instance).getSerializedData().containsKey(fieldName)) { + // Skip fields that are not set, so that they are not set to null in the request. + continue; + } + if (association == null) { + result.put(fieldName, extractFieldValue(modelField.getName(), instance, schema)); + } else if (association.isOwner()) { + 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, instance, schema, association); + } else { + final Object fieldValue = extractFieldValue(modelField.getName(), instance, schema); + if (fieldValue != null) { + String targetName = association.getTargetName(); + result.put(targetName, extractAssociateId(modelField, fieldValue)); + } + } + } + // Ignore if field is associated, but is not a "belongsTo" relationship + } + return result; + } + + public static void insertForeignKeyValues(Map result, ModelField modelField, + Model instance, ModelSchema schema, + ModelAssociation association) throws AmplifyException { + final Object fieldValue = extractFieldValue(modelField.getName(), instance, schema); + 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(); + + result.put(targetNames.next(), primaryKey.key().toString()); + + while (targetNames.hasNext()) { + result.put(targetNames.next(), sortedKeys.next().toString()); + } + } else if ((fieldValue instanceof SerializedModel)) { + SerializedModel serializedModel = ((SerializedModel) fieldValue); + ModelSchema serializedSchema = serializedModel.getModelSchema(); + if (serializedSchema != null && + serializedSchema.getPrimaryIndexFields().size() > 1) { + + 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()); + } + } else { + result.put(association.getTargetNames()[0], ((Model) fieldValue).resolveIdentifier().toString()); + } + } + } + + public static Object extractAssociateId(ModelField modelField, @NonNull Object fieldValue) { + if (modelField.isModel() && fieldValue instanceof Model) { + return ((Model) fieldValue).resolveIdentifier(); + } else if (modelField.isModel() && fieldValue instanceof Map) { + return ((Map) fieldValue).get("id"); + } else { + throw new IllegalStateException("Associated data is not Model or Map."); + } + } + + public static Object extractFieldValue(String fieldName, Model instance, ModelSchema schema) + throws AmplifyException { + if (instance instanceof SerializedModel) { + SerializedModel serializedModel = (SerializedModel) instance; + Map serializedData = serializedModel.getSerializedData(); + ModelField field = schema.getFields().get(fieldName); + Object fieldValue = serializedData.get(fieldName); + if (fieldValue != null && field != null && field.isCustomType()) { + return extractCustomTypeFieldValue(fieldName, serializedData.get(fieldName)); + } + return fieldValue; + } + try { + Field privateField = instance.getClass().getDeclaredField(fieldName); + privateField.setAccessible(true); + return privateField.get(instance); + } catch (Exception exception) { + throw new AmplifyException( + "An invalid field was provided. " + fieldName + " is not present in " + schema.getName(), + exception, + "Check if this model schema is a correct representation of the fields in the provided Object"); + } + } + + public static Object extractCustomTypeFieldValue(String fieldName, Object customTypeData) throws AmplifyException { + // Flutter use case: + // If a field is a CustomType, it's value is either a SerializedCustomType + // or a List of SerializedCustomType + if (customTypeData instanceof SerializedCustomType) { + return ((SerializedCustomType) customTypeData).getFlatSerializedData(); + } + + if (customTypeData instanceof List) { + ArrayList result = new ArrayList<>(); + @SuppressWarnings("unchecked") + List customTypeList = (List) customTypeData; + for (Object item : customTypeList) { + if (item instanceof SerializedCustomType) { + result.add(((SerializedCustomType) item).getFlatSerializedData()); + } else { + result.add(item); + } + } + return result; + } + + throw new AmplifyException( + "An invalid CustomType field was provided. " + fieldName + " must be an instance of " + + "SerializedCustomType or a List of instances of SerializedCustomType", + "Check if this model schema is a correct representation of the fields in the provided Object"); + } +} 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 index f9f98827e8..e3f4bcd59e 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.java @@ -15,7 +15,9 @@ package com.amplifyframework.api.aws; -import androidx.annotation.NonNull; +import static com.amplifyframework.api.aws.GraphQLRequestHelper.getDeleteMutationInputMap; +import static com.amplifyframework.api.aws.GraphQLRequestHelper.getMapOfFieldNameAndValues; +import static com.amplifyframework.api.aws.GraphQLRequestHelper.parsePredicate; import com.amplifyframework.AmplifyException; import com.amplifyframework.api.graphql.GraphQLRequest; @@ -23,41 +25,18 @@ import com.amplifyframework.api.graphql.PaginatedResult; import com.amplifyframework.api.graphql.QueryType; import com.amplifyframework.api.graphql.SubscriptionType; -import com.amplifyframework.core.model.AuthRule; -import com.amplifyframework.core.model.AuthStrategy; 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.ModelSchema; -import com.amplifyframework.core.model.query.predicate.BeginsWithQueryOperator; -import com.amplifyframework.core.model.query.predicate.BetweenQueryOperator; -import com.amplifyframework.core.model.query.predicate.ContainsQueryOperator; -import com.amplifyframework.core.model.query.predicate.EqualQueryOperator; -import com.amplifyframework.core.model.query.predicate.GreaterOrEqualQueryOperator; -import com.amplifyframework.core.model.query.predicate.GreaterThanQueryOperator; -import com.amplifyframework.core.model.query.predicate.LessOrEqualQueryOperator; -import com.amplifyframework.core.model.query.predicate.LessThanQueryOperator; -import com.amplifyframework.core.model.query.predicate.NotContainsQueryOperator; -import com.amplifyframework.core.model.query.predicate.NotEqualQueryOperator; -import com.amplifyframework.core.model.query.predicate.QueryOperator; import com.amplifyframework.core.model.query.predicate.QueryPredicate; -import com.amplifyframework.core.model.query.predicate.QueryPredicateGroup; -import com.amplifyframework.core.model.query.predicate.QueryPredicateOperation; 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.Field; import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Locale; -import java.util.Map; import java.util.Objects; /** @@ -324,167 +303,4 @@ public static GraphQLRequest buildSubscription( ); } } - - private static Map parsePredicate(QueryPredicate queryPredicate) { - if (queryPredicate instanceof QueryPredicateOperation) { - QueryPredicateOperation qpo = (QueryPredicateOperation) queryPredicate; - QueryOperator op = qpo.operator(); - return Collections.singletonMap( - qpo.field(), - Collections.singletonMap(appSyncOpType(op.type()), appSyncOpValue(op)) - ); - } else if (queryPredicate instanceof QueryPredicateGroup) { - QueryPredicateGroup qpg = (QueryPredicateGroup) queryPredicate; - - if (QueryPredicateGroup.Type.NOT.equals(qpg.type())) { - try { - return Collections.singletonMap("not", parsePredicate(qpg.predicates().get(0))); - } catch (IndexOutOfBoundsException exception) { - throw new IllegalStateException( - "Predicate group of type NOT must include a value to negate.", - exception - ); - } - } else { - List> predicates = new ArrayList<>(); - - for (QueryPredicate predicate : qpg.predicates()) { - predicates.add(parsePredicate(predicate)); - } - - return Collections.singletonMap(qpg.type().toString().toLowerCase(Locale.getDefault()), predicates); - } - } else { - throw new IllegalStateException( - "Invalid predicate type, supported values: QueryPredicateOperation, QueryPredicateGroup." - ); - } - } - - private static String appSyncOpType(QueryOperator.Type type) { - switch (type) { - case NOT_EQUAL: - return "ne"; - case EQUAL: - return "eq"; - case LESS_OR_EQUAL: - return "le"; - case LESS_THAN: - return "lt"; - case GREATER_OR_EQUAL: - return "ge"; - case GREATER_THAN: - return "gt"; - case CONTAINS: - return "contains"; - case BETWEEN: - return "between"; - case BEGINS_WITH: - return "beginsWith"; - default: - throw new IllegalStateException( - "Tried to parse an unsupported QueryOperator type. Check if a new QueryOperator.Type enum " + - "has been created which is not supported in the AppSyncGraphQLRequestFactory." - ); - } - } - - private static Object appSyncOpValue(QueryOperator qOp) { - switch (qOp.type()) { - case NOT_EQUAL: - return ((NotEqualQueryOperator) qOp).value(); - case EQUAL: - return ((EqualQueryOperator) qOp).value(); - case LESS_OR_EQUAL: - return ((LessOrEqualQueryOperator) qOp).value(); - case LESS_THAN: - return ((LessThanQueryOperator) qOp).value(); - case GREATER_OR_EQUAL: - return ((GreaterOrEqualQueryOperator) qOp).value(); - case GREATER_THAN: - return ((GreaterThanQueryOperator) qOp).value(); - case CONTAINS: - return ((ContainsQueryOperator) qOp).value(); - case NOT_CONTAINS: - return ((NotContainsQueryOperator) qOp).value(); - case BETWEEN: - BetweenQueryOperator betweenOp = (BetweenQueryOperator) qOp; - return Arrays.asList(betweenOp.start(), betweenOp.end()); - case BEGINS_WITH: - return ((BeginsWithQueryOperator) qOp).value(); - default: - throw new IllegalStateException( - "Tried to parse an unsupported QueryOperator type. Check if a new QueryOperator.Type enum " + - "has been created which is not implemented yet." - ); - } - } - - private 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)); - } - return input; - } - - private static Map getMapOfFieldNameAndValues( - @NonNull ModelSchema schema, @NonNull Model instance) throws AmplifyException { - if (!instance.getClass().getSimpleName().equals(schema.getName())) { - throw new AmplifyException( - "The object provided is not an instance of " + schema.getName() + ".", - "Please provide an instance of " + schema.getName() + " that matches the schema type." - ); - } - final Map result = new HashMap<>(); - for (ModelField modelField : schema.getFields().values()) { - if (modelField.isReadOnly()) { - // Skip read only fields, since they should not be included on the input object. - continue; - } - String fieldName = modelField.getName(); - Object fieldValue = extractFieldValue(fieldName, instance, schema); - final ModelAssociation association = schema.getAssociations().get(fieldName); - if (association == null) { - result.put(fieldName, fieldValue); - } else if (association.isOwner()) { - if (fieldValue != null) { - Model target = (Model) fieldValue; - result.put(association.getTargetName(), target.getPrimaryKeyString()); - } - } - // Ignore if field is associated, but is not a "belongsTo" relationship - } - - /* - * If the owner field exists on the model, and the value is null, it should be omitted when performing a - * mutation because the AppSync server will automatically populate it using the authentication token provided - * in the request header. The logic below filters out the owner field if null for this scenario. - */ - for (AuthRule authRule : schema.getAuthRules()) { - if (AuthStrategy.OWNER.equals(authRule.getAuthStrategy())) { - String ownerField = authRule.getOwnerFieldOrDefault(); - if (result.containsKey(ownerField) && result.get(ownerField) == null) { - result.remove(ownerField); - } - } - } - - return result; - } - - private static Object extractFieldValue(String fieldName, Model instance, ModelSchema schema) - throws AmplifyException { - try { - Field privateField = instance.getClass().getDeclaredField(fieldName); - privateField.setAccessible(true); - return privateField.get(instance); - } catch (Exception exception) { - throw new AmplifyException( - "An invalid field was provided. " + fieldName + " is not present in " + schema.getName(), - exception, - "Check if this model schema is a correct representation of the fields in the provided Object"); - } - } } diff --git a/aws-datastore/src/main/java/com/amplifyframework/datastore/appsync/AppSyncRequestFactory.java b/aws-datastore/src/main/java/com/amplifyframework/datastore/appsync/AppSyncRequestFactory.java index 24e24b2083..3b94e62d6f 100644 --- a/aws-datastore/src/main/java/com/amplifyframework/datastore/appsync/AppSyncRequestFactory.java +++ b/aws-datastore/src/main/java/com/amplifyframework/datastore/appsync/AppSyncRequestFactory.java @@ -15,6 +15,10 @@ package com.amplifyframework.datastore.appsync; +import static com.amplifyframework.api.aws.GraphQLRequestHelper.getDeleteMutationInputMap; +import static com.amplifyframework.api.aws.GraphQLRequestHelper.getMapOfFieldNameAndValues; +import static com.amplifyframework.api.aws.GraphQLRequestHelper.parsePredicate; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -26,45 +30,17 @@ import com.amplifyframework.api.graphql.QueryType; import com.amplifyframework.api.graphql.SubscriptionType; import com.amplifyframework.core.Amplify; -import com.amplifyframework.core.model.AuthRule; -import com.amplifyframework.core.model.AuthStrategy; 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.ModelSchema; -import com.amplifyframework.core.model.SerializedCustomType; -import com.amplifyframework.core.model.SerializedModel; -import com.amplifyframework.core.model.query.predicate.BeginsWithQueryOperator; -import com.amplifyframework.core.model.query.predicate.BetweenQueryOperator; -import com.amplifyframework.core.model.query.predicate.ContainsQueryOperator; -import com.amplifyframework.core.model.query.predicate.EqualQueryOperator; -import com.amplifyframework.core.model.query.predicate.GreaterOrEqualQueryOperator; -import com.amplifyframework.core.model.query.predicate.GreaterThanQueryOperator; -import com.amplifyframework.core.model.query.predicate.LessOrEqualQueryOperator; -import com.amplifyframework.core.model.query.predicate.LessThanQueryOperator; -import com.amplifyframework.core.model.query.predicate.NotContainsQueryOperator; -import com.amplifyframework.core.model.query.predicate.NotEqualQueryOperator; -import com.amplifyframework.core.model.query.predicate.QueryOperator; import com.amplifyframework.core.model.query.predicate.QueryPredicate; import com.amplifyframework.core.model.query.predicate.QueryPredicateGroup; -import com.amplifyframework.core.model.query.predicate.QueryPredicateOperation; import com.amplifyframework.core.model.query.predicate.QueryPredicates; import com.amplifyframework.datastore.DataStoreException; import com.amplifyframework.logging.Logger; import com.amplifyframework.util.Casing; import com.amplifyframework.util.TypeMaker; -import java.io.Serializable; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.ListIterator; -import java.util.Locale; import java.util.Map; /** @@ -228,105 +204,6 @@ static AppSyncGraphQLRequest> buildCreati } } - static Map parsePredicate(QueryPredicate queryPredicate) throws DataStoreException { - if (queryPredicate instanceof QueryPredicateOperation) { - QueryPredicateOperation qpo = (QueryPredicateOperation) queryPredicate; - QueryOperator op = qpo.operator(); - return Collections.singletonMap( - qpo.field(), - Collections.singletonMap(appSyncOpType(op.type()), appSyncOpValue(op)) - ); - } else if (queryPredicate instanceof QueryPredicateGroup) { - QueryPredicateGroup qpg = (QueryPredicateGroup) queryPredicate; - - if (QueryPredicateGroup.Type.NOT.equals(qpg.type())) { - try { - return Collections.singletonMap("not", parsePredicate(qpg.predicates().get(0))); - } catch (IndexOutOfBoundsException exception) { - throw new DataStoreException( - "Predicate group of type NOT must include a value to negate.", - exception, - "Check if you created a NOT condition in your Predicate with no included value." - ); - } - } else { - List> predicates = new ArrayList<>(); - - for (QueryPredicate predicate : qpg.predicates()) { - predicates.add(parsePredicate(predicate)); - } - - return Collections.singletonMap(qpg.type().toString().toLowerCase(Locale.getDefault()), predicates); - } - } else { - throw new DataStoreException( - "Tried to parse an unsupported QueryPredicate", - "Try changing to one of the supported values: QueryPredicateOperation, QueryPredicateGroup." - ); - } - } - - private static String appSyncOpType(QueryOperator.Type type) throws DataStoreException { - switch (type) { - case NOT_EQUAL: - return "ne"; - case EQUAL: - return "eq"; - case LESS_OR_EQUAL: - return "le"; - case LESS_THAN: - return "lt"; - case GREATER_OR_EQUAL: - return "ge"; - case GREATER_THAN: - return "gt"; - case CONTAINS: - return "contains"; - case BETWEEN: - return "between"; - case BEGINS_WITH: - return "beginsWith"; - default: - throw new DataStoreException( - "Tried to parse an unsupported QueryOperator type", - "Check if a new QueryOperator.Type enum has been created which is not supported " + - "in the AppSyncGraphQLRequestFactory." - ); - } - } - - private static Object appSyncOpValue(QueryOperator qOp) throws DataStoreException { - switch (qOp.type()) { - case NOT_EQUAL: - return ((NotEqualQueryOperator) qOp).value(); - case EQUAL: - return ((EqualQueryOperator) qOp).value(); - case LESS_OR_EQUAL: - return ((LessOrEqualQueryOperator) qOp).value(); - case LESS_THAN: - return ((LessThanQueryOperator) qOp).value(); - case GREATER_OR_EQUAL: - return ((GreaterOrEqualQueryOperator) qOp).value(); - case GREATER_THAN: - return ((GreaterThanQueryOperator) qOp).value(); - case CONTAINS: - return ((ContainsQueryOperator) qOp).value(); - case NOT_CONTAINS: - return ((NotContainsQueryOperator) qOp).value(); - case BETWEEN: - BetweenQueryOperator betweenOp = (BetweenQueryOperator) qOp; - return Arrays.asList(betweenOp.start(), betweenOp.end()); - case BEGINS_WITH: - return ((BeginsWithQueryOperator) qOp).value(); - default: - throw new DataStoreException( - "Tried to parse an unsupported QueryOperator type", - "Check if a new QueryOperator.Type enum has been created which is not supported " + - "in the AppSyncGraphQLRequestFactory." - ); - } - } - /** * Builds a mutation. * @param schema the model schema for the mutation @@ -372,178 +249,4 @@ private static AppSyncGraphQLRequest> bui amplifyException, "Validate your model file."); } } - - private 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)); - } - return input; - } - - private static Map getMapOfFieldNameAndValues( - @NonNull ModelSchema schema, @NonNull Model instance) throws AmplifyException { - boolean isSerializedModel = instance instanceof SerializedModel; - boolean hasMatchingModelName = instance.getClass().getSimpleName().equals(schema.getName()); - if (!(hasMatchingModelName || isSerializedModel)) { - throw new AmplifyException( - "The object provided is not an instance of " + schema.getName() + ".", - "Please provide an instance of " + schema.getName() + " that matches the schema type." - ); - } - - Map result = new HashMap<>(extractFieldLevelData(schema, instance)); - - /* - * If the owner field exists on the model, and the value is null, it should be omitted when performing a - * mutation because the AppSync server will automatically populate it using the authentication token provided - * in the request header. The logic below filters out the owner field if null for this scenario. - */ - for (AuthRule authRule : schema.getAuthRules()) { - if (AuthStrategy.OWNER.equals(authRule.getAuthStrategy())) { - String ownerField = authRule.getOwnerFieldOrDefault(); - if (result.containsKey(ownerField) && result.get(ownerField) == null) { - result.remove(ownerField); - } - } - } - - return result; - } - - private static Map extractFieldLevelData( - ModelSchema schema, Model instance) throws AmplifyException { - final Map result = new HashMap<>(); - for (ModelField modelField : schema.getFields().values()) { - if (modelField.isReadOnly()) { - // Skip read only fields, since they should not be included on the input object. - continue; - } - String fieldName = modelField.getName(); - final ModelAssociation association = schema.getAssociations().get(fieldName); - if (instance instanceof SerializedModel - && !((SerializedModel) instance).getSerializedData().containsKey(fieldName)) { - // Skip fields that are not set, so that they are not set to null in the request. - continue; - } - if (association == null) { - result.put(fieldName, extractFieldValue(modelField.getName(), instance, schema)); - } else if (association.isOwner()) { - 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, instance, schema, association); - } else { - String targetName = association.getTargetName(); - result.put(targetName, extractAssociateId(modelField, instance, schema)); - } - } - // Ignore if field is associated, but is not a "belongsTo" relationship - } - return result; - } - - private static void insertForeignKeyValues(Map result, ModelField modelField, - Model instance, ModelSchema schema, - ModelAssociation association) throws AmplifyException { - final Object fieldValue = extractFieldValue(modelField.getName(), instance, schema); - 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(); - - result.put(targetNames.next(), primaryKey.key().toString()); - - while (targetNames.hasNext()) { - result.put(targetNames.next(), sortedKeys.next().toString()); - } - } else if ((fieldValue instanceof SerializedModel)) { - SerializedModel serializedModel = ((SerializedModel) fieldValue); - ModelSchema serializedSchema = serializedModel.getModelSchema(); - if (serializedSchema != null && - serializedSchema.getPrimaryIndexFields().size() > 1) { - - 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()); - } - } else { - result.put(association.getTargetNames()[0], ((Model) fieldValue).resolveIdentifier().toString()); - } - } - } - - private static Object extractAssociateId(ModelField modelField, Model instance, ModelSchema schema) - throws AmplifyException { - final Object fieldValue = extractFieldValue(modelField.getName(), instance, schema); - if (modelField.isModel() && fieldValue instanceof Model) { - return ((Model) fieldValue).resolveIdentifier(); - } else if (modelField.isModel() && fieldValue instanceof Map) { - return ((Map) fieldValue).get("id"); - } else { - LOG.warn(String.format("Can't extract identifier: modelField=%s, isModel=%s, fieldValue=%s", - modelField.getName(), modelField.isModel(), fieldValue)); - throw new IllegalStateException("Associated data is not Model or Map."); - } - } - - private static Object extractFieldValue(String fieldName, Model instance, ModelSchema schema) - throws AmplifyException { - if (instance instanceof SerializedModel) { - SerializedModel serializedModel = (SerializedModel) instance; - Map serializedData = serializedModel.getSerializedData(); - ModelField field = schema.getFields().get(fieldName); - Object fieldValue = serializedData.get(fieldName); - if (fieldValue != null && field != null && field.isCustomType()) { - return extractCustomTypeFieldValue(fieldName, serializedData.get(fieldName)); - } - return fieldValue; - } - try { - Field privateField = instance.getClass().getDeclaredField(fieldName); - privateField.setAccessible(true); - return privateField.get(instance); - } catch (Exception exception) { - throw new AmplifyException( - "An invalid field was provided. " + fieldName + " is not present in " + schema.getName(), - exception, - "Check if this model schema is a correct representation of the fields in the provided Object"); - } - } - - private static Object extractCustomTypeFieldValue(String fieldName, Object customTypeData) throws AmplifyException { - // Flutter use case: - // If a field is a CustomType, it's value is either a SerializedCustomType - // or a List of SerializedCustomType - if (customTypeData instanceof SerializedCustomType) { - return ((SerializedCustomType) customTypeData).getFlatSerializedData(); - } - - if (customTypeData instanceof List) { - ArrayList result = new ArrayList<>(); - @SuppressWarnings("unchecked") - List customTypeList = (List) customTypeData; - for (Object item : customTypeList) { - if (item instanceof SerializedCustomType) { - result.add(((SerializedCustomType) item).getFlatSerializedData()); - } else { - result.add(item); - } - } - return result; - } - - throw new AmplifyException( - "An invalid CustomType field was provided. " + fieldName + " must be an instance of " + - "SerializedCustomType or a List of instances of SerializedCustomType", - "Check if this model schema is a correct representation of the fields in the provided Object"); - } } diff --git a/aws-datastore/src/test/java/com/amplifyframework/datastore/appsync/AppSyncRequestFactoryTest.java b/aws-datastore/src/test/java/com/amplifyframework/datastore/appsync/AppSyncRequestFactoryTest.java index 17fab052c4..7ecb1c9797 100644 --- a/aws-datastore/src/test/java/com/amplifyframework/datastore/appsync/AppSyncRequestFactoryTest.java +++ b/aws-datastore/src/test/java/com/amplifyframework/datastore/appsync/AppSyncRequestFactoryTest.java @@ -20,6 +20,7 @@ import com.amplifyframework.AmplifyException; import com.amplifyframework.api.aws.AppSyncGraphQLRequest; import com.amplifyframework.api.aws.AuthModeStrategyType; +import com.amplifyframework.api.aws.GraphQLRequestHelper; import com.amplifyframework.api.graphql.GraphQLRequest; import com.amplifyframework.api.graphql.SubscriptionType; import com.amplifyframework.core.model.AuthStrategy; @@ -282,13 +283,13 @@ public void validateDeleteWithCustomPrimaryKey() throws AmplifyException, JSONEx /** * Checks that the predicate expression matches the expected value. - * @throws DataStoreException If the output does not match. + * @throws AmplifyException If the output does not match. */ @Test - public void validatePredicateGeneration() throws DataStoreException { + public void validatePredicateGeneration() throws AmplifyException { assertEquals( Collections.singletonMap("name", Collections.singletonMap("eq", "Test Dummy")), - AppSyncRequestFactory.parsePredicate(BlogOwner.NAME.eq("Test Dummy")) + GraphQLRequestHelper.parsePredicate(BlogOwner.NAME.eq("Test Dummy")) ); assertEquals( @@ -296,7 +297,7 @@ public void validatePredicateGeneration() throws DataStoreException { Collections.singletonMap("name", Collections.singletonMap("beginsWith", "A day in the life of a...")), Collections.singletonMap("blogOwnerBlogId", Collections.singletonMap("eq", "DUMMY_OWNER_ID")) )), - AppSyncRequestFactory.parsePredicate( + GraphQLRequestHelper.parsePredicate( Blog.NAME.beginsWith("A day in the life of a...").and(Blog.OWNER.eq("DUMMY_OWNER_ID")) ) ); From 43edd8d3bfac62de0d255de7d75bc233ded936d8 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Tue, 6 Jun 2023 16:14:22 -0400 Subject: [PATCH 004/100] Mark helper to Amplify Internal --- .../api/aws/GraphQLRequestHelper.java | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) 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 d8377975a9..9c3284e35f 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 @@ -1,8 +1,24 @@ +/* + * 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.annotations.InternalAmplifyApi; import com.amplifyframework.core.model.AuthRule; import com.amplifyframework.core.model.AuthStrategy; import com.amplifyframework.core.model.Model; @@ -39,6 +55,12 @@ import java.util.Locale; import java.util.Map; +/** + * Although this has public access, it is intended for internal use and should not be used directly by host + * applications. The behavior of this may change without warning. + */ +@InternalAmplifyApi +@SuppressWarnings("HideUtilityClassConstructor") public class GraphQLRequestHelper { private static String appSyncOpType(QueryOperator.Type type) throws AmplifyException { @@ -70,7 +92,7 @@ private static String appSyncOpType(QueryOperator.Type type) throws AmplifyExcep } } - private static Object appSyncOpValue(QueryOperator qOp) throws AmplifyException{ + private static Object appSyncOpValue(QueryOperator qOp) throws AmplifyException { switch (qOp.type()) { case NOT_EQUAL: return ((NotEqualQueryOperator) qOp).value(); @@ -102,6 +124,8 @@ private static Object appSyncOpValue(QueryOperator qOp) throws AmplifyExcepti } } + @InternalAmplifyApi + @SuppressWarnings("MissingJavadocMethod") public static Map parsePredicate(QueryPredicate queryPredicate) throws AmplifyException { if (queryPredicate instanceof QueryPredicateOperation) { QueryPredicateOperation qpo = (QueryPredicateOperation) queryPredicate; @@ -139,6 +163,8 @@ public static Map parsePredicate(QueryPredicate queryPredicate) } } + @InternalAmplifyApi + @SuppressWarnings("MissingJavadocMethod") public static Map getDeleteMutationInputMap( @NonNull ModelSchema schema, @NonNull Model instance) throws AmplifyException { final Map input = new HashMap<>(); @@ -148,6 +174,8 @@ public static Map getDeleteMutationInputMap( return input; } + @InternalAmplifyApi + @SuppressWarnings("MissingJavadocMethod") public static Map getMapOfFieldNameAndValues( @NonNull ModelSchema schema, @NonNull Model instance) throws AmplifyException { boolean isSerializedModel = instance instanceof SerializedModel; @@ -178,7 +206,7 @@ public static Map getMapOfFieldNameAndValues( return result; } - public static Map extractFieldLevelData( + private static Map extractFieldLevelData( ModelSchema schema, Model instance) throws AmplifyException { final Map result = new HashMap<>(); for (ModelField modelField : schema.getFields().values()) { @@ -214,7 +242,7 @@ public static Map extractFieldLevelData( return result; } - public static void insertForeignKeyValues(Map result, ModelField modelField, + private static void insertForeignKeyValues(Map result, ModelField modelField, Model instance, ModelSchema schema, ModelAssociation association) throws AmplifyException { final Object fieldValue = extractFieldValue(modelField.getName(), instance, schema); @@ -250,7 +278,7 @@ public static void insertForeignKeyValues(Map result, ModelField } } - public static Object extractAssociateId(ModelField modelField, @NonNull Object fieldValue) { + private static Object extractAssociateId(ModelField modelField, @NonNull Object fieldValue) { if (modelField.isModel() && fieldValue instanceof Model) { return ((Model) fieldValue).resolveIdentifier(); } else if (modelField.isModel() && fieldValue instanceof Map) { @@ -260,7 +288,7 @@ public static Object extractAssociateId(ModelField modelField, @NonNull Object f } } - public static Object extractFieldValue(String fieldName, Model instance, ModelSchema schema) + private static Object extractFieldValue(String fieldName, Model instance, ModelSchema schema) throws AmplifyException { if (instance instanceof SerializedModel) { SerializedModel serializedModel = (SerializedModel) instance; @@ -284,7 +312,7 @@ public static Object extractFieldValue(String fieldName, Model instance, ModelSc } } - public static Object extractCustomTypeFieldValue(String fieldName, Object customTypeData) throws AmplifyException { + private static Object extractCustomTypeFieldValue(String fieldName, Object customTypeData) throws AmplifyException { // Flutter use case: // If a field is a CustomType, it's value is either a SerializedCustomType // or a List of SerializedCustomType From 1a6e0107e9c511f0e281483cbe96edf202a1814b Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Tue, 6 Jun 2023 16:32:42 -0400 Subject: [PATCH 005/100] checkstyle --- .../api/aws/AppSyncGraphQLRequestFactory.java | 31 ++++++++++++------- .../appsync/AppSyncRequestFactory.java | 23 ++++++++------ 2 files changed, 34 insertions(+), 20 deletions(-) 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 index e3f4bcd59e..8f85d1665f 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.java @@ -15,10 +15,6 @@ package com.amplifyframework.api.aws; -import static com.amplifyframework.api.aws.GraphQLRequestHelper.getDeleteMutationInputMap; -import static com.amplifyframework.api.aws.GraphQLRequestHelper.getMapOfFieldNameAndValues; -import static com.amplifyframework.api.aws.GraphQLRequestHelper.parsePredicate; - import com.amplifyframework.AmplifyException; import com.amplifyframework.api.graphql.GraphQLRequest; import com.amplifyframework.api.graphql.MutationType; @@ -83,7 +79,7 @@ public static GraphQLRequest buildQuery( Class modelClass, ModelIdentifier modelIdentifier ) { - GraphQLRequestVariable variables[]; + GraphQLRequestVariable[] variables; try { ModelSchema modelSchema = ModelSchema.fromModelClass(modelClass); List primaryIndexFields = modelSchema.getPrimaryIndexFields(); @@ -134,13 +130,13 @@ private static GraphQLRequest buildQuery( GraphQLRequestVariable... variables ) { try { - AppSyncGraphQLRequest.Builder builder = AppSyncGraphQLRequest.builder() + AppSyncGraphQLRequest.Builder builder = AppSyncGraphQLRequest.builder() .modelClass(modelClass) .operation(QueryType.GET) .requestOptions(new ApiGraphQLRequestOptions()) .responseType(modelClass); - for (GraphQLRequestVariable v: variables) { + for (GraphQLRequestVariable v : variables) { builder.variable(v.getKey(), v.getType(), v.getValue()); } return builder.build(); @@ -209,7 +205,11 @@ static GraphQLRequest buildQuery( if (!QueryPredicates.all().equals(predicate)) { String filterType = "Model" + Casing.capitalizeFirst(modelName) + "FilterInput"; - builder.variable("filter", filterType, parsePredicate(predicate)); + builder.variable( + "filter", + filterType, + GraphQLRequestHelper.parsePredicate(predicate) + ); } builder.variable("limit", "Int", limit); @@ -254,9 +254,17 @@ public static GraphQLRequest buildMutation( "Input!"; // CreateTodoInput if (MutationType.DELETE.equals(type)) { - builder.variable("input", inputType, getDeleteMutationInputMap(schema, model)); + builder.variable( + "input", + inputType, + GraphQLRequestHelper.getDeleteMutationInputMap(schema, model) + ); } else { - builder.variable("input", inputType, getMapOfFieldNameAndValues(schema, model)); + builder.variable( + "input", + inputType, + GraphQLRequestHelper.getMapOfFieldNameAndValues(schema, model) + ); } if (!QueryPredicates.all().equals(predicate)) { @@ -264,7 +272,8 @@ public static GraphQLRequest buildMutation( "Model" + Casing.capitalizeFirst(graphQlTypeName) + "ConditionInput"; - builder.variable("condition", conditionType, parsePredicate(predicate)); + builder.variable( + "condition", conditionType, GraphQLRequestHelper.parsePredicate(predicate)); } return builder.build(); diff --git a/aws-datastore/src/main/java/com/amplifyframework/datastore/appsync/AppSyncRequestFactory.java b/aws-datastore/src/main/java/com/amplifyframework/datastore/appsync/AppSyncRequestFactory.java index 3b94e62d6f..53a9fec67f 100644 --- a/aws-datastore/src/main/java/com/amplifyframework/datastore/appsync/AppSyncRequestFactory.java +++ b/aws-datastore/src/main/java/com/amplifyframework/datastore/appsync/AppSyncRequestFactory.java @@ -15,16 +15,13 @@ package com.amplifyframework.datastore.appsync; -import static com.amplifyframework.api.aws.GraphQLRequestHelper.getDeleteMutationInputMap; -import static com.amplifyframework.api.aws.GraphQLRequestHelper.getMapOfFieldNameAndValues; -import static com.amplifyframework.api.aws.GraphQLRequestHelper.parsePredicate; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.amplifyframework.AmplifyException; import com.amplifyframework.api.aws.AppSyncGraphQLRequest; import com.amplifyframework.api.aws.AuthModeStrategyType; +import com.amplifyframework.api.aws.GraphQLRequestHelper; import com.amplifyframework.api.graphql.MutationType; import com.amplifyframework.api.graphql.PaginatedResult; import com.amplifyframework.api.graphql.QueryType; @@ -128,7 +125,11 @@ static AppSyncGraphQLRequest buildSyncRequest( // optimization is not possible anyway. syncPredicate = QueryPredicateGroup.andOf(syncPredicate); } - builder.variable("filter", filterType, parsePredicate(syncPredicate)); + builder.variable( + "filter", + filterType, + GraphQLRequestHelper.parsePredicate(syncPredicate) + ); } return builder.build(); } catch (AmplifyException amplifyException) { @@ -166,7 +167,7 @@ static AppSyncGraphQLRequest> buildDeleti try { Map inputMap = new HashMap<>(); inputMap.put("_version", version); - inputMap.putAll(getDeleteMutationInputMap(schema, model)); + inputMap.putAll(GraphQLRequestHelper.getDeleteMutationInputMap(schema, model)); return buildMutation(schema, inputMap, predicate, MutationType.DELETE, strategyType); } catch (AmplifyException amplifyException) { throw new DataStoreException("Failed to get fields for model.", @@ -183,7 +184,7 @@ static AppSyncGraphQLRequest> buildUpdate try { Map inputMap = new HashMap<>(); inputMap.put("_version", version); - inputMap.putAll(getMapOfFieldNameAndValues(schema, model)); + inputMap.putAll(GraphQLRequestHelper.getMapOfFieldNameAndValues(schema, model)); return buildMutation(schema, inputMap, predicate, MutationType.UPDATE, strategyType); } catch (AmplifyException amplifyException) { throw new DataStoreException("Failed to get fields for model.", @@ -196,7 +197,7 @@ static AppSyncGraphQLRequest> buildCreati M model, AuthModeStrategyType strategyType) throws DataStoreException { try { - Map inputMap = getMapOfFieldNameAndValues(schema, model); + Map inputMap = GraphQLRequestHelper.getMapOfFieldNameAndValues(schema, model); return buildMutation(schema, inputMap, QueryPredicates.all(), MutationType.CREATE, strategyType); } catch (AmplifyException amplifyException) { throw new DataStoreException("Failed to get fields for model.", @@ -240,7 +241,11 @@ private static AppSyncGraphQLRequest> bui "Model" + Casing.capitalizeFirst(graphQlTypeName) + "ConditionInput"; - builder.variable("condition", conditionType, parsePredicate(predicate)); + builder.variable( + "condition", + conditionType, + GraphQLRequestHelper.parsePredicate(predicate) + ); } return builder.build(); From a5b63add04f53671aa9f51d71adc83eac39b1c78 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 7 Jun 2023 10:29:00 -0400 Subject: [PATCH 006/100] Add CPK ModelQuery test --- .../api/aws/AppSyncGraphQLRequestFactory.java | 3 ++- .../aws/AppSyncGraphQLRequestFactoryTest.java | 25 +++++++++++++++++++ .../query-for-person-by-model-identifier.txt | 18 +++++++++++++ .../testmodels/personcar/PersonWithCPK.java | 7 +++--- 4 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 aws-api/src/test/resources/query-for-person-by-model-identifier.txt 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 index 8f85d1665f..52ad0d2f4e 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.java @@ -96,7 +96,8 @@ public static GraphQLRequest buildQuery( ModelField targetField = Objects.requireNonNull(modelSchema.getFields().get(key)); - // Should create "ID!", "String!". Appends "!" if required + // Should create "ID!", "String!", "Float!", etc. + // Appends "!" if required (should always be the case with CPK requirements). String targetTypeString = targetField.getTargetType() + (targetField.isRequired() ? "!" : ""); 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 ad853fcc76..984bfb5750 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 @@ -35,6 +35,7 @@ import com.amplifyframework.testmodels.meeting.Meeting; import com.amplifyframework.testmodels.personcar.MaritalStatus; import com.amplifyframework.testmodels.personcar.Person; +import com.amplifyframework.testmodels.personcar.PersonWithCPK; import com.amplifyframework.testutils.Resources; import org.json.JSONException; @@ -77,6 +78,30 @@ public void buildQueryFromClassAndId() throws JSONException { ); } + /** + * Validate construction of a GraphQL query from a class and an object ID. + * @throws JSONException from JSONAssert.assertEquals + */ + @Test + public void buildQueryFromClassAndModelIdentifier() throws JSONException { + // Arrange a hard-coded name/age as found int the expected data file. + String name = "First"; + int age = 50; + + // Act: create a request + GraphQLRequest request = + AppSyncGraphQLRequestFactory.buildQuery(PersonWithCPK.class, new PersonWithCPK.PersonIdentifier(name, age)); + + // Assert: content is expected content + JSONAssert.assertEquals( + Resources.readAsString("query-for-person-by-model-identifier.txt"), + request.getContent(), + true + ); + } + + + /** * Validate construction of a GraphQL query from a class and a predicate. * @throws JSONException from JSONAssert.assertEquals diff --git a/aws-api/src/test/resources/query-for-person-by-model-identifier.txt b/aws-api/src/test/resources/query-for-person-by-model-identifier.txt new file mode 100644 index 0000000000..3019ff2e9b --- /dev/null +++ b/aws-api/src/test/resources/query-for-person-by-model-identifier.txt @@ -0,0 +1,18 @@ +{ + "query": "query GetPersonWithCPK($age: Int, $first_name: String!) { + getPersonWithCPK(age: $age, first_name: $first_name) { + age + createdAt + dob + first_name + last_name + relationship + updatedAt + } +} +", + "variables": { + "age": 50, + "first_name": "First" + } +} \ No newline at end of file diff --git a/testmodels/src/main/java/com/amplifyframework/testmodels/personcar/PersonWithCPK.java b/testmodels/src/main/java/com/amplifyframework/testmodels/personcar/PersonWithCPK.java index bd1eca531c..f5fcf0d2b5 100644 --- a/testmodels/src/main/java/com/amplifyframework/testmodels/personcar/PersonWithCPK.java +++ b/testmodels/src/main/java/com/amplifyframework/testmodels/personcar/PersonWithCPK.java @@ -50,7 +50,7 @@ public final class PersonWithCPK implements Model { @ModelField(isRequired = true) private final String last_name; - @ModelField(targetType = "Int") + @ModelField(targetType = "Int", isRequired = true) private final Integer age; @ModelField(targetType = "AWSDate") @@ -336,6 +336,7 @@ public FinalStep lastName(String last_name) { * @return Current Builder instance, for fluent method chaining */ public FinalStep age(Integer age) { + Objects.requireNonNull(age); this.age = age; return this; } @@ -418,9 +419,9 @@ public NewBuilder relationship(MaritalStatus relationship) { } } - public class PersonIdentifier extends ModelIdentifier { + public static class PersonIdentifier extends ModelIdentifier { private static final long serialVersionUID = 1L; - public PersonIdentifier(String firstName, int age){ + public PersonIdentifier(String firstName, Integer age){ super(firstName, age); } } From 4f8b9c5a502402574fe4cdae9c31d643227af25a Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Thu, 8 Jun 2023 09:31:33 -0400 Subject: [PATCH 007/100] Code cleanup --- .../api/aws/GraphQLRequestHelper.java | 25 ++++++++++--------- .../api/aws/AppSyncGraphQLRequestFactory.java | 6 +++-- .../aws/AppSyncGraphQLRequestFactoryTest.java | 5 +++- 3 files changed, 21 insertions(+), 15 deletions(-) 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 9c3284e35f..49ee553f5b 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 @@ -221,20 +221,20 @@ private static Map extractFieldLevelData( // Skip fields that are not set, so that they are not set to null in the request. continue; } + + Object fieldValue = extractFieldValue(modelField.getName(), instance, schema); + if (association == null) { - result.put(fieldName, extractFieldValue(modelField.getName(), instance, schema)); - } else if (association.isOwner()) { + result.put(fieldName, fieldValue); + } else if (association.isOwner() && fieldValue != null) { 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, instance, schema, association); + insertForeignKeyValues(result, modelField, fieldValue, association); } else { - final Object fieldValue = extractFieldValue(modelField.getName(), instance, schema); - if (fieldValue != null) { - String targetName = association.getTargetName(); - result.put(targetName, extractAssociateId(modelField, fieldValue)); - } + String targetName = association.getTargetName(); + result.put(targetName, extractAssociateId(modelField, fieldValue)); } } // Ignore if field is associated, but is not a "belongsTo" relationship @@ -242,10 +242,11 @@ private static Map extractFieldLevelData( return result; } - private static void insertForeignKeyValues(Map result, ModelField modelField, - Model instance, ModelSchema schema, - ModelAssociation association) throws AmplifyException { - final Object fieldValue = extractFieldValue(modelField.getName(), instance, schema); + private static void insertForeignKeyValues( + Map result, + ModelField modelField, + Object fieldValue, + ModelAssociation association) throws AmplifyException { if (modelField.isModel() && fieldValue instanceof Model) { if (((Model) fieldValue).resolveIdentifier() instanceof ModelIdentifier) { final ModelIdentifier primaryKey = (ModelIdentifier) ((Model) fieldValue).resolveIdentifier(); 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 index 52ad0d2f4e..f912488d7b 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.java @@ -15,6 +15,8 @@ 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; @@ -76,8 +78,8 @@ public static GraphQLRequest buildQuery( * @throws IllegalStateException when the model schema does not contain the expected information. */ public static GraphQLRequest buildQuery( - Class modelClass, - ModelIdentifier modelIdentifier + @NonNull Class modelClass, + @NonNull ModelIdentifier modelIdentifier ) { GraphQLRequestVariable[] variables; try { 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 984bfb5750..2df3100247 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 @@ -90,7 +90,10 @@ public void buildQueryFromClassAndModelIdentifier() throws JSONException { // Act: create a request GraphQLRequest request = - AppSyncGraphQLRequestFactory.buildQuery(PersonWithCPK.class, new PersonWithCPK.PersonIdentifier(name, age)); + AppSyncGraphQLRequestFactory.buildQuery( + PersonWithCPK.class, new + PersonWithCPK.PersonIdentifier(name, age) + ); // Assert: content is expected content JSONAssert.assertEquals( From c71680f8e670fe3398bd8754dba7f3c489dfbe2d Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Thu, 8 Jun 2023 14:42:50 -0400 Subject: [PATCH 008/100] fix test --- .../src/test/resources/query-for-person-by-model-identifier.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-api/src/test/resources/query-for-person-by-model-identifier.txt b/aws-api/src/test/resources/query-for-person-by-model-identifier.txt index 3019ff2e9b..1fe6d158bd 100644 --- a/aws-api/src/test/resources/query-for-person-by-model-identifier.txt +++ b/aws-api/src/test/resources/query-for-person-by-model-identifier.txt @@ -1,5 +1,5 @@ { - "query": "query GetPersonWithCPK($age: Int, $first_name: String!) { + "query": "query GetPersonWithCPK($age: Int!, $first_name: String!) { getPersonWithCPK(age: $age, first_name: $first_name) { age createdAt From a3666d29d3833f34965f3b51670c33574395a5c8 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Fri, 9 Jun 2023 15:14:45 -0400 Subject: [PATCH 009/100] GSON serializer should decide how to insert. Inserting as string is incorrect for many types --- .../com/amplifyframework/api/aws/GraphQLRequestHelper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 49ee553f5b..418626d16f 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 @@ -253,10 +253,10 @@ private static void insertForeignKeyValues( ListIterator targetNames = Arrays.asList(association.getTargetNames()).listIterator(); Iterator sortedKeys = primaryKey.sortedKeys().listIterator(); - result.put(targetNames.next(), primaryKey.key().toString()); + result.put(targetNames.next(), primaryKey.key()); while (targetNames.hasNext()) { - result.put(targetNames.next(), sortedKeys.next().toString()); + result.put(targetNames.next(), sortedKeys.next()); } } else if ((fieldValue instanceof SerializedModel)) { SerializedModel serializedModel = ((SerializedModel) fieldValue); From bc26c470b7a9cd1a255491bd1c13224d3952567e Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Fri, 9 Jun 2023 15:17:09 -0400 Subject: [PATCH 010/100] Temporal Date/Times need to be Serializable to support usage as sort key --- .../core/model/temporal/Temporal.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/aws-api-appsync/src/main/java/com/amplifyframework/core/model/temporal/Temporal.java b/aws-api-appsync/src/main/java/com/amplifyframework/core/model/temporal/Temporal.java index 9bba857755..c44a4a36df 100644 --- a/aws-api-appsync/src/main/java/com/amplifyframework/core/model/temporal/Temporal.java +++ b/aws-api-appsync/src/main/java/com/amplifyframework/core/model/temporal/Temporal.java @@ -18,6 +18,7 @@ import androidx.annotation.NonNull; import androidx.core.util.ObjectsCompat; +import java.io.Serializable; import java.time.Instant; import java.time.LocalDate; import java.time.LocalTime; @@ -49,7 +50,9 @@ private Temporal() {} *

* https://docs.aws.amazon.com/appsync/latest/devguide/scalars.html#appsync-defined-scalars */ - public static final class Date implements Comparable { + public static final class Date implements Comparable, Serializable { + + private static final long serialVersionUID = 1L; private final LocalDate localDate; private final ZoneOffset zoneOffset; @@ -195,7 +198,9 @@ public int compareTo(Date date) { *

* https://docs.aws.amazon.com/appsync/latest/devguide/scalars.html#appsync-defined-scalars */ - public static final class DateTime implements Comparable { + public static final class DateTime implements Comparable, Serializable { + + private static final long serialVersionUID = 1L; private final OffsetDateTime offsetDateTime; /** @@ -293,7 +298,9 @@ public int compareTo(DateTime dateTime) { *

* https://docs.aws.amazon.com/appsync/latest/devguide/scalars.html#appsync-defined-scalars */ - public static final class Time implements Comparable

- * 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..b974971f7e --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.kt @@ -0,0 +1,328 @@ +/* + * 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.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. + * @param

the model's ModelPath type. + * @return a valid [GraphQLRequest] instance. + * @throws IllegalStateException when the model schema does not contain the expected information. + */ + @JvmOverloads + @JvmStatic + fun > buildQuery( + modelClass: Class, + objectId: String, + includes: ((P) -> List)? = null + ): 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 buildQuery(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 + @JvmOverloads + fun > buildQuery( + modelClass: Class, + modelIdentifier: ModelIdentifier, + includes: ((P) -> List)? = null + ): 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 buildQuery(modelClass, includes, *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. + * @param modelClass the model class. + * @param variables the variables. + * @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. + */ + private fun > buildQuery( + 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) + } + includes?.invoke(ModelPath.getRootPath(modelClass))?.let { associations -> + val selectionSet = SelectionSet.builder() + .modelClass(modelClass) + .operation(QueryType.GET) + .requestOptions(ApiGraphQLRequestOptions()) + .includeAssociations(associations) + .build() + + builder.selectionSet(selectionSet) + } + 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 buildQuery(modelClass, predicate, DEFAULT_QUERY_LIMIT, dataType) + } + + /** + * 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 buildQuery(modelClass, predicate, limit, responseType) + } + + @JvmStatic + fun buildQuery( + modelClass: Class, + predicate: QueryPredicate, + limit: Int, + responseType: Type + ): 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) + 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 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) + ) + } + 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 try { + AppSyncGraphQLRequest.builder() + .modelClass(modelClass) + .operation(subscriptionType) + .requestOptions(ApiGraphQLRequestOptions()) + .responseType(modelClass) + .build() + } catch (exception: AmplifyException) { + throw IllegalStateException( + "Failed to build GraphQLRequest", + exception + ) + } + } +} \ No newline at end of file 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 index 93e2737779..dfea6433a5 100644 --- 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 @@ -30,6 +30,24 @@ import java.util.Objects * 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, null) + } + /** * 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 @@ -40,16 +58,30 @@ object ModelQuery { * @param the concrete model type. * @return a valid [GraphQLRequest] instance. */ - @JvmOverloads @JvmStatic operator fun > get( modelType: Class, modelId: String, - includes: ((P) -> List)? = null + includes: ((P) -> List) ): GraphQLRequest { + return AppSyncGraphQLRequestFactory.buildQuery(modelType, modelId, includes) + } - val associations = includes?.invoke(ModelPath.getRootPath(modelType)) - return AppSyncGraphQLRequestFactory.buildQuery(modelType, modelId, associations) + /** + * 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, null) } /** @@ -62,15 +94,13 @@ object ModelQuery { * @param the concrete model type. * @return a valid [GraphQLRequest] instance. */ - @JvmOverloads @JvmStatic operator fun > get( modelType: Class, modelIdentifier: ModelIdentifier, - includes: ((P) -> List)? = null + includes: ((P) -> List) ): GraphQLRequest { - val associations = includes?.invoke(ModelPath.getRootPath(modelType)) - return AppSyncGraphQLRequestFactory.buildQuery(modelType, modelIdentifier, associations) + return AppSyncGraphQLRequestFactory.buildQuery(modelType, modelIdentifier, includes) } /** From e7b1c0831b2654964971ddeef54a999bb195b11d Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Thu, 27 Jul 2023 08:39:01 -0400 Subject: [PATCH 034/100] cleanup custom selection set --- .../api/aws/AppSyncGraphQLRequest.java | 14 -------------- .../amplifyframework/api/aws/SelectionSet.java | 16 ---------------- .../core/model/ModelPropertyPath.kt | 7 ++++--- 3 files changed, 4 insertions(+), 33 deletions(-) diff --git a/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequest.java b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequest.java index 871f69fda9..904b94323c 100644 --- a/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequest.java +++ b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequest.java @@ -259,7 +259,6 @@ public static final class Builder { private GraphQLRequestOptions requestOptions; private Type responseType; private SelectionSet selectionSet; - private List includedAssociations; private AuthorizationType authorizationType; private final Map variables; private final Map variableTypes; @@ -340,16 +339,6 @@ public Builder selectionSet(@NonNull SelectionSet selectionSet) { return Builder.this; } - /** - * Sets the included associations and returns this builder. - * @param associations the associations to include in addition to the selection set. - * @return this builder instance. - */ - public Builder includeAssociations(@NonNull List associations) { - this.includedAssociations = Objects.requireNonNull(associations); - return Builder.this; - } - /** * Sets the authorization type for the request. If this field is set, * {@link Builder#authModeStrategyType} will be ignored. @@ -417,11 +406,8 @@ public AppSyncGraphQLRequest build() throws AmplifyException { .modelSchema(this.modelSchema) .modelClass(this.modelClass) .operation(this.operation) - .includeAssociations(this.includedAssociations) .requestOptions(Objects.requireNonNull(this.requestOptions)) .build(); - } else if (includedAssociations != null){ - selectionSet = new SelectionSet(selectionSet, includedAssociations); } if (authModeStrategyType == null || authorizationType != null) { 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 a177100be8..d119721af6 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 @@ -73,22 +73,6 @@ public SelectionSet(SelectionSet selectionSet) { this(selectionSet.value, new HashSet<>(selectionSet.nodes)); } - // non public constructor to add associations. - // This is needed if a customer provided their own selection set + associations, - // since we wouldn't be creating our own SelectionSet with the builder. - SelectionSet(SelectionSet selectionSet, List includeAssociations) { - this(selectionSet.value, new HashSet<>(selectionSet.nodes)); - - if (includeAssociations != null) { - for (PropertyContainerPath association : includeAssociations) { - SelectionSet included = SelectionSetUtils.asSelectionSet(association, false); - if (included != null) { - SelectionSetUtils.merge(this, included); - } - } - } - } - /** * Constructor for a leaf node (no children). * @param value String value of the field. diff --git a/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt b/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt index 2a3758b858..4d16c90c53 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt +++ b/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt @@ -15,6 +15,8 @@ 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 @@ -149,7 +151,7 @@ open class ModelPath( * @throws ModelException.PropertyPathNotFound in case the path could not be read or found. */ @Throws(ModelException.PropertyPathNotFound::class) - @JvmStatic + @InternalAmplifyApi fun >getRootPath(clazz: Class): P { val field = try { clazz.getDeclaredField("rootPath") @@ -176,12 +178,11 @@ class FieldPath( name = name, parent = parent ) - } /** * Function used to define which associations are included in the selection set - * in an idiomatic manner. It's a simple delegation to `arrayOf` with the main + * in an idiomatic manner. It's a simple delegation to `listOf` with the main * goal of improved code readability. * * Example: From 4d818912415f8f6b4abd5ae3d5828874b3f908ab Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Thu, 27 Jul 2023 09:59:34 -0400 Subject: [PATCH 035/100] Adding includes to additional query/mutations --- .../api/aws/AppSyncGraphQLRequestFactory.kt | 269 ++++++++++++++++-- .../api/graphql/model/ModelQuery.kt | 105 ++++++- 2 files changed, 340 insertions(+), 34 deletions(-) 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 index b974971f7e..a671e7ac2f 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.kt @@ -17,6 +17,7 @@ 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 @@ -46,16 +47,47 @@ object AppSyncGraphQLRequestFactory { * @param objectId the model identifier. * @param the response type. * @param the concrete model type. - * @param

the model's ModelPath type. * @return a valid [GraphQLRequest] instance. * @throws IllegalStateException when the model schema does not contain the expected information. */ - @JvmOverloads + @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 associations 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)? = null + includes: ((P) -> List) ): GraphQLRequest { val variable: GraphQLRequestVariable = try { val modelSchema = ModelSchema.fromModelClass(modelClass) @@ -69,7 +101,7 @@ object AppSyncGraphQLRequestFactory { // If we fail to pull primary key name and type, fallback to default id/ID! GraphQLRequestVariable("id", objectId, "ID!") } - return buildQuery(modelClass, includes, variable) + return buildQueryInternal(modelClass, includes, variable) } /** @@ -84,11 +116,9 @@ object AppSyncGraphQLRequestFactory { * @throws IllegalStateException when the model schema does not contain the expected information. */ @JvmStatic - @JvmOverloads - fun > buildQuery( + fun buildQuery( modelClass: Class, modelIdentifier: ModelIdentifier, - includes: ((P) -> List)? = null ): GraphQLRequest { try { val modelSchema = ModelSchema.fromModelClass(modelClass) @@ -109,7 +139,7 @@ object AppSyncGraphQLRequestFactory { GraphQLRequestVariable(key, value, targetTypeString) } - return buildQuery(modelClass, includes, *variables.toTypedArray()) + return buildQueryInternal(modelClass, null, *variables.toTypedArray()) } catch (exception: AmplifyException) { throw IllegalStateException( "Could not generate a schema for the specified class", @@ -120,15 +150,52 @@ object AppSyncGraphQLRequestFactory { /** * 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. + * will be created with the correct document based on the model schema and variables based on given + * `modelIdentifier`. * @param modelClass the model class. - * @param variables the variables. + * @param modelIdentifier the model identifier. + * @param includes lambda returning list of associations 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. */ - private fun > buildQuery( + @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 + ) + } + } + + private fun > buildQueryInternal( modelClass: Class, includes: ((P) -> List)?, vararg variables: GraphQLRequestVariable @@ -142,16 +209,10 @@ object AppSyncGraphQLRequestFactory { for ((key, value, type) in variables) { builder.variable(key, type, value) } - includes?.invoke(ModelPath.getRootPath(modelClass))?.let { associations -> - val selectionSet = SelectionSet.builder() - .modelClass(modelClass) - .operation(QueryType.GET) - .requestOptions(ApiGraphQLRequestOptions()) - .includeAssociations(associations) - .build() - - builder.selectionSet(selectionSet) - } + + val customSelectionSet = includes?.let { createApiSelectionSet(modelClass, QueryType.GET, it) } + customSelectionSet?.let { builder.selectionSet(it) } + builder.build() } catch (exception: AmplifyException) { throw IllegalStateException( @@ -178,7 +239,30 @@ object AppSyncGraphQLRequestFactory { predicate: QueryPredicate ): GraphQLRequest { val dataType = TypeMaker.getParameterizedType(PaginatedResult::class.java, modelClass) - return buildQuery(modelClass, predicate, DEFAULT_QUERY_LIMIT, dataType) + return buildListQueryInternal(modelClass, predicate, DEFAULT_QUERY_LIMIT, dataType, null) + } + + /** + * 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 associations 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) } /** @@ -202,15 +286,59 @@ object AppSyncGraphQLRequestFactory { limit: Int ): GraphQLRequest { val responseType = TypeMaker.getParameterizedType(PaginatedResult::class.java, modelClass) - return buildQuery(modelClass, predicate, limit, responseType) + 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 associations 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 buildQuery( + 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 associations 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 + responseType: Type, + includes: ((P) -> List)? ): GraphQLRequest { return try { val modelName = ModelSchema.fromModelClass( @@ -230,6 +358,10 @@ object AppSyncGraphQLRequestFactory { ) } builder.variable("limit", "Int", limit) + + val customSelectionSet = includes?.let { createApiSelectionSet(modelClass, QueryType.LIST, it) } + customSelectionSet?.let { builder.selectionSet(it) } + builder.build() } catch (exception: AmplifyException) { throw IllegalStateException( @@ -254,6 +386,37 @@ object AppSyncGraphQLRequestFactory { 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 associations 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 @@ -288,6 +451,10 @@ object AppSyncGraphQLRequestFactory { "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( @@ -297,6 +464,7 @@ object AppSyncGraphQLRequestFactory { } } + /** * Creates a [GraphQLRequest] that represents a subscription of a given type. * @param modelClass the model type. @@ -310,14 +478,46 @@ object AppSyncGraphQLRequestFactory { 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 associations 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 { - AppSyncGraphQLRequest.builder() + val builder = AppSyncGraphQLRequest.builder() .modelClass(modelClass) .operation(subscriptionType) .requestOptions(ApiGraphQLRequestOptions()) .responseType(modelClass) - .build() + + val customSelectionSet = includes?.let { createApiSelectionSet(modelClass, subscriptionType, it) } + customSelectionSet?.let { builder.selectionSet(it) } + + builder.build() } catch (exception: AmplifyException) { throw IllegalStateException( "Failed to build GraphQLRequest", @@ -325,4 +525,19 @@ object AppSyncGraphQLRequestFactory { ) } } + + private fun > createApiSelectionSet( + modelClass: Class, + operationType: Operation, + includes: ((P) -> List) + ): SelectionSet { + includes(ModelPath.getRootPath(modelClass)).let { associations -> + return SelectionSet.builder() + .modelClass(modelClass) + .operation(operationType) + .requestOptions(ApiGraphQLRequestOptions()) + .includeAssociations(associations) + .build() + } + } } \ No newline at end of file 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 index dfea6433a5..715aa46ba9 100644 --- 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 @@ -23,7 +23,6 @@ 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 -import java.util.Objects /** * Helper class that provides methods to create [GraphQLRequest] for queries @@ -45,7 +44,7 @@ object ModelQuery { modelType: Class, modelId: String, ): GraphQLRequest { - return AppSyncGraphQLRequestFactory.buildQuery(modelType, modelId, null) + return AppSyncGraphQLRequestFactory.buildQuery(modelType, modelId) } /** @@ -54,8 +53,9 @@ object ModelQuery { * variables based on given `modelId`. * @param modelType the model class. * @param modelId the model identifier. - * @param includes list of associations that should be included in the selection set + * @param includes lambda returning list of associations 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 @@ -81,7 +81,7 @@ object ModelQuery { modelType: Class, modelIdentifier: ModelIdentifier ): GraphQLRequest { - return AppSyncGraphQLRequestFactory.buildQuery(modelType, modelIdentifier, null) + return AppSyncGraphQLRequestFactory.buildQuery(modelType, modelIdentifier) } /** @@ -90,8 +90,9 @@ object ModelQuery { * variables based on given `modelIdentifier`. * @param modelType the model class. * @param modelIdentifier the model identifier. - * @param includes list of associations that should be included in the selection set + * @param includes lambda returning list of associations 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 @@ -117,11 +118,29 @@ object ModelQuery { modelType: Class, predicate: QueryPredicate = QueryPredicates.all() ): GraphQLRequest> { - Objects.requireNonNull(modelType) - Objects.requireNonNull(predicate) 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 associations 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 = QueryPredicates.all(), + 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. @@ -135,6 +154,24 @@ object ModelQuery { 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 associations 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). @@ -160,6 +197,34 @@ object ModelQuery { ) } + /** + * 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 associations 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). @@ -182,4 +247,30 @@ object ModelQuery { 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 associations 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 + ) + } } \ No newline at end of file From f4ca874274470f48bc0434c529699525db6b78ef Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Thu, 27 Jul 2023 10:06:23 -0400 Subject: [PATCH 036/100] Rename .java to .kt --- .../model/{ModelSubscription.java => ModelSubscription.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename aws-api/src/main/java/com/amplifyframework/api/graphql/model/{ModelSubscription.java => ModelSubscription.kt} (100%) 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.kt similarity index 100% rename from aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelSubscription.java rename to aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelSubscription.kt From 5899222ac571e268db60e3d5f8e1198bc30ae3a9 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Thu, 27 Jul 2023 10:06:23 -0400 Subject: [PATCH 037/100] Add includes to ModelSubscription --- .../api/graphql/model/ModelSubscription.kt | 141 +++++++++++++----- 1 file changed, 105 insertions(+), 36 deletions(-) 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 index a42ae6914b..1a0153c9a4 100644 --- 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 @@ -12,65 +12,134 @@ * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ +package com.amplifyframework.api.graphql.model -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; +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 {@link GraphQLRequest} that represents - * subscriptions from {@link Model}. + * Helper class that provides methods to create [GraphQLRequest] that represents + * subscriptions from [Model]. */ -public final class ModelSubscription { - - /** This class should not be instantiated. */ - private ModelSubscription() {} +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 {@code type} for a {@code modelType}. + * 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 associations 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 {@link GraphQLRequest} instance. - */ - public static GraphQLRequest of(Class modelType, SubscriptionType type) { - return AppSyncGraphQLRequestFactory.buildSubscription(modelType, type); + * @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 {@link SubscriptionType#ON_CREATE}. + * Creates a subscription request of type [SubscriptionType.ON_CREATE]. * @param modelType the model class. + * @param includes lambda returning list of associations that should be included in the selection set * @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); + * @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 {@link SubscriptionType#ON_DELETE}. + * Creates a subscription request of type [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); + * @return a valid [GraphQLRequest] instance. + * @see .of + */ + fun onDelete(modelType: Class): GraphQLRequest { + return of(modelType, SubscriptionType.ON_DELETE) } /** - * Creates a subscription request of type {@link SubscriptionType#ON_UPDATE}. + * Creates a subscription request of type [SubscriptionType.ON_DELETE]. * @param modelType the model class. + * @param includes lambda returning list of associations that should be included in the selection set * @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); + * @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 associations 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) + } +} \ No newline at end of file From 866791ba3727dd526b5b63b0a2140ba74379eaf9 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Thu, 27 Jul 2023 10:10:51 -0400 Subject: [PATCH 038/100] Rename .java to .kt --- .../api/graphql/model/{ModelMutation.java => ModelMutation.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename aws-api/src/main/java/com/amplifyframework/api/graphql/model/{ModelMutation.java => ModelMutation.kt} (100%) 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.kt similarity index 100% rename from aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelMutation.java rename to aws-api/src/main/java/com/amplifyframework/api/graphql/model/ModelMutation.kt From 749feddac11854d93f181661aa2921f82ea25e80 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Thu, 27 Jul 2023 10:10:52 -0400 Subject: [PATCH 039/100] Add includes to ModelMutation --- .../api/graphql/model/ModelMutation.kt | 195 +++++++++++++----- 1 file changed, 143 insertions(+), 52 deletions(-) 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 index e36a2b8659..162615a1e0 100644 --- 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 @@ -12,86 +12,177 @@ * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ +package com.amplifyframework.api.graphql.model -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; +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 {@link GraphQLRequest} from {@link Model}. + * Helper class that provides methods to create [GraphQLRequest] from [Model]. */ -public final class ModelMutation { - private ModelMutation() {} +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 associations 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 {@link GraphQLRequest} that represents a create mutation for a given {@code model} instance. + * 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 {@code GraphQLRequest} instance. - * @see MutationType#CREATE - */ - public static GraphQLRequest create(@NonNull M model) { - return AppSyncGraphQLRequestFactory.buildMutation(model, QueryPredicates.all(), MutationType.CREATE); + * @return a valid `GraphQLRequest` instance. + * @see MutationType.UPDATE + */ + fun update( + model: M, + predicate: QueryPredicate + ): GraphQLRequest { + return buildMutation(model, predicate, MutationType.UPDATE) } /** - * Creates a {@link GraphQLRequest} that represents an update mutation for a given {@code model} instance. + * 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 associations 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 associations that should be included in the selection set * @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); + * @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 {@link GraphQLRequest} that represents an update mutation for a given {@code model} instance. + * 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 {@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); + * @return a valid `GraphQLRequest` instance. + * @see MutationType.DELETE + */ + fun delete( + model: M, + predicate: QueryPredicate + ): GraphQLRequest { + return buildMutation(model, predicate, MutationType.DELETE) } /** - * Creates a {@link GraphQLRequest} that represents a delete mutation for a given {@code model} instance. + * 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 associations 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 {@code GraphQLRequest} instance. - * @see MutationType#DELETE - */ - public static GraphQLRequest delete( - @NonNull M model, - @NonNull QueryPredicate predicate - ) { - return AppSyncGraphQLRequestFactory.buildMutation(model, predicate, MutationType.DELETE); + * @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 {@link GraphQLRequest} that represents a delete mutation for a given {@code model} instance. + * 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 associations that should be included in the selection set * @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); + * @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) } -} +} \ No newline at end of file From deb74327f99f418586a2416c2a36372d9bb1a5ef Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Thu, 27 Jul 2023 10:26:22 -0400 Subject: [PATCH 040/100] lint --- .../api/aws/SelectionSetDepth.kt | 3 +- .../api/aws/SelectionSetExtensions.kt | 2 +- .../api/aws/AppSyncGraphQLRequestFactory.kt | 55 +++++++++---------- .../api/aws/AppSyncLazyListModel.kt | 4 +- .../api/aws/AppSyncLazyModel.kt | 4 +- .../api/graphql/model/ModelMutation.kt | 22 ++++---- .../api/graphql/model/ModelQuery.kt | 26 ++++----- .../api/graphql/model/ModelSubscription.kt | 19 +++---- .../amplifyframework/core/model/LazyList.kt | 1 - .../amplifyframework/core/model/LazyModel.kt | 4 +- .../core/model/ModelException.kt | 3 +- .../core/model/ModelPropertyPath.kt | 3 +- 12 files changed, 69 insertions(+), 77 deletions(-) diff --git a/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/SelectionSetDepth.kt b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/SelectionSetDepth.kt index ee304d4d6c..0f49aa2876 100644 --- a/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/SelectionSetDepth.kt +++ b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/SelectionSetDepth.kt @@ -48,6 +48,5 @@ class SelectionSetDepth(private val maxDepth: Int = 2) : GraphQLRequestOptions { @JvmStatic fun onlyIncluded() = SelectionSetDepth(maxDepth = 0) - } -} \ No newline at end of file +} 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 index 25f06856ee..bc1db5e4de 100644 --- 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 @@ -126,4 +126,4 @@ internal fun SelectionSet.mergeWith(selectionSet: SelectionSet) { } else { nodes.add(selectionSet) } -} \ No newline at end of file +} 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 index a671e7ac2f..cd55c235aa 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.kt @@ -49,7 +49,7 @@ object AppSyncGraphQLRequestFactory { * @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, @@ -82,7 +82,7 @@ object AppSyncGraphQLRequestFactory { * @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, @@ -114,7 +114,7 @@ object AppSyncGraphQLRequestFactory { * @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, @@ -134,7 +134,7 @@ object AppSyncGraphQLRequestFactory { val value = if (i == 0) { modelIdentifier.key().toString() } else { - sortedKeys[i-1] + sortedKeys[i - 1] } GraphQLRequestVariable(key, value, targetTypeString) @@ -160,7 +160,7 @@ object AppSyncGraphQLRequestFactory { * @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, @@ -181,7 +181,7 @@ object AppSyncGraphQLRequestFactory { val value = if (i == 0) { modelIdentifier.key().toString() } else { - sortedKeys[i-1] + sortedKeys[i - 1] } GraphQLRequestVariable(key, value, targetTypeString) @@ -209,10 +209,10 @@ object AppSyncGraphQLRequestFactory { 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( @@ -232,7 +232,7 @@ object AppSyncGraphQLRequestFactory { * @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, @@ -254,13 +254,13 @@ object AppSyncGraphQLRequestFactory { * @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 { + ): GraphQLRequest { val dataType = TypeMaker.getParameterizedType(PaginatedResult::class.java, modelClass) return buildListQueryInternal(modelClass, predicate, DEFAULT_QUERY_LIMIT, dataType, includes) } @@ -278,7 +278,7 @@ object AppSyncGraphQLRequestFactory { * @param the response type. * @param the concrete model type. * @return a valid [GraphQLRequest] instance. - */ + */ @JvmStatic fun buildPaginatedResultQuery( modelClass: Class, @@ -304,14 +304,14 @@ object AppSyncGraphQLRequestFactory { * @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 { + ): GraphQLRequest { val responseType = TypeMaker.getParameterizedType(PaginatedResult::class.java, modelClass) return buildListQueryInternal(modelClass, predicate, limit, responseType, includes) } @@ -332,7 +332,7 @@ object AppSyncGraphQLRequestFactory { * @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, @@ -380,7 +380,7 @@ object AppSyncGraphQLRequestFactory { * @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, @@ -401,7 +401,7 @@ object AppSyncGraphQLRequestFactory { * @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, @@ -428,8 +428,8 @@ object AppSyncGraphQLRequestFactory { .requestOptions(ApiGraphQLRequestOptions()) .responseType(modelClass) val inputType = Casing.capitalize(type.toString()) + - Casing.capitalizeFirst(graphQlTypeName) + - "Input!" // CreateTodoInput + Casing.capitalizeFirst(graphQlTypeName) + + "Input!" // CreateTodoInput if (MutationType.DELETE == type) { builder.variable( "input", @@ -445,8 +445,8 @@ object AppSyncGraphQLRequestFactory { } if (QueryPredicates.all() != predicate) { val conditionType = "Model" + - Casing.capitalizeFirst(graphQlTypeName) + - "ConditionInput" + Casing.capitalizeFirst(graphQlTypeName) + + "ConditionInput" builder.variable( "condition", conditionType, GraphQLRequestHelper.parsePredicate(predicate) ) @@ -464,7 +464,6 @@ object AppSyncGraphQLRequestFactory { } } - /** * Creates a [GraphQLRequest] that represents a subscription of a given type. * @param modelClass the model type. @@ -473,7 +472,7 @@ object AppSyncGraphQLRequestFactory { * @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, @@ -492,7 +491,7 @@ object AppSyncGraphQLRequestFactory { * @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, @@ -525,10 +524,10 @@ object AppSyncGraphQLRequestFactory { ) } } - + private fun > createApiSelectionSet( - modelClass: Class, - operationType: Operation, + modelClass: Class, + operationType: Operation, includes: ((P) -> List) ): SelectionSet { includes(ModelPath.getRootPath(modelClass)).let { associations -> @@ -540,4 +539,4 @@ object AppSyncGraphQLRequestFactory { .build() } } -} \ No newline at end of file +} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyListModel.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyListModel.kt index a1058e4b04..3e6ad3dabe 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyListModel.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyListModel.kt @@ -21,11 +21,11 @@ import com.amplifyframework.api.ApiException import com.amplifyframework.api.graphql.GraphQLRequest import com.amplifyframework.api.graphql.GraphQLResponse import com.amplifyframework.api.graphql.PaginatedResult +import com.amplifyframework.core.Amplify as coreAmplify import com.amplifyframework.core.Consumer import com.amplifyframework.core.model.LazyList import com.amplifyframework.core.model.Model import com.amplifyframework.kotlin.core.Amplify -import com.amplifyframework.core.Amplify as coreAmplify @InternalAmplifyApi class AppSyncLazyListModel( @@ -71,7 +71,7 @@ class AppSyncLazyListModel( override fun hasNextPage(): Boolean { return paginatedResult?.hasNextResult() ?: true } - + private fun createGraphQLRequest(): GraphQLRequest> { return if (paginatedResult != null) { paginatedResult!!.requestForNextResult diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyModel.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyModel.kt index c64c95b937..1e63361328 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyModel.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyModel.kt @@ -20,11 +20,11 @@ import com.amplifyframework.annotations.InternalAmplifyApi import com.amplifyframework.api.ApiException import com.amplifyframework.api.graphql.GraphQLResponse import com.amplifyframework.api.graphql.PaginatedResult +import com.amplifyframework.core.Amplify as coreAmplify import com.amplifyframework.core.Consumer import com.amplifyframework.core.model.LazyModel import com.amplifyframework.core.model.Model import com.amplifyframework.kotlin.core.Amplify -import com.amplifyframework.core.Amplify as coreAmplify @InternalAmplifyApi class AppSyncLazyModel( @@ -46,7 +46,7 @@ class AppSyncLazyModel( override suspend fun getModel(): M? { if (loadedValue) { - return value + return value } try { val resultIterator = Amplify.API.query( 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 index 162615a1e0..be5a4e7dde 100644 --- 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 @@ -33,7 +33,7 @@ object ModelMutation { * @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) @@ -47,7 +47,7 @@ object ModelMutation { * @param

the concrete model path for the M model type * @return a valid `GraphQLRequest` instance. * @see MutationType.CREATE - */ + */ @JvmStatic fun > create( model: M, @@ -63,7 +63,7 @@ object ModelMutation { * @param the model concrete type. * @return a valid `GraphQLRequest` instance. * @see MutationType.UPDATE - */ + */ fun update( model: M, predicate: QueryPredicate @@ -80,7 +80,7 @@ object ModelMutation { * @param

the concrete model path for the M model type * @return a valid `GraphQLRequest` instance. * @see MutationType.UPDATE - */ + */ @JvmStatic fun > update( model: M, @@ -98,7 +98,7 @@ object ModelMutation { * @see MutationType.UPDATE * * @see .update - */ + */ @JvmStatic fun update(model: M): GraphQLRequest { return buildMutation(model, QueryPredicates.all(), MutationType.UPDATE) @@ -112,7 +112,7 @@ object ModelMutation { * @param

the concrete model path for the M model type * @return a valid `GraphQLRequest` instance. * @see MutationType.UPDATE - */ + */ @JvmStatic fun > update( model: M, @@ -128,7 +128,7 @@ object ModelMutation { * @param the model concrete type. * @return a valid `GraphQLRequest` instance. * @see MutationType.DELETE - */ + */ fun delete( model: M, predicate: QueryPredicate @@ -145,7 +145,7 @@ object ModelMutation { * @param

the concrete model path for the M model type * @return a valid `GraphQLRequest` instance. * @see MutationType.DELETE - */ + */ @JvmStatic fun > delete( model: M, @@ -163,7 +163,7 @@ object ModelMutation { * @see MutationType.DELETE * * @see .delete - */ + */ @JvmStatic fun delete(model: M): GraphQLRequest { return buildMutation(model, QueryPredicates.all(), MutationType.DELETE) @@ -177,7 +177,7 @@ object ModelMutation { * @param

the concrete model path for the M model type * @return a valid `GraphQLRequest` instance. * @see MutationType.DELETE - */ + */ @JvmStatic fun > delete( model: M, @@ -185,4 +185,4 @@ object ModelMutation { ): GraphQLRequest { return buildMutation(model, QueryPredicates.all(), MutationType.DELETE, includes) } -} \ No newline at end of file +} 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 index 715aa46ba9..d36dca4ce5 100644 --- 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 @@ -38,7 +38,7 @@ object ModelQuery { * @param modelId the model identifier. * @param the concrete model type. * @return a valid [GraphQLRequest] instance. - */ + */ @JvmStatic operator fun get( modelType: Class, @@ -57,7 +57,7 @@ object ModelQuery { * @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, @@ -75,7 +75,7 @@ object ModelQuery { * @param modelIdentifier the model identifier. * @param the concrete model type. * @return a valid [GraphQLRequest] instance. - */ + */ @JvmStatic operator fun get( modelType: Class, @@ -94,7 +94,7 @@ object ModelQuery { * @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, @@ -112,7 +112,7 @@ object ModelQuery { * @param predicate the predicate for filtering. * @param the concrete model type. * @return a valid [GraphQLRequest] instance. - */ + */ @JvmStatic fun list( modelType: Class, @@ -131,7 +131,7 @@ object ModelQuery { * @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, @@ -148,7 +148,7 @@ object ModelQuery { * @param the concrete model type. * @return a valid [GraphQLRequest] instance. * @see .list - */ + */ @JvmStatic fun list(modelType: Class): GraphQLRequest> { return list(modelType, QueryPredicates.all()) @@ -163,7 +163,7 @@ object ModelQuery { * @param

the concrete model path for the M model type * @return a valid [GraphQLRequest] instance. * @see .list - */ + */ @JvmStatic fun > list( modelType: Class, @@ -185,7 +185,7 @@ object ModelQuery { * @param the concrete model type. * @return a valid [GraphQLRequest] instance. * @see ModelPagination.firstPage - */ + */ @JvmStatic fun list( modelType: Class, @@ -212,7 +212,7 @@ object ModelQuery { * @param

the concrete model path for the M model type * @return a valid [GraphQLRequest] instance. * @see ModelPagination.firstPage - */ + */ @JvmStatic fun > list( modelType: Class, @@ -237,7 +237,7 @@ object ModelQuery { * @param the concrete model type. * @return a valid [GraphQLRequest] instance. * @see ModelPagination.firstPage - */ + */ @JvmStatic fun list( modelType: Class, @@ -262,7 +262,7 @@ object ModelQuery { * @param

the concrete model path for the M model type * @return a valid [GraphQLRequest] instance. * @see ModelPagination.firstPage - */ + */ @JvmStatic fun > list( modelType: Class, @@ -273,4 +273,4 @@ object ModelQuery { modelType, QueryPredicates.all(), pagination.limit, includes ) } -} \ No newline at end of file +} 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 index 1a0153c9a4..88cf87e3d2 100644 --- 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 @@ -32,7 +32,7 @@ object ModelSubscription { * @param type the subscription type. * @param the concrete type of the model. * @return a valid [GraphQLRequest] instance. - */ + */ fun of( modelType: Class, type: SubscriptionType, @@ -48,7 +48,7 @@ object ModelSubscription { * @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, @@ -63,7 +63,7 @@ object ModelSubscription { * @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) @@ -77,7 +77,7 @@ object ModelSubscription { * @param

the concrete model path for the M model type * @return a valid [GraphQLRequest] instance. * @see .of - */ + */ @JvmStatic fun > onCreate( modelType: Class, @@ -92,7 +92,7 @@ object ModelSubscription { * @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) } @@ -105,7 +105,7 @@ object ModelSubscription { * @param

the concrete model path for the M model type * @return a valid [GraphQLRequest] instance. * @see .of - */ + */ @JvmStatic fun > onDelete( modelType: Class, @@ -114,14 +114,13 @@ object ModelSubscription { 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) } @@ -134,7 +133,7 @@ object ModelSubscription { * @param

the concrete model path for the M model type * @return a valid [GraphQLRequest] instance. * @see .of - */ + */ @JvmStatic fun > onUpdate( modelType: Class, @@ -142,4 +141,4 @@ object ModelSubscription { ): GraphQLRequest { return of(modelType, SubscriptionType.ON_UPDATE, includes) } -} \ No newline at end of file +} diff --git a/core/src/main/java/com/amplifyframework/core/model/LazyList.kt b/core/src/main/java/com/amplifyframework/core/model/LazyList.kt index a3b427eae2..b61504f3bd 100644 --- a/core/src/main/java/com/amplifyframework/core/model/LazyList.kt +++ b/core/src/main/java/com/amplifyframework/core/model/LazyList.kt @@ -16,7 +16,6 @@ package com.amplifyframework.core.model import com.amplifyframework.AmplifyException -import com.amplifyframework.api.ApiException import com.amplifyframework.core.Consumer interface LazyList { diff --git a/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt b/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt index 07b6ee7cc4..f1439a176e 100644 --- a/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt +++ b/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt @@ -21,7 +21,7 @@ import com.amplifyframework.core.Consumer interface LazyModel { fun getValue(): M? - + @InternalAmplifyApi fun getIdentifier(): Map @@ -30,5 +30,3 @@ interface LazyModel { fun getModel(onSuccess: (M?) -> Unit, onError: Consumer) } - - diff --git a/core/src/main/java/com/amplifyframework/core/model/ModelException.kt b/core/src/main/java/com/amplifyframework/core/model/ModelException.kt index 0fce18f594..130d53803d 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelException.kt +++ b/core/src/main/java/com/amplifyframework/core/model/ModelException.kt @@ -31,5 +31,4 @@ sealed class ModelException( "Check if the model types were generated with the latest Amplify CLI and try again", cause ) - -} \ No newline at end of file +} diff --git a/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt b/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt index 4d16c90c53..e5c262e961 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt +++ b/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt @@ -77,7 +77,6 @@ interface PropertyContainerPath : PropertyPath { * */ fun getModelType(): Class - } /** @@ -152,7 +151,7 @@ open class ModelPath( */ @Throws(ModelException.PropertyPathNotFound::class) @InternalAmplifyApi - fun >getRootPath(clazz: Class): P { + fun > getRootPath(clazz: Class): P { val field = try { clazz.getDeclaredField("rootPath") } catch (e: NoSuchFieldException) { From 7fd06224cb4a04e2a3be8b926f611847401c4751 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Thu, 27 Jul 2023 11:15:01 -0400 Subject: [PATCH 041/100] Fix tests --- .../datastore/appsync/SerializedModelAdapterTest.java | 4 ++-- .../test/resources/serde-for-blog-in-serialized-model.json | 4 ++++ .../resources/serde-for-comment-in-serialized-model.json | 7 +++++++ .../resources/serde-for-meeting-in-serialized-model.json | 6 ++++++ ...d-model-with-nested-custom-type-se-deserialization.json | 3 +++ .../amplifyframework/api/graphql/model/ModelMutation.kt | 2 ++ 6 files changed, 24 insertions(+), 2 deletions(-) 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 9fb39aa6d8..c37c08e7f7 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 @@ -125,7 +125,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); @@ -175,7 +175,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/serde-for-blog-in-serialized-model.json b/aws-api-appsync/src/test/resources/serde-for-blog-in-serialized-model.json index b9410208a1..b84423fd0b 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,7 @@ "isArray": false, "isEnum": false, "isModel": false, + "isLazyModel": false, "authRules": [] }, "name": { @@ -28,6 +29,7 @@ "isArray": false, "isEnum": false, "isModel": false, + "isLazyModel": false, "authRules": [] }, "owner": { @@ -40,6 +42,7 @@ "isArray": false, "isEnum": false, "isModel": true, + "isLazyModel": false, "authRules": [] }, "posts": { @@ -52,6 +55,7 @@ "isArray": true, "isEnum": false, "isModel": false, + "isLazyModel": 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..be508126a2 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,7 @@ "isCustomType": false, "isReadOnly": true, "isModel": false, + "isLazyModel": false, "authRules": [], "name": "createdAt", "isEnum": false, @@ -54,6 +55,7 @@ "isCustomType": false, "isReadOnly": false, "isModel": true, + "isLazyModel": false, "authRules": [], "name": "post", "isEnum": false, @@ -66,6 +68,7 @@ "isCustomType": false, "isReadOnly": false, "isModel": false, + "isLazyModel": false, "authRules": [], "name": "description", "isEnum": false, @@ -78,6 +81,7 @@ "isCustomType": false, "isReadOnly": false, "isModel": false, + "isLazyModel": false, "authRules": [], "name": "title", "isEnum": false, @@ -90,6 +94,7 @@ "isCustomType": false, "isReadOnly": false, "isModel": false, + "isLazyModel": false, "authRules": [], "name": "content", "isEnum": false, @@ -102,6 +107,7 @@ "isCustomType": false, "isReadOnly": false, "isModel": false, + "isLazyModel": false, "authRules": [], "name": "likes", "isEnum": false, @@ -114,6 +120,7 @@ "isCustomType": false, "isReadOnly": true, "isModel": false, + "isLazyModel": 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 2aae8fe5ae..6409ed33c0 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 @@ -16,6 +16,7 @@ "isArray": false, "isEnum": false, "isModel": false, + "isLazyModel": false, "authRules": [] }, "dateTime": { @@ -28,6 +29,7 @@ "isArray": false, "isEnum": false, "isModel": false, + "isLazyModel": false, "authRules": [] }, "id": { @@ -40,6 +42,7 @@ "isArray": false, "isEnum": false, "isModel": false, + "isLazyModel": false, "authRules": [] }, "name": { @@ -52,6 +55,7 @@ "isArray": false, "isEnum": false, "isModel": false, + "isLazyModel": false, "authRules": [] }, "time": { @@ -64,6 +68,7 @@ "isArray": false, "isEnum": false, "isModel": false, + "isLazyModel": false, "authRules": [] }, "timestamp": { @@ -76,6 +81,7 @@ "isArray": false, "isEnum": false, "isModel": false, + "isLazyModel": 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 6c55113882..8ad2fc834a 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 @@ -77,6 +77,7 @@ "isCustomType": true, "isReadOnly": false, "isModel": false, + "isLazyModel": false, "authRules": [], "name": "contact", "isEnum": false, @@ -89,6 +90,7 @@ "isCustomType": false, "isReadOnly": false, "isModel": false, + "isLazyModel": false, "authRules": [], "name": "fullName", "isEnum": false, @@ -101,6 +103,7 @@ "isCustomType": false, "isReadOnly": false, "isModel": false, + "isLazyModel": false, "authRules": [], "name": "id", "isEnum": false, 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 index be5a4e7dde..13096510ec 100644 --- 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 @@ -64,6 +64,7 @@ object ModelMutation { * @return a valid `GraphQLRequest` instance. * @see MutationType.UPDATE */ + @JvmStatic fun update( model: M, predicate: QueryPredicate @@ -129,6 +130,7 @@ object ModelMutation { * @return a valid `GraphQLRequest` instance. * @see MutationType.DELETE */ + @JvmStatic fun delete( model: M, predicate: QueryPredicate From d1274cb5641553ede14a46aca4f6e359c83e6206 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Fri, 28 Jul 2023 10:22:06 -0400 Subject: [PATCH 042/100] Move API request options to be set for selection set --- .../api/aws/ApiGraphQLRequestOptions.java | 12 +++++++++++- .../api/aws/SelectionSetExtensions.kt | 3 +-- 2 files changed, 12 insertions(+), 3 deletions(-) rename {aws-api => aws-api-appsync}/src/main/java/com/amplifyframework/api/aws/ApiGraphQLRequestOptions.java (86%) 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 86% 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..9aee579827 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,16 @@ 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 ApiGraphQLRequestOptions() {} + + ApiGraphQLRequestOptions(int maxDepth) { + this.maxDepth = maxDepth; + } + @NonNull @Override public List paginationFields() { @@ -47,7 +57,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/SelectionSetExtensions.kt b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/SelectionSetExtensions.kt index bc1db5e4de..7c3f5e8b19 100644 --- 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 @@ -17,7 +17,6 @@ package com.amplifyframework.api.aws -import com.amplifyframework.api.aws.SelectionSetDepth.Companion.onlyIncluded import com.amplifyframework.api.graphql.QueryType import com.amplifyframework.core.model.PropertyContainerPath @@ -67,7 +66,7 @@ private fun getSelectionSet(node: PropertyContainerPath): SelectionSet { var selectionSet = SelectionSet.builder() .operation(QueryType.GET) .value(name) - .requestOptions(onlyIncluded()) + .requestOptions(ApiGraphQLRequestOptions(0)) .modelClass(node.getModelType()) .build() From fe24f283e9562e9b0890decf9b4bfbf793158afb Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Fri, 28 Jul 2023 10:23:50 -0400 Subject: [PATCH 043/100] remove unused file --- .../api/aws/SelectionSetDepth.kt | 52 ------------------- 1 file changed, 52 deletions(-) delete mode 100644 aws-api-appsync/src/main/java/com/amplifyframework/api/aws/SelectionSetDepth.kt diff --git a/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/SelectionSetDepth.kt b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/SelectionSetDepth.kt deleted file mode 100644 index 0f49aa2876..0000000000 --- a/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/SelectionSetDepth.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 - -/** - * GraphQL request options that allows for configurable depth. - */ -class SelectionSetDepth(private val maxDepth: Int = 2) : GraphQLRequestOptions { - - override fun paginationFields(): List { - return listOf(NEXT_TOKEN_KEY) - } - - override fun modelMetaFields(): List { - return emptyList() - } - - override fun listField(): String { - return ITEMS_KEY - } - - override fun maxDepth(): Int { - return maxDepth - } - - override fun leafSerializationBehavior(): LeafSerializationBehavior { - return LeafSerializationBehavior.ALL_FIELDS - } - - companion object { - private const val NEXT_TOKEN_KEY = "nextToken" - private const val ITEMS_KEY = "items" - - @JvmStatic - fun defaultDepth() = SelectionSetDepth() - - @JvmStatic - fun onlyIncluded() = SelectionSetDepth(maxDepth = 0) - } -} From b57fccbb1fd07bfd7d2c06e4d5155435bc193ab2 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 2 Aug 2023 09:34:36 -0400 Subject: [PATCH 044/100] Better Java support --- .../api/aws/AppSyncLazyModel.kt | 7 ++-- .../core/NullableConsumer.java | 32 +++++++++++++++++++ .../core/model/InMemoryLazyModel.kt | 5 +-- .../amplifyframework/core/model/LazyModel.kt | 3 +- 4 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 core/src/main/java/com/amplifyframework/core/NullableConsumer.java diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyModel.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyModel.kt index 1e63361328..53500e9e19 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyModel.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyModel.kt @@ -22,6 +22,7 @@ import com.amplifyframework.api.graphql.GraphQLResponse import com.amplifyframework.api.graphql.PaginatedResult import com.amplifyframework.core.Amplify as coreAmplify import com.amplifyframework.core.Consumer +import com.amplifyframework.core.NullableConsumer import com.amplifyframework.core.model.LazyModel import com.amplifyframework.core.model.Model import com.amplifyframework.kotlin.core.Amplify @@ -67,9 +68,9 @@ class AppSyncLazyModel( return value } - override fun getModel(onSuccess: (M?) -> Unit, onError: Consumer) { + override fun getModel(onSuccess: NullableConsumer, onError: Consumer) { if (loadedValue) { - onSuccess(value) + onSuccess.accept(value) return } val onQuerySuccess = Consumer>> { @@ -80,7 +81,7 @@ class AppSyncLazyModel( null } loadedValue = true - onSuccess(value) + onSuccess.accept(value) } val onApiFailure = Consumer { onError.accept(it) } coreAmplify.API.query( 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/InMemoryLazyModel.kt b/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt index 1d42cbfe33..0857f4ccdb 100644 --- a/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt +++ b/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt @@ -18,6 +18,7 @@ package com.amplifyframework.core.model import com.amplifyframework.AmplifyException import com.amplifyframework.annotations.InternalAmplifyApi import com.amplifyframework.core.Consumer +import com.amplifyframework.core.NullableConsumer @InternalAmplifyApi class InMemoryLazyModel(model: M? = null) : LazyModel { @@ -36,7 +37,7 @@ class InMemoryLazyModel(model: M? = null) : LazyModel { return value } - override fun getModel(onSuccess: (M?) -> Unit, onError: Consumer) { - onSuccess(value) + override fun getModel(onSuccess: NullableConsumer, onError: Consumer) { + onSuccess.accept(value) } } diff --git a/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt b/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt index f1439a176e..1ab5031bba 100644 --- a/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt +++ b/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt @@ -18,6 +18,7 @@ package com.amplifyframework.core.model import com.amplifyframework.AmplifyException import com.amplifyframework.annotations.InternalAmplifyApi import com.amplifyframework.core.Consumer +import com.amplifyframework.core.NullableConsumer interface LazyModel { fun getValue(): M? @@ -28,5 +29,5 @@ interface LazyModel { @Throws(AmplifyException::class) suspend fun getModel(): M? - fun getModel(onSuccess: (M?) -> Unit, onError: Consumer) + fun getModel(onSuccess: NullableConsumer, onError: Consumer) } From 1cf07b12dddc3dc78fdf55faba8653bb6af319fc Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 2 Aug 2023 16:46:16 -0400 Subject: [PATCH 045/100] Fix a few selection set merge issues. --- .../api/aws/SelectionSet.java | 27 +++++++++---------- .../api/aws/SelectionSetExtensions.kt | 12 +++++++-- 2 files changed, 23 insertions(+), 16 deletions(-) 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 d119721af6..4834c43efc 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 @@ -240,10 +240,8 @@ public SelectionSet build() throws AmplifyException { SerializedModel.class == modelClass ? getModelFields(modelSchema, requestOptions.maxDepth(), operation) : getModelFields(modelClass, requestOptions.maxDepth(), operation, false)); - if (QueryType.LIST.equals(operation) || QueryType.SYNC.equals(operation)) { - node = wrapPagination(node); - } + // Associations need to be added before wrapping pagination if (includeAssociations != null) { for (PropertyContainerPath association : includeAssociations) { SelectionSet included = SelectionSetUtils.asSelectionSet(association, false); @@ -252,6 +250,10 @@ public SelectionSet build() throws AmplifyException { } } } + + if (QueryType.LIST.equals(operation) || QueryType.SYNC.equals(operation)) { + node = wrapPagination(node); + } return node; } @@ -324,18 +326,15 @@ private Set getModelFields(Class clazz, int depth operation, false)); result.add(new SelectionSet(fieldName, fields)); } + } else if (LazyModel.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) { - Class modalClass; - if (LazyModel.class.isAssignableFrom(field.getType())) { - ParameterizedType pType = (ParameterizedType) field.getGenericType(); - modalClass = (Class) pType.getActualTypeArguments()[0]; - Set fields = getModelFields(modalClass, 0, operation, true); - result.add(new SelectionSet(fieldName, fields)); - } else { - modalClass = (Class) field.getType(); - Set fields = getModelFields(modalClass, depth - 1, operation, false); - result.add(new SelectionSet(fieldName, fields)); - } + Class modalClass = (Class) field.getType(); + Set fields = getModelFields(modalClass, depth - 1, operation, false); + result.add(new SelectionSet(fieldName, fields)); } } else if (isCustomType(field)) { result.add(new SelectionSet(fieldName, getNestedCustomTypeFields(getClassForField(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 index 7c3f5e8b19..c230ca0942 100644 --- 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 @@ -45,7 +45,11 @@ internal fun SelectionSet.replaceChild(selectionSet: SelectionSet) { * Transforms the entire property path (walking up the tree) into a `SelectionSet`. */ internal fun PropertyContainerPath.asSelectionSet(includeRoot: Boolean = true): 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, includeRoot).map { + // always add to lookup list so that indexes match + isCollectionLookup.add(it.getMetadata().isCollection) getSelectionSet(it) } @@ -53,8 +57,12 @@ internal fun PropertyContainerPath.asSelectionSet(includeRoot: Boolean = true): return null } - return selectionSets.reduce { acc, selectionSet -> - selectionSet.replaceChild(acc) + return selectionSets.reduceIndexed { i, acc, selectionSet -> + if (isCollectionLookup[i]) { + selectionSet.nodes.find { it.value == "items" }?.replaceChild(acc) + } else { + selectionSet.replaceChild(acc) + } selectionSet } } From e375efa8fb7fc3e774ad34e041f796c792511bee Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Thu, 3 Aug 2023 09:54:09 -0400 Subject: [PATCH 046/100] remove kotlin core dep on aws-api --- .../api/aws/AppSyncGraphQLRequest.java | 1 - aws-api/build.gradle.kts | 1 - .../{AppSyncLazyModel.kt => ApiLazyTypes.kt} | 85 +++++++++++++++++-- .../api/aws/AppSyncLazyListModel.kt | 85 ------------------- 4 files changed, 80 insertions(+), 92 deletions(-) rename aws-api/src/main/java/com/amplifyframework/api/aws/{AppSyncLazyModel.kt => ApiLazyTypes.kt} (52%) delete mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyListModel.kt diff --git a/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequest.java b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequest.java index 904b94323c..a30c6d6e8c 100644 --- a/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequest.java +++ b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequest.java @@ -27,7 +27,6 @@ import com.amplifyframework.core.model.Model; import com.amplifyframework.core.model.ModelOperation; import com.amplifyframework.core.model.ModelSchema; -import com.amplifyframework.core.model.PropertyContainerPath; import com.amplifyframework.util.Casing; import com.amplifyframework.util.Immutable; import com.amplifyframework.util.Wrap; diff --git a/aws-api/build.gradle.kts b/aws-api/build.gradle.kts index 1f8c56eab5..72799ee46e 100644 --- a/aws-api/build.gradle.kts +++ b/aws-api/build.gradle.kts @@ -25,7 +25,6 @@ group = properties["POM_GROUP"].toString() dependencies { api(project(":core")) - api(project(":core-kotlin")) api(project(":aws-core")) implementation(project(":aws-api-appsync")) diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyModel.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt similarity index 52% rename from aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyModel.kt rename to aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt index 53500e9e19..69fbdf40b2 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyModel.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt @@ -18,17 +18,21 @@ package com.amplifyframework.api.aws import com.amplifyframework.AmplifyException import com.amplifyframework.annotations.InternalAmplifyApi import com.amplifyframework.api.ApiException +import com.amplifyframework.api.graphql.GraphQLRequest import com.amplifyframework.api.graphql.GraphQLResponse import com.amplifyframework.api.graphql.PaginatedResult -import com.amplifyframework.core.Amplify as coreAmplify +import com.amplifyframework.core.Amplify import com.amplifyframework.core.Consumer import com.amplifyframework.core.NullableConsumer +import com.amplifyframework.core.model.LazyList import com.amplifyframework.core.model.LazyModel import com.amplifyframework.core.model.Model -import com.amplifyframework.kotlin.core.Amplify +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine @InternalAmplifyApi -class AppSyncLazyModel( +class ApiLazyModel( private val clazz: Class, private val keyMap: Map ) : LazyModel { @@ -50,7 +54,7 @@ class AppSyncLazyModel( return value } try { - val resultIterator = Amplify.API.query( + val resultIterator = query( AppSyncGraphQLRequestFactory.buildQuery, M>( clazz, queryPredicate @@ -84,10 +88,81 @@ class AppSyncLazyModel( onSuccess.accept(value) } val onApiFailure = Consumer { onError.accept(it) } - coreAmplify.API.query( + Amplify.API.query( AppSyncGraphQLRequestFactory.buildQuery(clazz, queryPredicate), onQuerySuccess, onApiFailure ) } } + +@InternalAmplifyApi +class ApiLazyListModel( + private val clazz: Class, + keyMap: Map, +) : LazyList { + + private var value: MutableList = mutableListOf() + private var paginatedResult: PaginatedResult? = null + private val queryPredicate = AppSyncLazyQueryPredicate().createPredicate(clazz, keyMap) + + override fun getItems(): List { + return value + } + + override suspend fun getNextPage(): List { + if (!hasNextPage()) { + return emptyList() + } + val request = createGraphQLRequest() + paginatedResult = query(request).data + val nextPageOfItems = paginatedResult!!.items.toList() + value.addAll(nextPageOfItems) + return nextPageOfItems + } + + override fun getNextPage(onSuccess: Consumer>, onError: Consumer) { + if (!hasNextPage()) { + onSuccess.accept(emptyList()) + return + } + val onQuerySuccess = Consumer>> { + paginatedResult = it.data + val nextPageOfItems = paginatedResult!!.items.toList() + value.addAll(nextPageOfItems) + onSuccess.accept(nextPageOfItems) + } + val onApiFailure = Consumer { onError.accept(it) } + val request = createGraphQLRequest() + Amplify.API.query(request, onQuerySuccess, onApiFailure) + } + + override fun hasNextPage(): Boolean { + return paginatedResult?.hasNextResult() ?: true + } + + private fun createGraphQLRequest(): GraphQLRequest> { + return if (paginatedResult != null) { + paginatedResult!!.requestForNextResult + } else { + AppSyncGraphQLRequestFactory.buildQuery( + clazz, + queryPredicate + ) + } + } +} + +/* + Duplicating the query Kotlin Facade method so we aren't pulling in Kotlin Core + */ +@Throws(ApiException::class) +private suspend fun query(request: GraphQLRequest): GraphQLResponse { + return suspendCoroutine { continuation -> + Amplify.API.query( + request, + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) + } +} \ No newline at end of file diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyListModel.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyListModel.kt deleted file mode 100644 index 3e6ad3dabe..0000000000 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyListModel.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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.annotations.InternalAmplifyApi -import com.amplifyframework.api.ApiException -import com.amplifyframework.api.graphql.GraphQLRequest -import com.amplifyframework.api.graphql.GraphQLResponse -import com.amplifyframework.api.graphql.PaginatedResult -import com.amplifyframework.core.Amplify as coreAmplify -import com.amplifyframework.core.Consumer -import com.amplifyframework.core.model.LazyList -import com.amplifyframework.core.model.Model -import com.amplifyframework.kotlin.core.Amplify - -@InternalAmplifyApi -class AppSyncLazyListModel( - private val clazz: Class, - keyMap: Map, -) : LazyList { - - private var value: MutableList = mutableListOf() - private var paginatedResult: PaginatedResult? = null - private val queryPredicate = AppSyncLazyQueryPredicate().createPredicate(clazz, keyMap) - - override fun getItems(): List { - return value - } - - override suspend fun getNextPage(): List { - if (!hasNextPage()) { - return emptyList() - } - val request = createGraphQLRequest() - paginatedResult = Amplify.API.query(request).data - val nextPageOfItems = paginatedResult!!.items.toList() - value.addAll(nextPageOfItems) - return nextPageOfItems - } - - override fun getNextPage(onSuccess: Consumer>, onError: Consumer) { - if (!hasNextPage()) { - onSuccess.accept(emptyList()) - return - } - val onQuerySuccess = Consumer>> { - paginatedResult = it.data - val nextPageOfItems = paginatedResult!!.items.toList() - value.addAll(nextPageOfItems) - onSuccess.accept(nextPageOfItems) - } - val onApiFailure = Consumer { onError.accept(it) } - val request = createGraphQLRequest() - coreAmplify.API.query(request, onQuerySuccess, onApiFailure) - } - - override fun hasNextPage(): Boolean { - return paginatedResult?.hasNextResult() ?: true - } - - private fun createGraphQLRequest(): GraphQLRequest> { - return if (paginatedResult != null) { - paginatedResult!!.requestForNextResult - } else { - AppSyncGraphQLRequestFactory.buildQuery( - clazz, - queryPredicate - ) - } - } -} From 5f6bdb2c74ffc8cd5251b15520f5b5a41bf0596d Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Mon, 7 Aug 2023 13:20:15 -0400 Subject: [PATCH 047/100] Create LazyList during response deserialization --- .../amplifyframework/api/aws/ApiLazyTypes.kt | 60 +++++++++++++--- .../amplifyframework/api/aws/GsonFactory.java | 6 +- .../api/aws/GsonGraphQLResponseFactory.java | 9 ++- .../api/aws/LazyListAdapter.kt | 69 ++++++++++++++++++ .../api/aws/LazyModelAdapter.java | 70 ------------------- .../api/aws/LazyModelAdapter.kt | 66 +++++++++++++++++ .../api/aws/ModelDeserializer.kt | 54 ++++++++++++++ .../api/aws/ModelExtensions.kt | 12 ++++ core/build.gradle.kts | 3 - .../core/model/ModelField.java | 27 +++++++ .../core/model/ModelSchema.java | 1 + 11 files changed, 294 insertions(+), 83 deletions(-) create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/LazyListAdapter.kt delete mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.java create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/ModelExtensions.kt diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt index 69fbdf40b2..27764ce130 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt @@ -32,13 +32,13 @@ import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine @InternalAmplifyApi -class ApiLazyModel( +class ApiLazyModel private constructor( private val clazz: Class, - private val keyMap: Map + private val keyMap: Map, + private var loadedValue: Boolean = false, + private var value: M? = null ) : LazyModel { - private var value: M? = null - private var loadedValue = false private val queryPredicate = AppSyncLazyQueryPredicate().createPredicate(clazz, keyMap) override fun getValue(): M? { @@ -53,6 +53,7 @@ class ApiLazyModel( if (loadedValue) { return value } + try { val resultIterator = query( AppSyncGraphQLRequestFactory.buildQuery, M>( @@ -94,15 +95,37 @@ class ApiLazyModel( onApiFailure ) } + + companion object { + @JvmStatic + fun createPreloaded( + clazz: Class, + keyMap: Map, + value: M? + + ): ApiLazyModel { + return ApiLazyModel(clazz, keyMap, true, value) + } + + @JvmStatic + fun createLazy( + clazz: Class, + keyMap: Map + + ): ApiLazyModel { + return ApiLazyModel(clazz, keyMap) + } + } } @InternalAmplifyApi -class ApiLazyListModel( +class ApiLazyListModel private constructor( private val clazz: Class, keyMap: Map, + preloadedValue: MutableList? = null ) : LazyList { - - private var value: MutableList = mutableListOf() + private var isPreloaded = preloadedValue != null + private var value: MutableList = preloadedValue ?: mutableListOf() private var paginatedResult: PaginatedResult? = null private val queryPredicate = AppSyncLazyQueryPredicate().createPredicate(clazz, keyMap) @@ -138,7 +161,7 @@ class ApiLazyListModel( } override fun hasNextPage(): Boolean { - return paginatedResult?.hasNextResult() ?: true + return !isPreloaded && paginatedResult?.hasNextResult() ?: true } private fun createGraphQLRequest(): GraphQLRequest> { @@ -151,6 +174,27 @@ class ApiLazyListModel( ) } } + + companion object { + @JvmStatic + fun createPreloaded( + clazz: Class, + keyMap: Map, + value: List + + ): ApiLazyListModel { + return ApiLazyListModel(clazz, keyMap, value.toMutableList()) + } + + @JvmStatic + fun createLazy( + clazz: Class, + keyMap: Map + + ): ApiLazyListModel { + return ApiLazyListModel(clazz, keyMap) + } + } } /* 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 index 7edbf8df4f..7568bae73b 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/GsonFactory.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/GsonFactory.java @@ -16,7 +16,9 @@ package com.amplifyframework.api.aws; import com.amplifyframework.api.graphql.GsonResponseAdapters; +import com.amplifyframework.core.model.LazyList; import com.amplifyframework.core.model.LazyModel; +import com.amplifyframework.core.model.Model; import com.amplifyframework.core.model.query.predicate.GsonPredicateAdapters; import com.amplifyframework.core.model.temporal.GsonTemporalAdapters; import com.amplifyframework.core.model.types.GsonJavaTypeAdapters; @@ -55,7 +57,9 @@ private static Gson create() { ModelWithMetadataAdapter.register(builder); SerializedModelAdapter.register(builder); SerializedCustomTypeAdapter.register(builder); - builder.registerTypeAdapter(LazyModel.class, new LazyModelAdapter<>()); + builder.registerTypeAdapter(Model.class, new LazyModelAdapter()); + builder.registerTypeAdapter(LazyModel.class, new LazyModelAdapter()); + builder.registerTypeAdapter(LazyList.class, new LazyListAdapter()); 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 2b4b9d72cb..2f37cf809b 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 @@ -22,6 +22,7 @@ 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.util.Empty; import com.amplifyframework.util.TypeMaker; @@ -73,7 +74,13 @@ public GraphQLResponse buildResponse(GraphQLRequest request, String re Gson responseGson = gson.newBuilder() .registerTypeHierarchyAdapter(Iterable.class, new IterableDeserializer<>(request)) .create(); - return responseGson.fromJson(responseJson, responseType); + + + Gson modelDeserializerGson = responseGson.newBuilder() + .registerTypeHierarchyAdapter(Model.class, new ModelDeserializer(responseGson)) + .create(); + + return modelDeserializerGson.fromJson(responseJson, responseType); } catch (JsonParseException jsonParseException) { throw new ApiException( "Amplify encountered an error while deserializing an object.", diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/LazyListAdapter.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyListAdapter.kt new file mode 100644 index 0000000000..c6435a72f1 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyListAdapter.kt @@ -0,0 +1,69 @@ +/* + * 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.annotations.InternalAmplifyApi +import com.amplifyframework.api.aws.ApiLazyListModel.Companion.createPreloaded +import com.amplifyframework.core.model.LazyList +import com.amplifyframework.core.model.Model +import com.amplifyframework.core.model.SchemaRegistry +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +@InternalAmplifyApi +class LazyListAdapter : JsonDeserializer>, JsonSerializer> { + @Throws(JsonParseException::class) + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext + ): LazyList { + val pType = typeOfT as ParameterizedType + val type = pType.actualTypeArguments[0] as Class + val jsonObject = json.asJsonObject + val itemsJsonArray = jsonObject.getAsJsonArray("items") + + val items = itemsJsonArray.map { + context.deserialize(it.asJsonObject, type) + } + + val primaryKeysIterator: Iterator = SchemaRegistry.instance() + .getModelSchemaForModelClass(type) + .primaryIndexFields.iterator() + val predicateKeyMap: MutableMap = HashMap() + while (primaryKeysIterator.hasNext()) { + val key = primaryKeysIterator.next() + val value = jsonObject.get(key) + if (value != null) { + predicateKeyMap[key] = value + } + } + + return createPreloaded(type, predicateKeyMap, items) + } + + override fun serialize( + src: LazyList, typeOfSrc: Type, + context: JsonSerializationContext + ): JsonElement? { + return null + } +} \ No newline at end of file diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.java b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.java deleted file mode 100644 index 3e94efdb0c..0000000000 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.util.Log; - - -import com.amplifyframework.annotations.InternalAmplifyApi; -import com.amplifyframework.core.model.LazyModel; -import com.amplifyframework.core.model.Model; -import com.amplifyframework.core.model.SchemaRegistry; -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 com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; - -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -@InternalAmplifyApi -public class LazyModelAdapter implements JsonDeserializer>, - JsonSerializer> { - - @SuppressWarnings("unchecked") - @Override - public LazyModel deserialize(JsonElement json, Type typeOfT, - JsonDeserializationContext context) throws JsonParseException { - ParameterizedType pType = (ParameterizedType) typeOfT; - Class type = (Class) pType.getActualTypeArguments()[0]; - - Log.d("LazyModelAdapter", "json: "+ json + " typeOfT " + typeOfT + - " typeOfT type name" + type + " context " + - context); - Map predicateKeyMap = new HashMap<>(); - Iterator primaryKeysIterator = SchemaRegistry.instance() - .getModelSchemaForModelClass(type) - .getPrimaryIndexFields().iterator(); - JsonObject jsonObject = (JsonObject) json; - while (primaryKeysIterator.hasNext()){ - String primaryKey = primaryKeysIterator.next(); - predicateKeyMap.put(primaryKey, jsonObject.get(primaryKey)); - } - return new AppSyncLazyModel<>(type, predicateKeyMap); - } - - @Override - public JsonElement serialize(LazyModel src, Type typeOfSrc, - JsonSerializationContext context) { - return null; - } -} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt new file mode 100644 index 0000000000..2fe9f97422 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt @@ -0,0 +1,66 @@ +/* + * 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.annotations.InternalAmplifyApi +import com.amplifyframework.api.aws.ApiLazyModel.Companion.createLazy +import com.amplifyframework.api.aws.ApiLazyModel.Companion.createPreloaded +import com.amplifyframework.core.model.LazyModel +import com.amplifyframework.core.model.Model +import com.amplifyframework.core.model.SchemaRegistry +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import com.google.gson.JsonSerializationContext +import com.google.gson.JsonSerializer +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +@InternalAmplifyApi +class LazyModelAdapter : JsonDeserializer>, + JsonSerializer> { + @Throws(JsonParseException::class) + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext + ): LazyModel { + val pType = typeOfT as ParameterizedType + val type = pType.actualTypeArguments[0] as Class + val jsonObject = json.asJsonObject + val predicateKeyMap = SchemaRegistry.instance() + .getModelSchemaForModelClass(type) + .primaryIndexFields + .associateWith { jsonObject[it] } + + if (jsonObject.size() > predicateKeyMap.size) { + try { + val preloadedValue = context.deserialize(json, type) + return createPreloaded(type, predicateKeyMap, preloadedValue) + } catch (e: Exception) { + // fallback to create lazy + } + } + return createLazy(type, predicateKeyMap) + } + + override fun serialize( + src: LazyModel, typeOfSrc: Type, + context: JsonSerializationContext + ): JsonElement? { + return null + } +} \ No newline at end of file diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt new file mode 100644 index 0000000000..d1d68b2f17 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt @@ -0,0 +1,54 @@ +package com.amplifyframework.api.aws + +import com.amplifyframework.core.model.Model +import com.amplifyframework.core.model.SchemaRegistry +import com.google.gson.Gson +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import java.lang.reflect.Type + +/** + * Here we are Deserializing Model types and Injecting values into lazy list fields. Lazy list fields will be null + * from the server unless the list was provided in the selection set. + * + * @param responseGson is a Gson object that does not have the model deserializer. Otherwise context.fromJson would + * cause a recursion issue. + */ +class ModelDeserializer(private val responseGson: Gson) : JsonDeserializer { + + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Model { + val parent = responseGson.fromJson(json, typeOfT) + val parentType = (typeOfT as Class<*>).simpleName + val parentModelSchema = SchemaRegistry.instance().getModelSchemaForModelClass(parentType) + + parentModelSchema.fields.filter { it.value.isLazyList }.map { fieldMap -> + val fieldToUpdate = parent.javaClass.getDeclaredField(fieldMap.key) + fieldToUpdate.isAccessible= true + if (fieldToUpdate.get(parent) == null) { + val lazyField = fieldMap.value + val lazyFieldModelSchema = SchemaRegistry.instance().getModelSchemaForModelClass(lazyField.targetType) + + + val lazyFieldTargetNames = lazyFieldModelSchema + .associations + .entries + .first { it.value.associatedType == parentType } + .value + .targetNames + + val parentIdentifiers = parent.getSortedIdentifiers() + + val queryKeys = lazyFieldTargetNames.mapIndexed { idx, name -> + name to parentIdentifiers[idx] + }.toMap() + + val lazyList = ApiLazyListModel.createLazy(lazyFieldModelSchema.modelClass, queryKeys) + + fieldToUpdate.isAccessible = true + fieldToUpdate.set(parent, lazyList) + } + } + return parent + } +} \ No newline at end of file diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelExtensions.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelExtensions.kt new file mode 100644 index 0000000000..1a4ad3cc53 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelExtensions.kt @@ -0,0 +1,12 @@ +package com.amplifyframework.api.aws + +import com.amplifyframework.core.model.Model +import com.amplifyframework.core.model.ModelIdentifier +import java.io.Serializable + +fun Model.getSortedIdentifiers(): List { + return when(val identifier = resolveIdentifier()) { + is ModelIdentifier<*> -> { listOf(identifier.key()) + identifier.sortedKeys() } + else -> listOf(identifier.toString()) + } +} \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 9e5cf3d5e1..b91595dfba 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -37,9 +37,6 @@ dependencies { implementation(dependency.androidx.nav.ui) implementation(dependency.androidx.security) implementation(dependency.kotlin.serializationJson) - implementation(dependency.kotlin.stdlib) - implementation(dependency.kotlin.coroutines) - implementation(dependency.androidx.core.ktx) api(project(":common-core")) 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 f68178102f..1c7ec36df9 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelField.java +++ b/core/src/main/java/com/amplifyframework/core/model/ModelField.java @@ -57,6 +57,9 @@ public final class ModelField { // True if the field is an instance of lazy model. private final boolean isLazyModel; + // True if the field is an instance of lazy model. + private final boolean isLazyList; + // True if the field is an instance of CustomType private final boolean isCustomType; @@ -76,6 +79,7 @@ private ModelField(@NonNull ModelFieldBuilder builder) { this.isEnum = builder.isEnum; this.isModel = builder.isModel; this.isLazyModel = builder.isLazyModel; + this.isLazyList = builder.isLazyList; this.isCustomType = builder.isCustomType; this.authRules = builder.authRules; } @@ -167,6 +171,16 @@ public boolean isLazyModel() { return isLazyModel; } + /** + * Returns true if the field's target type is Model. + * + * @return True if the field's target type is Model. + */ + public boolean isLazyList() { + return isLazyList; + } + + /** * Returns true if the field's target type is CustomType. * @@ -285,6 +299,9 @@ public static class ModelFieldBuilder { // True if the field's target type is LazyModel. private boolean isLazyModel = false; + // True if the field's target type is LazyModel. + private boolean isLazyList = false; + // True if the field's target type is CustomType. private boolean isCustomType = false; @@ -391,6 +408,16 @@ public ModelFieldBuilder isLazyModel(boolean isLazyModel) { return this; } + /** + * Sets a flag indicating whether or not the field's type is a LazyList. + * @param isLazyList flag indicating if the field is a LazyList + * @return the builder object + */ + public ModelFieldBuilder isLazyList(boolean isLazyList) { + this.isLazyList = isLazyList; + 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/ModelSchema.java b/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java index 52948a7daf..a367e6de67 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java +++ b/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java @@ -234,6 +234,7 @@ private static ModelField createModelField(Field field) { .isEnum(Enum.class.isAssignableFrom(field.getType())) .isModel(Model.class.isAssignableFrom(field.getType())) .isLazyModel(LazyModel.class.isAssignableFrom(field.getType())) + .isLazyList(LazyList.class.isAssignableFrom(field.getType())) .authRules(authRules) .build(); } From e1b5422a80067707c2fd3d6422454b6919f0f8f5 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Mon, 21 Aug 2023 16:58:22 -0400 Subject: [PATCH 048/100] Move lazy list to use PaginatedResult --- .../api/aws/SelectionSet.java | 4 +- .../amplifyframework/api/aws/ApiLazyTypes.kt | 72 +++---------------- .../amplifyframework/api/aws/GsonFactory.java | 3 - .../api/aws/GsonGraphQLResponseFactory.java | 2 +- .../api/aws/LazyListAdapter.kt | 69 ------------------ .../api/aws/ModelDeserializer.kt | 2 +- .../amplifyframework/core/model/LazyList.kt | 30 -------- .../core/model/ModelPropertyPath.kt | 41 ----------- .../core/model/ModelSchema.java | 3 +- 9 files changed, 15 insertions(+), 211 deletions(-) delete mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/LazyListAdapter.kt delete mode 100644 core/src/main/java/com/amplifyframework/core/model/LazyList.kt 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 4834c43efc..30e588b3a2 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 @@ -23,12 +23,12 @@ import com.amplifyframework.AmplifyException; import com.amplifyframework.api.graphql.Operation; +import com.amplifyframework.api.graphql.PaginatedResult; import com.amplifyframework.api.graphql.QueryType; import com.amplifyframework.core.model.AuthRule; import com.amplifyframework.core.model.AuthStrategy; import com.amplifyframework.core.model.CustomTypeField; import com.amplifyframework.core.model.CustomTypeSchema; -import com.amplifyframework.core.model.LazyList; import com.amplifyframework.core.model.LazyModel; import com.amplifyframework.core.model.Model; import com.amplifyframework.core.model.ModelAssociation; @@ -315,7 +315,7 @@ private Set getModelFields(Class clazz, int depth for (Field field : FieldFinder.findModelFieldsIn(clazz)) { String fieldName = field.getName(); if (schema.getAssociations().containsKey(fieldName)) { - if (LazyList.class.isAssignableFrom(field.getType())) { + if (PaginatedResult.class.isAssignableFrom(field.getType())) { continue; } else if (List.class.isAssignableFrom(field.getType())) { if (depth >= 1) { diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt index 27764ce130..5ad9473af0 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt @@ -24,7 +24,6 @@ import com.amplifyframework.api.graphql.PaginatedResult import com.amplifyframework.core.Amplify import com.amplifyframework.core.Consumer import com.amplifyframework.core.NullableConsumer -import com.amplifyframework.core.model.LazyList import com.amplifyframework.core.model.LazyModel import com.amplifyframework.core.model.Model import kotlin.coroutines.resume @@ -118,72 +117,15 @@ class ApiLazyModel private constructor( } } -@InternalAmplifyApi -class ApiLazyListModel private constructor( - private val clazz: Class, - keyMap: Map, - preloadedValue: MutableList? = null -) : LazyList { - private var isPreloaded = preloadedValue != null - private var value: MutableList = preloadedValue ?: mutableListOf() - private var paginatedResult: PaginatedResult? = null - private val queryPredicate = AppSyncLazyQueryPredicate().createPredicate(clazz, keyMap) - - override fun getItems(): List { - return value - } - - override suspend fun getNextPage(): List { - if (!hasNextPage()) { - return emptyList() - } - val request = createGraphQLRequest() - paginatedResult = query(request).data - val nextPageOfItems = paginatedResult!!.items.toList() - value.addAll(nextPageOfItems) - return nextPageOfItems - } - - override fun getNextPage(onSuccess: Consumer>, onError: Consumer) { - if (!hasNextPage()) { - onSuccess.accept(emptyList()) - return - } - val onQuerySuccess = Consumer>> { - paginatedResult = it.data - val nextPageOfItems = paginatedResult!!.items.toList() - value.addAll(nextPageOfItems) - onSuccess.accept(nextPageOfItems) - } - val onApiFailure = Consumer { onError.accept(it) } - val request = createGraphQLRequest() - Amplify.API.query(request, onQuerySuccess, onApiFailure) - } - - override fun hasNextPage(): Boolean { - return !isPreloaded && paginatedResult?.hasNextResult() ?: true - } - - private fun createGraphQLRequest(): GraphQLRequest> { - return if (paginatedResult != null) { - paginatedResult!!.requestForNextResult - } else { - AppSyncGraphQLRequestFactory.buildQuery( - clazz, - queryPredicate - ) - } - } +internal class LazyListHelper { companion object { @JvmStatic fun createPreloaded( - clazz: Class, - keyMap: Map, value: List - ): ApiLazyListModel { - return ApiLazyListModel(clazz, keyMap, value.toMutableList()) + ): PaginatedResult { + return PaginatedResult(value, null) } @JvmStatic @@ -191,8 +133,12 @@ class ApiLazyListModel private constructor( clazz: Class, keyMap: Map - ): ApiLazyListModel { - return ApiLazyListModel(clazz, keyMap) + ): PaginatedResult { + val request: GraphQLRequest> = AppSyncGraphQLRequestFactory.buildQuery( + clazz, + AppSyncLazyQueryPredicate().createPredicate(clazz, keyMap) + ) + return PaginatedResult(emptyList(), request) } } } 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 index 7568bae73b..a094a92ff1 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/GsonFactory.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/GsonFactory.java @@ -16,7 +16,6 @@ package com.amplifyframework.api.aws; import com.amplifyframework.api.graphql.GsonResponseAdapters; -import com.amplifyframework.core.model.LazyList; import com.amplifyframework.core.model.LazyModel; import com.amplifyframework.core.model.Model; import com.amplifyframework.core.model.query.predicate.GsonPredicateAdapters; @@ -57,9 +56,7 @@ private static Gson create() { ModelWithMetadataAdapter.register(builder); SerializedModelAdapter.register(builder); SerializedCustomTypeAdapter.register(builder); - builder.registerTypeAdapter(Model.class, new LazyModelAdapter()); builder.registerTypeAdapter(LazyModel.class, new LazyModelAdapter()); - builder.registerTypeAdapter(LazyList.class, new LazyListAdapter()); 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 2f37cf809b..63d6a5edd9 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 @@ -174,7 +174,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/LazyListAdapter.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyListAdapter.kt deleted file mode 100644 index c6435a72f1..0000000000 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/LazyListAdapter.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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.annotations.InternalAmplifyApi -import com.amplifyframework.api.aws.ApiLazyListModel.Companion.createPreloaded -import com.amplifyframework.core.model.LazyList -import com.amplifyframework.core.model.Model -import com.amplifyframework.core.model.SchemaRegistry -import com.google.gson.JsonDeserializationContext -import com.google.gson.JsonDeserializer -import com.google.gson.JsonElement -import com.google.gson.JsonParseException -import com.google.gson.JsonSerializationContext -import com.google.gson.JsonSerializer -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type - -@InternalAmplifyApi -class LazyListAdapter : JsonDeserializer>, JsonSerializer> { - @Throws(JsonParseException::class) - override fun deserialize( - json: JsonElement, - typeOfT: Type, - context: JsonDeserializationContext - ): LazyList { - val pType = typeOfT as ParameterizedType - val type = pType.actualTypeArguments[0] as Class - val jsonObject = json.asJsonObject - val itemsJsonArray = jsonObject.getAsJsonArray("items") - - val items = itemsJsonArray.map { - context.deserialize(it.asJsonObject, type) - } - - val primaryKeysIterator: Iterator = SchemaRegistry.instance() - .getModelSchemaForModelClass(type) - .primaryIndexFields.iterator() - val predicateKeyMap: MutableMap = HashMap() - while (primaryKeysIterator.hasNext()) { - val key = primaryKeysIterator.next() - val value = jsonObject.get(key) - if (value != null) { - predicateKeyMap[key] = value - } - } - - return createPreloaded(type, predicateKeyMap, items) - } - - override fun serialize( - src: LazyList, typeOfSrc: Type, - context: JsonSerializationContext - ): JsonElement? { - return null - } -} \ No newline at end of file diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt index d1d68b2f17..8f8a3c11ea 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt @@ -43,7 +43,7 @@ class ModelDeserializer(private val responseGson: Gson) : JsonDeserializer { - fun getItems(): List - - @Throws(AmplifyException::class) - suspend fun getNextPage(): List - - fun getNextPage(onSuccess: Consumer>, onError: Consumer) - - fun hasNextPage(): Boolean -} diff --git a/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt b/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt index e5c262e961..2e70418312 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt +++ b/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt @@ -112,32 +112,6 @@ open class ModelPath( override fun getModelType(): Class = modelType as Class - protected fun field(name: String, type: Class) = FieldPath( - name = name, - parent = this, - propertyType = type - ) - - protected inline fun field(name: String) = FieldPath( - name = name, - parent = this, - propertyType = T::class.java - ) - - protected fun string(name: String) = field(name) - - protected fun integer(name: String) = field(name) - - protected fun double(name: String) = field(name) - - protected fun boolean(name: String) = field(name) - - protected inline fun > enumeration(name: String) = FieldPath( - name = name, - parent = this, - propertyType = E::class.java - ) - companion object { /** @@ -164,21 +138,6 @@ open class ModelPath( } } -/** - * Represents a scalar (i.e. data type) of a model property. - */ -class FieldPath( - private val name: String, - private val parent: PropertyPath? = null, - val propertyType: Class -) : PropertyPath { - - override fun getMetadata() = PropertyPathMetadata( - name = name, - parent = parent - ) -} - /** * Function used to define which associations are included in the selection set * in an idiomatic manner. It's a simple delegation to `listOf` with the main 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 a367e6de67..5d5ac8af51 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java +++ b/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java @@ -21,6 +21,7 @@ import androidx.core.util.ObjectsCompat; import com.amplifyframework.AmplifyException; +import com.amplifyframework.api.graphql.PaginatedResult; import com.amplifyframework.core.model.annotations.BelongsTo; import com.amplifyframework.core.model.annotations.HasMany; import com.amplifyframework.core.model.annotations.HasOne; @@ -234,7 +235,7 @@ private static ModelField createModelField(Field field) { .isEnum(Enum.class.isAssignableFrom(field.getType())) .isModel(Model.class.isAssignableFrom(field.getType())) .isLazyModel(LazyModel.class.isAssignableFrom(field.getType())) - .isLazyList(LazyList.class.isAssignableFrom(field.getType())) + .isLazyList(PaginatedResult.class.isAssignableFrom(field.getType())) .authRules(authRules) .build(); } From 30123547a5edcc3ad983cf9b8c2f65af846c7f08 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Tue, 22 Aug 2023 12:57:28 -0400 Subject: [PATCH 049/100] Pass apiName through to gson response parser to support custom apiName on subsequent lazymodel calls --- .../api/aws/SubscriptionEndpointTest.java | 4 +- .../api/aws/AWSApiPlugin.java | 6 +- .../api/aws/AWSGraphQLOperation.java | 74 +++++++++++++++++++ .../amplifyframework/api/aws/ApiLazyTypes.kt | 35 ++++++--- .../api/aws/AppSyncGraphQLOperation.java | 11 ++- .../amplifyframework/api/aws/GsonFactory.java | 3 - .../api/aws/GsonGraphQLResponseFactory.java | 42 ++++++++--- .../api/aws/LazyModelAdapter.kt | 4 +- .../aws/MultiAuthAppSyncGraphQLOperation.java | 11 ++- .../aws/MutiAuthSubscriptionOperation.java | 12 ++- .../api/aws/SubscriptionEndpoint.java | 34 ++++++++- .../api/aws/SubscriptionOperation.java | 12 ++- .../api/graphql/GraphQLOperation.java | 4 +- 13 files changed, 203 insertions(+), 49 deletions(-) create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/AWSGraphQLOperation.java 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/main/java/com/amplifyframework/api/aws/AWSApiPlugin.java b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiPlugin.java index bc4e70c5ec..9d2e1d468a 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 @@ -194,7 +194,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, @@ -625,6 +625,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 MutiAuthSubscriptionOperation.builder() + .apiName(apiName) .subscriptionEndpoint(clientDetails.getSubscriptionEndpoint()) .graphQlRequest((AppSyncGraphQLRequest) graphQLRequest) .responseFactory(gqlResponseFactory) @@ -648,6 +649,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) @@ -679,6 +681,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) @@ -691,6 +694,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/AWSGraphQLOperation.java b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSGraphQLOperation.java new file mode 100644 index 0000000000..3597d5e1f2 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSGraphQLOperation.java @@ -0,0 +1,74 @@ +/* + * 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 androidx.annotation.Nullable; + +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. + */ +@InternalAmplifyApi +public abstract class AWSGraphQLOperation extends GraphQLOperation { + private String apiName; + + /** + * Constructs a new instance of a GraphQLOperation. + * + * @param graphQLRequest A GraphQL request + * @param responseFactory an implementation of ResponseFactory + */ + public AWSGraphQLOperation( + @NonNull GraphQLRequest graphQLRequest, + @NonNull GraphQLResponse.Factory responseFactory, + @Nullable String apiName + ) { + super(graphQLRequest, responseFactory); + this.apiName = apiName; + } + + @Override + protected GraphQLResponse wrapResponse(String jsonResponse) throws ApiException { + 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 + 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(getRequest(), jsonResponse, apiName); + } catch (ClassCastException cce) { + throw new 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/ApiLazyTypes.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt index 5ad9473af0..7429cb0910 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt @@ -35,7 +35,8 @@ class ApiLazyModel private constructor( private val clazz: Class, private val keyMap: Map, private var loadedValue: Boolean = false, - private var value: M? = null + private var value: M? = null, + private val apiName: String? = null ) : LazyModel { private val queryPredicate = AppSyncLazyQueryPredicate().createPredicate(clazz, keyMap) @@ -58,7 +59,8 @@ class ApiLazyModel private constructor( AppSyncGraphQLRequestFactory.buildQuery, M>( clazz, queryPredicate - ) + ), + apiName ).data.items.iterator() value = if (resultIterator.hasNext()) { resultIterator.next() @@ -101,7 +103,6 @@ class ApiLazyModel private constructor( clazz: Class, keyMap: Map, value: M? - ): ApiLazyModel { return ApiLazyModel(clazz, keyMap, true, value) } @@ -109,10 +110,10 @@ class ApiLazyModel private constructor( @JvmStatic fun createLazy( clazz: Class, - keyMap: Map - + keyMap: Map, + apiName: String? ): ApiLazyModel { - return ApiLazyModel(clazz, keyMap) + return ApiLazyModel(clazz, keyMap, apiName = apiName) } } } @@ -147,12 +148,22 @@ internal class LazyListHelper { Duplicating the query Kotlin Facade method so we aren't pulling in Kotlin Core */ @Throws(ApiException::class) -private suspend fun query(request: GraphQLRequest): GraphQLResponse { +private suspend fun query(request: GraphQLRequest, apiName: String?): + GraphQLResponse { return suspendCoroutine { continuation -> - Amplify.API.query( - request, - { continuation.resume(it) }, - { continuation.resumeWithException(it) } - ) + if (apiName != null) { + Amplify.API.query( + apiName, + request, + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) + } else { + Amplify.API.query( + request, + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) + } } } \ No newline at end of file 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 42c00f8a90..c808309041 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); @@ -176,6 +175,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); @@ -217,6 +217,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/GsonFactory.java b/aws-api/src/main/java/com/amplifyframework/api/aws/GsonFactory.java index a094a92ff1..e846110ede 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/GsonFactory.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/GsonFactory.java @@ -16,8 +16,6 @@ package com.amplifyframework.api.aws; import com.amplifyframework.api.graphql.GsonResponseAdapters; -import com.amplifyframework.core.model.LazyModel; -import com.amplifyframework.core.model.Model; import com.amplifyframework.core.model.query.predicate.GsonPredicateAdapters; import com.amplifyframework.core.model.temporal.GsonTemporalAdapters; import com.amplifyframework.core.model.types.GsonJavaTypeAdapters; @@ -56,7 +54,6 @@ private static Gson create() { ModelWithMetadataAdapter.register(builder); SerializedModelAdapter.register(builder); SerializedCustomTypeAdapter.register(builder); - builder.registerTypeAdapter(LazyModel.class, new LazyModelAdapter()); 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 63d6a5edd9..efc9a0edc1 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 @@ -22,6 +22,7 @@ import com.amplifyframework.api.graphql.GraphQLRequest; import com.amplifyframework.api.graphql.GraphQLResponse; import com.amplifyframework.api.graphql.PaginatedResult; +import com.amplifyframework.core.model.LazyModel; import com.amplifyframework.core.model.Model; import com.amplifyframework.util.Empty; import com.amplifyframework.util.TypeMaker; @@ -54,27 +55,37 @@ final class GsonGraphQLResponseFactory implements GraphQLResponse.Factory { this.gson = gson; } - @Override - public GraphQLResponse buildResponse(GraphQLRequest request, String responseJson) - throws ApiException { - + public GraphQLResponse buildResponse( + GraphQLRequest request, + String responseJson, + 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( + LazyModel.class, + new LazyModelAdapter(apiName) + ) + .create(); Gson modelDeserializerGson = responseGson.newBuilder() .registerTypeHierarchyAdapter(Model.class, new ModelDeserializer(responseGson)) @@ -90,6 +101,15 @@ public GraphQLResponse buildResponse(GraphQLRequest request, String re } } + + // Do not use this method. Instead opt for overload with apiName + @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"; diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt index 2fe9f97422..01171e1552 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt @@ -30,7 +30,7 @@ import java.lang.reflect.ParameterizedType import java.lang.reflect.Type @InternalAmplifyApi -class LazyModelAdapter : JsonDeserializer>, +class LazyModelAdapter(val apiName: String?) : JsonDeserializer>, JsonSerializer> { @Throws(JsonParseException::class) override fun deserialize( @@ -54,7 +54,7 @@ class LazyModelAdapter : JsonDeserializer>, // fallback to create lazy } } - return createLazy(type, predicateKeyMap) + return createLazy(type, predicateKeyMap, apiName) } override fun serialize( 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/MutiAuthSubscriptionOperation.java b/aws-api/src/main/java/com/amplifyframework/api/aws/MutiAuthSubscriptionOperation.java index e9c9acee01..ba65d51abb 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/MutiAuthSubscriptionOperation.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/MutiAuthSubscriptionOperation.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 MutiAuthSubscriptionOperation extends GraphQLOperation { +final class MutiAuthSubscriptionOperation extends AWSGraphQLOperation { private static final Logger LOG = Amplify.Logging.logger(CategoryType.API, "amplify:aws-api"); private final SubscriptionEndpoint subscriptionEndpoint; @@ -55,7 +54,7 @@ final class MutiAuthSubscriptionOperation extends GraphQLOperation { private Future subscriptionFuture; private MutiAuthSubscriptionOperation(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; @@ -189,6 +188,7 @@ static final class Builder { private Consumer onSubscriptionError; private Action onSubscriptionComplete; private AuthRuleRequestDecorator requestDecorator; + private String apiName; @NonNull public Builder subscriptionEndpoint(@NonNull SubscriptionEndpoint subscriptionEndpoint) { @@ -243,6 +243,12 @@ public Builder requestDecorator(AuthRuleRequestDecorator requestDecorator) { return this; } + @NonNull + public Builder apiName(String apiName) { + this.apiName = apiName; + return this; + } + @NonNull public MutiAuthSubscriptionOperation build() { return new MutiAuthSubscriptionOperation<>(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 0abf2433b4..ab9063461c 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 @@ -75,12 +75,14 @@ final class SubscriptionEndpoint { private final OkHttpClient okHttpClient; 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<>(); @@ -88,6 +90,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); @@ -187,7 +190,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()) { @@ -360,6 +363,7 @@ static final class Subscription { private final CountDownLatch subscriptionReadyAcknowledgment; private final CountDownLatch subscriptionCompletionAcknowledgement; private boolean failed; + private String apiName; Subscription( Consumer> onNextItem, @@ -367,13 +371,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; @@ -432,9 +439,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..a6db5e4234 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 @@ -32,7 +32,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 +48,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,7 +121,7 @@ static final class Builder { private Consumer onSubscriptionError; private Action onSubscriptionComplete; private AuthorizationType authorizationType; - + private String apiName; @NonNull public Builder subscriptionEndpoint(@NonNull SubscriptionEndpoint subscriptionEndpoint) { this.subscriptionEndpoint = Objects.requireNonNull(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/core/src/main/java/com/amplifyframework/api/graphql/GraphQLOperation.java b/core/src/main/java/com/amplifyframework/api/graphql/GraphQLOperation.java index 2dab61c8c5..236a7f3cb5 100644 --- a/core/src/main/java/com/amplifyframework/api/graphql/GraphQLOperation.java +++ b/core/src/main/java/com/amplifyframework/api/graphql/GraphQLOperation.java @@ -26,7 +26,7 @@ * @param The type of data contained in the GraphQLResponse. */ public abstract class GraphQLOperation extends ApiOperation> { - private final GraphQLResponse.Factory responseFactory; + protected final GraphQLResponse.Factory responseFactory; /** * Constructs a new instance of a GraphQLOperation. @@ -48,7 +48,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) { From 28248b3b37530e20c828127a77ed39b3ab344f15 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Tue, 22 Aug 2023 14:49:52 -0400 Subject: [PATCH 050/100] lint --- .../api/aws/ApiGraphQLRequestOptions.java | 3 ++ .../api/aws/GraphQLRequestHelper.java | 38 +++++++++++++------ .../api/aws/SelectionSet.java | 16 +++++--- .../core/model/types/JavaFieldType.java | 3 -- .../api/aws/AWSGraphQLOperation.java | 9 +++-- .../amplifyframework/api/aws/ApiLazyTypes.kt | 4 +- .../amplifyframework/api/aws/GsonFactory.java | 1 + .../api/aws/LazyModelAdapter.kt | 8 ++-- .../api/aws/ModelDeserializer.kt | 5 +-- .../api/aws/ModelExtensions.kt | 4 +- .../api/aws/ModelProviderLocator.java | 1 - .../api/aws/SubscriptionOperation.java | 2 +- .../api/graphql/GraphQLOperation.java | 12 +++++- .../core/model/ModelField.java | 1 - .../core/model/ModelSchema.java | 4 +- 15 files changed, 72 insertions(+), 39 deletions(-) diff --git a/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/ApiGraphQLRequestOptions.java b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/ApiGraphQLRequestOptions.java index 9aee579827..c0e6a5a0e1 100644 --- a/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/ApiGraphQLRequestOptions.java +++ b/aws-api-appsync/src/main/java/com/amplifyframework/api/aws/ApiGraphQLRequestOptions.java @@ -31,6 +31,9 @@ public final class ApiGraphQLRequestOptions implements GraphQLRequestOptions { private int maxDepth = DEFAULT_MAX_DEPTH; + /** + * Public constructor to create ApiGraphQLRequestOptions. + */ public ApiGraphQLRequestOptions() {} ApiGraphQLRequestOptions(int maxDepth) { 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 5ff6d8d933..9ec3d7bf2b 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 @@ -237,10 +237,11 @@ private static Map extractFieldLevelData( if (association == null) { result.put(fieldName, fieldValue); } else if (association.isOwner()) { - if ( - (fieldValue == null || (modelField.isLazyModel() && underlyingFieldValue == null && identifiersIfLazyModel.isEmpty())) - && MutationType.CREATE.equals(type) - ) { + if ((fieldValue == null || + (modelField.isLazyModel() && + underlyingFieldValue == null && + identifiersIfLazyModel.isEmpty())) && + 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) { @@ -270,9 +271,12 @@ private static void insertForeignKeyValues( } } else if ((modelField.isModel() || modelField.isLazyModel()) && underlyingFieldValue instanceof Model) { if (((Model) underlyingFieldValue).resolveIdentifier() instanceof ModelIdentifier) { - final ModelIdentifier primaryKey = (ModelIdentifier) ((Model) underlyingFieldValue).resolveIdentifier(); - ListIterator targetNames = Arrays.asList(association.getTargetNames()).listIterator(); - Iterator sortedKeys = primaryKey.sortedKeys().listIterator(); + 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()); @@ -285,17 +289,24 @@ private static void insertForeignKeyValues( 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) underlyingFieldValue).resolveIdentifier().toString()); + result.put( + association.getTargetNames()[0], + ((Model) underlyingFieldValue).resolveIdentifier().toString() + ); } } else { - result.put(association.getTargetNames()[0], ((Model) underlyingFieldValue).resolveIdentifier().toString()); + result.put( + association.getTargetNames()[0], + ((Model) underlyingFieldValue).resolveIdentifier().toString() + ); } } else if (modelField.isLazyModel() && fieldValue instanceof LazyModel) { Map identifiers = ((LazyModel) fieldValue).getIdentifier(); @@ -327,7 +338,12 @@ private static Object extractAssociateId(ModelField modelField, Object fieldValu } } - private static Object extractFieldValue(String fieldName, Model instance, ModelSchema schema, Boolean extractLazyValue) + private static Object extractFieldValue( + String fieldName, + Model instance, + ModelSchema schema, + Boolean extractLazyValue + ) throws AmplifyException { if (instance instanceof SerializedModel) { SerializedModel serializedModel = (SerializedModel) instance; 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 30e588b3a2..9e5d346fd2 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 @@ -16,7 +16,6 @@ package com.amplifyframework.api.aws; import android.text.TextUtils; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.ObjectsCompat; @@ -287,12 +286,17 @@ private Set wrapPagination(Set nodes) { * * @param clazz Class from which to build selection set * @param depth Number of children deep to explore - * @param primaryKeyOnly - * @return Selection Set + * @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, Boolean primaryKeyOnly) + private Set getModelFields( + Class clazz, + int depth, + Operation operation, + Boolean primaryKeyOnly + ) throws AmplifyException { if (depth < 0) { return new HashSet<>(); @@ -303,7 +307,9 @@ private Set getModelFields(Class clazz, int depth if ( (depth == 0 - && (LeafSerializationBehavior.JUST_ID.equals(requestOptions.leafSerializationBehavior()) || primaryKeyOnly) + && (LeafSerializationBehavior.JUST_ID.equals( + requestOptions.leafSerializationBehavior() + ) || primaryKeyOnly) && operation != QueryType.SYNC) ) { for (String s : schema.getPrimaryIndexFields()) { diff --git a/aws-api-appsync/src/main/java/com/amplifyframework/core/model/types/JavaFieldType.java b/aws-api-appsync/src/main/java/com/amplifyframework/core/model/types/JavaFieldType.java index b2ba46d6b8..72bf36c7d1 100644 --- a/aws-api-appsync/src/main/java/com/amplifyframework/core/model/types/JavaFieldType.java +++ b/aws-api-appsync/src/main/java/com/amplifyframework/core/model/types/JavaFieldType.java @@ -18,7 +18,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.amplifyframework.core.model.LazyModel; import com.amplifyframework.core.model.Model; import com.amplifyframework.core.model.temporal.Temporal; @@ -87,8 +86,6 @@ public enum JavaFieldType { */ MODEL(Model.class), - LAZY_MODEL(LazyModel.class), - /** * Represents any custom type (objects that are not models). */ diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/AWSGraphQLOperation.java b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSGraphQLOperation.java index 3597d5e1f2..31ee46f10f 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AWSGraphQLOperation.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSGraphQLOperation.java @@ -26,7 +26,7 @@ import com.amplifyframework.api.graphql.GraphQLResponse; /** - * A Base AWS GraphQLOperation that also takes an apiName to allow LazyModel support + * A Base AWS GraphQLOperation that also takes an apiName to allow LazyModel support. * @param The type of data contained in the GraphQLResponse. */ @InternalAmplifyApi @@ -38,6 +38,7 @@ public abstract class AWSGraphQLOperation extends GraphQLOperation { * * @param graphQLRequest A GraphQL request * @param responseFactory an implementation of ResponseFactory + * @param apiName to use */ public AWSGraphQLOperation( @NonNull GraphQLRequest graphQLRequest, @@ -49,7 +50,7 @@ public AWSGraphQLOperation( } @Override - protected GraphQLResponse wrapResponse(String jsonResponse) throws ApiException { + protected final GraphQLResponse wrapResponse(String jsonResponse) throws ApiException { return buildResponse(jsonResponse); } @@ -57,14 +58,14 @@ protected GraphQLResponse wrapResponse(String jsonResponse) throws ApiExcepti // 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 private GraphQLResponse buildResponse(String jsonResponse) throws ApiException { - if (!(responseFactory instanceof GsonGraphQLResponseFactory)) { + if (!(getResponseFactory() 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) + return ((GsonGraphQLResponseFactory) getResponseFactory()) .buildResponse(getRequest(), jsonResponse, apiName); } catch (ClassCastException cce) { throw new ApiException("Amplify encountered an error while deserializing an object", diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt index 7429cb0910..6ac8af046f 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt @@ -149,7 +149,7 @@ internal class LazyListHelper { */ @Throws(ApiException::class) private suspend fun query(request: GraphQLRequest, apiName: String?): - GraphQLResponse { + GraphQLResponse { return suspendCoroutine { continuation -> if (apiName != null) { Amplify.API.query( @@ -166,4 +166,4 @@ private suspend fun query(request: GraphQLRequest, apiName: String?): ) } } -} \ No newline at end of file +} 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 index e846110ede..3bd8e9744f 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/GsonFactory.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/GsonFactory.java @@ -22,6 +22,7 @@ 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; diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt index 01171e1552..6d3c2fabd1 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt @@ -30,7 +30,8 @@ import java.lang.reflect.ParameterizedType import java.lang.reflect.Type @InternalAmplifyApi -class LazyModelAdapter(val apiName: String?) : JsonDeserializer>, +class LazyModelAdapter(val apiName: String?) : + JsonDeserializer>, JsonSerializer> { @Throws(JsonParseException::class) override fun deserialize( @@ -58,9 +59,10 @@ class LazyModelAdapter(val apiName: String?) : JsonDeserializer, typeOfSrc: Type, + src: LazyModel, + typeOfSrc: Type, context: JsonSerializationContext ): JsonElement? { return null } -} \ No newline at end of file +} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt index 8f8a3c11ea..2bbac7631f 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt @@ -24,12 +24,11 @@ class ModelDeserializer(private val responseGson: Gson) : JsonDeserializer val fieldToUpdate = parent.javaClass.getDeclaredField(fieldMap.key) - fieldToUpdate.isAccessible= true + fieldToUpdate.isAccessible = true if (fieldToUpdate.get(parent) == null) { val lazyField = fieldMap.value val lazyFieldModelSchema = SchemaRegistry.instance().getModelSchemaForModelClass(lazyField.targetType) - val lazyFieldTargetNames = lazyFieldModelSchema .associations .entries @@ -51,4 +50,4 @@ class ModelDeserializer(private val responseGson: Gson) : JsonDeserializer { - return when(val identifier = resolveIdentifier()) { + return when (val identifier = resolveIdentifier()) { is ModelIdentifier<*> -> { listOf(identifier.key()) + identifier.sortedKeys() } else -> listOf(identifier.toString()) } -} \ No newline at end of file +} 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 index d82370a224..ea900266e6 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelProviderLocator.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelProviderLocator.java @@ -19,7 +19,6 @@ import com.amplifyframework.api.ApiException; import com.amplifyframework.core.model.ModelProvider; -import com.amplifyframework.datastore.DataStoreException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; 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 a6db5e4234..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; @@ -122,6 +121,7 @@ static final class Builder { private Action onSubscriptionComplete; private AuthorizationType authorizationType; private String apiName; + @NonNull public Builder subscriptionEndpoint(@NonNull SubscriptionEndpoint subscriptionEndpoint) { this.subscriptionEndpoint = Objects.requireNonNull(subscriptionEndpoint); 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 236a7f3cb5..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,7 +26,9 @@ * @param The type of data contained in the GraphQLResponse. */ public abstract class GraphQLOperation extends ApiOperation> { - protected final GraphQLResponse.Factory responseFactory; + + // responseFactory used to parse responses + private final GraphQLResponse.Factory responseFactory; /** * Constructs a new instance of a GraphQLOperation. @@ -56,4 +58,12 @@ protected GraphQLResponse wrapResponse(String jsonResponse) throws ApiExcepti 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/model/ModelField.java b/core/src/main/java/com/amplifyframework/core/model/ModelField.java index 1c7ec36df9..6450e5f2af 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelField.java +++ b/core/src/main/java/com/amplifyframework/core/model/ModelField.java @@ -180,7 +180,6 @@ public boolean isLazyList() { return isLazyList; } - /** * Returns true if the field's target type is CustomType. * 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 5d5ac8af51..278ba78dec 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java +++ b/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java @@ -214,8 +214,8 @@ private static ModelField createModelField(Field field) { final String fieldName = field.getName(); final Class fieldType; if (field.getType() == LazyModel.class && field.getGenericType() - instanceof ParameterizedType){ - ParameterizedType pType = (ParameterizedType)field.getGenericType() ; + instanceof ParameterizedType) { + ParameterizedType pType = (ParameterizedType) field.getGenericType(); fieldType = (Class) pType.getActualTypeArguments()[0]; } else { fieldType = field.getType(); From 9591ab3a16438f062cde09118af428522f275e02 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Tue, 22 Aug 2023 17:02:25 -0400 Subject: [PATCH 051/100] Fix tests --- .../serde-for-blog-in-serialized-model.json | 4 ++++ .../serde-for-meeting-in-serialized-model.json | 6 ++++++ ...with-nested-custom-type-se-deserialization.json | 3 +++ .../amplifyframework/api/aws/ModelDeserializer.kt | 3 ++- .../amplifyframework/api/aws/AWSApiPluginTest.java | 6 +++++- .../aws/AppSyncGraphQLRequestAndResponseCPKTest.kt | 9 +++++++++ .../api/aws/GsonGraphQLResponseFactoryTest.java | 14 +++++++++++++- 7 files changed, 42 insertions(+), 3 deletions(-) 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 b84423fd0b..e1ec6eb3db 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 @@ -17,6 +17,7 @@ "isEnum": false, "isModel": false, "isLazyModel": false, + "isLazyList": false, "authRules": [] }, "name": { @@ -30,6 +31,7 @@ "isEnum": false, "isModel": false, "isLazyModel": false, + "isLazyList": false, "authRules": [] }, "owner": { @@ -43,6 +45,7 @@ "isEnum": false, "isModel": true, "isLazyModel": false, + "isLazyList": false, "authRules": [] }, "posts": { @@ -56,6 +59,7 @@ "isEnum": false, "isModel": false, "isLazyModel": false, + "isLazyList": false, "authRules": [] } }, 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 6409ed33c0..5e820b2c63 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,7 @@ "isEnum": false, "isModel": false, "isLazyModel": false, + "isLazyList": false, "authRules": [] }, "dateTime": { @@ -30,6 +31,7 @@ "isEnum": false, "isModel": false, "isLazyModel": false, + "isLazyList": false, "authRules": [] }, "id": { @@ -43,6 +45,7 @@ "isEnum": false, "isModel": false, "isLazyModel": false, + "isLazyList": false, "authRules": [] }, "name": { @@ -56,6 +59,7 @@ "isEnum": false, "isModel": false, "isLazyModel": false, + "isLazyList": false, "authRules": [] }, "time": { @@ -69,6 +73,7 @@ "isEnum": false, "isModel": false, "isLazyModel": false, + "isLazyList": false, "authRules": [] }, "timestamp": { @@ -82,6 +87,7 @@ "isEnum": false, "isModel": false, "isLazyModel": false, + "isLazyList": 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 8ad2fc834a..53d0c6e869 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 @@ -78,6 +78,7 @@ "isReadOnly": false, "isModel": false, "isLazyModel": false, + "isLazyList": false, "authRules": [], "name": "contact", "isEnum": false, @@ -91,6 +92,7 @@ "isReadOnly": false, "isModel": false, "isLazyModel": false, + "isLazyList": false, "authRules": [], "name": "fullName", "isEnum": false, @@ -104,6 +106,7 @@ "isReadOnly": false, "isModel": false, "isLazyModel": false, + "isLazyList": false, "authRules": [], "name": "id", "isEnum": false, diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt index 2bbac7631f..90cb26a2b6 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt @@ -1,6 +1,7 @@ package com.amplifyframework.api.aws import com.amplifyframework.core.model.Model +import com.amplifyframework.core.model.ModelSchema import com.amplifyframework.core.model.SchemaRegistry import com.google.gson.Gson import com.google.gson.JsonDeserializationContext @@ -20,7 +21,7 @@ class ModelDeserializer(private val responseGson: Gson) : JsonDeserializer(json, typeOfT) val parentType = (typeOfT as Class<*>).simpleName - val parentModelSchema = SchemaRegistry.instance().getModelSchemaForModelClass(parentType) + val parentModelSchema = ModelSchema.fromModelClass(parent.javaClass) parentModelSchema.fields.filter { it.value.isLazyList }.map { fieldMap -> val fieldToUpdate = parent.javaClass.getDeclaredField(fieldMap.key) 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 0a70335111..dc9b48d62c 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 @@ -31,8 +31,10 @@ import com.amplifyframework.api.graphql.model.ModelPagination; import com.amplifyframework.api.graphql.model.ModelQuery; import com.amplifyframework.core.Consumer; +import com.amplifyframework.core.model.SchemaRegistry; import com.amplifyframework.hub.HubChannel; import com.amplifyframework.hub.HubEvent; +import com.amplifyframework.testmodels.commentsblog.AmplifyModelProvider; import com.amplifyframework.testmodels.commentsblog.BlogOwner; import com.amplifyframework.testutils.Await; import com.amplifyframework.testutils.HubAccumulator; @@ -85,7 +87,8 @@ public final class AWSApiPluginTest { * @throws JSONException On failure to arrange configuration JSON */ @Before - public void setup() throws ApiException, IOException, JSONException { + public void setup() throws AmplifyException, IOException, JSONException { + SchemaRegistry.instance().register(AmplifyModelProvider.getInstance().models()); webServer = new MockWebServer(); webServer.start(8080); baseUrl = webServer.url("/"); @@ -136,6 +139,7 @@ public String getUsername() { @After public void cleanup() throws IOException { webServer.shutdown(); + SchemaRegistry.instance().clear(); } /** diff --git a/aws-api/src/test/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestAndResponseCPKTest.kt b/aws-api/src/test/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestAndResponseCPKTest.kt index fe994ff8b7..d5fb0e1387 100644 --- a/aws-api/src/test/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestAndResponseCPKTest.kt +++ b/aws-api/src/test/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestAndResponseCPKTest.kt @@ -19,8 +19,10 @@ import com.amplifyframework.api.graphql.GraphQLResponse import com.amplifyframework.api.graphql.MutationType import com.amplifyframework.api.graphql.model.ModelMutation import com.amplifyframework.api.graphql.model.ModelQuery +import com.amplifyframework.core.model.SchemaRegistry import com.amplifyframework.core.model.query.predicate.QueryPredicates import com.amplifyframework.core.model.temporal.Temporal +import com.amplifyframework.testmodels.cpk.AmplifyModelProvider import com.amplifyframework.testmodels.cpk.Blog import com.amplifyframework.testmodels.cpk.Blog.BlogIdentifier import com.amplifyframework.testmodels.cpk.Comment @@ -29,6 +31,7 @@ import com.amplifyframework.testmodels.cpk.Post import com.amplifyframework.testmodels.cpk.Post.PostIdentifier import com.amplifyframework.testutils.Resources import com.amplifyframework.util.GsonFactory +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull @@ -49,10 +52,16 @@ class AppSyncGraphQLRequestAndResponseCPKTest { @Before fun setup() { + SchemaRegistry.instance().register(AmplifyModelProvider.getInstance().models()) val gson = GsonFactory.instance() responseFactory = GsonGraphQLResponseFactory(gson) } + @After + fun tearDown() { + SchemaRegistry.instance().clear() + } + @Test fun create_with_cpk() { // GIVEN diff --git a/aws-api/src/test/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactoryTest.java b/aws-api/src/test/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactoryTest.java index f4eec9df72..b3cc36cd7f 100644 --- a/aws-api/src/test/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactoryTest.java +++ b/aws-api/src/test/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactoryTest.java @@ -23,6 +23,8 @@ import com.amplifyframework.api.graphql.GraphQLResponse; import com.amplifyframework.api.graphql.PaginatedResult; import com.amplifyframework.api.graphql.QueryType; +import com.amplifyframework.core.model.ModelSchema; +import com.amplifyframework.core.model.SchemaRegistry; import com.amplifyframework.core.model.temporal.Temporal; import com.amplifyframework.testmodels.meeting.Meeting; import com.amplifyframework.testutils.Resources; @@ -32,6 +34,7 @@ import com.google.gson.Gson; import org.json.JSONException; import org.json.JSONObject; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -62,11 +65,20 @@ public final class GsonGraphQLResponseFactoryTest { * Set up the object under test, a GsonGraphQLResponseFactory. */ @Before - public void setup() { + public void setup() throws AmplifyException { + SchemaRegistry registry = SchemaRegistry.instance(); + registry.register(Todo.class.getSimpleName(), ModelSchema.fromModelClass(Todo.class)); + registry.register(Meeting.class.getSimpleName(), ModelSchema.fromModelClass(Meeting.class)); + Gson gson = GsonFactory.instance(); responseFactory = new GsonGraphQLResponseFactory(gson); } + @After + public void tearDown() { + SchemaRegistry.instance().clear(); + } + /** * Validates that response with null content does not break * the response object builder. Null data and/or error will From 944068ff7205be4563e1b526dd652db71959cfa8 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 23 Aug 2023 12:27:51 -0400 Subject: [PATCH 052/100] Rename .java to .kt --- .../api/aws/{AWSGraphQLOperation.java => AWSGraphQLOperation.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename aws-api/src/main/java/com/amplifyframework/api/aws/{AWSGraphQLOperation.java => AWSGraphQLOperation.kt} (100%) diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/AWSGraphQLOperation.java b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSGraphQLOperation.kt similarity index 100% rename from aws-api/src/main/java/com/amplifyframework/api/aws/AWSGraphQLOperation.java rename to aws-api/src/main/java/com/amplifyframework/api/aws/AWSGraphQLOperation.kt From 0d83f264c795a25135dcde42cd5e3736016b6796 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 23 Aug 2023 12:27:52 -0400 Subject: [PATCH 053/100] cleanup --- .../api/aws/AWSGraphQLOperation.kt | 79 ++++++++----------- .../amplifyframework/api/aws/ApiLazyTypes.kt | 31 ++++---- .../api/aws/LazyModelAdapter.kt | 4 +- .../api/aws/ModelDeserializer.kt | 11 ++- .../api/aws/ModelExtensions.kt | 12 --- .../api/aws/AWSApiPluginTest.java | 2 +- .../aws/GsonGraphQLResponseFactoryTest.java | 4 + .../core/model/InMemoryLazyModel.kt | 1 + .../core/model/ModelException.kt | 6 +- .../core/model/ModelField.java | 18 ++--- 10 files changed, 78 insertions(+), 90 deletions(-) delete mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/ModelExtensions.kt 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 index 31ee46f10f..cca6fd46ac 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AWSGraphQLOperation.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSGraphQLOperation.kt @@ -12,64 +12,51 @@ * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ +package com.amplifyframework.api.aws -package com.amplifyframework.api.aws; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -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; +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 -public abstract class AWSGraphQLOperation extends GraphQLOperation { - private String apiName; - - /** - * Constructs a new instance of a GraphQLOperation. - * - * @param graphQLRequest A GraphQL request - * @param responseFactory an implementation of ResponseFactory - * @param apiName to use - */ - public AWSGraphQLOperation( - @NonNull GraphQLRequest graphQLRequest, - @NonNull GraphQLResponse.Factory responseFactory, - @Nullable String apiName - ) { - super(graphQLRequest, responseFactory); - this.apiName = apiName; - } - - @Override - protected final GraphQLResponse wrapResponse(String jsonResponse) throws ApiException { - return buildResponse(jsonResponse); +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 - private GraphQLResponse buildResponse(String jsonResponse) throws ApiException { - if (!(getResponseFactory() 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) getResponseFactory()) - .buildResponse(getRequest(), jsonResponse, apiName); - } catch (ClassCastException cce) { - throw new ApiException("Amplify encountered an error while deserializing an object", - AmplifyException.TODO_RECOVERY_SUGGESTION); + @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/ApiLazyTypes.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt index 6ac8af046f..cb8417c38d 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt @@ -90,15 +90,23 @@ class ApiLazyModel private constructor( onSuccess.accept(value) } val onApiFailure = Consumer { onError.accept(it) } - Amplify.API.query( - AppSyncGraphQLRequestFactory.buildQuery(clazz, queryPredicate), - onQuerySuccess, - onApiFailure - ) + if (apiName != null) { + Amplify.API.query( + apiName, + AppSyncGraphQLRequestFactory.buildQuery(clazz, queryPredicate), + onQuerySuccess, + onApiFailure + ) + } else { + Amplify.API.query( + AppSyncGraphQLRequestFactory.buildQuery(clazz, queryPredicate), + onQuerySuccess, + onApiFailure + ) + } } - companion object { - @JvmStatic + internal companion object { fun createPreloaded( clazz: Class, keyMap: Map, @@ -107,7 +115,6 @@ class ApiLazyModel private constructor( return ApiLazyModel(clazz, keyMap, true, value) } - @JvmStatic fun createLazy( clazz: Class, keyMap: Map, @@ -121,14 +128,6 @@ class ApiLazyModel private constructor( internal class LazyListHelper { companion object { - @JvmStatic - fun createPreloaded( - value: List - - ): PaginatedResult { - return PaginatedResult(value, null) - } - @JvmStatic fun createLazy( clazz: Class, diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt index 6d3c2fabd1..7b76c6f704 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt @@ -14,7 +14,6 @@ */ package com.amplifyframework.api.aws -import com.amplifyframework.annotations.InternalAmplifyApi import com.amplifyframework.api.aws.ApiLazyModel.Companion.createLazy import com.amplifyframework.api.aws.ApiLazyModel.Companion.createPreloaded import com.amplifyframework.core.model.LazyModel @@ -29,8 +28,7 @@ import com.google.gson.JsonSerializer import java.lang.reflect.ParameterizedType import java.lang.reflect.Type -@InternalAmplifyApi -class LazyModelAdapter(val apiName: String?) : +internal class LazyModelAdapter(val apiName: String?) : JsonDeserializer>, JsonSerializer> { @Throws(JsonParseException::class) diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt index 90cb26a2b6..6a56262146 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt @@ -1,12 +1,14 @@ package com.amplifyframework.api.aws import com.amplifyframework.core.model.Model +import com.amplifyframework.core.model.ModelIdentifier import com.amplifyframework.core.model.ModelSchema import com.amplifyframework.core.model.SchemaRegistry import com.google.gson.Gson import com.google.gson.JsonDeserializationContext import com.google.gson.JsonDeserializer import com.google.gson.JsonElement +import java.io.Serializable import java.lang.reflect.Type /** @@ -16,7 +18,7 @@ import java.lang.reflect.Type * @param responseGson is a Gson object that does not have the model deserializer. Otherwise context.fromJson would * cause a recursion issue. */ -class ModelDeserializer(private val responseGson: Gson) : JsonDeserializer { +internal class ModelDeserializer(private val responseGson: Gson) : JsonDeserializer { override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Model { val parent = responseGson.fromJson(json, typeOfT) @@ -52,3 +54,10 @@ class ModelDeserializer(private val responseGson: Gson) : JsonDeserializer { + 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/ModelExtensions.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelExtensions.kt deleted file mode 100644 index 551b41a679..0000000000 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelExtensions.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.amplifyframework.api.aws - -import com.amplifyframework.core.model.Model -import com.amplifyframework.core.model.ModelIdentifier -import java.io.Serializable - -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/test/java/com/amplifyframework/api/aws/AWSApiPluginTest.java b/aws-api/src/test/java/com/amplifyframework/api/aws/AWSApiPluginTest.java index bc4d2ec3b6..6881a9a309 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 @@ -83,7 +83,7 @@ public final class AWSApiPluginTest { /** * Sets up the test. - * @throws ApiException On failure to configure plugin + * @throws AmplifyException on SchemaRegistry failure * @throws IOException On failure to start web server * @throws JSONException On failure to arrange configuration JSON */ diff --git a/aws-api/src/test/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactoryTest.java b/aws-api/src/test/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactoryTest.java index b3cc36cd7f..4390dc18da 100644 --- a/aws-api/src/test/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactoryTest.java +++ b/aws-api/src/test/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactoryTest.java @@ -63,6 +63,7 @@ public final class GsonGraphQLResponseFactoryTest { /** * Set up the object under test, a GsonGraphQLResponseFactory. + * @throws AmplifyException on SchemaRegistry failure */ @Before public void setup() throws AmplifyException { @@ -74,6 +75,9 @@ public void setup() throws AmplifyException { responseFactory = new GsonGraphQLResponseFactory(gson); } + /** + * Clear schema registry. + */ @After public void tearDown() { SchemaRegistry.instance().clear(); diff --git a/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt b/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt index 0857f4ccdb..b1c3415251 100644 --- a/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt +++ b/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt @@ -30,6 +30,7 @@ class InMemoryLazyModel(model: M? = null) : LazyModel { } override fun getIdentifier(): Map { + // TODO: Should fill out map to be safe for future implementations return 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 index 130d53803d..71963f64e2 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelException.kt +++ b/core/src/main/java/com/amplifyframework/core/model/ModelException.kt @@ -16,14 +16,16 @@ package com.amplifyframework.core.model import com.amplifyframework.AmplifyException +import com.amplifyframework.annotations.InternalAmplifyApi -sealed class ModelException( +@InternalAmplifyApi +internal sealed class ModelException( message: String, recoverySuggestion: String, cause: Exception? = null ) : AmplifyException(message, cause, recoverySuggestion) { - class PropertyPathNotFound( + internal class PropertyPathNotFound( val modelName: String, cause: Exception? = null ) : ModelException( 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 6450e5f2af..5d4158fd66 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelField.java +++ b/core/src/main/java/com/amplifyframework/core/model/ModelField.java @@ -163,18 +163,18 @@ public boolean isModel() { } /** - * Returns true if the field's target type is Model. + * Returns true if the field's target type is a lazy Model. * - * @return True if the field's target type is Model. + * @return True if the field's target type is a lazy Model. */ public boolean isLazyModel() { return isLazyModel; } /** - * Returns true if the field's target type is Model. + * Returns true if the field's target type is a lazy list of Models. * - * @return True if the field's target type is Model. + * @return True if the field's target type is a lazy list of Models. */ public boolean isLazyList() { return isLazyList; @@ -298,7 +298,7 @@ public static class ModelFieldBuilder { // True if the field's target type is LazyModel. private boolean isLazyModel = false; - // True if the field's target type is LazyModel. + // True if the field's target type is a lazy list type (PaginatedResult). private boolean isLazyList = false; // True if the field's target type is CustomType. @@ -398,8 +398,8 @@ public ModelFieldBuilder isModel(boolean isModel) { } /** - * Sets a flag indicating whether or not the field's target type is a Model. - * @param isLazyModel flag indicating if the field is a model + * Sets a flag indicating whether or not the field's target type is a LazyModel. + * @param isLazyModel flag indicating if the field is a lazy model * @return the builder object */ public ModelFieldBuilder isLazyModel(boolean isLazyModel) { @@ -408,8 +408,8 @@ public ModelFieldBuilder isLazyModel(boolean isLazyModel) { } /** - * Sets a flag indicating whether or not the field's type is a LazyList. - * @param isLazyList flag indicating if the field is a LazyList + * Sets a flag indicating whether or not the field's type is a a lazy list type. + * @param isLazyList flag indicating if the field is a lazy list type (PaginatedResult) * @return the builder object */ public ModelFieldBuilder isLazyList(boolean isLazyList) { From ed390936176bb0253c5212e914e7fc39b038ea75 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Wed, 23 Aug 2023 15:19:31 -0400 Subject: [PATCH 054/100] cleanup --- .../java/com/amplifyframework/core/model/InMemoryLazyModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt b/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt index b1c3415251..0857f4ccdb 100644 --- a/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt +++ b/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt @@ -30,7 +30,6 @@ class InMemoryLazyModel(model: M? = null) : LazyModel { } override fun getIdentifier(): Map { - // TODO: Should fill out map to be safe for future implementations return emptyMap() } From c972f82285832f818d53a6ade7dbc3b055de2c47 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Mon, 28 Aug 2023 09:15:42 -0400 Subject: [PATCH 055/100] naming changes --- .../main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt | 6 ++++-- .../com/amplifyframework/core/model/InMemoryLazyModel.kt | 6 ++++-- .../main/java/com/amplifyframework/core/model/LazyModel.kt | 6 ++++-- .../core/model/annotations/ModelConfig.java | 6 ++++++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt index cb8417c38d..13aa5929e9 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt @@ -49,7 +49,7 @@ class ApiLazyModel private constructor( return keyMap } - override suspend fun getModel(): M? { + override suspend fun fetchModel(): M? { if (loadedValue) { return value } @@ -74,7 +74,7 @@ class ApiLazyModel private constructor( return value } - override fun getModel(onSuccess: NullableConsumer, onError: Consumer) { + override fun fetchModel(onSuccess: NullableConsumer, onError: Consumer) { if (loadedValue) { onSuccess.accept(value) return @@ -106,6 +106,8 @@ class ApiLazyModel private constructor( } } + override fun isLoaded() = loadedValue + internal companion object { fun createPreloaded( clazz: Class, diff --git a/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt b/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt index 0857f4ccdb..05875e515a 100644 --- a/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt +++ b/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt @@ -33,11 +33,13 @@ class InMemoryLazyModel(model: M? = null) : LazyModel { return emptyMap() } - override suspend fun getModel(): M? { + override suspend fun fetchModel(): M? { return value } - override fun getModel(onSuccess: NullableConsumer, onError: Consumer) { + override fun fetchModel(onSuccess: NullableConsumer, onError: Consumer) { onSuccess.accept(value) } + + override fun isLoaded() = true } diff --git a/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt b/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt index 1ab5031bba..dfd32fa778 100644 --- a/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt +++ b/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt @@ -27,7 +27,9 @@ interface LazyModel { fun getIdentifier(): Map @Throws(AmplifyException::class) - suspend fun getModel(): M? + suspend fun fetchModel(): M? - fun getModel(onSuccess: NullableConsumer, onError: Consumer) + fun fetchModel(onSuccess: NullableConsumer, onError: Consumer) + + fun isLoaded(): Boolean } 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; } From 5c19a55c98c386c60c732df3a8cde45ffe0947c3 Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Fri, 1 Sep 2023 11:00:54 -0400 Subject: [PATCH 056/100] lazy list loading updates --- .../api/aws/SelectionSet.java | 6 +- .../api/graphql/GsonResponseAdapters.java | 4 + .../amplifyframework/api/aws/ApiLazyTypes.kt | 33 +---- .../amplifyframework/api/aws/ApiModelList.kt | 107 ++++++++++++++ .../api/aws/AppSyncGraphQLRequestFactory.kt | 16 ++- .../api/aws/GsonGraphQLResponseFactory.java | 17 ++- .../api/aws/LazyModelAdapter.kt | 66 --------- .../api/aws/ModelDeserializer.kt | 9 +- .../api/aws/ModelListDeserializers.kt | 131 ++++++++++++++++++ .../core/model/InMemoryLazyModel.kt | 12 +- .../amplifyframework/core/model/LazyModel.kt | 21 ++- .../amplifyframework/core/model/ModelList.kt | 82 +++++++++++ .../core/model/ModelSchema.java | 2 +- 13 files changed, 390 insertions(+), 116 deletions(-) create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelList.kt delete mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt create mode 100644 core/src/main/java/com/amplifyframework/core/model/ModelList.kt 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 9e5d346fd2..683f8c3e65 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 @@ -22,7 +22,6 @@ import com.amplifyframework.AmplifyException; import com.amplifyframework.api.graphql.Operation; -import com.amplifyframework.api.graphql.PaginatedResult; import com.amplifyframework.api.graphql.QueryType; import com.amplifyframework.core.model.AuthRule; import com.amplifyframework.core.model.AuthStrategy; @@ -32,6 +31,7 @@ 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.ModelSchema; import com.amplifyframework.core.model.PropertyContainerPath; import com.amplifyframework.core.model.SchemaRegistry; @@ -95,7 +95,7 @@ public SelectionSet(String value, @NonNull Set nodes) { * @return node value */ @Nullable - public String getValue() { + protected String getValue() { return value; } @@ -321,7 +321,7 @@ private Set getModelFields( for (Field field : FieldFinder.findModelFieldsIn(clazz)) { String fieldName = field.getName(); if (schema.getAssociations().containsKey(fieldName)) { - if (PaginatedResult.class.isAssignableFrom(field.getType())) { + if (ModelList.class.isAssignableFrom(field.getType())) { continue; } else if (List.class.isAssignableFrom(field.getType())) { if (depth >= 1) { 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/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt index 13aa5929e9..9663413ce5 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt @@ -34,16 +34,15 @@ import kotlin.coroutines.suspendCoroutine class ApiLazyModel private constructor( private val clazz: Class, private val keyMap: Map, + private var model: M? = null, private var loadedValue: Boolean = false, - private var value: M? = null, private val apiName: String? = null ) : LazyModel { private val queryPredicate = AppSyncLazyQueryPredicate().createPredicate(clazz, keyMap) - override fun getValue(): M? { - return value - } + override val value: M? + get() = model override fun getIdentifier(): Map { return keyMap @@ -62,7 +61,7 @@ class ApiLazyModel private constructor( ), apiName ).data.items.iterator() - value = if (resultIterator.hasNext()) { + model = if (resultIterator.hasNext()) { resultIterator.next() } else { null @@ -81,7 +80,7 @@ class ApiLazyModel private constructor( } val onQuerySuccess = Consumer>> { val resultIterator = it.data.items.iterator() - value = if (resultIterator.hasNext()) { + model = if (resultIterator.hasNext()) { resultIterator.next() } else { null @@ -106,15 +105,13 @@ class ApiLazyModel private constructor( } } - override fun isLoaded() = loadedValue - internal companion object { fun createPreloaded( clazz: Class, keyMap: Map, value: M? ): ApiLazyModel { - return ApiLazyModel(clazz, keyMap, true, value) + return ApiLazyModel(clazz, keyMap, value, true) } fun createLazy( @@ -127,24 +124,6 @@ class ApiLazyModel private constructor( } } -internal class LazyListHelper { - - companion object { - @JvmStatic - fun createLazy( - clazz: Class, - keyMap: Map - - ): PaginatedResult { - val request: GraphQLRequest> = AppSyncGraphQLRequestFactory.buildQuery( - clazz, - AppSyncLazyQueryPredicate().createPredicate(clazz, keyMap) - ) - return PaginatedResult(emptyList(), request) - } - } -} - /* Duplicating the query Kotlin Facade method so we aren't pulling in Kotlin Core */ diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelList.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelList.kt new file mode 100644 index 0000000000..1f9d18f8f2 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelList.kt @@ -0,0 +1,107 @@ +package com.amplifyframework.api.aws + +import com.amplifyframework.AmplifyException +import com.amplifyframework.annotations.InternalAmplifyApi +import com.amplifyframework.api.ApiException +import com.amplifyframework.api.graphql.GraphQLRequest +import com.amplifyframework.api.graphql.GraphQLResponse +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 kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +@InternalAmplifyApi +class ApiLoadedModelList( + override val items: List +) : LoadedModelList + +class ApiModelPage( + override val items: List, + override val nextToken: ApiPaginationToken? +): ModelPage +class ApiPaginationToken(val nextToken: String) : PaginationToken + +class ApiLazyModelList constructor( + private val clazz: Class, + keyMap: Map, + private val apiName: String? +) : LazyModelList { + + private val queryPredicate = AppSyncLazyQueryPredicate().createPredicate(clazz, keyMap) + + override suspend fun loadPage(paginationToken: PaginationToken?): ModelPage { + val response = query(apiName, createRequest(paginationToken)) + return response.data + } + + override fun loadPage(onSuccess: Consumer>, onError: Consumer) { + query(apiName, createRequest(), onSuccess, onError) + } + + override fun loadPage( + paginationToken: PaginationToken, + onSuccess: Consumer>, + onError: Consumer + ) { + query(apiName, createRequest(paginationToken), onSuccess, onError) + } + + private fun createRequest(paginationToken: PaginationToken? = null): GraphQLRequest> { + return AppSyncGraphQLRequestFactory.buildModelPageQuery( + clazz, + queryPredicate, + (paginationToken as? ApiPaginationToken)?.nextToken + ) + } + + private fun query( + apiName: String?, + request: GraphQLRequest>, + onSuccess: Consumer>, + onError: Consumer) { + + if (apiName != null) { + Amplify.API.query( + apiName, + request, + { onSuccess.accept(it.data) }, + { onError.accept(it) } + ) + } else { + Amplify.API.query( + request, + { onSuccess.accept(it.data) }, + { onError.accept(it) } + ) + } + + } + + @Throws(ApiException::class) + private suspend fun query(apiName: String?, request: GraphQLRequest): + GraphQLResponse { + return suspendCoroutine { continuation -> + if (apiName != null) { + Amplify.API.query( + apiName, + request, + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) + } else { + Amplify.API.query( + request, + { continuation.resume(it) }, + { continuation.resumeWithException(it) } + ) + } + } + } +} + 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 index cd55c235aa..2d0c5d3272 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.kt @@ -242,6 +242,15 @@ object AppSyncGraphQLRequestFactory { 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 @@ -338,7 +347,8 @@ object AppSyncGraphQLRequestFactory { predicate: QueryPredicate, limit: Int, responseType: Type, - includes: ((P) -> List)? + includes: ((P) -> List)?, + pageToken: String? = null ): GraphQLRequest { return try { val modelName = ModelSchema.fromModelClass( @@ -359,6 +369,10 @@ object AppSyncGraphQLRequestFactory { } 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) } 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 efc9a0edc1..d61a2fee4f 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 @@ -24,6 +24,8 @@ import com.amplifyframework.api.graphql.PaginatedResult; import com.amplifyframework.core.model.LazyModel; import com.amplifyframework.core.model.Model; +import com.amplifyframework.core.model.ModelList; +import com.amplifyframework.core.model.ModelPage; import com.amplifyframework.util.Empty; import com.amplifyframework.util.TypeMaker; @@ -77,18 +79,29 @@ public GraphQLResponse buildResponse( ); try { Gson responseGson = gson.newBuilder() + .registerTypeAdapter( + ModelList.class, + new ModelListAdapter() + ) + .registerTypeHierarchyAdapter( + ModelPage.class, + new ModelPageDeserializer() + ) .registerTypeHierarchyAdapter( Iterable.class, new IterableDeserializer<>(request) ) .registerTypeAdapter( LazyModel.class, - new LazyModelAdapter(apiName) + new LazyModelDeserializer(apiName) ) .create(); Gson modelDeserializerGson = responseGson.newBuilder() - .registerTypeHierarchyAdapter(Model.class, new ModelDeserializer(responseGson)) + .registerTypeHierarchyAdapter( + Model.class, + new ModelDeserializer(responseGson, apiName) + ) .create(); return modelDeserializerGson.fromJson(responseJson, responseType); diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt deleted file mode 100644 index 7b76c6f704..0000000000 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/LazyModelAdapter.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.aws.ApiLazyModel.Companion.createLazy -import com.amplifyframework.api.aws.ApiLazyModel.Companion.createPreloaded -import com.amplifyframework.core.model.LazyModel -import com.amplifyframework.core.model.Model -import com.amplifyframework.core.model.SchemaRegistry -import com.google.gson.JsonDeserializationContext -import com.google.gson.JsonDeserializer -import com.google.gson.JsonElement -import com.google.gson.JsonParseException -import com.google.gson.JsonSerializationContext -import com.google.gson.JsonSerializer -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type - -internal class LazyModelAdapter(val apiName: String?) : - JsonDeserializer>, - JsonSerializer> { - @Throws(JsonParseException::class) - override fun deserialize( - json: JsonElement, - typeOfT: Type, - context: JsonDeserializationContext - ): LazyModel { - val pType = typeOfT as ParameterizedType - val type = pType.actualTypeArguments[0] as Class - val jsonObject = json.asJsonObject - val predicateKeyMap = SchemaRegistry.instance() - .getModelSchemaForModelClass(type) - .primaryIndexFields - .associateWith { jsonObject[it] } - - if (jsonObject.size() > predicateKeyMap.size) { - try { - val preloadedValue = context.deserialize(json, type) - return createPreloaded(type, predicateKeyMap, preloadedValue) - } catch (e: Exception) { - // fallback to create lazy - } - } - return createLazy(type, predicateKeyMap, apiName) - } - - override fun serialize( - src: LazyModel, - typeOfSrc: Type, - context: JsonSerializationContext - ): JsonElement? { - return null - } -} diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt index 6a56262146..d6ed72e879 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt @@ -18,7 +18,10 @@ import java.lang.reflect.Type * @param responseGson is a Gson object that does not have the model deserializer. Otherwise context.fromJson would * cause a recursion issue. */ -internal class ModelDeserializer(private val responseGson: Gson) : JsonDeserializer { +internal class ModelDeserializer( + private val responseGson: Gson, + private val apiName: String? +) : JsonDeserializer { override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Model { val parent = responseGson.fromJson(json, typeOfT) @@ -45,10 +48,10 @@ internal class ModelDeserializer(private val responseGson: Gson) : JsonDeseriali name to parentIdentifiers[idx] }.toMap() - val lazyList = LazyListHelper.createLazy(lazyFieldModelSchema.modelClass, queryKeys) + val modelList = ApiLazyModelList(lazyFieldModelSchema.modelClass, queryKeys, apiName) fieldToUpdate.isAccessible = true - fieldToUpdate.set(parent, lazyList) + fieldToUpdate.set(parent, modelList) } } return parent diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt new file mode 100644 index 0000000000..becc6145a2 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt @@ -0,0 +1,131 @@ +/* + * 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.LazyModel +import com.amplifyframework.core.model.Model +import com.amplifyframework.core.model.ModelList +import com.amplifyframework.core.model.ModelPage +import com.amplifyframework.core.model.SchemaRegistry +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 LazyModelDeserializer(val apiName: String?) : + JsonDeserializer> { + @Throws(JsonParseException::class) + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext + ): LazyModel { + 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.instance() + .getModelSchemaForModelClass(type) + .primaryIndexFields + .associateWith { jsonObject[it] } + + if (jsonObject.size() > predicateKeyMap.size) { + try { + val preloadedValue = context.deserialize(json, type) + return ApiLazyModel.createPreloaded(type, predicateKeyMap, preloadedValue) + } catch (e: Exception) { + // fallback to create lazy + } + } + return ApiLazyModel.createLazy(type, predicateKeyMap, apiName) + } +} + +internal class ModelListAdapter : JsonDeserializer> { + @Throws(JsonParseException::class) + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext + ): ModelList { + val items = deserializeItems(json, typeOfT, context) + return ApiLoadedModelList(items) + } +} + + +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) + } +} + +@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) } +} \ No newline at end of file diff --git a/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt b/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt index 05875e515a..7448bf4ba8 100644 --- a/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt +++ b/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt @@ -21,13 +21,9 @@ import com.amplifyframework.core.Consumer import com.amplifyframework.core.NullableConsumer @InternalAmplifyApi -class InMemoryLazyModel(model: M? = null) : LazyModel { - - private var value: M? = model - - override fun getValue(): M? { - return value - } +class InMemoryLazyModel( + override val value: M? = null +) : LazyModel { override fun getIdentifier(): Map { return emptyMap() @@ -40,6 +36,4 @@ class InMemoryLazyModel(model: M? = null) : LazyModel { override fun fetchModel(onSuccess: NullableConsumer, onError: Consumer) { onSuccess.accept(value) } - - override fun isLoaded() = true } diff --git a/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt b/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt index dfd32fa778..c8d95ff1cc 100644 --- a/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt +++ b/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt @@ -21,15 +21,28 @@ import com.amplifyframework.core.Consumer import com.amplifyframework.core.NullableConsumer interface LazyModel { - fun getValue(): M? + + /** The loaded model value */ + val value: M? @InternalAmplifyApi fun getIdentifier(): Map + /** + * Load the model represented by this LazyModel instance if not already loaded. + * + * @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 represented by this LazyModel instance if not already loaded. + * + * @param onSuccess Called upon successfully loading the model. + * @param onError Called when loading the model fails. + */ fun fetchModel(onSuccess: NullableConsumer, onError: Consumer) - - fun isLoaded(): Boolean -} +} \ No newline at end of file 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..6478c00784 --- /dev/null +++ b/core/src/main/java/com/amplifyframework/core/model/ModelList.kt @@ -0,0 +1,82 @@ +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 loadPage(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 loadPage( + 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 loadPage( + 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/ModelSchema.java b/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java index 278ba78dec..ccd0fc07ab 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java +++ b/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java @@ -235,7 +235,7 @@ private static ModelField createModelField(Field field) { .isEnum(Enum.class.isAssignableFrom(field.getType())) .isModel(Model.class.isAssignableFrom(field.getType())) .isLazyModel(LazyModel.class.isAssignableFrom(field.getType())) - .isLazyList(PaginatedResult.class.isAssignableFrom(field.getType())) + .isLazyList(ModelList.class.isAssignableFrom(field.getType())) .authRules(authRules) .build(); } From 3eecd22f99fcf63bc99305db201722ab25a4610f Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Fri, 1 Sep 2023 11:30:55 -0400 Subject: [PATCH 057/100] hide internal apis --- .../java/com/amplifyframework/api/aws/ApiLazyTypes.kt | 4 +--- .../api/aws/{ApiModelList.kt => ApiModelListTypes.kt} | 11 +++++------ 2 files changed, 6 insertions(+), 9 deletions(-) rename aws-api/src/main/java/com/amplifyframework/api/aws/{ApiModelList.kt => ApiModelListTypes.kt} (92%) diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt index 9663413ce5..4066d91735 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt @@ -16,7 +16,6 @@ package com.amplifyframework.api.aws import com.amplifyframework.AmplifyException -import com.amplifyframework.annotations.InternalAmplifyApi import com.amplifyframework.api.ApiException import com.amplifyframework.api.graphql.GraphQLRequest import com.amplifyframework.api.graphql.GraphQLResponse @@ -30,8 +29,7 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine -@InternalAmplifyApi -class ApiLazyModel private constructor( +internal class ApiLazyModel private constructor( private val clazz: Class, private val keyMap: Map, private var model: M? = null, diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelList.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt similarity index 92% rename from aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelList.kt rename to aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt index 1f9d18f8f2..68b201496a 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelList.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt @@ -1,7 +1,6 @@ package com.amplifyframework.api.aws import com.amplifyframework.AmplifyException -import com.amplifyframework.annotations.InternalAmplifyApi import com.amplifyframework.api.ApiException import com.amplifyframework.api.graphql.GraphQLRequest import com.amplifyframework.api.graphql.GraphQLResponse @@ -16,18 +15,18 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine -@InternalAmplifyApi -class ApiLoadedModelList( +internal class ApiLoadedModelList( override val items: List ) : LoadedModelList -class ApiModelPage( +internal class ApiModelPage( override val items: List, override val nextToken: ApiPaginationToken? ): ModelPage -class ApiPaginationToken(val nextToken: String) : PaginationToken -class ApiLazyModelList constructor( +internal class ApiPaginationToken(val nextToken: String) : PaginationToken + +internal class ApiLazyModelList constructor( private val clazz: Class, keyMap: Map, private val apiName: String? From b7d23564a70311fecacfff59b5b0461c41b3428f Mon Sep 17 00:00:00 2001 From: Tyler Roach Date: Fri, 1 Sep 2023 17:01:03 -0400 Subject: [PATCH 058/100] model updates --- .../api/aws/ApiModelListTypes.kt | 8 ++--- .../api/aws/ModelDeserializer.kt | 2 +- .../core/model/ModelException.kt | 6 ++-- .../core/model/ModelField.java | 24 ++++++------- .../amplifyframework/core/model/ModelList.kt | 8 ++--- .../core/model/ModelPropertyPath.kt | 34 +++++++++---------- .../core/model/ModelSchema.java | 3 +- 7 files changed, 40 insertions(+), 45 deletions(-) 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 index 68b201496a..4367921f7d 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt @@ -34,17 +34,17 @@ internal class ApiLazyModelList constructor( private val queryPredicate = AppSyncLazyQueryPredicate().createPredicate(clazz, keyMap) - override suspend fun loadPage(paginationToken: PaginationToken?): ModelPage { + override suspend fun fetchPage(paginationToken: PaginationToken?): ModelPage { val response = query(apiName, createRequest(paginationToken)) return response.data } - override fun loadPage(onSuccess: Consumer>, onError: Consumer) { + override fun fetchPage(onSuccess: Consumer>, onError: Consumer) { query(apiName, createRequest(), onSuccess, onError) } - override fun loadPage( - paginationToken: PaginationToken, + override fun fetchPage( + paginationToken: PaginationToken?, onSuccess: Consumer>, onError: Consumer ) { diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt index d6ed72e879..6b7df16625 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt @@ -28,7 +28,7 @@ internal class ModelDeserializer( val parentType = (typeOfT as Class<*>).simpleName val parentModelSchema = ModelSchema.fromModelClass(parent.javaClass) - parentModelSchema.fields.filter { it.value.isLazyList }.map { fieldMap -> + parentModelSchema.fields.filter { it.value.isModelList }.map { fieldMap -> val fieldToUpdate = parent.javaClass.getDeclaredField(fieldMap.key) fieldToUpdate.isAccessible = true if (fieldToUpdate.get(parent) == null) { diff --git a/core/src/main/java/com/amplifyframework/core/model/ModelException.kt b/core/src/main/java/com/amplifyframework/core/model/ModelException.kt index 71963f64e2..130d53803d 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelException.kt +++ b/core/src/main/java/com/amplifyframework/core/model/ModelException.kt @@ -16,16 +16,14 @@ package com.amplifyframework.core.model import com.amplifyframework.AmplifyException -import com.amplifyframework.annotations.InternalAmplifyApi -@InternalAmplifyApi -internal sealed class ModelException( +sealed class ModelException( message: String, recoverySuggestion: String, cause: Exception? = null ) : AmplifyException(message, cause, recoverySuggestion) { - internal class PropertyPathNotFound( + class PropertyPathNotFound( val modelName: String, cause: Exception? = null ) : ModelException( 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 5d4158fd66..ecccac7a2b 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelField.java +++ b/core/src/main/java/com/amplifyframework/core/model/ModelField.java @@ -58,7 +58,7 @@ public final class ModelField { private final boolean isLazyModel; // True if the field is an instance of lazy model. - private final boolean isLazyList; + private final boolean isModelList; // True if the field is an instance of CustomType private final boolean isCustomType; @@ -79,7 +79,7 @@ private ModelField(@NonNull ModelFieldBuilder builder) { this.isEnum = builder.isEnum; this.isModel = builder.isModel; this.isLazyModel = builder.isLazyModel; - this.isLazyList = builder.isLazyList; + this.isModelList = builder.isModelList; this.isCustomType = builder.isCustomType; this.authRules = builder.authRules; } @@ -172,12 +172,12 @@ public boolean isLazyModel() { } /** - * Returns true if the field's target type is a lazy list of Models. + * Returns true if the field's target type is ModelList. * - * @return True if the field's target type is a lazy list of Models. + * @return True if the field's target type is ModelList. */ - public boolean isLazyList() { - return isLazyList; + public boolean isModelList() { + return isModelList; } /** @@ -298,8 +298,8 @@ public static class ModelFieldBuilder { // True if the field's target type is LazyModel. private boolean isLazyModel = false; - // True if the field's target type is a lazy list type (PaginatedResult). - private boolean isLazyList = 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; @@ -408,12 +408,12 @@ public ModelFieldBuilder isLazyModel(boolean isLazyModel) { } /** - * Sets a flag indicating whether or not the field's type is a a lazy list type. - * @param isLazyList flag indicating if the field is a lazy list type (PaginatedResult) + * 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 isLazyList(boolean isLazyList) { - this.isLazyList = isLazyList; + public ModelFieldBuilder isModelList(boolean isModelList) { + this.isModelList = isModelList; return this; } diff --git a/core/src/main/java/com/amplifyframework/core/model/ModelList.kt b/core/src/main/java/com/amplifyframework/core/model/ModelList.kt index 6478c00784..680837ca0f 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelList.kt +++ b/core/src/main/java/com/amplifyframework/core/model/ModelList.kt @@ -32,7 +32,7 @@ interface LazyModelList : ModelList { */ @JvmSynthetic @Throws(AmplifyException::class) - suspend fun loadPage(paginationToken: PaginationToken? = null): ModelPage + suspend fun fetchPage(paginationToken: PaginationToken? = null): ModelPage /** * Loads the next page of models. @@ -40,7 +40,7 @@ interface LazyModelList : ModelList { * @param onSuccess called upon successfully loading the next page of models. * @param onError called when loading the page fails. */ - fun loadPage( + fun fetchPage( onSuccess: Consumer>, onError: Consumer ) @@ -52,8 +52,8 @@ interface LazyModelList : ModelList { * @param onSuccess called upon successfully loading the next page of models. * @param onError called when loading the page fails. */ - fun loadPage( - paginationToken: PaginationToken, + fun fetchPage( + paginationToken: PaginationToken?, onSuccess: Consumer>, onError: Consumer ) diff --git a/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt b/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt index 2e70418312..ec84ae85a4 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt +++ b/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt @@ -59,9 +59,20 @@ interface PropertyPath { * 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( +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 ) @@ -74,42 +85,29 @@ data class PropertyPathMetadata( interface PropertyContainerPath : PropertyPath { /** - * + * Returns the model type of the property container. */ fun getModelType(): Class } /** * Represents the `Model` structure itself, a container of property references. - * - * ```kotlin - * class Path(name: String = "root", isCollection: Boolean = false, parent: PropertyPath? = null) - * : ModelPath(name, isCollection, modelType = Post::class.java, parent = parent) { - * - * val id = string("id") - * val title = string("title") - * val blog by lazy { Blog.Path("blog", parent = this) } - * val comments by lazy { Comment.Path("comments", isCollection = true, parent = this) } - * } - * - * companion object { - * val rootPath = Path() - * } - * ``` */ -open class ModelPath( +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 { 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 ccd0fc07ab..8aff5bc388 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java +++ b/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java @@ -21,7 +21,6 @@ import androidx.core.util.ObjectsCompat; import com.amplifyframework.AmplifyException; -import com.amplifyframework.api.graphql.PaginatedResult; import com.amplifyframework.core.model.annotations.BelongsTo; import com.amplifyframework.core.model.annotations.HasMany; import com.amplifyframework.core.model.annotations.HasOne; @@ -235,7 +234,7 @@ private static ModelField createModelField(Field field) { .isEnum(Enum.class.isAssignableFrom(field.getType())) .isModel(Model.class.isAssignableFrom(field.getType())) .isLazyModel(LazyModel.class.isAssignableFrom(field.getType())) - .isLazyList(ModelList.class.isAssignableFrom(field.getType())) + .isModelList(ModelList.class.isAssignableFrom(field.getType())) .authRules(authRules) .build(); } From 181bf119cd3d3f7924446e55537445e0cac5fa55 Mon Sep 17 00:00:00 2001 From: tjroach Date: Tue, 12 Sep 2023 16:25:00 -0400 Subject: [PATCH 059/100] change includeAssociations to includeRelationships --- .../com/amplifyframework/api/aws/SelectionSet.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 683f8c3e65..a30ade7992 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 @@ -190,7 +190,7 @@ static final class Builder { private Operation operation; private GraphQLRequestOptions requestOptions; private ModelSchema modelSchema; - private List includeAssociations; + private List includeRelationships; Builder() { } @@ -219,8 +219,8 @@ public Builder requestOptions(@NonNull GraphQLRequestOptions requestOptions) { return Builder.this; } - public Builder includeAssociations(@NonNull List associations) { - this.includeAssociations = associations; + public Builder includeRelationships(@NonNull List relationships) { + this.includeRelationships = relationships; return Builder.this; } @@ -240,9 +240,9 @@ public SelectionSet build() throws AmplifyException { ? getModelFields(modelSchema, requestOptions.maxDepth(), operation) : getModelFields(modelClass, requestOptions.maxDepth(), operation, false)); - // Associations need to be added before wrapping pagination - if (includeAssociations != null) { - for (PropertyContainerPath association : includeAssociations) { + // Relationships need to be added before wrapping pagination + if (includeRelationships != null) { + for (PropertyContainerPath association : includeRelationships) { SelectionSet included = SelectionSetUtils.asSelectionSet(association, false); if (included != null) { SelectionSetUtils.merge(node, included); From cae26670bb3c6375a929db858b3f5adb81e3c5dc Mon Sep 17 00:00:00 2001 From: tjroach Date: Tue, 12 Sep 2023 16:25:31 -0400 Subject: [PATCH 060/100] Remove unused targetNames from HasMany interface --- .../java/com/amplifyframework/core/model/ModelSchema.java | 1 - .../amplifyframework/core/model/annotations/HasMany.java | 7 ------- 2 files changed, 8 deletions(-) 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 8aff5bc388..41407663e1 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java +++ b/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java @@ -266,7 +266,6 @@ private static ModelAssociation createModelAssociation(Field field) { HasMany association = Objects.requireNonNull(field.getAnnotation(HasMany.class)); return ModelAssociation.builder() .name(HasMany.class.getSimpleName()) - .targetNames(association.targetNames()) .associatedName(association.associatedWith()) .associatedType(association.type().getSimpleName()) .build(); diff --git a/core/src/main/java/com/amplifyframework/core/model/annotations/HasMany.java b/core/src/main/java/com/amplifyframework/core/model/annotations/HasMany.java index b850eb9f20..b148e3cdf4 100644 --- a/core/src/main/java/com/amplifyframework/core/model/annotations/HasMany.java +++ b/core/src/main/java/com/amplifyframework/core/model/annotations/HasMany.java @@ -49,11 +49,4 @@ * @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 {}; } From d0883d1e5adeba8006dcaa29855d815fa764a31e Mon Sep 17 00:00:00 2001 From: tjroach Date: Tue, 12 Sep 2023 16:25:58 -0400 Subject: [PATCH 061/100] includeRelationships fix --- .../amplifyframework/api/aws/AppSyncGraphQLRequestFactory.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 2d0c5d3272..34b50a7db3 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.kt @@ -549,7 +549,7 @@ object AppSyncGraphQLRequestFactory { .modelClass(modelClass) .operation(operationType) .requestOptions(ApiGraphQLRequestOptions()) - .includeAssociations(associations) + .includeRelationships(associations) .build() } } From daa6da5521193e0337ed26c705038bb929de9afd Mon Sep 17 00:00:00 2001 From: tjroach Date: Thu, 14 Sep 2023 10:06:23 -0400 Subject: [PATCH 062/100] includeRelationships fix --- .../api/aws/GraphQLRequestHelper.java | 31 ++++++++------- .../api/aws/SelectionSet.java | 4 +- .../api/aws/AWSSchemaRegistry.kt | 39 +++++++++++++++++++ ...iLazyTypes.kt => ApiLazyModelReference.kt} | 34 ++++------------ .../api/aws/GsonGraphQLResponseFactory.java | 6 +-- .../api/aws/ModelDeserializer.kt | 4 +- .../api/aws/ModelListDeserializers.kt | 16 ++++---- .../api/aws/ModelProviderLocator.java | 2 +- ...zyModel.kt => LoadedModelReferenceImpl.kt} | 19 ++------- .../core/model/ModelField.java | 28 ++++++------- .../model/{LazyModel.kt => ModelReference.kt} | 25 +++++++++--- .../core/model/ModelSchema.java | 4 +- 12 files changed, 118 insertions(+), 94 deletions(-) create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/AWSSchemaRegistry.kt rename aws-api/src/main/java/com/amplifyframework/api/aws/{ApiLazyTypes.kt => ApiLazyModelReference.kt} (81%) rename core/src/main/java/com/amplifyframework/core/model/{InMemoryLazyModel.kt => LoadedModelReferenceImpl.kt} (60%) rename core/src/main/java/com/amplifyframework/core/model/{LazyModel.kt => ModelReference.kt} (74%) 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 9ec3d7bf2b..6c80b8e252 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,11 +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.LazyModel; +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; @@ -228,17 +229,19 @@ private static Map extractFieldLevelData( Object fieldValue = extractFieldValue(modelField.getName(), instance, schema, false); Object underlyingFieldValue = fieldValue; Map identifiersIfLazyModel = new HashMap<>(); - if (modelField.isLazyModel() && fieldValue != null) { - LazyModel lazyModel = (LazyModel) fieldValue; - underlyingFieldValue = lazyModel.getValue(); - identifiersIfLazyModel = lazyModel.getIdentifier(); + if (modelField.isModelReference() && fieldValue != null) { + ModelReference modelReference = (ModelReference) fieldValue; + if (modelReference instanceof LoadedModelReference) { + underlyingFieldValue = ((LoadedModelReference)modelReference).getValue(); + } + identifiersIfLazyModel = modelReference.getIdentifier(); } if (association == null) { result.put(fieldName, fieldValue); } else if (association.isOwner()) { if ((fieldValue == null || - (modelField.isLazyModel() && + (modelField.isModelReference() && underlyingFieldValue == null && identifiersIfLazyModel.isEmpty())) && MutationType.CREATE.equals(type)) { @@ -269,7 +272,7 @@ private static void insertForeignKeyValues( for (String key : association.getTargetNames()) { result.put(key, null); } - } else if ((modelField.isModel() || modelField.isLazyModel()) && underlyingFieldValue instanceof Model) { + } else if ((modelField.isModel() || modelField.isModelReference()) && underlyingFieldValue instanceof Model) { if (((Model) underlyingFieldValue).resolveIdentifier() instanceof ModelIdentifier) { final ModelIdentifier primaryKey = (ModelIdentifier) ((Model) underlyingFieldValue).resolveIdentifier(); @@ -308,8 +311,8 @@ private static void insertForeignKeyValues( ((Model) underlyingFieldValue).resolveIdentifier().toString() ); } - } else if (modelField.isLazyModel() && fieldValue instanceof LazyModel) { - Map identifiers = ((LazyModel) fieldValue).getIdentifier(); + } else if (modelField.isModelReference() && fieldValue instanceof ModelReference) { + Map identifiers = ((ModelReference) fieldValue).getIdentifier(); if (identifiers.isEmpty()) { for (String key : association.getTargetNames()) { result.put(key, null); @@ -319,15 +322,15 @@ private static void insertForeignKeyValues( } private static Object extractAssociateId(ModelField modelField, Object fieldValue, Object underlyingFieldValue) { - if ((modelField.isModel() || modelField.isLazyModel()) && underlyingFieldValue instanceof Model) { + 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.isLazyModel() && fieldValue instanceof LazyModel) { - Map identifiers = ((LazyModel) fieldValue).getIdentifier(); + } else if (modelField.isModelReference() && fieldValue instanceof ModelReference) { + Map identifiers = ((ModelReference) fieldValue).getIdentifier(); if (identifiers.isEmpty()) { return null; } else { @@ -359,8 +362,8 @@ private static Object extractFieldValue( Field privateField = instance.getClass().getDeclaredField(fieldName); privateField.setAccessible(true); Object fieldInstance = privateField.get(instance); - if (extractLazyValue && fieldInstance != null && privateField.getType() == LazyModel.class) { - return ((LazyModel) fieldInstance).getValue(); + if (extractLazyValue && fieldInstance != null && privateField.getType() == LoadedModelReference.class) { + return ((LoadedModelReference) fieldInstance).getValue(); } return fieldInstance; } catch (Exception exception) { 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 a30ade7992..fdc7a6a6fb 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 @@ -27,11 +27,11 @@ import com.amplifyframework.core.model.AuthStrategy; import com.amplifyframework.core.model.CustomTypeField; import com.amplifyframework.core.model.CustomTypeSchema; -import com.amplifyframework.core.model.LazyModel; 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; @@ -332,7 +332,7 @@ private Set getModelFields( operation, false)); result.add(new SelectionSet(fieldName, fields)); } - } else if (LazyModel.class.isAssignableFrom(field.getType())) { + } 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); diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/AWSSchemaRegistry.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSSchemaRegistry.kt new file mode 100644 index 0000000000..cfa623c3b7 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSSchemaRegistry.kt @@ -0,0 +1,39 @@ +package com.amplifyframework.api.aws + +import com.amplifyframework.api.ApiException +import com.amplifyframework.core.model.CustomTypeSchema +import com.amplifyframework.core.model.Model +import com.amplifyframework.core.model.ModelSchema + +internal object AWSSchemaRegistry { + private val modelSchemaMap = mutableMapOf() + // CustomType name => CustomTypeSchema map + private val customTypeSchemaMap = mutableMapOf() + + init { + val modelProvider = ModelProviderLocator.locate() + register(modelProvider.modelSchemas(), modelProvider.customTypeSchemas()) + } + + @Synchronized + 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." + ) + } + + @Synchronized + fun getModelSchemaForModelClass(modelClass: Class): ModelSchema { + return getModelSchemaForModelClass(modelClass.simpleName) + } + + @Synchronized + fun register( + modelSchemas: Map, + customTypeSchemas: Map + ) { + modelSchemaMap.putAll(modelSchemas) + customTypeSchemaMap.putAll(customTypeSchemas) + } +} \ No newline at end of file diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt similarity index 81% rename from aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt rename to aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt index 4066d91735..c457b77a81 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyTypes.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt @@ -23,24 +23,22 @@ import com.amplifyframework.api.graphql.PaginatedResult import com.amplifyframework.core.Amplify import com.amplifyframework.core.Consumer import com.amplifyframework.core.NullableConsumer -import com.amplifyframework.core.model.LazyModel +import com.amplifyframework.core.model.LazyModelReference import com.amplifyframework.core.model.Model import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine -internal class ApiLazyModel private constructor( +internal class ApiLazyModelReference internal constructor( private val clazz: Class, private val keyMap: Map, - private var model: M? = null, - private var loadedValue: Boolean = false, private val apiName: String? = null -) : LazyModel { +) : LazyModelReference { private val queryPredicate = AppSyncLazyQueryPredicate().createPredicate(clazz, keyMap) - override val value: M? - get() = model + private var value: M? = null + private var loadedValue = false override fun getIdentifier(): Map { return keyMap @@ -59,7 +57,7 @@ internal class ApiLazyModel private constructor( ), apiName ).data.items.iterator() - model = if (resultIterator.hasNext()) { + value = if (resultIterator.hasNext()) { resultIterator.next() } else { null @@ -78,7 +76,7 @@ internal class ApiLazyModel private constructor( } val onQuerySuccess = Consumer>> { val resultIterator = it.data.items.iterator() - model = if (resultIterator.hasNext()) { + value = if (resultIterator.hasNext()) { resultIterator.next() } else { null @@ -102,24 +100,6 @@ internal class ApiLazyModel private constructor( ) } } - - internal companion object { - fun createPreloaded( - clazz: Class, - keyMap: Map, - value: M? - ): ApiLazyModel { - return ApiLazyModel(clazz, keyMap, value, true) - } - - fun createLazy( - clazz: Class, - keyMap: Map, - apiName: String? - ): ApiLazyModel { - return ApiLazyModel(clazz, keyMap, apiName = apiName) - } - } } /* 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 d61a2fee4f..1f57e858a3 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 @@ -22,10 +22,10 @@ import com.amplifyframework.api.graphql.GraphQLRequest; import com.amplifyframework.api.graphql.GraphQLResponse; import com.amplifyframework.api.graphql.PaginatedResult; -import com.amplifyframework.core.model.LazyModel; 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.amplifyframework.util.Empty; import com.amplifyframework.util.TypeMaker; @@ -92,8 +92,8 @@ public GraphQLResponse buildResponse( new IterableDeserializer<>(request) ) .registerTypeAdapter( - LazyModel.class, - new LazyModelDeserializer(apiName) + ModelReference.class, + new ModelReferenceDeserializer(apiName) ) .create(); diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt index 6b7df16625..ef207217cc 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt @@ -1,9 +1,9 @@ package com.amplifyframework.api.aws +import com.amplifyframework.api.ApiException import com.amplifyframework.core.model.Model import com.amplifyframework.core.model.ModelIdentifier import com.amplifyframework.core.model.ModelSchema -import com.amplifyframework.core.model.SchemaRegistry import com.google.gson.Gson import com.google.gson.JsonDeserializationContext import com.google.gson.JsonDeserializer @@ -33,7 +33,7 @@ internal class ModelDeserializer( fieldToUpdate.isAccessible = true if (fieldToUpdate.get(parent) == null) { val lazyField = fieldMap.value - val lazyFieldModelSchema = SchemaRegistry.instance().getModelSchemaForModelClass(lazyField.targetType) + val lazyFieldModelSchema = AWSSchemaRegistry.getModelSchemaForModelClass(lazyField.targetType) val lazyFieldTargetNames = lazyFieldModelSchema .associations diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt index becc6145a2..a7a0900d78 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt @@ -14,11 +14,11 @@ */ package com.amplifyframework.api.aws -import com.amplifyframework.core.model.LazyModel +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.SchemaRegistry +import com.amplifyframework.core.model.ModelReference import com.google.gson.JsonDeserializationContext import com.google.gson.JsonDeserializer import com.google.gson.JsonElement @@ -30,21 +30,21 @@ import java.lang.reflect.Type const val ITEMS_KEY = "items" const val NEXT_TOKEN_KEY = "nextToken" -internal class LazyModelDeserializer(val apiName: String?) : - JsonDeserializer> { +internal class ModelReferenceDeserializer(val apiName: String?) : + JsonDeserializer> { @Throws(JsonParseException::class) override fun deserialize( json: JsonElement, typeOfT: Type, context: JsonDeserializationContext - ): LazyModel { + ): 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.instance() + val predicateKeyMap = AWSSchemaRegistry .getModelSchemaForModelClass(type) .primaryIndexFields .associateWith { jsonObject[it] } @@ -52,12 +52,12 @@ internal class LazyModelDeserializer(val apiName: String?) : if (jsonObject.size() > predicateKeyMap.size) { try { val preloadedValue = context.deserialize(json, type) - return ApiLazyModel.createPreloaded(type, predicateKeyMap, preloadedValue) + return LoadedModelReferenceImpl(preloadedValue) } catch (e: Exception) { // fallback to create lazy } } - return ApiLazyModel.createLazy(type, predicateKeyMap, apiName) + return ApiLazyModelReference(type, predicateKeyMap, apiName) } } 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 index ea900266e6..e436435eb9 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelProviderLocator.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelProviderLocator.java @@ -38,7 +38,7 @@ */ public final class ModelProviderLocator { private static final String DEFAULT_MODEL_PROVIDER_CLASS_NAME = - "com.amplifyframework.api.generated.model.AmplifyModelProvider"; + "com.amplifyframework.datastore.generated.model.AmplifyModelProvider"; private static final String GET_INSTANCE_ACCESSOR_METHOD_NAME = "getInstance"; private ModelProviderLocator() {} diff --git a/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt b/core/src/main/java/com/amplifyframework/core/model/LoadedModelReferenceImpl.kt similarity index 60% rename from core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt rename to core/src/main/java/com/amplifyframework/core/model/LoadedModelReferenceImpl.kt index 7448bf4ba8..d86fafb875 100644 --- a/core/src/main/java/com/amplifyframework/core/model/InMemoryLazyModel.kt +++ b/core/src/main/java/com/amplifyframework/core/model/LoadedModelReferenceImpl.kt @@ -15,25 +15,12 @@ package com.amplifyframework.core.model -import com.amplifyframework.AmplifyException import com.amplifyframework.annotations.InternalAmplifyApi -import com.amplifyframework.core.Consumer -import com.amplifyframework.core.NullableConsumer @InternalAmplifyApi -class InMemoryLazyModel( +class LoadedModelReferenceImpl( override val value: M? = null -) : LazyModel { +) : LoadedModelReference { - override fun getIdentifier(): Map { - return emptyMap() - } - - override suspend fun fetchModel(): M? { - return value - } - - override fun fetchModel(onSuccess: NullableConsumer, onError: Consumer) { - onSuccess.accept(value) - } + override fun getIdentifier() = emptyMap() } 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 ecccac7a2b..1f40d22d09 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelField.java +++ b/core/src/main/java/com/amplifyframework/core/model/ModelField.java @@ -54,10 +54,10 @@ 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 lazy model. - private final boolean isLazyModel; + // True if the field is an instance of ModelReference. + private final boolean isModelReference; - // True if the field is an instance of lazy model. + // True if the field is an instance of ModelList. private final boolean isModelList; // True if the field is an instance of CustomType @@ -78,7 +78,7 @@ private ModelField(@NonNull ModelFieldBuilder builder) { this.isArray = builder.isArray; this.isEnum = builder.isEnum; this.isModel = builder.isModel; - this.isLazyModel = builder.isLazyModel; + this.isModelReference = builder.isModelReference; this.isModelList = builder.isModelList; this.isCustomType = builder.isCustomType; this.authRules = builder.authRules; @@ -163,12 +163,12 @@ public boolean isModel() { } /** - * Returns true if the field's target type is a lazy Model. + * Returns true if the field's target type is ModelReference. * - * @return True if the field's target type is a lazy Model. + * @return True if the field's target type is ModelReference. */ - public boolean isLazyModel() { - return isLazyModel; + public boolean isModelReference() { + return isModelReference; } /** @@ -295,8 +295,8 @@ 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 LazyModel. - private boolean isLazyModel = 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; @@ -398,12 +398,12 @@ public ModelFieldBuilder isModel(boolean isModel) { } /** - * Sets a flag indicating whether or not the field's target type is a LazyModel. - * @param isLazyModel flag indicating if the field is a lazy model + * 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 isLazyModel(boolean isLazyModel) { - this.isLazyModel = isLazyModel; + public ModelFieldBuilder isModelReference(boolean isModelReference) { + this.isModelReference = isModelReference; return this; } diff --git a/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt b/core/src/main/java/com/amplifyframework/core/model/ModelReference.kt similarity index 74% rename from core/src/main/java/com/amplifyframework/core/model/LazyModel.kt rename to core/src/main/java/com/amplifyframework/core/model/ModelReference.kt index c8d95ff1cc..39ae91048c 100644 --- a/core/src/main/java/com/amplifyframework/core/model/LazyModel.kt +++ b/core/src/main/java/com/amplifyframework/core/model/ModelReference.kt @@ -20,16 +20,31 @@ import com.amplifyframework.annotations.InternalAmplifyApi import com.amplifyframework.core.Consumer import com.amplifyframework.core.NullableConsumer -interface LazyModel { + +/** + * 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? +} - @InternalAmplifyApi - fun getIdentifier(): Map +/** + * A reference holder that allows lazily loading a model + */ +interface LazyModelReference: ModelReference { /** - * Load the model represented by this LazyModel instance if not already loaded. + * 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. @@ -39,7 +54,7 @@ interface LazyModel { suspend fun fetchModel(): M? /** - * Load the model represented by this LazyModel instance if not already loaded. + * Load the model instance represented by this LazyModelReference. * * @param onSuccess Called upon successfully loading the model. * @param onError Called when loading the model fails. 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 41407663e1..ffbf09d012 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java +++ b/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java @@ -212,7 +212,7 @@ private static ModelField createModelField(Field field) { if (annotation != null) { final String fieldName = field.getName(); final Class fieldType; - if (field.getType() == LazyModel.class && field.getGenericType() + if (field.getType() == ModelReference.class && field.getGenericType() instanceof ParameterizedType) { ParameterizedType pType = (ParameterizedType) field.getGenericType(); fieldType = (Class) pType.getActualTypeArguments()[0]; @@ -233,7 +233,7 @@ private static ModelField createModelField(Field field) { .isArray(Collection.class.isAssignableFrom(field.getType())) .isEnum(Enum.class.isAssignableFrom(field.getType())) .isModel(Model.class.isAssignableFrom(field.getType())) - .isLazyModel(LazyModel.class.isAssignableFrom(field.getType())) + .isModelReference(ModelReference.class.isAssignableFrom(field.getType())) .isModelList(ModelList.class.isAssignableFrom(field.getType())) .authRules(authRules) .build(); From ae172c417da7e282347b02339ca8ad762b28498b Mon Sep 17 00:00:00 2001 From: tjroach Date: Thu, 14 Sep 2023 11:45:02 -0400 Subject: [PATCH 063/100] single execution of lazy model reference --- .../api/aws/ApiLazyModelReference.kt | 111 ++++++++++-------- 1 file changed, 61 insertions(+), 50 deletions(-) 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 index c457b77a81..dfbc74c700 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt @@ -25,6 +25,12 @@ import com.amplifyframework.core.Consumer import com.amplifyframework.core.NullableConsumer import com.amplifyframework.core.model.LazyModelReference import com.amplifyframework.core.model.Model +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import java.util.concurrent.atomic.AtomicReference import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine @@ -34,72 +40,77 @@ internal class ApiLazyModelReference internal constructor( private val keyMap: Map, private val apiName: String? = null ) : LazyModelReference { - - private val queryPredicate = AppSyncLazyQueryPredicate().createPredicate(clazz, keyMap) - - private var value: M? = null - private var loadedValue = false + private val cachedValue = AtomicReference?>(null) + private val semaphore = Semaphore(1) // prevents multiple fetches + private val callbackScope = CoroutineScope(Dispatchers.IO) override fun getIdentifier(): Map { return keyMap } override suspend fun fetchModel(): M? { - if (loadedValue) { - return value + val cached = cachedValue.get() + if (cached != null) { + // Quick return if value is already present + return cached.value } - try { - val resultIterator = query( - AppSyncGraphQLRequestFactory.buildQuery, M>( - clazz, - queryPredicate - ), - apiName - ).data.items.iterator() - value = if (resultIterator.hasNext()) { - resultIterator.next() - } else { - null - } - loadedValue = true - } catch (error: ApiException) { - throw AmplifyException("Error lazy loading the model.", error, error.message ?: "") - } - return value + return fetchInternal() } override fun fetchModel(onSuccess: NullableConsumer, onError: Consumer) { - if (loadedValue) { - onSuccess.accept(value) - return + val cached = cachedValue.get() + if (cached != null) { + // Quick return if value is already present + onSuccess.accept(cached.value) } - val onQuerySuccess = Consumer>> { - val resultIterator = it.data.items.iterator() - value = if (resultIterator.hasNext()) { - resultIterator.next() - } else { - null + + // To ensure we only have 1 live request at a time, we jump to our own single threaded scope + callbackScope.launch { + try { + val model = fetchInternal() + onSuccess.accept(model) + } catch (e: AmplifyException) { + onError.accept(e) } - loadedValue = true - onSuccess.accept(value) } - val onApiFailure = Consumer { onError.accept(it) } - if (apiName != null) { - Amplify.API.query( - apiName, - AppSyncGraphQLRequestFactory.buildQuery(clazz, queryPredicate), - onQuerySuccess, - onApiFailure - ) - } else { - Amplify.API.query( - AppSyncGraphQLRequestFactory.buildQuery(clazz, queryPredicate), - onQuerySuccess, - onApiFailure - ) + } + + private suspend fun fetchInternal(): M? { + // Use Semaphore with 1 permit to only allow 1 execution at a time + semaphore.withPermit { + + // Quick return if value is already present + val cached = cachedValue.get() + if (cached != null) { + return cached.value + } + + return try { + val resultIterator = query( + AppSyncGraphQLRequestFactory.buildQuery, M>( + clazz, + AppSyncLazyQueryPredicate().createPredicate(clazz, keyMap) + ), + apiName + ).data.items.iterator() + val value = if (resultIterator.hasNext()) { + resultIterator.next() + } else { + null + } + 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?) + } } /* From d26a0447298a7113aeca000733dcae35356f4643 Mon Sep 17 00:00:00 2001 From: tjroach Date: Tue, 19 Sep 2023 11:03:43 -0400 Subject: [PATCH 064/100] Fail DataStore initialization if lazy model type discovered --- .../api/aws/AWSApiPlugin.java | 14 ------- .../core/model/SchemaRegistry.java | 8 ++-- .../core/model/SchemaRegistryExtensions.kt | 38 +++++++++++++++++++ 3 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 core/src/main/java/com/amplifyframework/core/model/SchemaRegistryExtensions.kt 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 56363817f7..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 @@ -19,10 +19,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import androidx.annotation.WorkerThread; import androidx.core.util.ObjectsCompat; -import com.amplifyframework.AmplifyException; import com.amplifyframework.api.ApiException; import com.amplifyframework.api.ApiPlugin; import com.amplifyframework.api.aws.auth.ApiRequestDecoratorFactory; @@ -42,8 +40,6 @@ import com.amplifyframework.core.Action; import com.amplifyframework.core.Amplify; import com.amplifyframework.core.Consumer; -import com.amplifyframework.core.model.ModelProvider; -import com.amplifyframework.core.model.SchemaRegistry; import com.amplifyframework.hub.HubChannel; import com.amplifyframework.util.Immutable; import com.amplifyframework.util.UserAgent; @@ -129,16 +125,6 @@ public String getPluginKey() { return "awsAPIPlugin"; } - @WorkerThread - @Override - public void initialize(@NonNull Context context) throws AmplifyException { - SchemaRegistry schemaRegistry = SchemaRegistry.instance(); - if (schemaRegistry.getModelSchemaMap().isEmpty()) { - ModelProvider modelProvider = ModelProviderLocator.locate(); - schemaRegistry.register(modelProvider.modelSchemas(), modelProvider.customTypeSchemas()); - } - } - @Override public void configure( JSONObject pluginConfiguration, 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/SchemaRegistryExtensions.kt b/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryExtensions.kt new file mode 100644 index 0000000000..dc0aeed509 --- /dev/null +++ b/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryExtensions.kt @@ -0,0 +1,38 @@ +package com.amplifyframework.core.model + +import com.amplifyframework.core.model.annotations.ModelConfig +import com.amplifyframework.datastore.DataStoreException.IrRecoverableException + +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) { + 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." + ) + } else { + modelSchemaMap[modelName] = modelSchema + } + } +} \ No newline at end of file From e212550191eadf5a13911a468be7b8d882734ba2 Mon Sep 17 00:00:00 2001 From: tjroach Date: Tue, 19 Sep 2023 11:13:23 -0400 Subject: [PATCH 065/100] lint --- .../api/aws/AWSSchemaRegistry.kt | 4 +-- .../api/aws/ApiLazyModelReference.kt | 10 ++++---- .../api/aws/ApiModelListTypes.kt | 11 ++++---- .../api/aws/ModelDeserializer.kt | 1 - .../api/aws/ModelListDeserializers.kt | 25 +++++++++---------- .../amplifyframework/core/model/ModelList.kt | 3 +-- .../core/model/ModelReference.kt | 7 +++--- ...ryExtensions.kt => SchemaRegistryUtils.kt} | 5 ++-- 8 files changed, 31 insertions(+), 35 deletions(-) rename core/src/main/java/com/amplifyframework/core/model/{SchemaRegistryExtensions.kt => SchemaRegistryUtils.kt} (96%) diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/AWSSchemaRegistry.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSSchemaRegistry.kt index cfa623c3b7..cb2c9d2ceb 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AWSSchemaRegistry.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSSchemaRegistry.kt @@ -18,7 +18,7 @@ internal object AWSSchemaRegistry { @Synchronized fun getModelSchemaForModelClass(classSimpleName: String): ModelSchema { return modelSchemaMap[classSimpleName] ?: throw ApiException( - "Model type of `${classSimpleName}` not found.", + "Model type of `$classSimpleName` not found.", "Please regenerate codegen models and verify the class is found in AmplifyModelProvider." ) } @@ -36,4 +36,4 @@ internal object AWSSchemaRegistry { modelSchemaMap.putAll(modelSchemas) customTypeSchemaMap.putAll(customTypeSchemas) } -} \ No newline at end of file +} 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 index dfbc74c700..0b6211d79f 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt @@ -25,15 +25,15 @@ import com.amplifyframework.core.Consumer import com.amplifyframework.core.NullableConsumer import com.amplifyframework.core.model.LazyModelReference import com.amplifyframework.core.model.Model +import java.util.concurrent.atomic.AtomicReference +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit -import java.util.concurrent.atomic.AtomicReference -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine internal class ApiLazyModelReference internal constructor( private val clazz: Class, @@ -109,7 +109,7 @@ internal class ApiLazyModelReference internal constructor( private companion object { // Wraps the value to determine difference between null/unloaded and null/loaded - private class LoadedValue(val value: M?) + 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 index 4367921f7d..64de7edc88 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt @@ -22,11 +22,11 @@ internal class ApiLoadedModelList( internal class ApiModelPage( override val items: List, override val nextToken: ApiPaginationToken? -): ModelPage +) : ModelPage internal class ApiPaginationToken(val nextToken: String) : PaginationToken -internal class ApiLazyModelList constructor( +internal class ApiLazyModelList constructor( private val clazz: Class, keyMap: Map, private val apiName: String? @@ -63,7 +63,8 @@ internal class ApiLazyModelList constructor( apiName: String?, request: GraphQLRequest>, onSuccess: Consumer>, - onError: Consumer) { + onError: Consumer + ) { if (apiName != null) { Amplify.API.query( @@ -79,12 +80,11 @@ internal class ApiLazyModelList constructor( { onError.accept(it) } ) } - } @Throws(ApiException::class) private suspend fun query(apiName: String?, request: GraphQLRequest): - GraphQLResponse { + GraphQLResponse { return suspendCoroutine { continuation -> if (apiName != null) { Amplify.API.query( @@ -103,4 +103,3 @@ internal class ApiLazyModelList constructor( } } } - diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt index ef207217cc..75ec87c41c 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt @@ -1,6 +1,5 @@ package com.amplifyframework.api.aws -import com.amplifyframework.api.ApiException import com.amplifyframework.core.model.Model import com.amplifyframework.core.model.ModelIdentifier import com.amplifyframework.core.model.ModelSchema diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt index a7a0900d78..48c057f085 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt @@ -38,8 +38,8 @@ internal class ModelReferenceDeserializer(val apiName: String?) : typeOfT: Type, context: JsonDeserializationContext ): ModelReference { - val pType = typeOfT as? ParameterizedType ?: - throw JsonParseException("Expected a parameterized type during list deserialization.") + 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) @@ -73,7 +73,6 @@ internal class ModelListAdapter : JsonDeserializer> { } } - internal class ModelPageDeserializer : JsonDeserializer> { @Throws(JsonParseException::class) override fun deserialize( @@ -91,18 +90,18 @@ internal class ModelPageDeserializer : JsonDeserializer> 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" + "Unable to deserialize the response" ) } @Throws(JsonParseException::class) -private fun deserializeItems( +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 pType = typeOfT as? ParameterizedType + ?: throw JsonParseException("Expected a parameterized type during list deserialization.") val type = pType.actualTypeArguments[0] val jsonObject = getJsonObject(json) @@ -112,10 +111,10 @@ private fun deserializeItems( } 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." + "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." ) } @@ -126,6 +125,6 @@ private fun deserializeItems( @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 { if (it.isJsonPrimitive) it.asString else null } ?.let { ApiPaginationToken(it) } -} \ No newline at end of file +} diff --git a/core/src/main/java/com/amplifyframework/core/model/ModelList.kt b/core/src/main/java/com/amplifyframework/core/model/ModelList.kt index 680837ca0f..a828a45b86 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelList.kt +++ b/core/src/main/java/com/amplifyframework/core/model/ModelList.kt @@ -67,7 +67,7 @@ interface PaginationToken /** * A page of loaded models. */ -interface ModelPage { +interface ModelPage { /** The list of loaded models. */ val items: List @@ -79,4 +79,3 @@ interface ModelPage { val hasNextPage: Boolean get() = nextToken != null } - diff --git a/core/src/main/java/com/amplifyframework/core/model/ModelReference.kt b/core/src/main/java/com/amplifyframework/core/model/ModelReference.kt index 39ae91048c..bb50d74f57 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelReference.kt +++ b/core/src/main/java/com/amplifyframework/core/model/ModelReference.kt @@ -20,7 +20,6 @@ import com.amplifyframework.annotations.InternalAmplifyApi import com.amplifyframework.core.Consumer import com.amplifyframework.core.NullableConsumer - /** * Base interface for a class holding a Model type */ @@ -32,7 +31,7 @@ sealed interface ModelReference { /** * A reference holder that holds an in-memory model */ -interface LoadedModelReference: ModelReference { +interface LoadedModelReference : ModelReference { /** The loaded model value */ val value: M? @@ -41,7 +40,7 @@ interface LoadedModelReference: ModelReference { /** * A reference holder that allows lazily loading a model */ -interface LazyModelReference: ModelReference { +interface LazyModelReference : ModelReference { /** * Load the model instance represented by this LazyModelReference. @@ -60,4 +59,4 @@ interface LazyModelReference: ModelReference { * @param onError Called when loading the model fails. */ fun fetchModel(onSuccess: NullableConsumer, onError: Consumer) -} \ No newline at end of file +} diff --git a/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryExtensions.kt b/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.kt similarity index 96% rename from core/src/main/java/com/amplifyframework/core/model/SchemaRegistryExtensions.kt rename to core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.kt index dc0aeed509..56f3e6afd4 100644 --- a/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryExtensions.kt +++ b/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.kt @@ -25,7 +25,8 @@ internal object SchemaRegistryUtils { fun registerSchema( modelSchemaMap: MutableMap, modelName: String, - modelSchema: ModelSchema) { + modelSchema: ModelSchema + ) { if (modelSchema.modelClass.getAnnotation(ModelConfig::class.java)?.hasLazySupport == true) { throw IrRecoverableException( "Unsupported model type. Lazy model types are not yet supported on DataStore.", @@ -35,4 +36,4 @@ internal object SchemaRegistryUtils { modelSchemaMap[modelName] = modelSchema } } -} \ No newline at end of file +} From 0cb41505b933069ce75b1f111ddd1e562869a27d Mon Sep 17 00:00:00 2001 From: tjroach Date: Thu, 21 Sep 2023 10:02:36 -0400 Subject: [PATCH 066/100] fix tests --- .../serde-for-blog-in-serialized-model.json | 16 ++++++------ ...serde-for-comment-in-serialized-model.json | 21 ++++++++++------ ...serde-for-meeting-in-serialized-model.json | 24 +++++++++--------- ...nested-custom-type-se-deserialization.json | 14 ++++++----- .../aws/MultiAuthSubscriptionOperationTest.kt | 1 + .../core/model/SchemaRegistryUtils.kt | 25 +++++++++++++------ 6 files changed, 61 insertions(+), 40 deletions(-) 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 681dce3456..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,8 +16,8 @@ "isArray": false, "isEnum": false, "isModel": false, - "isLazyModel": false, - "isLazyList": false, + "isModelReference": false, + "isModelList": false, "authRules": [] }, "name": { @@ -30,8 +30,8 @@ "isArray": false, "isEnum": false, "isModel": false, - "isLazyModel": false, - "isLazyList": false, + "isModelReference": false, + "isModelList": false, "authRules": [] }, "owner": { @@ -44,8 +44,8 @@ "isArray": false, "isEnum": false, "isModel": true, - "isLazyModel": false, - "isLazyList": false, + "isModelReference": false, + "isModelList": false, "authRules": [] }, "posts": { @@ -58,8 +58,8 @@ "isArray": true, "isEnum": false, "isModel": false, - "isLazyModel": false, - "isLazyList": 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 be508126a2..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,7 +42,8 @@ "isCustomType": false, "isReadOnly": true, "isModel": false, - "isLazyModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "createdAt", "isEnum": false, @@ -55,7 +56,8 @@ "isCustomType": false, "isReadOnly": false, "isModel": true, - "isLazyModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "post", "isEnum": false, @@ -68,7 +70,8 @@ "isCustomType": false, "isReadOnly": false, "isModel": false, - "isLazyModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "description", "isEnum": false, @@ -81,7 +84,8 @@ "isCustomType": false, "isReadOnly": false, "isModel": false, - "isLazyModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "title", "isEnum": false, @@ -94,7 +98,8 @@ "isCustomType": false, "isReadOnly": false, "isModel": false, - "isLazyModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "content", "isEnum": false, @@ -107,7 +112,8 @@ "isCustomType": false, "isReadOnly": false, "isModel": false, - "isLazyModel": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "likes", "isEnum": false, @@ -120,7 +126,8 @@ "isCustomType": false, "isReadOnly": true, "isModel": false, - "isLazyModel": 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 d99364c91b..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,8 +17,8 @@ "isArray": false, "isEnum": false, "isModel": false, - "isLazyModel": false, - "isLazyList": false, + "isModelReference": false, + "isModelList": false, "authRules": [] }, "dateTime": { @@ -31,8 +31,8 @@ "isArray": false, "isEnum": false, "isModel": false, - "isLazyModel": false, - "isLazyList": false, + "isModelReference": false, + "isModelList": false, "authRules": [] }, "id": { @@ -45,8 +45,8 @@ "isArray": false, "isEnum": false, "isModel": false, - "isLazyModel": false, - "isLazyList": false, + "isModelReference": false, + "isModelList": false, "authRules": [] }, "name": { @@ -59,8 +59,8 @@ "isArray": false, "isEnum": false, "isModel": false, - "isLazyModel": false, - "isLazyList": false, + "isModelReference": false, + "isModelList": false, "authRules": [] }, "time": { @@ -73,8 +73,8 @@ "isArray": false, "isEnum": false, "isModel": false, - "isLazyModel": false, - "isLazyList": false, + "isModelReference": false, + "isModelList": false, "authRules": [] }, "timestamp": { @@ -87,8 +87,8 @@ "isArray": false, "isEnum": false, "isModel": false, - "isLazyModel": false, - "isLazyList": 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 b29c86e510..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,8 +176,8 @@ "isCustomType": true, "isReadOnly": false, "isModel": false, - "isLazyModel": false, - "isLazyList": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "contact", "isEnum": false, @@ -188,8 +190,8 @@ "isCustomType": false, "isReadOnly": false, "isModel": false, - "isLazyModel": false, - "isLazyList": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "fullName", "isEnum": false, @@ -202,8 +204,8 @@ "isCustomType": false, "isReadOnly": false, "isModel": false, - "isLazyModel": false, - "isLazyList": false, + "isModelReference": false, + "isModelList": false, "authRules": [], "name": "id", "isEnum": false, 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/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.kt b/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.kt index 56f3e6afd4..a61b50e751 100644 --- a/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.kt +++ b/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.kt @@ -2,6 +2,7 @@ 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 { @@ -27,13 +28,23 @@ internal object SchemaRegistryUtils { modelName: String, modelSchema: ModelSchema ) { - 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." - ) - } else { - modelSchemaMap[modelName] = 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 on. Since the builder is public and + modelClass isn't a mandatory builder param, we add this block for additional safety. + */ } + + modelSchemaMap[modelName] = modelSchema } } From 2329d7ae75d7295886139bc0386494789f86119d Mon Sep 17 00:00:00 2001 From: tjroach Date: Thu, 21 Sep 2023 12:15:39 -0400 Subject: [PATCH 067/100] Code cleanup to ensure public contracts are correct --- .../api/aws/SelectionSet.java | 14 +++++---- ...emaRegistry.kt => AWSApiSchemaRegistry.kt} | 2 +- .../api/aws/ApiLazyModelReference.kt | 1 - .../api/aws/AppSyncGraphQLRequestFactory.kt | 18 ++++++------ .../api/aws/ModelDeserializer.kt | 2 +- .../api/aws/ModelListDeserializers.kt | 2 +- .../api/aws/ModelProviderLocator.java | 4 +-- .../api/graphql/model/ModelMutation.kt | 10 +++---- .../api/graphql/model/ModelQuery.kt | 12 ++++---- .../api/graphql/model/ModelSubscription.kt | 8 ++--- .../api/aws/AWSApiPluginTest.java | 5 ---- ...AppSyncGraphQLRequestAndResponseCPKTest.kt | 9 ------ .../aws/GsonGraphQLResponseFactoryTest.java | 16 ---------- .../core/model/ModelPropertyPath.kt | 29 ++++--------------- 14 files changed, 44 insertions(+), 88 deletions(-) rename aws-api/src/main/java/com/amplifyframework/api/aws/{AWSSchemaRegistry.kt => AWSApiSchemaRegistry.kt} (97%) 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 fdc7a6a6fb..830f0f9985 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 @@ -194,7 +194,7 @@ static final class Builder { Builder() { } - public Builder value(@Nullable String value) { + Builder value(@Nullable String value) { this.value = value; return Builder.this; } @@ -327,9 +327,14 @@ private Set getModelFields( if (depth >= 1) { ParameterizedType listType = (ParameterizedType) field.getGenericType(); Class listTypeClass = (Class) listType.getActualTypeArguments()[0]; - Set fields = wrapPagination(getModelFields(listTypeClass, - depth - 1, - operation, false)); + Set fields = wrapPagination( + getModelFields( + listTypeClass, + depth - 1, + operation, + false + ) + ); result.add(new SelectionSet(fieldName, fields)); } } else if (ModelReference.class.isAssignableFrom(field.getType())) { @@ -357,7 +362,6 @@ private Set getModelFields( for (String fieldName : requestOptions.modelMetaFields()) { result.add(new SelectionSet(fieldName)); } - return result; } diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/AWSSchemaRegistry.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiSchemaRegistry.kt similarity index 97% rename from aws-api/src/main/java/com/amplifyframework/api/aws/AWSSchemaRegistry.kt rename to aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiSchemaRegistry.kt index cb2c9d2ceb..0edbb3f25f 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AWSSchemaRegistry.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiSchemaRegistry.kt @@ -5,7 +5,7 @@ import com.amplifyframework.core.model.CustomTypeSchema import com.amplifyframework.core.model.Model import com.amplifyframework.core.model.ModelSchema -internal object AWSSchemaRegistry { +internal object AWSApiSchemaRegistry { private val modelSchemaMap = mutableMapOf() // CustomType name => CustomTypeSchema map private val customTypeSchemaMap = mutableMapOf() 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 index 0b6211d79f..9a0e6c7235 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt @@ -65,7 +65,6 @@ internal class ApiLazyModelReference internal constructor( onSuccess.accept(cached.value) } - // To ensure we only have 1 live request at a time, we jump to our own single threaded scope callbackScope.launch { try { val model = fetchInternal() 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 index 34b50a7db3..fdcc81cdf4 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.kt @@ -76,7 +76,7 @@ object AppSyncGraphQLRequestFactory { * `objectId`. * @param modelClass the model class. * @param objectId the model identifier. - * @param includes lambda returning list of associations that should be included in the selection set + * @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 @@ -154,7 +154,7 @@ object AppSyncGraphQLRequestFactory { * `modelIdentifier`. * @param modelClass the model class. * @param modelIdentifier the model identifier. - * @param includes lambda returning list of associations that should be included in the selection set + * @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 @@ -257,7 +257,7 @@ object AppSyncGraphQLRequestFactory { * given predicate. * @param modelClass the model class. * @param predicate the model predicate. - * @param includes lambda returning list of associations that should be included in the selection set + * @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 @@ -308,7 +308,7 @@ object AppSyncGraphQLRequestFactory { * @param modelClass the model class. * @param predicate the predicate for filtering. * @param limit the page size/limit. - * @param includes lambda returning list of associations that should be included in the selection set + * @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 @@ -336,7 +336,7 @@ object AppSyncGraphQLRequestFactory { * @param predicate the predicate for filtering. * @param limit the page size/limit. * @param responseType the response type - * @param includes lambda returning list of associations that should be included in the selection set + * @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 @@ -409,7 +409,7 @@ object AppSyncGraphQLRequestFactory { * @param model the model instance. * @param predicate the model predicate. * @param type the mutation type. - * @param includes lambda returning list of associations that should be included in the selection set + * @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 @@ -499,7 +499,7 @@ object AppSyncGraphQLRequestFactory { * 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 associations that should be included in the selection set + * @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 @@ -544,12 +544,12 @@ object AppSyncGraphQLRequestFactory { operationType: Operation, includes: ((P) -> List) ): SelectionSet { - includes(ModelPath.getRootPath(modelClass)).let { associations -> + includes(ModelPath.getRootPath(modelClass)).let { relationships -> return SelectionSet.builder() .modelClass(modelClass) .operation(operationType) .requestOptions(ApiGraphQLRequestOptions()) - .includeRelationships(associations) + .includeRelationships(relationships) .build() } } diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt index 75ec87c41c..2c4db17d2e 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt @@ -32,7 +32,7 @@ internal class ModelDeserializer( fieldToUpdate.isAccessible = true if (fieldToUpdate.get(parent) == null) { val lazyField = fieldMap.value - val lazyFieldModelSchema = AWSSchemaRegistry.getModelSchemaForModelClass(lazyField.targetType) + val lazyFieldModelSchema = AWSApiSchemaRegistry.getModelSchemaForModelClass(lazyField.targetType) val lazyFieldTargetNames = lazyFieldModelSchema .associations diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt index 48c057f085..2037f4821e 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt @@ -44,7 +44,7 @@ internal class ModelReferenceDeserializer(val apiName: String?) : val jsonObject = getJsonObject(json) - val predicateKeyMap = AWSSchemaRegistry + val predicateKeyMap = AWSApiSchemaRegistry .getModelSchemaForModelClass(type) .primaryIndexFields .associateWith { jsonObject[it] } 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 index e436435eb9..3a82690003 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelProviderLocator.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelProviderLocator.java @@ -36,7 +36,7 @@ * needed to begin using the API category. This utility class is used from * {@link com.amplifyframework.api.aws.AWSApiPlugin#initialize}. */ -public final class ModelProviderLocator { +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"; @@ -48,7 +48,7 @@ private ModelProviderLocator() {} * @return The code-generated model provider, if found * @throws ApiException If unable to find the code-generated model provider */ - public static ModelProvider locate() throws ApiException { + static ModelProvider locate() throws ApiException { return locate(DEFAULT_MODEL_PROVIDER_CLASS_NAME); } 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 index 13096510ec..6c5c47ab9a 100644 --- 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 @@ -42,7 +42,7 @@ object ModelMutation { /** * 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 associations that should be included in the selection set + * @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. @@ -76,7 +76,7 @@ object ModelMutation { * 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 associations that should be included in the selection set + * @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. @@ -108,7 +108,7 @@ object ModelMutation { /** * 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 associations that should be included in the selection set + * @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. @@ -142,7 +142,7 @@ object ModelMutation { * 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 associations that should be included in the selection set + * @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. @@ -174,7 +174,7 @@ object ModelMutation { /** * 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 associations that should be included in the selection set + * @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. 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 index d36dca4ce5..02481cddae 100644 --- 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 @@ -53,7 +53,7 @@ object ModelQuery { * variables based on given `modelId`. * @param modelType the model class. * @param modelId the model identifier. - * @param includes lambda returning list of associations that should be included in the selection set + * @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. @@ -90,7 +90,7 @@ object ModelQuery { * variables based on given `modelIdentifier`. * @param modelType the model class. * @param modelIdentifier the model identifier. - * @param includes lambda returning list of associations that should be included in the selection set + * @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. @@ -127,7 +127,7 @@ object ModelQuery { * for filtering based on the given predicate. * @param modelType the model class. * @param predicate the predicate for filtering. - * @param includes lambda returning list of associations that should be included in the selection set + * @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. @@ -158,7 +158,7 @@ object ModelQuery { * 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 associations that should be included in the selection set + * @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. @@ -207,7 +207,7 @@ object ModelQuery { * @param modelType the model class. * @param predicate the predicate for filtering. * @param pagination the pagination settings. - * @param includes lambda returning list of associations that should be included in the selection set + * @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. @@ -257,7 +257,7 @@ object ModelQuery { * * @param modelType the model class. * @param pagination the pagination settings. - * @param includes lambda returning list of associations that should be included in the selection set + * @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. 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 index 88cf87e3d2..c5d883c0e7 100644 --- 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 @@ -44,7 +44,7 @@ object ModelSubscription { * 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 associations that should be included in the selection set + * @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. @@ -72,7 +72,7 @@ object ModelSubscription { /** * Creates a subscription request of type [SubscriptionType.ON_CREATE]. * @param modelType the model class. - * @param includes lambda returning list of associations that should be included in the selection set + * @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. @@ -100,7 +100,7 @@ object ModelSubscription { /** * Creates a subscription request of type [SubscriptionType.ON_DELETE]. * @param modelType the model class. - * @param includes lambda returning list of associations that should be included in the selection set + * @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. @@ -128,7 +128,7 @@ object ModelSubscription { /** * Creates a subscription request of type [SubscriptionType.ON_UPDATE]. * @param modelType the model class. - * @param includes lambda returning list of associations that should be included in the selection set + * @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. 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 874123b481..97b998e6d4 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 @@ -31,10 +31,8 @@ import com.amplifyframework.api.graphql.model.ModelPagination; import com.amplifyframework.api.graphql.model.ModelQuery; import com.amplifyframework.core.Consumer; -import com.amplifyframework.core.model.SchemaRegistry; import com.amplifyframework.hub.HubChannel; import com.amplifyframework.hub.HubEvent; -import com.amplifyframework.testmodels.commentsblog.AmplifyModelProvider; import com.amplifyframework.testmodels.commentsblog.BlogOwner; import com.amplifyframework.testutils.Await; import com.amplifyframework.testutils.HubAccumulator; @@ -86,13 +84,11 @@ public final class AWSApiPluginTest { /** * Sets up the test. - * @throws AmplifyException on SchemaRegistry failure * @throws IOException On failure to start web server * @throws JSONException On failure to arrange configuration JSON */ @Before public void setup() throws AmplifyException, IOException, JSONException { - SchemaRegistry.instance().register(AmplifyModelProvider.getInstance().models()); webServer = new MockWebServer(); webServer.start(8080); baseUrl = webServer.url("/"); @@ -143,7 +139,6 @@ public String getUsername() { @After public void cleanup() throws IOException { webServer.shutdown(); - SchemaRegistry.instance().clear(); } /** diff --git a/aws-api/src/test/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestAndResponseCPKTest.kt b/aws-api/src/test/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestAndResponseCPKTest.kt index d5fb0e1387..fe994ff8b7 100644 --- a/aws-api/src/test/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestAndResponseCPKTest.kt +++ b/aws-api/src/test/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestAndResponseCPKTest.kt @@ -19,10 +19,8 @@ import com.amplifyframework.api.graphql.GraphQLResponse import com.amplifyframework.api.graphql.MutationType import com.amplifyframework.api.graphql.model.ModelMutation import com.amplifyframework.api.graphql.model.ModelQuery -import com.amplifyframework.core.model.SchemaRegistry import com.amplifyframework.core.model.query.predicate.QueryPredicates import com.amplifyframework.core.model.temporal.Temporal -import com.amplifyframework.testmodels.cpk.AmplifyModelProvider import com.amplifyframework.testmodels.cpk.Blog import com.amplifyframework.testmodels.cpk.Blog.BlogIdentifier import com.amplifyframework.testmodels.cpk.Comment @@ -31,7 +29,6 @@ import com.amplifyframework.testmodels.cpk.Post import com.amplifyframework.testmodels.cpk.Post.PostIdentifier import com.amplifyframework.testutils.Resources import com.amplifyframework.util.GsonFactory -import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull @@ -52,16 +49,10 @@ class AppSyncGraphQLRequestAndResponseCPKTest { @Before fun setup() { - SchemaRegistry.instance().register(AmplifyModelProvider.getInstance().models()) val gson = GsonFactory.instance() responseFactory = GsonGraphQLResponseFactory(gson) } - @After - fun tearDown() { - SchemaRegistry.instance().clear() - } - @Test fun create_with_cpk() { // GIVEN diff --git a/aws-api/src/test/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactoryTest.java b/aws-api/src/test/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactoryTest.java index 4390dc18da..ea3d19d21d 100644 --- a/aws-api/src/test/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactoryTest.java +++ b/aws-api/src/test/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactoryTest.java @@ -23,8 +23,6 @@ import com.amplifyframework.api.graphql.GraphQLResponse; import com.amplifyframework.api.graphql.PaginatedResult; import com.amplifyframework.api.graphql.QueryType; -import com.amplifyframework.core.model.ModelSchema; -import com.amplifyframework.core.model.SchemaRegistry; import com.amplifyframework.core.model.temporal.Temporal; import com.amplifyframework.testmodels.meeting.Meeting; import com.amplifyframework.testutils.Resources; @@ -34,7 +32,6 @@ import com.google.gson.Gson; import org.json.JSONException; import org.json.JSONObject; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -63,26 +60,13 @@ public final class GsonGraphQLResponseFactoryTest { /** * Set up the object under test, a GsonGraphQLResponseFactory. - * @throws AmplifyException on SchemaRegistry failure */ @Before public void setup() throws AmplifyException { - SchemaRegistry registry = SchemaRegistry.instance(); - registry.register(Todo.class.getSimpleName(), ModelSchema.fromModelClass(Todo.class)); - registry.register(Meeting.class.getSimpleName(), ModelSchema.fromModelClass(Meeting.class)); - Gson gson = GsonFactory.instance(); responseFactory = new GsonGraphQLResponseFactory(gson); } - /** - * Clear schema registry. - */ - @After - public void tearDown() { - SchemaRegistry.instance().clear(); - } - /** * Validates that response with null content does not break * the response object builder. Null data and/or error will diff --git a/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt b/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt index ec84ae85a4..92c22a9869 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt +++ b/core/src/main/java/com/amplifyframework/core/model/ModelPropertyPath.kt @@ -34,25 +34,6 @@ interface PropertyPath { * @return the property metadata, that contains the name and a reference to its parent. */ fun getMetadata(): PropertyPathMetadata - - /** - * Returns the full path of the property, e.g. `"post.author.name"`. - * - * @param includesRoot whether it should include the root name or not. It's `false` by default. - * @return path as a string - */ - fun getKeyPath(includesRoot: Boolean = false): String { - var metadata = getMetadata() - val path = mutableListOf() - while (metadata.parent != null) { - path.add(index = 0, element = metadata.name) - metadata = metadata.parent!!.getMetadata() - } - if (includesRoot) { - path.add(index = 0, metadata.name) - } - return path.joinToString(separator = ".") - } } /** @@ -137,17 +118,19 @@ open class ModelPath protected constructor( } /** - * Function used to define which associations are included in the selection set + * 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 - * getById("id") { includes(it.comments) } + * ModelQuery.get(Post::class.java, "id") { postPath -> + * includes(postPath.comments) + * } * ``` * - * @param associations the associations that should be included + * @param relationships the relationships that should be included * @return the passed associations as an array */ -fun includes(vararg associations: PropertyContainerPath) = listOf(*associations) +fun includes(vararg relationships: PropertyContainerPath) = listOf(*relationships) From a16001d312bb6668acef55a2bc5a5205dfd11c07 Mon Sep 17 00:00:00 2001 From: tjroach Date: Thu, 21 Sep 2023 12:47:17 -0400 Subject: [PATCH 068/100] checkstyle fixes --- .../java/com/amplifyframework/api/aws/GraphQLRequestHelper.java | 2 +- .../java/com/amplifyframework/api/aws/AWSApiPluginTest.java | 1 + .../api/aws/GsonGraphQLResponseFactoryTest.java | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) 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 6c80b8e252..97ec4f0068 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 @@ -232,7 +232,7 @@ private static Map extractFieldLevelData( if (modelField.isModelReference() && fieldValue != null) { ModelReference modelReference = (ModelReference) fieldValue; if (modelReference instanceof LoadedModelReference) { - underlyingFieldValue = ((LoadedModelReference)modelReference).getValue(); + underlyingFieldValue = ((LoadedModelReference) modelReference).getValue(); } identifiersIfLazyModel = modelReference.getIdentifier(); } 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 97b998e6d4..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 @@ -86,6 +86,7 @@ public final class AWSApiPluginTest { * Sets up the test. * @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 AmplifyException, IOException, JSONException { diff --git a/aws-api/src/test/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactoryTest.java b/aws-api/src/test/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactoryTest.java index ea3d19d21d..f4eec9df72 100644 --- a/aws-api/src/test/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactoryTest.java +++ b/aws-api/src/test/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactoryTest.java @@ -62,7 +62,7 @@ public final class GsonGraphQLResponseFactoryTest { * Set up the object under test, a GsonGraphQLResponseFactory. */ @Before - public void setup() throws AmplifyException { + public void setup() { Gson gson = GsonFactory.instance(); responseFactory = new GsonGraphQLResponseFactory(gson); } From 1f4db83030b27d42dde2045a0acd166a2db3c3e6 Mon Sep 17 00:00:00 2001 From: tjroach Date: Fri, 22 Sep 2023 11:48:34 -0400 Subject: [PATCH 069/100] test additions --- .../api/aws/ApiGraphQLRequestOptionsTest.kt | 19 ++ .../api/aws/AWSApiSchemaRegistry.kt | 22 +- .../api/aws/GsonGraphQLResponseFactory.java | 6 +- .../api/aws/ModelDeserializer.kt | 5 +- .../api/aws/ModelListDeserializers.kt | 7 +- .../api/aws/AWSApiSchemaRegistryTest.kt | 41 ++++ core/build.gradle.kts | 1 + .../core/model/ModelField.java | 10 + .../core/model/ModelSchema.java | 1 - .../core/model/annotations/HasOne.java | 7 - .../model/LoadedModelReferenceImplTest.kt | 27 +++ .../core/model/ModelPathTest.kt | 44 ++++ .../core/model/ModelSchemaTest.java | 102 ++++++++ .../core/model/SchemaRegistryUtilsTest.kt | 69 ++++++ .../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/LazyTypeTest.kt | 29 +++ 22 files changed, 1128 insertions(+), 33 deletions(-) create mode 100644 aws-api-appsync/src/test/java/com/amplifyframework/api/aws/ApiGraphQLRequestOptionsTest.kt create mode 100644 aws-api/src/test/java/com/amplifyframework/api/aws/AWSApiSchemaRegistryTest.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/test/java/com/amplifyframework/testmodels/lazy/LazyTypeTest.kt 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..be8537a801 --- /dev/null +++ b/aws-api-appsync/src/test/java/com/amplifyframework/api/aws/ApiGraphQLRequestOptionsTest.kt @@ -0,0 +1,19 @@ +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/src/main/java/com/amplifyframework/api/aws/AWSApiSchemaRegistry.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiSchemaRegistry.kt index 0edbb3f25f..77c18aacd8 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiSchemaRegistry.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiSchemaRegistry.kt @@ -1,21 +1,15 @@ package com.amplifyframework.api.aws import com.amplifyframework.api.ApiException -import com.amplifyframework.core.model.CustomTypeSchema import com.amplifyframework.core.model.Model import com.amplifyframework.core.model.ModelSchema -internal object AWSApiSchemaRegistry { - private val modelSchemaMap = mutableMapOf() - // CustomType name => CustomTypeSchema map - private val customTypeSchemaMap = mutableMapOf() - - init { +internal class AWSApiSchemaRegistry { + private val modelSchemaMap: MutableMap by lazy { val modelProvider = ModelProviderLocator.locate() - register(modelProvider.modelSchemas(), modelProvider.customTypeSchemas()) + modelProvider.modelSchemas() } - @Synchronized fun getModelSchemaForModelClass(classSimpleName: String): ModelSchema { return modelSchemaMap[classSimpleName] ?: throw ApiException( "Model type of `$classSimpleName` not found.", @@ -23,17 +17,7 @@ internal object AWSApiSchemaRegistry { ) } - @Synchronized fun getModelSchemaForModelClass(modelClass: Class): ModelSchema { return getModelSchemaForModelClass(modelClass.simpleName) } - - @Synchronized - fun register( - modelSchemas: Map, - customTypeSchemas: Map - ) { - modelSchemaMap.putAll(modelSchemas) - customTypeSchemaMap.putAll(customTypeSchemas) - } } 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 1f57e858a3..da91550fce 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 @@ -48,6 +48,8 @@ final class GsonGraphQLResponseFactory implements GraphQLResponse.Factory { private final Gson gson; + private final AWSApiSchemaRegistry schemaRegistry = new AWSApiSchemaRegistry(); + GsonGraphQLResponseFactory() { this(GsonFactory.instance()); } @@ -93,14 +95,14 @@ public GraphQLResponse buildResponse( ) .registerTypeAdapter( ModelReference.class, - new ModelReferenceDeserializer(apiName) + new ModelReferenceDeserializer(apiName, schemaRegistry) ) .create(); Gson modelDeserializerGson = responseGson.newBuilder() .registerTypeHierarchyAdapter( Model.class, - new ModelDeserializer(responseGson, apiName) + new ModelDeserializer(responseGson, apiName, schemaRegistry) ) .create(); diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt index 2c4db17d2e..f823c9460e 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt @@ -19,7 +19,8 @@ import java.lang.reflect.Type */ internal class ModelDeserializer( private val responseGson: Gson, - private val apiName: String? + private val apiName: String?, + private val schemaRegistry: AWSApiSchemaRegistry ) : JsonDeserializer { override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Model { @@ -32,7 +33,7 @@ internal class ModelDeserializer( fieldToUpdate.isAccessible = true if (fieldToUpdate.get(parent) == null) { val lazyField = fieldMap.value - val lazyFieldModelSchema = AWSApiSchemaRegistry.getModelSchemaForModelClass(lazyField.targetType) + val lazyFieldModelSchema = schemaRegistry.getModelSchemaForModelClass(lazyField.targetType) val lazyFieldTargetNames = lazyFieldModelSchema .associations diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt index 2037f4821e..4f0c694c4a 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt @@ -30,7 +30,10 @@ import java.lang.reflect.Type const val ITEMS_KEY = "items" const val NEXT_TOKEN_KEY = "nextToken" -internal class ModelReferenceDeserializer(val apiName: String?) : +internal class ModelReferenceDeserializer( + val apiName: String?, + private val schemaRegistry: AWSApiSchemaRegistry +) : JsonDeserializer> { @Throws(JsonParseException::class) override fun deserialize( @@ -44,7 +47,7 @@ internal class ModelReferenceDeserializer(val apiName: String?) : val jsonObject = getJsonObject(json) - val predicateKeyMap = AWSApiSchemaRegistry + val predicateKeyMap = schemaRegistry .getModelSchemaForModelClass(type) .primaryIndexFields .associateWith { jsonObject[it] } 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..f04fadf1a9 --- /dev/null +++ b/aws-api/src/test/java/com/amplifyframework/api/aws/AWSApiSchemaRegistryTest.kt @@ -0,0 +1,41 @@ +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/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/core/model/ModelField.java b/core/src/main/java/com/amplifyframework/core/model/ModelField.java index 1f40d22d09..44fb34a0cf 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelField.java +++ b/core/src/main/java/com/amplifyframework/core/model/ModelField.java @@ -224,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; } @@ -246,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; } @@ -261,6 +269,8 @@ public String toString() { ", isArray=" + isArray + ", isEnum=" + isEnum + ", isModel=" + isModel + + ", isModelReference=" + isModelReference + + ", isModelList=" + isModelList + ", isCustomType=" + isCustomType + '}'; } 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 ffbf09d012..0a771e83f5 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java +++ b/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java @@ -258,7 +258,6 @@ private static ModelAssociation createModelAssociation(Field field) { return ModelAssociation.builder() .name(HasOne.class.getSimpleName()) .associatedName(association.associatedWith()) - .targetNames(association.targetNames()) .associatedType(association.type().getSimpleName()) .build(); } 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 c00322f11c..5b2ae729fe 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,11 +49,4 @@ * @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/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..042c0de7e5 --- /dev/null +++ b/core/src/test/java/com/amplifyframework/core/model/LoadedModelReferenceImplTest.kt @@ -0,0 +1,27 @@ +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..00a696e5ca --- /dev/null +++ b/core/src/test/java/com/amplifyframework/core/model/ModelPathTest.kt @@ -0,0 +1,44 @@ +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) + } +} \ No newline at end of file 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..6621b04ed8 --- /dev/null +++ b/core/src/test/java/com/amplifyframework/core/model/SchemaRegistryUtilsTest.kt @@ -0,0 +1,69 @@ +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) + } +} \ No newline at end of file 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/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..aee4c78907 --- /dev/null +++ b/testmodels/src/test/java/com/amplifyframework/testmodels/lazy/LazyTypeTest.kt @@ -0,0 +1,29 @@ +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 ?: false + ) + } +} From f282d1feb9441adc09c1553664ed22347d5626a1 Mon Sep 17 00:00:00 2001 From: tjroach Date: Fri, 22 Sep 2023 13:31:55 -0400 Subject: [PATCH 070/100] Add copyright notices --- .../api/aws/ApiGraphQLRequestOptionsTest.kt | 15 +++++++++++++++ .../api/aws/AWSApiSchemaRegistry.kt | 15 +++++++++++++++ .../api/aws/ApiModelListTypes.kt | 15 +++++++++++++++ .../api/aws/ModelDeserializer.kt | 15 +++++++++++++++ .../api/aws/AWSApiSchemaRegistryTest.kt | 17 +++++++++++++++++ .../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 +++++++++++++++ .../amplifyframework/core/model/ModelList.kt | 15 +++++++++++++++ .../core/model/SchemaRegistryUtils.kt | 15 +++++++++++++++ .../core/model/LoadedModelReferenceImplTest.kt | 15 +++++++++++++++ .../core/model/ModelPathTest.kt | 17 +++++++++++++++++ .../core/model/SchemaRegistryUtilsTest.kt | 15 +++++++++++++++ .../amplifyframework/testutils/RepeatRule.kt | 15 +++++++++++++++ 17 files changed, 259 insertions(+) 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 index be8537a801..0a1d43b5be 100644 --- 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 @@ -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.api.aws import org.junit.Assert.assertEquals 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 index 77c18aacd8..d8ac2f55d1 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiSchemaRegistry.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiSchemaRegistry.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.api.aws import com.amplifyframework.api.ApiException 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 index 64de7edc88..2a52662092 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.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.api.aws import com.amplifyframework.AmplifyException diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt index f823c9460e..9e95584af7 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.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.api.aws import com.amplifyframework.core.model.Model 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 index f04fadf1a9..76624dce43 100644 --- a/aws-api/src/test/java/com/amplifyframework/api/aws/AWSApiSchemaRegistryTest.kt +++ b/aws-api/src/test/java/com/amplifyframework/api/aws/AWSApiSchemaRegistryTest.kt @@ -1,3 +1,20 @@ +/* + * + * * 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 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/core/src/main/java/com/amplifyframework/core/model/ModelList.kt b/core/src/main/java/com/amplifyframework/core/model/ModelList.kt index a828a45b86..457faf1b8f 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelList.kt +++ b/core/src/main/java/com/amplifyframework/core/model/ModelList.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.core.model import com.amplifyframework.AmplifyException diff --git a/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.kt b/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.kt index a61b50e751..fd3e5ab2ce 100644 --- a/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.kt +++ b/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.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.core.model import com.amplifyframework.core.model.annotations.ModelConfig diff --git a/core/src/test/java/com/amplifyframework/core/model/LoadedModelReferenceImplTest.kt b/core/src/test/java/com/amplifyframework/core/model/LoadedModelReferenceImplTest.kt index 042c0de7e5..80eec0db0c 100644 --- a/core/src/test/java/com/amplifyframework/core/model/LoadedModelReferenceImplTest.kt +++ b/core/src/test/java/com/amplifyframework/core/model/LoadedModelReferenceImplTest.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.core.model import com.amplifyframework.testmodels.lazy.Comment diff --git a/core/src/test/java/com/amplifyframework/core/model/ModelPathTest.kt b/core/src/test/java/com/amplifyframework/core/model/ModelPathTest.kt index 00a696e5ca..be51da5cf2 100644 --- a/core/src/test/java/com/amplifyframework/core/model/ModelPathTest.kt +++ b/core/src/test/java/com/amplifyframework/core/model/ModelPathTest.kt @@ -1,3 +1,20 @@ +/* + * + * * 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 diff --git a/core/src/test/java/com/amplifyframework/core/model/SchemaRegistryUtilsTest.kt b/core/src/test/java/com/amplifyframework/core/model/SchemaRegistryUtilsTest.kt index 6621b04ed8..a8eac285ad 100644 --- a/core/src/test/java/com/amplifyframework/core/model/SchemaRegistryUtilsTest.kt +++ b/core/src/test/java/com/amplifyframework/core/model/SchemaRegistryUtilsTest.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.core.model import com.amplifyframework.datastore.DataStoreException.IrRecoverableException diff --git a/testutils/src/main/java/com/amplifyframework/testutils/RepeatRule.kt b/testutils/src/main/java/com/amplifyframework/testutils/RepeatRule.kt index 482f95b438..b56a38ddd5 100644 --- a/testutils/src/main/java/com/amplifyframework/testutils/RepeatRule.kt +++ b/testutils/src/main/java/com/amplifyframework/testutils/RepeatRule.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.testutils /* From 1fd4e1f1afd916f5e237e89d4292925feebdcd24 Mon Sep 17 00:00:00 2001 From: tjroach Date: Fri, 22 Sep 2023 13:33:07 -0400 Subject: [PATCH 071/100] lint --- .../core/model/ModelPathTest.kt | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/core/src/test/java/com/amplifyframework/core/model/ModelPathTest.kt b/core/src/test/java/com/amplifyframework/core/model/ModelPathTest.kt index be51da5cf2..8c5b8ad44c 100644 --- a/core/src/test/java/com/amplifyframework/core/model/ModelPathTest.kt +++ b/core/src/test/java/com/amplifyframework/core/model/ModelPathTest.kt @@ -1,18 +1,16 @@ /* + * Copyright 2023 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. - * * 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. + * 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 @@ -58,4 +56,4 @@ class ModelPathTest { fun get_root_path_fails_on_non_lazy_supported_model() { ModelPath.getRootPath(Todo::class.java) } -} \ No newline at end of file +} From 9702183eac1b975eed5c509b310375bed8615902 Mon Sep 17 00:00:00 2001 From: tjroach Date: Fri, 22 Sep 2023 16:09:04 -0400 Subject: [PATCH 072/100] tests --- .../api/graphql/model/ModelQuery.kt | 4 +- .../graphql/model/ModelMutationTest.kt | 221 ++++++++++++++++ .../graphql/model/ModelQueryTest.kt | 244 ++++++++++++++++++ .../graphql/model/ModelSubscriptionTest.kt | 172 ++++++++++++ .../core/model/SchemaRegistryUtilsTest.kt | 2 +- 5 files changed, 640 insertions(+), 3 deletions(-) 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 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 index 02481cddae..fc51cf39ee 100644 --- 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 @@ -116,7 +116,7 @@ object ModelQuery { @JvmStatic fun list( modelType: Class, - predicate: QueryPredicate = QueryPredicates.all() + predicate: QueryPredicate ): GraphQLRequest> { return AppSyncGraphQLRequestFactory.buildQuery(modelType, predicate) } @@ -135,7 +135,7 @@ object ModelQuery { @JvmStatic fun > list( modelType: Class, - predicate: QueryPredicate = QueryPredicates.all(), + predicate: QueryPredicate, includes: ((P) -> List) ): GraphQLRequest> { return AppSyncGraphQLRequestFactory.buildQuery(modelType, predicate, includes) 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..37048f84d2 --- /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..5671252d1a --- /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..9b984092e1 --- /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/core/src/test/java/com/amplifyframework/core/model/SchemaRegistryUtilsTest.kt b/core/src/test/java/com/amplifyframework/core/model/SchemaRegistryUtilsTest.kt index a8eac285ad..815467f1aa 100644 --- a/core/src/test/java/com/amplifyframework/core/model/SchemaRegistryUtilsTest.kt +++ b/core/src/test/java/com/amplifyframework/core/model/SchemaRegistryUtilsTest.kt @@ -81,4 +81,4 @@ class SchemaRegistryUtilsTest { assertEquals(1, schemaMap.size) } -} \ No newline at end of file +} From 106e6e57538ccd91afcf3167f971c7735d16a359 Mon Sep 17 00:00:00 2001 From: tjroach Date: Mon, 25 Sep 2023 10:46:09 -0400 Subject: [PATCH 073/100] sipmle selection set tests --- .../api/aws/SelectionSet.java | 2 +- .../api/aws/SelectionSetExtensions.kt | 4 +- .../api/aws/SelectionSetTest.java | 40 ++++++++++++++++++- .../selection-set-lazy-empty-includes.txt | 9 +++++ .../selection-set-lazy-with-includes.txt | 40 +++++++++++++++++++ .../testmodels/lazy/schema.graphql | 19 +++++++++ 6 files changed, 109 insertions(+), 5 deletions(-) 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 testmodels/src/main/java/com/amplifyframework/testmodels/lazy/schema.graphql 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 830f0f9985..f1ca53d069 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 @@ -243,7 +243,7 @@ public SelectionSet build() throws AmplifyException { // Relationships need to be added before wrapping pagination if (includeRelationships != null) { for (PropertyContainerPath association : includeRelationships) { - SelectionSet included = SelectionSetUtils.asSelectionSet(association, false); + SelectionSet included = SelectionSetUtils.asSelectionSetWithoutRoot(association); if (included != null) { SelectionSetUtils.merge(node, included); } 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 index c230ca0942..f8ff949e6e 100644 --- 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 @@ -44,10 +44,10 @@ internal fun SelectionSet.replaceChild(selectionSet: SelectionSet) { /** * Transforms the entire property path (walking up the tree) into a `SelectionSet`. */ -internal fun PropertyContainerPath.asSelectionSet(includeRoot: Boolean = true): 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, includeRoot).map { + val selectionSets = nodesInPath(this, false).map { // always add to lookup list so that indexes match isCollectionLookup.add(it.getMetadata().isCollection) getSelectionSet(it) 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..0ffb120f40 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,11 +1,11 @@ /* - * 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. * A copy of the License is located at * - * http://aws.amazon.com/apache2.0 + * 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 @@ -15,6 +15,8 @@ package com.amplifyframework.api.aws; +import static com.amplifyframework.core.model.ModelPropertyPathKt.includes; + import com.amplifyframework.AmplifyException; import com.amplifyframework.api.graphql.QueryType; import com.amplifyframework.core.model.AuthRule; @@ -28,6 +30,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; @@ -340,4 +343,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/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/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 From c2beec2ea99fc36a3198221fa434adebf776f3e3 Mon Sep 17 00:00:00 2001 From: tjroach Date: Mon, 25 Sep 2023 17:00:07 -0400 Subject: [PATCH 074/100] When belongsTo orHas one, fetch single value rather than get first result from list --- .../api/aws/ApiLazyModelReference.kt | 35 ++++++++++++------- .../api/aws/AppSyncGraphQLRequestFactory.kt | 2 +- .../core/model/ModelSchema.java | 1 + .../core/model/annotations/HasOne.java | 7 ++++ 4 files changed, 32 insertions(+), 13 deletions(-) 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 index 9a0e6c7235..ded8dcd02e 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt @@ -19,12 +19,12 @@ import com.amplifyframework.AmplifyException import com.amplifyframework.api.ApiException import com.amplifyframework.api.graphql.GraphQLRequest import com.amplifyframework.api.graphql.GraphQLResponse -import com.amplifyframework.api.graphql.PaginatedResult 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 kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -86,18 +86,29 @@ internal class ApiLazyModelReference internal constructor( } return try { - val resultIterator = query( - AppSyncGraphQLRequestFactory.buildQuery, M>( - clazz, - AppSyncLazyQueryPredicate().createPredicate(clazz, keyMap) - ), - apiName - ).data.items.iterator() - val value = if (resultIterator.hasNext()) { - resultIterator.next() - } else { - null + 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() + ) + + request + + val value = query( + request, + apiName + ).data cachedValue.set(LoadedValue(value)) value } catch (error: ApiException) { 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 index fdcc81cdf4..b5faa3b7ed 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncGraphQLRequestFactory.kt @@ -195,7 +195,7 @@ object AppSyncGraphQLRequestFactory { } } - private fun > buildQueryInternal( + internal fun > buildQueryInternal( modelClass: Class, includes: ((P) -> List)?, vararg variables: GraphQLRequestVariable 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 0a771e83f5..d966fd26ff 100644 --- a/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java +++ b/core/src/main/java/com/amplifyframework/core/model/ModelSchema.java @@ -257,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/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 {}; } From 0441d2dcdf286874486403600ee6e75c135ed70c Mon Sep 17 00:00:00 2001 From: tjroach Date: Tue, 26 Sep 2023 09:49:51 -0400 Subject: [PATCH 075/100] change name of lazy deserializer file --- .../aws/{ModelListDeserializers.kt => LazyTypeDeserializers.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename aws-api/src/main/java/com/amplifyframework/api/aws/{ModelListDeserializers.kt => LazyTypeDeserializers.kt} (100%) diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyTypeDeserializers.kt similarity index 100% rename from aws-api/src/main/java/com/amplifyframework/api/aws/ModelListDeserializers.kt rename to aws-api/src/main/java/com/amplifyframework/api/aws/LazyTypeDeserializers.kt From bff9003eb35c88c9c32763402146b7552f981da0 Mon Sep 17 00:00:00 2001 From: tjroach Date: Tue, 26 Sep 2023 16:05:48 -0400 Subject: [PATCH 076/100] adding ll/css tests --- .gitignore | 1 + .../api/aws/SelectionSetTest.java | 5 +- aws-api/build.gradle.kts | 14 + .../GraphQLLazyCreateInstrumentationTest.kt | 120 +++++++++ .../GraphQLLazyDeleteInstrumentationTest.kt | 118 +++++++++ .../GraphQLLazyQueryInstrumentationTest.kt | 243 ++++++++++++++++++ .../GraphQLLazyUpdateInstrumentationTest.kt | 130 ++++++++++ .../api/aws/ApiLazyModelReference.kt | 2 - .../graphql/model/ModelMutationTest.kt | 2 +- .../graphql/model/ModelQueryTest.kt | 2 +- .../graphql/model/ModelSubscriptionTest.kt | 2 +- gradle/libs.versions.toml | 4 +- .../AmplifyModelProvider.java | 68 +++++ .../lazyinstrumented/HasManyChild.java | 227 ++++++++++++++++ .../lazyinstrumented/HasManyChildPath.java | 37 +++ .../lazyinstrumented/HasOneChild.java | 197 ++++++++++++++ .../lazyinstrumented/HasOneChildPath.java | 29 +++ .../testmodels/lazyinstrumented/Parent.java | 211 +++++++++++++++ .../lazyinstrumented/ParentPath.java | 45 ++++ 19 files changed, 1447 insertions(+), 10 deletions(-) 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/GraphQLLazyUpdateInstrumentationTest.kt create mode 100644 testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/AmplifyModelProvider.java create mode 100644 testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasManyChild.java create mode 100644 testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasManyChildPath.java create mode 100644 testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasOneChild.java create mode 100644 testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasOneChildPath.java create mode 100644 testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/Parent.java create mode 100644 testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/ParentPath.java 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-appsync/src/test/java/com/amplifyframework/api/aws/SelectionSetTest.java b/aws-api-appsync/src/test/java/com/amplifyframework/api/aws/SelectionSetTest.java index 0ffb120f40..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 @@ -5,7 +5,7 @@ * 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 + * 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 @@ -15,8 +15,6 @@ package com.amplifyframework.api.aws; -import static com.amplifyframework.core.model.ModelPropertyPathKt.includes; - import com.amplifyframework.AmplifyException; import com.amplifyframework.api.graphql.QueryType; import com.amplifyframework.core.model.AuthRule; @@ -45,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) diff --git a/aws-api/build.gradle.kts b/aws-api/build.gradle.kts index 91d42d9c4c..48d2f4ec38 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")) @@ -49,7 +60,10 @@ dependencies { 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) + + 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..d35e1fbbdd --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyCreateInstrumentationTest.kt @@ -0,0 +1,120 @@ +/* + * 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.kotlin.core.Amplify +import com.amplifyframework.testmodels.lazyinstrumented.HasManyChild +import com.amplifyframework.testmodels.lazyinstrumented.HasOneChild +import com.amplifyframework.testmodels.lazyinstrumented.Parent +import com.amplifyframework.testmodels.lazyinstrumented.ParentPath +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Ignore +import org.junit.Test + +@Ignore("Waiting to add test config") +class GraphQLLazyCreateInstrumentationTest { + + @Before + 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() = runBlocking { + // 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)) + return@runBlocking + } + + @Test + fun create_with_includes() = runBlocking { + // 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)) + return@runBlocking + } +} 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..035272a668 --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyDeleteInstrumentationTest.kt @@ -0,0 +1,118 @@ +/* + * 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.kotlin.core.Amplify +import com.amplifyframework.testmodels.lazyinstrumented.HasManyChild +import com.amplifyframework.testmodels.lazyinstrumented.HasOneChild +import com.amplifyframework.testmodels.lazyinstrumented.Parent +import com.amplifyframework.testmodels.lazyinstrumented.ParentPath +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Ignore +import org.junit.Test + +@Ignore("Waiting to add test config") +class GraphQLLazyDeleteInstrumentationTest { + + @Before + 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() = runBlocking { + // 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)) + return@runBlocking + } + + @Test + fun delete_with_includes() = runBlocking { + // 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)) + return@runBlocking + } +} 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..546a3b6616 --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyQueryInstrumentationTest.kt @@ -0,0 +1,243 @@ +/* + * 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.kotlin.core.Amplify +import com.amplifyframework.testmodels.lazyinstrumented.Parent +import com.amplifyframework.testmodels.lazyinstrumented.ParentPath +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Ignore +import org.junit.Test + +@Ignore("Waiting to add test config") +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" + } + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + val config = AmplifyConfiguration.fromConfigFile(context, R.raw.amplifyconfigurationlazy) + Amplify.addPlugin(AWSApiPlugin()) + Amplify.configure(config, context) + } + +// 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)) +// } +// } + + @Test + fun query_parent_no_includes() = runBlocking { + // 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) + return@runBlocking + } + + @Test + fun query_parent_with_includes() = runBlocking { + // 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) + return@runBlocking + } + + @Test + fun query_list_with_no_includes() = runBlocking { + + 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) + return@runBlocking + } + + @Test + fun query_list_with_includes() = runBlocking { + + 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) + return@runBlocking + } +} 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..f688e653c9 --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyUpdateInstrumentationTest.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 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.kotlin.core.Amplify +import com.amplifyframework.testmodels.lazyinstrumented.HasManyChild +import com.amplifyframework.testmodels.lazyinstrumented.HasOneChild +import com.amplifyframework.testmodels.lazyinstrumented.Parent +import com.amplifyframework.testmodels.lazyinstrumented.ParentPath +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Ignore +import org.junit.Test + +@Ignore("Waiting to add test config") +class GraphQLLazyUpdateInstrumentationTest { + + @Before + 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() = runBlocking { + // 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)) + return@runBlocking + } + + @Test + fun update_with_includes() = runBlocking { + // 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)) + return@runBlocking + } +} 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 index ded8dcd02e..9152a16b03 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt @@ -103,8 +103,6 @@ internal class ApiLazyModelReference internal constructor( *variables.toTypedArray() ) - request - val value = query( request, apiName 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 index 37048f84d2..e0e36b6dbd 100644 --- a/aws-api/src/test/java/com/amplifyframework/graphql/model/ModelMutationTest.kt +++ b/aws-api/src/test/java/com/amplifyframework/graphql/model/ModelMutationTest.kt @@ -5,7 +5,7 @@ * 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 + * 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 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 index 5671252d1a..e9150a2b61 100644 --- a/aws-api/src/test/java/com/amplifyframework/graphql/model/ModelQueryTest.kt +++ b/aws-api/src/test/java/com/amplifyframework/graphql/model/ModelQueryTest.kt @@ -5,7 +5,7 @@ * 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 + * 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 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 index 9b984092e1..4e3a00f0ee 100644 --- a/aws-api/src/test/java/com/amplifyframework/graphql/model/ModelSubscriptionTest.kt +++ b/aws-api/src/test/java/com/amplifyframework/graphql/model/ModelSubscriptionTest.kt @@ -5,7 +5,7 @@ * 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 + * 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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2059cd784a..e26d7e20d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,8 +12,8 @@ androidx-security = "1.0.0" androidx-sqlite = "2.2.0" androidx-test-core = "1.3.0" androidx-test-junit = "1.1.2" -androidx-test-orchestrator = "1.3.0" -androidx-test-runner = "1.3.0" +androidx-test-orchestrator = "1.4.2" +androidx-test-runner = "1.5.2" androidx-workmanager = "2.7.1" aws-kotlin = "0.29.1-beta" # ensure proper aws-smithy version also set aws-sdk = "2.62.2" diff --git a/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/AmplifyModelProvider.java b/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/AmplifyModelProvider.java new file mode 100644 index 0000000000..93efb70a0a --- /dev/null +++ b/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/AmplifyModelProvider.java @@ -0,0 +1,68 @@ +/* + * 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.lazyinstrumented; + +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 = "c169331ddfff87d2c8885b0e7aa0de68"; + 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) + ); + + 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/lazyinstrumented/HasManyChild.java b/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasManyChild.java new file mode 100644 index 0000000000..9fd9f492ce --- /dev/null +++ b/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasManyChild.java @@ -0,0 +1,227 @@ +/* + * 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.lazyinstrumented; + +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/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasManyChildPath.java b/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasManyChildPath.java new file mode 100644 index 0000000000..43163c46c8 --- /dev/null +++ b/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasManyChildPath.java @@ -0,0 +1,37 @@ +/* + * 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.lazyinstrumented; + +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/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasOneChild.java b/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasOneChild.java new file mode 100644 index 0000000000..c17db18ab9 --- /dev/null +++ b/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasOneChild.java @@ -0,0 +1,197 @@ +/* + * 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.lazyinstrumented; + +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/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasOneChildPath.java b/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasOneChildPath.java new file mode 100644 index 0000000000..399f191c3e --- /dev/null +++ b/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasOneChildPath.java @@ -0,0 +1,29 @@ +/* + * 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.lazyinstrumented; + +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/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/Parent.java b/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/Parent.java new file mode 100644 index 0000000000..75d0502235 --- /dev/null +++ b/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/Parent.java @@ -0,0 +1,211 @@ +/* + * 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.lazyinstrumented; + +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/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/ParentPath.java b/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/ParentPath.java new file mode 100644 index 0000000000..d7f93fc3fc --- /dev/null +++ b/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/ParentPath.java @@ -0,0 +1,45 @@ +/* + * 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.lazyinstrumented; + +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; + } +} From 2c6059be2832d67be783af82e5887d9c43de9175 Mon Sep 17 00:00:00 2001 From: tjroach Date: Tue, 26 Sep 2023 16:43:39 -0400 Subject: [PATCH 077/100] Adding subscribe tests --- aws-api/build.gradle.kts | 1 + .../GraphQLLazyCreateInstrumentationTest.kt | 8 +- .../GraphQLLazyDeleteInstrumentationTest.kt | 8 +- .../GraphQLLazyQueryInstrumentationTest.kt | 4 +- ...GraphQLLazySubscribeInstrumentationTest.kt | 143 ++++++++++++++++++ .../GraphQLLazyUpdateInstrumentationTest.kt | 8 +- .../model}/AmplifyModelProvider.java | 4 +- .../generated/model}/HasManyChild.java | 4 +- .../generated/model}/HasManyChildPath.java | 4 +- .../generated/model}/HasOneChild.java | 4 +- .../generated/model}/HasOneChildPath.java | 4 +- .../datastore/generated/model}/Parent.java | 4 +- .../generated/model}/ParentPath.java | 4 +- 13 files changed, 172 insertions(+), 28 deletions(-) create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazySubscribeInstrumentationTest.kt rename {testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented => aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model}/AmplifyModelProvider.java (95%) rename {testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented => aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model}/HasManyChild.java (98%) rename {testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented => aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model}/HasManyChildPath.java (93%) rename {testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented => aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model}/HasOneChild.java (98%) rename {testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented => aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model}/HasOneChildPath.java (91%) rename {testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented => aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model}/Parent.java (98%) rename {testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented => aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model}/ParentPath.java (93%) diff --git a/aws-api/build.gradle.kts b/aws-api/build.gradle.kts index 48d2f4ec38..a37ddd26ec 100644 --- a/aws-api/build.gradle.kts +++ b/aws-api/build.gradle.kts @@ -64,6 +64,7 @@ dependencies { 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 index d35e1fbbdd..6a9740c34c 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyCreateInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyCreateInstrumentationTest.kt @@ -27,10 +27,10 @@ import com.amplifyframework.core.model.LoadedModelReference import com.amplifyframework.core.model.PaginationToken import com.amplifyframework.core.model.includes import com.amplifyframework.kotlin.core.Amplify -import com.amplifyframework.testmodels.lazyinstrumented.HasManyChild -import com.amplifyframework.testmodels.lazyinstrumented.HasOneChild -import com.amplifyframework.testmodels.lazyinstrumented.Parent -import com.amplifyframework.testmodels.lazyinstrumented.ParentPath +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 kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.fail 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 index 035272a668..b28a2fe55d 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyDeleteInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyDeleteInstrumentationTest.kt @@ -26,10 +26,10 @@ import com.amplifyframework.core.model.LoadedModelList import com.amplifyframework.core.model.LoadedModelReference import com.amplifyframework.core.model.includes import com.amplifyframework.kotlin.core.Amplify -import com.amplifyframework.testmodels.lazyinstrumented.HasManyChild -import com.amplifyframework.testmodels.lazyinstrumented.HasOneChild -import com.amplifyframework.testmodels.lazyinstrumented.Parent -import com.amplifyframework.testmodels.lazyinstrumented.ParentPath +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 kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.fail 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 index 546a3b6616..23ea49e8ad 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyQueryInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyQueryInstrumentationTest.kt @@ -27,8 +27,8 @@ import com.amplifyframework.core.model.LoadedModelReference import com.amplifyframework.core.model.PaginationToken import com.amplifyframework.core.model.includes import com.amplifyframework.kotlin.core.Amplify -import com.amplifyframework.testmodels.lazyinstrumented.Parent -import com.amplifyframework.testmodels.lazyinstrumented.ParentPath +import com.amplifyframework.datastore.generated.model.Parent +import com.amplifyframework.datastore.generated.model.ParentPath import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse 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..e8b66b0d9f --- /dev/null +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazySubscribeInstrumentationTest.kt @@ -0,0 +1,143 @@ +/* + * 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 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.Before +import org.junit.Ignore +import org.junit.Test +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +@Ignore("Waiting to add test config") +class GraphQLLazySubscribeInstrumentationTest { + + @Before + 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_crate() = runTest { + // GIVEN + val hasOneChild = HasOneChild.builder() + .content("Child1") + .build() + val parent = Parent.builder().parentChildId(hasOneChild.id).build() + + + val latch = CountDownLatch(1) + + var capturedParent: Parent? = null + var capturedChild: HasOneChild? = null + CoroutineScope(Dispatchers.IO).launch { + Amplify.API.subscribe(ModelSubscription.onCreate(Parent::class.java)).collect { + assertEquals(parent.id, it.data.id) + capturedParent = it.data + capturedChild = (it.data.child as LazyModelReference).fetchModel()!! + latch.countDown() + } + } + + // 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.content, capturedChild!!.content) + + // 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) + + var capturedParent: Parent? = null + var capturedChild: HasOneChild? = null + CoroutineScope(Dispatchers.IO).launch { + val request = ModelSubscription.onCreate(Parent::class.java) { + includes(it.child) + } + Amplify.API.subscribe(request).collect { + assertEquals(parent.id, it.data.id) + capturedParent = it.data + capturedChild = (it.data.child as LoadedModelReference).value + latch.countDown() + } + } + + // 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.content, capturedChild!!.content) + + // CLEANUP + Amplify.API.mutate(ModelMutation.delete(hasOneChild)) + Amplify.API.mutate(ModelMutation.delete(parent)) + } +} 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 index f688e653c9..0ced8297e7 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyUpdateInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyUpdateInstrumentationTest.kt @@ -26,10 +26,10 @@ import com.amplifyframework.core.model.LoadedModelList import com.amplifyframework.core.model.LoadedModelReference import com.amplifyframework.core.model.includes import com.amplifyframework.kotlin.core.Amplify -import com.amplifyframework.testmodels.lazyinstrumented.HasManyChild -import com.amplifyframework.testmodels.lazyinstrumented.HasOneChild -import com.amplifyframework.testmodels.lazyinstrumented.Parent -import com.amplifyframework.testmodels.lazyinstrumented.ParentPath +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 kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.fail diff --git a/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/AmplifyModelProvider.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/AmplifyModelProvider.java similarity index 95% rename from testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/AmplifyModelProvider.java rename to aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/AmplifyModelProvider.java index 93efb70a0a..a499f4814c 100644 --- a/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/AmplifyModelProvider.java +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/AmplifyModelProvider.java @@ -5,7 +5,7 @@ * 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 + * 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 @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -package com.amplifyframework.testmodels.lazyinstrumented; +package com.amplifyframework.datastore.generated.model; import com.amplifyframework.core.model.Model; import com.amplifyframework.core.model.ModelProvider; diff --git a/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasManyChild.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasManyChild.java similarity index 98% rename from testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasManyChild.java rename to aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasManyChild.java index 9fd9f492ce..61a5ea2f4f 100644 --- a/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasManyChild.java +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasManyChild.java @@ -5,7 +5,7 @@ * 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 + * 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 @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -package com.amplifyframework.testmodels.lazyinstrumented; +package com.amplifyframework.datastore.generated.model; import static com.amplifyframework.core.model.query.predicate.QueryField.field; diff --git a/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasManyChildPath.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasManyChildPath.java similarity index 93% rename from testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasManyChildPath.java rename to aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasManyChildPath.java index 43163c46c8..741b1eb9ed 100644 --- a/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasManyChildPath.java +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasManyChildPath.java @@ -5,7 +5,7 @@ * 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 + * 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 @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -package com.amplifyframework.testmodels.lazyinstrumented; +package com.amplifyframework.datastore.generated.model; import androidx.annotation.NonNull; import androidx.annotation.Nullable; diff --git a/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasOneChild.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasOneChild.java similarity index 98% rename from testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasOneChild.java rename to aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasOneChild.java index c17db18ab9..d48da83a36 100644 --- a/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasOneChild.java +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasOneChild.java @@ -5,7 +5,7 @@ * 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 + * 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 @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -package com.amplifyframework.testmodels.lazyinstrumented; +package com.amplifyframework.datastore.generated.model; import static com.amplifyframework.core.model.query.predicate.QueryField.field; diff --git a/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasOneChildPath.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasOneChildPath.java similarity index 91% rename from testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasOneChildPath.java rename to aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasOneChildPath.java index 399f191c3e..d8cc07f6c2 100644 --- a/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/HasOneChildPath.java +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/HasOneChildPath.java @@ -5,7 +5,7 @@ * 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 + * 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 @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -package com.amplifyframework.testmodels.lazyinstrumented; +package com.amplifyframework.datastore.generated.model; import androidx.annotation.NonNull; import androidx.annotation.Nullable; diff --git a/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/Parent.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Parent.java similarity index 98% rename from testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/Parent.java rename to aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Parent.java index 75d0502235..b282e0c1a8 100644 --- a/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/Parent.java +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/Parent.java @@ -5,7 +5,7 @@ * 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 + * 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 @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -package com.amplifyframework.testmodels.lazyinstrumented; +package com.amplifyframework.datastore.generated.model; import static com.amplifyframework.core.model.query.predicate.QueryField.field; diff --git a/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/ParentPath.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/ParentPath.java similarity index 93% rename from testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/ParentPath.java rename to aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/ParentPath.java index d7f93fc3fc..4e853877c8 100644 --- a/testmodels/src/main/java/com/amplifyframework/testmodels/lazyinstrumented/ParentPath.java +++ b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/ParentPath.java @@ -5,7 +5,7 @@ * 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 + * 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 @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -package com.amplifyframework.testmodels.lazyinstrumented; +package com.amplifyframework.datastore.generated.model; import androidx.annotation.NonNull; import androidx.annotation.Nullable; From f1d41c9fe64dd2748dc4e9b1f97cd00537db946f Mon Sep 17 00:00:00 2001 From: tjroach Date: Wed, 27 Sep 2023 10:15:17 -0400 Subject: [PATCH 078/100] Adding more ll/css subscribe tests --- .../GraphQLLazyCreateInstrumentationTest.kt | 2 +- .../GraphQLLazyDeleteInstrumentationTest.kt | 2 +- .../GraphQLLazyQueryInstrumentationTest.kt | 2 +- ...GraphQLLazySubscribeInstrumentationTest.kt | 206 +++++++++++++++++- .../GraphQLLazyUpdateInstrumentationTest.kt | 2 +- 5 files changed, 203 insertions(+), 11 deletions(-) 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 index 6a9740c34c..4944b45457 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyCreateInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyCreateInstrumentationTest.kt @@ -26,11 +26,11 @@ 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.kotlin.core.Amplify 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.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.fail 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 index b28a2fe55d..adbcad6793 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyDeleteInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyDeleteInstrumentationTest.kt @@ -25,11 +25,11 @@ 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.kotlin.core.Amplify 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.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.fail 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 index 23ea49e8ad..c2c965dfb0 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyQueryInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyQueryInstrumentationTest.kt @@ -26,9 +26,9 @@ 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.kotlin.core.Amplify import com.amplifyframework.datastore.generated.model.Parent import com.amplifyframework.datastore.generated.model.ParentPath +import com.amplifyframework.kotlin.core.Amplify import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse 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 index e8b66b0d9f..23e5042816 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazySubscribeInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazySubscribeInstrumentationTest.kt @@ -28,6 +28,8 @@ 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 @@ -39,8 +41,6 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Ignore import org.junit.Test -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit @Ignore("Waiting to add test config") class GraphQLLazySubscribeInstrumentationTest { @@ -55,14 +55,13 @@ class GraphQLLazySubscribeInstrumentationTest { @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @Test - fun subscribe_with_no_includes_crate() = runTest { + 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) var capturedParent: Parent? = null @@ -87,7 +86,7 @@ class GraphQLLazySubscribeInstrumentationTest { assertEquals(parent.id, capturedParent!!.id) assertEquals(hasOneChild.content, capturedChild!!.content) - assertEquals(hasOneChild.content, capturedChild!!.content) + assertEquals(hasOneChild.id, capturedChild!!.id) // CLEANUP Amplify.API.mutate(ModelMutation.delete(hasOneChild)) @@ -103,7 +102,6 @@ class GraphQLLazySubscribeInstrumentationTest { .build() val parent = Parent.builder().parentChildId(hasOneChild.id).build() - val latch = CountDownLatch(1) var capturedParent: Parent? = null @@ -134,10 +132,204 @@ class GraphQLLazySubscribeInstrumentationTest { assertEquals(parent.id, capturedParent!!.id) assertEquals(hasOneChild.content, capturedChild!!.content) - 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 latch = CountDownLatch(1) + + var capturedParent: Parent? = null + var capturedChild: HasOneChild? = null + CoroutineScope(Dispatchers.IO).launch { + Amplify.API.subscribe(ModelSubscription.onUpdate(Parent::class.java)).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 latch = CountDownLatch(1) + + var capturedParent: Parent? = null + var capturedChild: HasOneChild? = null + CoroutineScope(Dispatchers.IO).launch { + val request = ModelSubscription.onUpdate(Parent::class.java) { + includes(it.child) + } + Amplify.API.subscribe(request).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 latch = CountDownLatch(1) + + var capturedParent: Parent? = null + var capturedChild: HasOneChild? = null + CoroutineScope(Dispatchers.IO).launch { + Amplify.API.subscribe(ModelSubscription.onDelete(Parent::class.java)).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 latch = CountDownLatch(1) + + var capturedParent: Parent? = null + var capturedChild: HasOneChild? = null + CoroutineScope(Dispatchers.IO).launch { + val request = ModelSubscription.onDelete(Parent::class.java) { + includes(it.child) + } + Amplify.API.subscribe(request).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 index 0ced8297e7..6fe88f7096 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyUpdateInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyUpdateInstrumentationTest.kt @@ -25,11 +25,11 @@ 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.kotlin.core.Amplify 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.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.fail From b3b8ade05c9af85ef4fdb46ebb8d9ff587f9c78c Mon Sep 17 00:00:00 2001 From: tjroach Date: Wed, 27 Sep 2023 12:38:16 -0400 Subject: [PATCH 079/100] Fix case of null lazy reference --- .../GraphQLLazyCreateInstrumentationTest.kt | 8 +-- .../GraphQLLazyDeleteInstrumentationTest.kt | 8 +-- .../GraphQLLazyQueryInstrumentationTest.kt | 61 ++++++++++++++++--- .../GraphQLLazyUpdateInstrumentationTest.kt | 8 +-- .../api/aws/ApiLazyModelReference.kt | 19 ++++-- .../api/aws/ModelDeserializer.kt | 36 ++++++----- 6 files changed, 96 insertions(+), 44 deletions(-) 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 index 4944b45457..8fb597a86a 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyCreateInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyCreateInstrumentationTest.kt @@ -31,7 +31,7 @@ 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.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.fail import org.junit.Before @@ -50,7 +50,7 @@ class GraphQLLazyCreateInstrumentationTest { } @Test - fun create_with_no_includes() = runBlocking { + fun create_with_no_includes() = runTest { // GIVEN val hasOneChild = HasOneChild.builder().content("Child1").build() Amplify.API.mutate(ModelMutation.create(hasOneChild)) @@ -85,11 +85,10 @@ class GraphQLLazyCreateInstrumentationTest { Amplify.API.mutate(ModelMutation.delete(hasOneChild)) Amplify.API.mutate(ModelMutation.delete(hasManyChild)) Amplify.API.mutate(ModelMutation.delete(parent)) - return@runBlocking } @Test - fun create_with_includes() = runBlocking { + fun create_with_includes() = runTest { // GIVEN val hasOneChild = HasOneChild.builder().content("Child1").build() Amplify.API.mutate(ModelMutation.create(hasOneChild)) @@ -115,6 +114,5 @@ class GraphQLLazyCreateInstrumentationTest { // CLEANUP Amplify.API.mutate(ModelMutation.delete(hasOneChild)) Amplify.API.mutate(ModelMutation.delete(parent)) - return@runBlocking } } 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 index adbcad6793..a77955b626 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyDeleteInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyDeleteInstrumentationTest.kt @@ -30,7 +30,7 @@ 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.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.fail import org.junit.Before @@ -49,7 +49,7 @@ class GraphQLLazyDeleteInstrumentationTest { } @Test - fun delete_with_no_includes() = runBlocking { + fun delete_with_no_includes() = runTest { // GIVEN val hasOneChild = HasOneChild.builder().content("Child1").build() Amplify.API.mutate(ModelMutation.create(hasOneChild)) @@ -78,11 +78,10 @@ class GraphQLLazyDeleteInstrumentationTest { // CLEANUP Amplify.API.mutate(ModelMutation.delete(hasOneChild)) Amplify.API.mutate(ModelMutation.delete(hasManyChild)) - return@runBlocking } @Test - fun delete_with_includes() = runBlocking { + fun delete_with_includes() = runTest { // GIVEN val hasOneChild = HasOneChild.builder().content("Child1").build() Amplify.API.mutate(ModelMutation.create(hasOneChild)) @@ -113,6 +112,5 @@ class GraphQLLazyDeleteInstrumentationTest { // CLEANUP Amplify.API.mutate(ModelMutation.delete(hasOneChild)) Amplify.API.mutate(ModelMutation.delete(hasManyChild)) - return@runBlocking } } 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 index c2c965dfb0..4a599b94cb 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyQueryInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyQueryInstrumentationTest.kt @@ -29,9 +29,10 @@ import com.amplifyframework.core.model.includes import com.amplifyframework.datastore.generated.model.Parent import com.amplifyframework.datastore.generated.model.ParentPath import com.amplifyframework.kotlin.core.Amplify -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull import org.junit.Assert.fail import org.junit.Before import org.junit.Ignore @@ -82,10 +83,13 @@ class GraphQLLazyQueryInstrumentationTest { // .build() // Amplify.API.mutate(ModelMutation.create(hasManyChild)) // } +// +// val parent = Parent.builder().id("GraphQLLazyQueryInstrumentationTest.ParentWithNoChildren").build() +// Amplify.API.mutate(ModelMutation.create(parent)) // } @Test - fun query_parent_no_includes() = runBlocking { + fun query_parent_no_includes() = runTest { // GIVEN val request = ModelQuery[Parent::class.java, Parent.ParentIdentifier(PARENT1_ID)] @@ -110,11 +114,10 @@ class GraphQLLazyQueryInstrumentationTest { children1Count += page.items.size } assertEquals(1001, children1Count) - return@runBlocking } @Test - fun query_parent_with_includes() = runBlocking { + fun query_parent_with_includes() = runTest { // GIVEN val request = ModelQuery.get( Parent::class.java, Parent.ParentIdentifier(PARENT1_ID) @@ -135,11 +138,10 @@ class GraphQLLazyQueryInstrumentationTest { val children = responseParent.children as LoadedModelList assertEquals(100, children.items.size) - return@runBlocking } @Test - fun query_list_with_no_includes() = runBlocking { + fun query_list_with_no_includes() = runTest { val request = ModelQuery.list( Parent::class.java, @@ -195,11 +197,10 @@ class GraphQLLazyQueryInstrumentationTest { children2Count += page.items.size } assertEquals(0, children2Count) - return@runBlocking } @Test - fun query_list_with_includes() = runBlocking { + fun query_list_with_includes() = runTest { val request = ModelQuery.list( Parent::class.java, @@ -238,6 +239,48 @@ class GraphQLLazyQueryInstrumentationTest { val children = parent2.children as LoadedModelList assertEquals(0, children.items.size) - return@runBlocking + } + + @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) } } 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 index 6fe88f7096..cd136ccc93 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyUpdateInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyUpdateInstrumentationTest.kt @@ -30,7 +30,7 @@ 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.runBlocking +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.fail import org.junit.Before @@ -49,7 +49,7 @@ class GraphQLLazyUpdateInstrumentationTest { } @Test - fun update_with_no_includes() = runBlocking { + fun update_with_no_includes() = runTest { // GIVEN val hasOneChild = HasOneChild.builder().content("Child1").build() Amplify.API.mutate(ModelMutation.create(hasOneChild)) @@ -84,11 +84,10 @@ class GraphQLLazyUpdateInstrumentationTest { Amplify.API.mutate(ModelMutation.delete(hasOneChild2)) Amplify.API.mutate(ModelMutation.delete(hasManyChild)) Amplify.API.mutate(ModelMutation.delete(parent)) - return@runBlocking } @Test - fun update_with_includes() = runBlocking { + fun update_with_includes() = runTest { // GIVEN val hasOneChild = HasOneChild.builder().content("Child1").build() Amplify.API.mutate(ModelMutation.create(hasOneChild)) @@ -125,6 +124,5 @@ class GraphQLLazyUpdateInstrumentationTest { Amplify.API.mutate(ModelMutation.delete(hasOneChild2)) Amplify.API.mutate(ModelMutation.delete(hasManyChild)) Amplify.API.mutate(ModelMutation.delete(parent)) - return@runBlocking } } 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 index 9152a16b03..e530e155b1 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt @@ -44,15 +44,22 @@ internal class ApiLazyModelReference internal constructor( private val semaphore = Semaphore(1) // 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(null) + } + } + override fun getIdentifier(): Map { return keyMap } override suspend fun fetchModel(): M? { val cached = cachedValue.get() - if (cached != null) { + if (cached != null || keyMap.isEmpty()) { // Quick return if value is already present - return cached.value + return cached?.value } return fetchInternal() @@ -60,9 +67,9 @@ internal class ApiLazyModelReference internal constructor( override fun fetchModel(onSuccess: NullableConsumer, onError: Consumer) { val cached = cachedValue.get() - if (cached != null) { + if (cached != null || keyMap.isEmpty()) { // Quick return if value is already present - onSuccess.accept(cached.value) + onSuccess.accept(cached?.value) } callbackScope.launch { @@ -81,8 +88,8 @@ internal class ApiLazyModelReference internal constructor( // Quick return if value is already present val cached = cachedValue.get() - if (cached != null) { - return cached.value + if (cached != null || keyMap.isEmpty()) { + return cached?.value } return try { diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt index 9e95584af7..adfc10c255 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt @@ -15,6 +15,7 @@ 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 @@ -43,30 +44,37 @@ internal class ModelDeserializer( val parentType = (typeOfT as Class<*>).simpleName val parentModelSchema = ModelSchema.fromModelClass(parent.javaClass) - parentModelSchema.fields.filter { it.value.isModelList }.map { fieldMap -> + 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 val lazyFieldModelSchema = schemaRegistry.getModelSchemaForModelClass(lazyField.targetType) - val lazyFieldTargetNames = lazyFieldModelSchema - .associations - .entries - .first { it.value.associatedType == parentType } - .value - .targetNames + when { + fieldMap.value.isModelReference -> { + val modelReference = LoadedModelReferenceImpl(null) + fieldToUpdate.set(parent, modelReference) + } + fieldMap.value.isModelList -> { + val lazyFieldTargetNames = lazyFieldModelSchema + .associations + .entries + .first { it.value.associatedType == parentType } + .value + .targetNames - val parentIdentifiers = parent.getSortedIdentifiers() + val parentIdentifiers = parent.getSortedIdentifiers() - val queryKeys = lazyFieldTargetNames.mapIndexed { idx, name -> - name to parentIdentifiers[idx] - }.toMap() + val queryKeys = lazyFieldTargetNames.mapIndexed { idx, name -> + name to parentIdentifiers[idx] + }.toMap() - val modelList = ApiLazyModelList(lazyFieldModelSchema.modelClass, queryKeys, apiName) + val modelList = ApiLazyModelList(lazyFieldModelSchema.modelClass, queryKeys, apiName) - fieldToUpdate.isAccessible = true - fieldToUpdate.set(parent, modelList) + fieldToUpdate.set(parent, modelList) + } + } } } return parent From e7eb2bfb743ef9eaec5bb17a8e9343c9a4ade6c0 Mon Sep 17 00:00:00 2001 From: tjroach Date: Wed, 27 Sep 2023 17:11:43 -0400 Subject: [PATCH 080/100] Add complex lazy cases --- .../GraphQLLazyQueryInstrumentationTest.kt | 350 +++++++++++++++++- .../generated/model/AmplifyModelProvider.java | 19 +- .../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 | 15 - .../generated/model/HasManyChildPath.java | 15 - .../generated/model/HasOneChild.java | 15 - .../generated/model/HasOneChildPath.java | 15 - .../datastore/generated/model/Parent.java | 15 - .../datastore/generated/model/ParentPath.java | 15 - .../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 ++ 18 files changed, 1551 insertions(+), 109 deletions(-) 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/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 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 index 4a599b94cb..1f833fbc95 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyQueryInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyQueryInstrumentationTest.kt @@ -26,13 +26,23 @@ 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.Before import org.junit.Ignore @@ -84,8 +94,53 @@ class GraphQLLazyQueryInstrumentationTest { // Amplify.API.mutate(ModelMutation.create(hasManyChild)) // } // -// val parent = Parent.builder().id("GraphQLLazyQueryInstrumentationTest.ParentWithNoChildren").build() -// Amplify.API.mutate(ModelMutation.create(parent)) +// 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 @@ -283,4 +338,295 @@ class GraphQLLazyQueryInstrumentationTest { 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/datastore/generated/model/AmplifyModelProvider.java b/aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/AmplifyModelProvider.java index a499f4814c..560c3877c3 100644 --- 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 @@ -1,18 +1,3 @@ -/* - * 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.generated.model; import com.amplifyframework.core.model.Model; @@ -28,7 +13,7 @@ */ public final class AmplifyModelProvider implements ModelProvider { - private static final String AMPLIFY_MODEL_VERSION = "c169331ddfff87d2c8885b0e7aa0de68"; + private static final String AMPLIFY_MODEL_VERSION = "8217f08a06a711a8c8d65895bbe74b4f"; private static AmplifyModelProvider amplifyGeneratedModelInstance; private AmplifyModelProvider() { @@ -49,7 +34,7 @@ public static synchronized AmplifyModelProvider getInstance() { @Override public Set> models() { final Set> modifiableSet = new HashSet<>( - Arrays.>asList(Parent.class, HasOneChild.class, HasManyChild.class) + Arrays.>asList(Parent.class, HasOneChild.class, HasManyChild.class, Project.class, Team.class, Blog.class, Post.class, Comment.class) ); return Immutable.of(modifiableSet); 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 index 61a5ea2f4f..a8658d954d 100644 --- 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 @@ -1,18 +1,3 @@ -/* - * 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.generated.model; import static com.amplifyframework.core.model.query.predicate.QueryField.field; 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 index 741b1eb9ed..47c32bf11c 100644 --- 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 @@ -1,18 +1,3 @@ -/* - * 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.generated.model; import androidx.annotation.NonNull; 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 index d48da83a36..8718600f36 100644 --- 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 @@ -1,18 +1,3 @@ -/* - * 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.generated.model; import static com.amplifyframework.core.model.query.predicate.QueryField.field; 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 index d8cc07f6c2..13c57232eb 100644 --- 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 @@ -1,18 +1,3 @@ -/* - * 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.generated.model; import androidx.annotation.NonNull; 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 index b282e0c1a8..02b55e0e93 100644 --- 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 @@ -1,18 +1,3 @@ -/* - * 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.generated.model; import static com.amplifyframework.core.model.query.predicate.QueryField.field; 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 index 4e853877c8..3137c0d03a 100644 --- 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 @@ -1,18 +1,3 @@ -/* - * 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.generated.model; import androidx.annotation.NonNull; 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; + } +} From 49ab9b512822f2674725a36970562b0fd58e10ad Mon Sep 17 00:00:00 2001 From: tjroach Date: Thu, 28 Sep 2023 10:33:07 -0400 Subject: [PATCH 081/100] Add ApiLazyModelReference unit tests --- aws-api/build.gradle.kts | 1 + .../api/aws/ApiLazyModelReference.kt | 11 +- .../api/aws/ApiLazyModelReferenceTest.kt | 253 ++++++++++++++++++ 3 files changed, 261 insertions(+), 4 deletions(-) create mode 100644 aws-api/src/test/java/com/amplifyframework/api/aws/ApiLazyModelReferenceTest.kt diff --git a/aws-api/build.gradle.kts b/aws-api/build.gradle.kts index a37ddd26ec..58a7852bdb 100644 --- a/aws-api/build.gradle.kts +++ b/aws-api/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { testImplementation(libs.test.mockwebserver) testImplementation(libs.rxjava) testImplementation(libs.test.robolectric) + testImplementation(libs.test.kotlin.coroutines) androidTestImplementation(project(":testutils")) androidTestImplementation(project(":testmodels")) 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 index e530e155b1..4bd6812f46 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt @@ -16,6 +16,7 @@ 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 @@ -38,7 +39,8 @@ import kotlinx.coroutines.sync.withPermit internal class ApiLazyModelReference internal constructor( private val clazz: Class, private val keyMap: Map, - private val apiName: String? = null + private val apiName: String? = null, + private val apiCategory: ApiCategory = Amplify.API ) : LazyModelReference { private val cachedValue = AtomicReference?>(null) private val semaphore = Semaphore(1) // prevents multiple fetches @@ -111,6 +113,7 @@ internal class ApiLazyModelReference internal constructor( ) val value = query( + apiCategory, request, apiName ).data @@ -132,18 +135,18 @@ internal class ApiLazyModelReference internal constructor( Duplicating the query Kotlin Facade method so we aren't pulling in Kotlin Core */ @Throws(ApiException::class) -private suspend fun query(request: GraphQLRequest, apiName: String?): +private suspend fun query(apiCategory: ApiCategory, request: GraphQLRequest, apiName: String?): GraphQLResponse { return suspendCoroutine { continuation -> if (apiName != null) { - Amplify.API.query( + apiCategory.query( apiName, request, { continuation.resume(it) }, { continuation.resumeWithException(it) } ) } else { - Amplify.API.query( + apiCategory.query( request, { continuation.resume(it) }, { continuation.resumeWithException(it) } 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) + } +} From cc005b944cf09e78f972ea6754931731b89447d7 Mon Sep 17 00:00:00 2001 From: tjroach Date: Thu, 28 Sep 2023 11:12:05 -0400 Subject: [PATCH 082/100] Add testability to ApiModelListTypes --- .../api/aws/ApiModelListTypes.kt | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) 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 index 2a52662092..73d6e582b4 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt @@ -16,6 +16,7 @@ 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 @@ -44,18 +45,19 @@ internal class ApiPaginationToken(val nextToken: String) : PaginationToken internal class ApiLazyModelList constructor( private val clazz: Class, keyMap: Map, - private val apiName: String? + private val apiName: String?, + private val apiCategory: ApiCategory = Amplify.API ) : LazyModelList { private val queryPredicate = AppSyncLazyQueryPredicate().createPredicate(clazz, keyMap) override suspend fun fetchPage(paginationToken: PaginationToken?): ModelPage { - val response = query(apiName, createRequest(paginationToken)) + val response = query(apiCategory, apiName, createRequest(paginationToken)) return response.data } override fun fetchPage(onSuccess: Consumer>, onError: Consumer) { - query(apiName, createRequest(), onSuccess, onError) + query(apiCategory, apiName, createRequest(), onSuccess, onError) } override fun fetchPage( @@ -63,7 +65,7 @@ internal class ApiLazyModelList constructor( onSuccess: Consumer>, onError: Consumer ) { - query(apiName, createRequest(paginationToken), onSuccess, onError) + query(apiCategory, apiName, createRequest(paginationToken), onSuccess, onError) } private fun createRequest(paginationToken: PaginationToken? = null): GraphQLRequest> { @@ -75,6 +77,7 @@ internal class ApiLazyModelList constructor( } private fun query( + apiCategory: ApiCategory, apiName: String?, request: GraphQLRequest>, onSuccess: Consumer>, @@ -82,14 +85,14 @@ internal class ApiLazyModelList constructor( ) { if (apiName != null) { - Amplify.API.query( + apiCategory.query( apiName, request, { onSuccess.accept(it.data) }, { onError.accept(it) } ) } else { - Amplify.API.query( + apiCategory.query( request, { onSuccess.accept(it.data) }, { onError.accept(it) } @@ -98,18 +101,18 @@ internal class ApiLazyModelList constructor( } @Throws(ApiException::class) - private suspend fun query(apiName: String?, request: GraphQLRequest): + private suspend fun query(apiCategory: ApiCategory, apiName: String?, request: GraphQLRequest): GraphQLResponse { return suspendCoroutine { continuation -> if (apiName != null) { - Amplify.API.query( + apiCategory.query( apiName, request, { continuation.resume(it) }, { continuation.resumeWithException(it) } ) } else { - Amplify.API.query( + apiCategory.query( request, { continuation.resume(it) }, { continuation.resumeWithException(it) } From 4c0071508efe0791691c623267ac26a49f2c9595 Mon Sep 17 00:00:00 2001 From: tjroach Date: Thu, 28 Sep 2023 13:36:50 -0400 Subject: [PATCH 083/100] Added testing --- .../api/aws/ApiModelListTypes.kt | 57 +-- .../api/aws/ApiLazyModelListTest.kt | 382 ++++++++++++++++++ .../api/aws/ApiLoadedModelListTest.kt | 35 ++ 3 files changed, 446 insertions(+), 28 deletions(-) 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/ApiLoadedModelListTest.kt 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 index 73d6e582b4..c2b9f4f2de 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt @@ -30,6 +30,9 @@ import com.amplifyframework.core.model.PaginationToken import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch internal class ApiLoadedModelList( override val items: List @@ -49,15 +52,27 @@ internal class ApiLazyModelList constructor( private val apiCategory: ApiCategory = Amplify.API ) : LazyModelList { + private val callbackScope = CoroutineScope(Dispatchers.IO) private val queryPredicate = AppSyncLazyQueryPredicate().createPredicate(clazz, keyMap) override suspend fun fetchPage(paginationToken: PaginationToken?): ModelPage { - val response = query(apiCategory, apiName, createRequest(paginationToken)) - return response.data + try { + val response = query(apiCategory, apiName, createRequest(paginationToken)) + return response.data + } catch (error: AmplifyException) { + throw createLazyException(error) + } } override fun fetchPage(onSuccess: Consumer>, onError: Consumer) { - query(apiCategory, apiName, createRequest(), onSuccess, onError) + callbackScope.launch { + try { + val page = fetchPage() + onSuccess.accept(page) + } catch (e: AmplifyException) { + onError.accept(e) + } + } } override fun fetchPage( @@ -65,7 +80,14 @@ internal class ApiLazyModelList constructor( onSuccess: Consumer>, onError: Consumer ) { - query(apiCategory, apiName, createRequest(paginationToken), onSuccess, onError) + callbackScope.launch { + try { + val page = fetchPage(paginationToken) + onSuccess.accept(page) + } catch (e: AmplifyException) { + onError.accept(e) + } + } } private fun createRequest(paginationToken: PaginationToken? = null): GraphQLRequest> { @@ -76,30 +98,6 @@ internal class ApiLazyModelList constructor( ) } - private fun query( - apiCategory: ApiCategory, - apiName: String?, - request: GraphQLRequest>, - onSuccess: Consumer>, - onError: Consumer - ) { - - if (apiName != null) { - apiCategory.query( - apiName, - request, - { onSuccess.accept(it.data) }, - { onError.accept(it) } - ) - } else { - apiCategory.query( - request, - { onSuccess.accept(it.data) }, - { onError.accept(it) } - ) - } - } - @Throws(ApiException::class) private suspend fun query(apiCategory: ApiCategory, apiName: String?, request: GraphQLRequest): GraphQLResponse { @@ -120,4 +118,7 @@ internal class ApiLazyModelList constructor( } } } + + private fun createLazyException(exception: AmplifyException) = + AmplifyException("Error lazy loading the model list.", exception, exception.message ?: "") } 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/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) + } +} From c507e054b6979d727c65e7ed6c8f647e663b6c58 Mon Sep 17 00:00:00 2001 From: tjroach Date: Thu, 28 Sep 2023 13:53:46 -0400 Subject: [PATCH 084/100] suppress datastore generated files --- configuration/checkstyle-suppressions.xml | 1 + 1 file changed, 1 insertion(+) 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 @@ + From 3e661ea250008363def2420158ecbe7ec8235fc2 Mon Sep 17 00:00:00 2001 From: tjroach Date: Thu, 28 Sep 2023 14:07:41 -0400 Subject: [PATCH 085/100] lint --- .../api/aws/GraphQLLazyQueryInstrumentationTest.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 index 1f833fbc95..57d2d2dfc3 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyQueryInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyQueryInstrumentationTest.kt @@ -437,8 +437,8 @@ class GraphQLLazyQueryInstrumentationTest { val expectedTeamId = "GraphQLLazyQueryInstrumentationTest-Team1" val expectedTeamName = "Team 1" val projectRequest = ModelQuery[ - Project::class.java, - Project.ProjectIdentifier(expectedProjectId, expectedProjectName) + Project::class.java, + Project.ProjectIdentifier(expectedProjectId, expectedProjectName) ] // WHEN @@ -616,7 +616,6 @@ class GraphQLLazyQueryInstrumentationTest { 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) From 322e88b1e9179f6d9aa8179a2193706b2d6ff4a6 Mon Sep 17 00:00:00 2001 From: tjroach Date: Thu, 28 Sep 2023 16:49:57 -0400 Subject: [PATCH 086/100] update ci config --- scripts/pull_backend_config_from_s3 | 1 + 1 file changed, 1 insertion(+) 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" From 9224baf9c82a9ad4a12eabe9460d48f23210656b Mon Sep 17 00:00:00 2001 From: tjroach Date: Fri, 29 Sep 2023 10:05:13 -0400 Subject: [PATCH 087/100] update tests --- .../GraphQLLazyCreateInstrumentationTest.kt | 21 +- .../GraphQLLazyDeleteInstrumentationTest.kt | 19 +- .../GraphQLLazyQueryInstrumentationTest.kt | 22 +- ...GraphQLLazySubscribeInstrumentationTest.kt | 19 +- .../GraphQLLazyUpdateInstrumentationTest.kt | 19 +- configuration/publishing.gradle | 262 +++++++++--------- 6 files changed, 184 insertions(+), 178 deletions(-) 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 index 8fb597a86a..5dd8f8dea0 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyCreateInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyCreateInstrumentationTest.kt @@ -34,21 +34,24 @@ import com.amplifyframework.kotlin.core.Amplify import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.fail -import org.junit.Before -import org.junit.Ignore +import org.junit.BeforeClass import org.junit.Test -@Ignore("Waiting to add test config") class GraphQLLazyCreateInstrumentationTest { - @Before - fun setUp() { - val context = ApplicationProvider.getApplicationContext() - val config = AmplifyConfiguration.fromConfigFile(context, R.raw.amplifyconfigurationlazy) - Amplify.addPlugin(AWSApiPlugin()) - Amplify.configure(config, context) + 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 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 index a77955b626..8b2e156141 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyDeleteInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyDeleteInstrumentationTest.kt @@ -33,19 +33,20 @@ import com.amplifyframework.kotlin.core.Amplify import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.fail -import org.junit.Before -import org.junit.Ignore +import org.junit.BeforeClass import org.junit.Test -@Ignore("Waiting to add test config") class GraphQLLazyDeleteInstrumentationTest { - @Before - fun setUp() { - val context = ApplicationProvider.getApplicationContext() - val config = AmplifyConfiguration.fromConfigFile(context, R.raw.amplifyconfigurationlazy) - Amplify.addPlugin(AWSApiPlugin()) - Amplify.configure(config, context) + 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 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 index 57d2d2dfc3..b45486df8b 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyQueryInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyQueryInstrumentationTest.kt @@ -44,28 +44,28 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Assert.fail -import org.junit.Before -import org.junit.Ignore +import org.junit.BeforeClass import org.junit.Test -@Ignore("Waiting to add test config") 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) + } } - @Before - 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") 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 index 23e5042816..59501859cb 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazySubscribeInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazySubscribeInstrumentationTest.kt @@ -38,19 +38,20 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withContext import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Ignore +import org.junit.BeforeClass import org.junit.Test -@Ignore("Waiting to add test config") class GraphQLLazySubscribeInstrumentationTest { - @Before - fun setUp() { - val context = ApplicationProvider.getApplicationContext() - val config = AmplifyConfiguration.fromConfigFile(context, R.raw.amplifyconfigurationlazy) - Amplify.addPlugin(AWSApiPlugin()) - Amplify.configure(config, context) + 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) 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 index cd136ccc93..7d5ef624fe 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyUpdateInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyUpdateInstrumentationTest.kt @@ -33,19 +33,20 @@ import com.amplifyframework.kotlin.core.Amplify import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.fail -import org.junit.Before -import org.junit.Ignore +import org.junit.BeforeClass import org.junit.Test -@Ignore("Waiting to add test config") class GraphQLLazyUpdateInstrumentationTest { - @Before - fun setUp() { - val context = ApplicationProvider.getApplicationContext() - val config = AmplifyConfiguration.fromConfigFile(context, R.raw.amplifyconfigurationlazy) - Amplify.addPlugin(AWSApiPlugin()) - Amplify.configure(config, context) + 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 diff --git a/configuration/publishing.gradle b/configuration/publishing.gradle index fdf2cbf547..9dbfb2c670 100644 --- a/configuration/publishing.gradle +++ b/configuration/publishing.gradle @@ -1,131 +1,131 @@ -/* - * Copyright 2013 Chris Banes - * Modifications 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. - */ - -apply plugin: 'signing' -apply plugin: 'maven-publish' - -version = project.ext.VERSION_NAME - -def isReleaseBuild() { - return VERSION_NAME.contains("SNAPSHOT") == false -} - -def getReleaseRepositoryUrl() { - return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL : - "https://aws.oss.sonatype.org/service/local/staging/deploy/maven2/" -} - -def getSnapshotRepositoryUrl() { - return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL : - "https://aws.oss.sonatype.org/content/repositories/snapshots/" -} - -def getRepositoryUsername() { - return hasProperty('SONATYPE_NEXUS_USERNAME') ? SONATYPE_NEXUS_USERNAME : "" -} - -def getRepositoryPassword() { - return hasProperty('SONATYPE_NEXUS_PASSWORD') ? SONATYPE_NEXUS_PASSWORD : "" -} - -afterEvaluate { project -> - task androidSourcesJar(type: Jar) { - classifier = 'sources' - from android.sourceSets.main.java.source - } - - - publishing { - publications { - library(MavenPublication) { - groupId POM_GROUP - artifactId POM_ARTIFACT_ID - version VERSION_NAME - - artifact("${buildDir}/outputs/aar/${artifactId}-release.aar") - if (project.getPlugins().hasPlugin('com.android.application') || - project.getPlugins().hasPlugin('com.android.library')) { - artifact(androidSourcesJar) - } else { - artifact(sourcesJar) - } - - pom { - name = POM_NAME - packaging = POM_PACKAGING - description = POM_DESCRIPTION - url = POM_URL - - scm { - url = POM_SCM_URL - connection = POM_SCM_CONNECTION - developerConnection = POM_SCM_DEV_CONNECTION - } - - licenses { - license { - name = POM_LICENSE_NAME - url = POM_LICENSE_URL - distribution = POM_LICENSE_DIST - } - } - - developers { - developer { - id = POM_DEVELOPER_ID - organizationUrl = POM_DEVELOPER_ORGANIZATION_URL - roles = ["developer"] - } - } - - withXml { - def dependenciesNode = asNode().appendNode('dependencies') - // Note that this only handles implementation - // dependencies. In the future, may need to add api, - // etc. - configurations.implementation.allDependencies.each { - def dependencyNode = dependenciesNode.appendNode('dependency') - dependencyNode.appendNode('groupId', it.group) - dependencyNode.appendNode('artifactId', it.name) - dependencyNode.appendNode('version', it.version) - } - } - } - } - } - repositories { - maven { - url = isReleaseBuild() ? getReleaseRepositoryUrl() : getSnapshotRepositoryUrl() - credentials { - username = getRepositoryUsername() - password = getRepositoryPassword() - } - } - } - } - - - signing { - required { isReleaseBuild() && gradle.taskGraph.hasTask("publish") } - if (project.hasProperty('signing.inMemoryKey')) { - def signingKey = findProperty("signing.inMemoryKey").replace("\\n", "\n") - def signingPassword = findProperty("signing.password") - def keyId = findProperty("signing.keyId") - useInMemoryPgpKeys(keyId, signingKey, signingPassword) - } - sign publishing.publications.library - } -} +///* +// * Copyright 2013 Chris Banes +// * Modifications 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. +// */ +// +//apply plugin: 'signing' +//apply plugin: 'maven-publish' +// +//version = project.ext.VERSION_NAME +// +//def isReleaseBuild() { +// return VERSION_NAME.contains("SNAPSHOT") == false +//} +// +//def getReleaseRepositoryUrl() { +// return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL : +// "https://aws.oss.sonatype.org/service/local/staging/deploy/maven2/" +//} +// +//def getSnapshotRepositoryUrl() { +// return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL : +// "https://aws.oss.sonatype.org/content/repositories/snapshots/" +//} +// +//def getRepositoryUsername() { +// return hasProperty('SONATYPE_NEXUS_USERNAME') ? SONATYPE_NEXUS_USERNAME : "" +//} +// +//def getRepositoryPassword() { +// return hasProperty('SONATYPE_NEXUS_PASSWORD') ? SONATYPE_NEXUS_PASSWORD : "" +//} +// +//afterEvaluate { project -> +// task androidSourcesJar(type: Jar) { +// classifier = 'sources' +// from android.sourceSets.main.java.source +// } +// +// +// publishing { +// publications { +// library(MavenPublication) { +// groupId POM_GROUP +// artifactId POM_ARTIFACT_ID +// version VERSION_NAME +// +// artifact("${buildDir}/outputs/aar/${artifactId}-release.aar") +// if (project.getPlugins().hasPlugin('com.android.application') || +// project.getPlugins().hasPlugin('com.android.library')) { +// artifact(androidSourcesJar) +// } else { +// artifact(sourcesJar) +// } +// +// pom { +// name = POM_NAME +// packaging = POM_PACKAGING +// description = POM_DESCRIPTION +// url = POM_URL +// +// scm { +// url = POM_SCM_URL +// connection = POM_SCM_CONNECTION +// developerConnection = POM_SCM_DEV_CONNECTION +// } +// +// licenses { +// license { +// name = POM_LICENSE_NAME +// url = POM_LICENSE_URL +// distribution = POM_LICENSE_DIST +// } +// } +// +// developers { +// developer { +// id = POM_DEVELOPER_ID +// organizationUrl = POM_DEVELOPER_ORGANIZATION_URL +// roles = ["developer"] +// } +// } +// +// withXml { +// def dependenciesNode = asNode().appendNode('dependencies') +// // Note that this only handles implementation +// // dependencies. In the future, may need to add api, +// // etc. +// configurations.implementation.allDependencies.each { +// def dependencyNode = dependenciesNode.appendNode('dependency') +// dependencyNode.appendNode('groupId', it.group) +// dependencyNode.appendNode('artifactId', it.name) +// dependencyNode.appendNode('version', it.version) +// } +// } +// } +// } +// } +// repositories { +// maven { +// url = isReleaseBuild() ? getReleaseRepositoryUrl() : getSnapshotRepositoryUrl() +// credentials { +// username = getRepositoryUsername() +// password = getRepositoryPassword() +// } +// } +// } +// } +// +// +// signing { +// required { isReleaseBuild() && gradle.taskGraph.hasTask("publish") } +// if (project.hasProperty('signing.inMemoryKey')) { +// def signingKey = findProperty("signing.inMemoryKey").replace("\\n", "\n") +// def signingPassword = findProperty("signing.password") +// def keyId = findProperty("signing.keyId") +// useInMemoryPgpKeys(keyId, signingKey, signingPassword) +// } +// sign publishing.publications.library +// } +//} From f6e4009b1c4edac897384464123763debdf6a432 Mon Sep 17 00:00:00 2001 From: tjroach Date: Fri, 29 Sep 2023 10:05:42 -0400 Subject: [PATCH 088/100] add schema / revert mistake --- .../datastore/generated/model/schema.graphql | 61 ++++ configuration/publishing.gradle | 262 +++++++++--------- 2 files changed, 192 insertions(+), 131 deletions(-) create mode 100644 aws-api/src/androidTest/java/com/amplifyframework/datastore/generated/model/schema.graphql 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/configuration/publishing.gradle b/configuration/publishing.gradle index 9dbfb2c670..fdf2cbf547 100644 --- a/configuration/publishing.gradle +++ b/configuration/publishing.gradle @@ -1,131 +1,131 @@ -///* -// * Copyright 2013 Chris Banes -// * Modifications 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. -// */ -// -//apply plugin: 'signing' -//apply plugin: 'maven-publish' -// -//version = project.ext.VERSION_NAME -// -//def isReleaseBuild() { -// return VERSION_NAME.contains("SNAPSHOT") == false -//} -// -//def getReleaseRepositoryUrl() { -// return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL : -// "https://aws.oss.sonatype.org/service/local/staging/deploy/maven2/" -//} -// -//def getSnapshotRepositoryUrl() { -// return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL : -// "https://aws.oss.sonatype.org/content/repositories/snapshots/" -//} -// -//def getRepositoryUsername() { -// return hasProperty('SONATYPE_NEXUS_USERNAME') ? SONATYPE_NEXUS_USERNAME : "" -//} -// -//def getRepositoryPassword() { -// return hasProperty('SONATYPE_NEXUS_PASSWORD') ? SONATYPE_NEXUS_PASSWORD : "" -//} -// -//afterEvaluate { project -> -// task androidSourcesJar(type: Jar) { -// classifier = 'sources' -// from android.sourceSets.main.java.source -// } -// -// -// publishing { -// publications { -// library(MavenPublication) { -// groupId POM_GROUP -// artifactId POM_ARTIFACT_ID -// version VERSION_NAME -// -// artifact("${buildDir}/outputs/aar/${artifactId}-release.aar") -// if (project.getPlugins().hasPlugin('com.android.application') || -// project.getPlugins().hasPlugin('com.android.library')) { -// artifact(androidSourcesJar) -// } else { -// artifact(sourcesJar) -// } -// -// pom { -// name = POM_NAME -// packaging = POM_PACKAGING -// description = POM_DESCRIPTION -// url = POM_URL -// -// scm { -// url = POM_SCM_URL -// connection = POM_SCM_CONNECTION -// developerConnection = POM_SCM_DEV_CONNECTION -// } -// -// licenses { -// license { -// name = POM_LICENSE_NAME -// url = POM_LICENSE_URL -// distribution = POM_LICENSE_DIST -// } -// } -// -// developers { -// developer { -// id = POM_DEVELOPER_ID -// organizationUrl = POM_DEVELOPER_ORGANIZATION_URL -// roles = ["developer"] -// } -// } -// -// withXml { -// def dependenciesNode = asNode().appendNode('dependencies') -// // Note that this only handles implementation -// // dependencies. In the future, may need to add api, -// // etc. -// configurations.implementation.allDependencies.each { -// def dependencyNode = dependenciesNode.appendNode('dependency') -// dependencyNode.appendNode('groupId', it.group) -// dependencyNode.appendNode('artifactId', it.name) -// dependencyNode.appendNode('version', it.version) -// } -// } -// } -// } -// } -// repositories { -// maven { -// url = isReleaseBuild() ? getReleaseRepositoryUrl() : getSnapshotRepositoryUrl() -// credentials { -// username = getRepositoryUsername() -// password = getRepositoryPassword() -// } -// } -// } -// } -// -// -// signing { -// required { isReleaseBuild() && gradle.taskGraph.hasTask("publish") } -// if (project.hasProperty('signing.inMemoryKey')) { -// def signingKey = findProperty("signing.inMemoryKey").replace("\\n", "\n") -// def signingPassword = findProperty("signing.password") -// def keyId = findProperty("signing.keyId") -// useInMemoryPgpKeys(keyId, signingKey, signingPassword) -// } -// sign publishing.publications.library -// } -//} +/* + * Copyright 2013 Chris Banes + * Modifications 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. + */ + +apply plugin: 'signing' +apply plugin: 'maven-publish' + +version = project.ext.VERSION_NAME + +def isReleaseBuild() { + return VERSION_NAME.contains("SNAPSHOT") == false +} + +def getReleaseRepositoryUrl() { + return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL : + "https://aws.oss.sonatype.org/service/local/staging/deploy/maven2/" +} + +def getSnapshotRepositoryUrl() { + return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL : + "https://aws.oss.sonatype.org/content/repositories/snapshots/" +} + +def getRepositoryUsername() { + return hasProperty('SONATYPE_NEXUS_USERNAME') ? SONATYPE_NEXUS_USERNAME : "" +} + +def getRepositoryPassword() { + return hasProperty('SONATYPE_NEXUS_PASSWORD') ? SONATYPE_NEXUS_PASSWORD : "" +} + +afterEvaluate { project -> + task androidSourcesJar(type: Jar) { + classifier = 'sources' + from android.sourceSets.main.java.source + } + + + publishing { + publications { + library(MavenPublication) { + groupId POM_GROUP + artifactId POM_ARTIFACT_ID + version VERSION_NAME + + artifact("${buildDir}/outputs/aar/${artifactId}-release.aar") + if (project.getPlugins().hasPlugin('com.android.application') || + project.getPlugins().hasPlugin('com.android.library')) { + artifact(androidSourcesJar) + } else { + artifact(sourcesJar) + } + + pom { + name = POM_NAME + packaging = POM_PACKAGING + description = POM_DESCRIPTION + url = POM_URL + + scm { + url = POM_SCM_URL + connection = POM_SCM_CONNECTION + developerConnection = POM_SCM_DEV_CONNECTION + } + + licenses { + license { + name = POM_LICENSE_NAME + url = POM_LICENSE_URL + distribution = POM_LICENSE_DIST + } + } + + developers { + developer { + id = POM_DEVELOPER_ID + organizationUrl = POM_DEVELOPER_ORGANIZATION_URL + roles = ["developer"] + } + } + + withXml { + def dependenciesNode = asNode().appendNode('dependencies') + // Note that this only handles implementation + // dependencies. In the future, may need to add api, + // etc. + configurations.implementation.allDependencies.each { + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', it.group) + dependencyNode.appendNode('artifactId', it.name) + dependencyNode.appendNode('version', it.version) + } + } + } + } + } + repositories { + maven { + url = isReleaseBuild() ? getReleaseRepositoryUrl() : getSnapshotRepositoryUrl() + credentials { + username = getRepositoryUsername() + password = getRepositoryPassword() + } + } + } + } + + + signing { + required { isReleaseBuild() && gradle.taskGraph.hasTask("publish") } + if (project.hasProperty('signing.inMemoryKey')) { + def signingKey = findProperty("signing.inMemoryKey").replace("\\n", "\n") + def signingPassword = findProperty("signing.password") + def keyId = findProperty("signing.keyId") + useInMemoryPgpKeys(keyId, signingKey, signingPassword) + } + sign publishing.publications.library + } +} From 354761972cf07db189b676f365f12e4442be4109 Mon Sep 17 00:00:00 2001 From: tjroach Date: Fri, 29 Sep 2023 10:18:36 -0400 Subject: [PATCH 089/100] lint --- .../api/aws/GraphQLLazyCreateInstrumentationTest.kt | 2 -- 1 file changed, 2 deletions(-) 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 index 5dd8f8dea0..4c62f90d8c 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyCreateInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyCreateInstrumentationTest.kt @@ -50,8 +50,6 @@ class GraphQLLazyCreateInstrumentationTest { } } - - @Test fun create_with_no_includes() = runTest { // GIVEN From 7749026d642965a500b26a741196cc4f713c5759 Mon Sep 17 00:00:00 2001 From: tjroach Date: Fri, 29 Sep 2023 14:12:00 -0400 Subject: [PATCH 090/100] fix tests --- ...GraphQLLazySubscribeInstrumentationTest.kt | 46 +++++++++++-------- .../testmodels/lazy/LazyTypeTest.kt | 15 ++++++ .../amplifyframework/testutils/RepeatRule.kt | 15 ------ 3 files changed, 42 insertions(+), 34 deletions(-) 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 index 59501859cb..fd5a6f7321 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazySubscribeInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazySubscribeInstrumentationTest.kt @@ -67,8 +67,9 @@ class GraphQLLazySubscribeInstrumentationTest { var capturedParent: Parent? = null var capturedChild: HasOneChild? = null + val subscription = Amplify.API.subscribe(ModelSubscription.onCreate(Parent::class.java)) CoroutineScope(Dispatchers.IO).launch { - Amplify.API.subscribe(ModelSubscription.onCreate(Parent::class.java)).collect { + subscription.collect { assertEquals(parent.id, it.data.id) capturedParent = it.data capturedChild = (it.data.child as LazyModelReference).fetchModel()!! @@ -105,13 +106,15 @@ class GraphQLLazySubscribeInstrumentationTest { val latch = 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 { - val request = ModelSubscription.onCreate(Parent::class.java) { - includes(it.child) - } - Amplify.API.subscribe(request).collect { + subscription.collect { assertEquals(parent.id, it.data.id) capturedParent = it.data capturedChild = (it.data.child as LoadedModelReference).value @@ -152,12 +155,13 @@ class GraphQLLazySubscribeInstrumentationTest { .build() val parent = Parent.builder().parentChildId(hasOneChild.id).build() - val latch = CountDownLatch(1) + 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 { - Amplify.API.subscribe(ModelSubscription.onUpdate(Parent::class.java)).collect { + subscription.collect { assertEquals(parent.id, it.data.id) capturedParent = it.data capturedChild = (it.data.child as LazyModelReference).fetchModel()!! @@ -202,15 +206,17 @@ class GraphQLLazySubscribeInstrumentationTest { .build() val parent = Parent.builder().parentChildId(hasOneChild.id).build() - val latch = CountDownLatch(1) + 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 { - val request = ModelSubscription.onUpdate(Parent::class.java) { - includes(it.child) - } - Amplify.API.subscribe(request).collect { + + subscription.collect { assertEquals(parent.id, it.data.id) capturedParent = it.data capturedChild = (it.data.child as LoadedModelReference).value @@ -254,12 +260,13 @@ class GraphQLLazySubscribeInstrumentationTest { .build() val parent = Parent.builder().parentChildId(hasOneChild.id).build() - val latch = CountDownLatch(1) + 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 { - Amplify.API.subscribe(ModelSubscription.onDelete(Parent::class.java)).collect { + subscription.collect { assertEquals(parent.id, it.data.id) capturedParent = it.data capturedChild = (it.data.child as LazyModelReference).fetchModel()!! @@ -296,15 +303,16 @@ class GraphQLLazySubscribeInstrumentationTest { .build() val parent = Parent.builder().parentChildId(hasOneChild.id).build() - val latch = CountDownLatch(1) + 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 { - val request = ModelSubscription.onDelete(Parent::class.java) { - includes(it.child) - } - Amplify.API.subscribe(request).collect { + subscription.collect { assertEquals(parent.id, it.data.id) capturedParent = it.data capturedChild = (it.data.child as LoadedModelReference).value diff --git a/testmodels/src/test/java/com/amplifyframework/testmodels/lazy/LazyTypeTest.kt b/testmodels/src/test/java/com/amplifyframework/testmodels/lazy/LazyTypeTest.kt index aee4c78907..d58bdac0c8 100644 --- a/testmodels/src/test/java/com/amplifyframework/testmodels/lazy/LazyTypeTest.kt +++ b/testmodels/src/test/java/com/amplifyframework/testmodels/lazy/LazyTypeTest.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.testmodels.lazy import com.amplifyframework.core.model.ModelSchema diff --git a/testutils/src/main/java/com/amplifyframework/testutils/RepeatRule.kt b/testutils/src/main/java/com/amplifyframework/testutils/RepeatRule.kt index b56a38ddd5..222fdccedc 100644 --- a/testutils/src/main/java/com/amplifyframework/testutils/RepeatRule.kt +++ b/testutils/src/main/java/com/amplifyframework/testutils/RepeatRule.kt @@ -15,21 +15,6 @@ package com.amplifyframework.testutils -/* - * 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. - */ - import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement From 6c41c2529e4823c6cf443614805eb684d4d92a32 Mon Sep 17 00:00:00 2001 From: tjroach Date: Fri, 29 Sep 2023 15:01:14 -0400 Subject: [PATCH 091/100] attempt to fix tests --- .../api/aws/GraphQLLazySubscribeInstrumentationTest.kt | 6 ++++++ .../api/aws/GsonGraphQLResponseFactory.java | 1 + 2 files changed, 7 insertions(+) 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 index fd5a6f7321..215944002c 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazySubscribeInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazySubscribeInstrumentationTest.kt @@ -64,6 +64,7 @@ class GraphQLLazySubscribeInstrumentationTest { val parent = Parent.builder().parentChildId(hasOneChild.id).build() val latch = CountDownLatch(1) + val collectRunningLatch = CountDownLatch(1) var capturedParent: Parent? = null var capturedChild: HasOneChild? = null @@ -75,7 +76,9 @@ class GraphQLLazySubscribeInstrumentationTest { capturedChild = (it.data.child as LazyModelReference).fetchModel()!! latch.countDown() } + collectRunningLatch.countDown() } + collectRunningLatch.await(1, TimeUnit.SECONDS) // WHEN Amplify.API.mutate(ModelMutation.create(hasOneChild)) @@ -105,6 +108,7 @@ class GraphQLLazySubscribeInstrumentationTest { 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) @@ -120,7 +124,9 @@ class GraphQLLazySubscribeInstrumentationTest { capturedChild = (it.data.child as LoadedModelReference).value latch.countDown() } + collectRunningLatch.countDown() } + collectRunningLatch.await(1, TimeUnit.SECONDS) // WHEN Amplify.API.mutate(ModelMutation.create(hasOneChild)) 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 da91550fce..504c0e17f0 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 @@ -99,6 +99,7 @@ public GraphQLResponse buildResponse( ) .create(); + // TODO: Comment why we have to do this Gson modelDeserializerGson = responseGson.newBuilder() .registerTypeHierarchyAdapter( Model.class, From a617cdfdac71e5cf080c3b6727af61f8c8f9199a Mon Sep 17 00:00:00 2001 From: tjroach Date: Mon, 2 Oct 2023 10:51:24 -0400 Subject: [PATCH 092/100] gson model deserialization cleanup --- .../amplifyframework/api/aws/GsonFactory.java | 2 + .../api/aws/GsonGraphQLResponseFactory.java | 22 +---- .../api/aws/LazyTypeDeserializers.kt | 17 +++- .../api/aws/ModelDeserializer.kt | 89 ----------------- .../api/aws/ModelPostProcessingTypeAdapter.kt | 98 +++++++++++++++++++ 5 files changed, 119 insertions(+), 109 deletions(-) delete mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/ModelPostProcessingTypeAdapter.kt 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 index 3bd8e9744f..e286d27046 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/GsonFactory.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/GsonFactory.java @@ -55,6 +55,8 @@ private static Gson create() { 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 504c0e17f0..c85f389a56 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 @@ -23,8 +23,6 @@ import com.amplifyframework.api.graphql.GraphQLResponse; import com.amplifyframework.api.graphql.PaginatedResult; 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.amplifyframework.util.Empty; import com.amplifyframework.util.TypeMaker; @@ -81,14 +79,6 @@ public GraphQLResponse buildResponse( ); try { Gson responseGson = gson.newBuilder() - .registerTypeAdapter( - ModelList.class, - new ModelListAdapter() - ) - .registerTypeHierarchyAdapter( - ModelPage.class, - new ModelPageDeserializer() - ) .registerTypeHierarchyAdapter( Iterable.class, new IterableDeserializer<>(request) @@ -97,17 +87,11 @@ public GraphQLResponse buildResponse( ModelReference.class, new ModelReferenceDeserializer(apiName, schemaRegistry) ) - .create(); - - // TODO: Comment why we have to do this - Gson modelDeserializerGson = responseGson.newBuilder() - .registerTypeHierarchyAdapter( - Model.class, - new ModelDeserializer(responseGson, apiName, schemaRegistry) + .registerTypeAdapterFactory( + new ModelPostProcessingTypeAdapter(apiName, schemaRegistry) ) .create(); - - return modelDeserializerGson.fromJson(responseJson, responseType); + return responseGson.fromJson(responseJson, responseType); } catch (JsonParseException jsonParseException) { throw new ApiException( "Amplify encountered an error while deserializing an object.", 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 index 4f0c694c4a..2736ad254f 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/LazyTypeDeserializers.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/LazyTypeDeserializers.kt @@ -19,6 +19,7 @@ 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 @@ -64,7 +65,7 @@ internal class ModelReferenceDeserializer( } } -internal class ModelListAdapter : JsonDeserializer> { +internal class ModelListDeserializer : JsonDeserializer> { @Throws(JsonParseException::class) override fun deserialize( json: JsonElement, @@ -74,6 +75,13 @@ internal class ModelListAdapter : JsonDeserializer> { 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> { @@ -87,6 +95,13 @@ internal class ModelPageDeserializer : JsonDeserializer> val nextToken = deserializeNextToken(json) return ApiModelPage(items, nextToken) } + + companion object { + @JvmStatic + fun register(builder: GsonBuilder) { + builder.registerTypeHierarchyAdapter(ModelPage::class.java, ModelPageDeserializer()) + } + } } @Throws(JsonParseException::class) diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt deleted file mode 100644 index adfc10c255..0000000000 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelDeserializer.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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.JsonDeserializationContext -import com.google.gson.JsonDeserializer -import com.google.gson.JsonElement -import java.io.Serializable -import java.lang.reflect.Type - -/** - * Here we are Deserializing Model types and Injecting values into lazy list fields. Lazy list fields will be null - * from the server unless the list was provided in the selection set. - * - * @param responseGson is a Gson object that does not have the model deserializer. Otherwise context.fromJson would - * cause a recursion issue. - */ -internal class ModelDeserializer( - private val responseGson: Gson, - private val apiName: String?, - private val schemaRegistry: AWSApiSchemaRegistry -) : JsonDeserializer { - - override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Model { - val parent = responseGson.fromJson(json, typeOfT) - val parentType = (typeOfT as Class<*>).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 - val lazyFieldModelSchema = schemaRegistry.getModelSchemaForModelClass(lazyField.targetType) - - when { - fieldMap.value.isModelReference -> { - val modelReference = LoadedModelReferenceImpl(null) - fieldToUpdate.set(parent, modelReference) - } - fieldMap.value.isModelList -> { - val lazyFieldTargetNames = lazyFieldModelSchema - .associations - .entries - .first { it.value.associatedType == parentType } - .value - .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) - } - } - } - } - return parent - } -} - -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/ModelPostProcessingTypeAdapter.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelPostProcessingTypeAdapter.kt new file mode 100644 index 0000000000..9d8ef63165 --- /dev/null +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelPostProcessingTypeAdapter.kt @@ -0,0 +1,98 @@ +/* + * 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 + +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 + val lazyFieldModelSchema = schemaRegistry.getModelSchemaForModelClass(lazyField.targetType) + + when { + fieldMap.value.isModelReference -> { + val modelReference = LoadedModelReferenceImpl(null) + fieldToUpdate.set(parent, modelReference) + } + fieldMap.value.isModelList -> { + val lazyFieldTargetNames = lazyFieldModelSchema + .associations + .entries + .first { it.value.associatedType == parentType } + .value + .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()) + } +} From d6959bd9bbee6f943770d0edcd00d922dfc67199 Mon Sep 17 00:00:00 2001 From: tjroach Date: Mon, 2 Oct 2023 11:44:33 -0400 Subject: [PATCH 093/100] add comments for clarity --- .../api/aws/GsonGraphQLResponseFactory.java | 2 ++ .../api/aws/ModelPostProcessingTypeAdapter.kt | 8 ++++++++ 2 files changed, 10 insertions(+) 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 c85f389a56..5dc7f18788 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 @@ -88,6 +88,8 @@ public GraphQLResponse buildResponse( 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(); 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 index 9d8ef63165..0966652767 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelPostProcessingTypeAdapter.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelPostProcessingTypeAdapter.kt @@ -28,6 +28,14 @@ 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 From 381b09b8d169be31866addef64ec437cb8fd6fa0 Mon Sep 17 00:00:00 2001 From: tjroach Date: Mon, 2 Oct 2023 11:54:17 -0400 Subject: [PATCH 094/100] add comments for clarity --- .../java/com/amplifyframework/api/aws/AWSApiSchemaRegistry.kt | 4 ++++ .../com/amplifyframework/api/aws/ApiLazyModelReference.kt | 2 ++ .../java/com/amplifyframework/api/aws/ApiModelListTypes.kt | 2 ++ .../amplifyframework/api/aws/GsonGraphQLResponseFactory.java | 4 +++- 4 files changed, 11 insertions(+), 1 deletion(-) 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 index d8ac2f55d1..6d83ce0e3d 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiSchemaRegistry.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/AWSApiSchemaRegistry.kt @@ -19,6 +19,10 @@ 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() 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 index 4bd6812f46..e0a1595e92 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt @@ -39,6 +39,8 @@ import kotlinx.coroutines.sync.withPermit 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 to fetch to lazy value private val apiName: String? = null, private val apiCategory: ApiCategory = Amplify.API ) : LazyModelReference { 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 index c2b9f4f2de..83d4cc918e 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt @@ -48,6 +48,8 @@ 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 to fetch the lazy list private val apiName: String?, private val apiCategory: ApiCategory = Amplify.API ) : LazyModelList { 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 5dc7f18788..f8957e54f2 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 @@ -104,7 +104,9 @@ public GraphQLResponse buildResponse( } - // Do not use this method. Instead opt for overload with apiName + // 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) From f23ccf977c5d9ed29fe329bdf027a3379b60b617 Mon Sep 17 00:00:00 2001 From: tjroach Date: Tue, 3 Oct 2023 16:59:39 -0400 Subject: [PATCH 095/100] PR Comments --- .../api/aws/GraphQLRequestHelper.java | 16 ++- .../api/aws/SelectionSet.java | 2 +- .../api/aws/SelectionSetExtensions.kt | 6 +- .../GraphQLLazyCreateInstrumentationTest.kt | 35 ++++++ .../GraphQLLazyUpdateInstrumentationTest.kt | 106 ++++++++++++++++++ .../api/aws/ApiLazyModelReference.kt | 4 +- .../api/aws/ApiModelListTypes.kt | 17 ++- .../api/aws/AppSyncLazyQueryPredicate.kt | 31 ----- .../api/aws/GsonGraphQLResponseFactory.java | 8 +- .../api/aws/ModelPostProcessingTypeAdapter.kt | 9 +- .../core/model/SchemaRegistryUtils.kt | 2 +- .../testmodels/lazy/LazyTypeTest.kt | 2 +- 12 files changed, 184 insertions(+), 54 deletions(-) delete mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyQueryPredicate.kt 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 97ec4f0068..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 @@ -228,22 +228,18 @@ private static Map extractFieldLevelData( Object fieldValue = extractFieldValue(modelField.getName(), instance, schema, false); Object underlyingFieldValue = fieldValue; - Map identifiersIfLazyModel = new HashMap<>(); if (modelField.isModelReference() && fieldValue != null) { ModelReference modelReference = (ModelReference) fieldValue; if (modelReference instanceof LoadedModelReference) { underlyingFieldValue = ((LoadedModelReference) modelReference).getValue(); } - identifiersIfLazyModel = modelReference.getIdentifier(); } if (association == null) { result.put(fieldName, fieldValue); } else if (association.isOwner()) { if ((fieldValue == null || - (modelField.isModelReference() && - underlyingFieldValue == null && - identifiersIfLazyModel.isEmpty())) && + (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 @@ -268,12 +264,15 @@ private static void insertForeignKeyValues( 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() || 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 = @@ -300,18 +299,21 @@ private static void insertForeignKeyValues( .get(primaryKeyFieldsIterator.next())); } } else { + // 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 { + // 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()) { @@ -362,6 +364,8 @@ private static Object extractFieldValue( Field privateField = instance.getClass().getDeclaredField(fieldName); privateField.setAccessible(true); 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(); } 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 f1ca53d069..a5ec7d7a9b 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 @@ -245,7 +245,7 @@ public SelectionSet build() throws AmplifyException { for (PropertyContainerPath association : includeRelationships) { SelectionSet included = SelectionSetUtils.asSelectionSetWithoutRoot(association); if (included != null) { - SelectionSetUtils.merge(node, included); + SelectionSetUtils.mergeChild(node, included); } } } 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 index f8ff949e6e..b16dbfaf51 100644 --- 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 @@ -110,8 +110,8 @@ private fun nodesInPath(node: PropertyContainerPath, includeRoot: Boolean): List * @see findChildByName * @see replaceChild */ -@JvmName("merge") -internal fun SelectionSet.mergeWith(selectionSet: SelectionSet) { +@JvmName("mergeChild") +internal fun SelectionSet.mergeChild(selectionSet: SelectionSet) { val name = selectionSet.value ?: "" val existingField = findChildByName(name) @@ -121,7 +121,7 @@ internal fun SelectionSet.mergeWith(selectionSet: SelectionSet) { val childName = child.value if (child.nodes.isNotEmpty() && childName != null) { if (existingField.findChildByName(childName) != null) { - existingField.mergeWith(child) + existingField.mergeChild(child) } else { replaceFields.add(child) } 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 index 4c62f90d8c..772d6518ac 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyCreateInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyCreateInstrumentationTest.kt @@ -33,6 +33,7 @@ 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 @@ -116,4 +117,38 @@ class GraphQLLazyCreateInstrumentationTest { 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/GraphQLLazyUpdateInstrumentationTest.kt b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyUpdateInstrumentationTest.kt index 7d5ef624fe..8b1d6ac878 100644 --- a/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyUpdateInstrumentationTest.kt +++ b/aws-api/src/androidTest/java/com/amplifyframework/api/aws/GraphQLLazyUpdateInstrumentationTest.kt @@ -26,12 +26,14 @@ 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 @@ -126,4 +128,108 @@ class GraphQLLazyUpdateInstrumentationTest { 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/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt index e0a1595e92..6806a85bec 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt @@ -40,7 +40,7 @@ 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 to fetch to lazy value + // 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 { @@ -51,7 +51,7 @@ internal class ApiLazyModelReference internal constructor( init { // If we have no keys, we have nothing to loads if (keyMap.isEmpty()) { - cachedValue.set(null) + cachedValue.set(LoadedValue(null)) } } 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 index 83d4cc918e..537931ffdc 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt @@ -27,6 +27,9 @@ 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 kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine @@ -49,13 +52,13 @@ 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 to fetch the lazy list + // 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 = AppSyncLazyQueryPredicate().createPredicate(clazz, keyMap) + private val queryPredicate = createPredicate(clazz, keyMap) override suspend fun fetchPage(paginationToken: PaginationToken?): ModelPage { try { @@ -123,4 +126,14 @@ internal class ApiLazyModelList constructor( 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/AppSyncLazyQueryPredicate.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyQueryPredicate.kt deleted file mode 100644 index 073a9f6804..0000000000 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/AppSyncLazyQueryPredicate.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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.Model -import com.amplifyframework.core.model.query.predicate.QueryField -import com.amplifyframework.core.model.query.predicate.QueryPredicate -import com.amplifyframework.core.model.query.predicate.QueryPredicates - -internal class AppSyncLazyQueryPredicate { - 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/GsonGraphQLResponseFactory.java b/aws-api/src/main/java/com/amplifyframework/api/aws/GsonGraphQLResponseFactory.java index f8957e54f2..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; @@ -58,9 +60,9 @@ final class GsonGraphQLResponseFactory implements GraphQLResponse.Factory { } public GraphQLResponse buildResponse( - GraphQLRequest request, - String responseJson, - String apiName + @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 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 index 0966652767..56cf7303b3 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ModelPostProcessingTypeAdapter.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ModelPostProcessingTypeAdapter.kt @@ -65,7 +65,6 @@ internal class ModelPostProcessingTypeAdapter( fieldToUpdate.isAccessible = true if (fieldToUpdate.get(parent) == null) { val lazyField = fieldMap.value - val lazyFieldModelSchema = schemaRegistry.getModelSchemaForModelClass(lazyField.targetType) when { fieldMap.value.isModelReference -> { @@ -73,11 +72,13 @@ internal class ModelPostProcessingTypeAdapter( fieldToUpdate.set(parent, modelReference) } fieldMap.value.isModelList -> { + val lazyFieldModelSchema = schemaRegistry + .getModelSchemaForModelClass(lazyField.targetType) + val lazyFieldTargetNames = lazyFieldModelSchema .associations - .entries - .first { it.value.associatedType == parentType } - .value + .values + .first { it.associatedType == parentType } .targetNames val parentIdentifiers = parent.getSortedIdentifiers() diff --git a/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.kt b/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.kt index fd3e5ab2ce..2b6c8f0afd 100644 --- a/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.kt +++ b/core/src/main/java/com/amplifyframework/core/model/SchemaRegistryUtils.kt @@ -55,7 +55,7 @@ internal object SchemaRegistryUtils { /* 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 on. Since the builder is public and + 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. */ } diff --git a/testmodels/src/test/java/com/amplifyframework/testmodels/lazy/LazyTypeTest.kt b/testmodels/src/test/java/com/amplifyframework/testmodels/lazy/LazyTypeTest.kt index d58bdac0c8..2d7a0d2987 100644 --- a/testmodels/src/test/java/com/amplifyframework/testmodels/lazy/LazyTypeTest.kt +++ b/testmodels/src/test/java/com/amplifyframework/testmodels/lazy/LazyTypeTest.kt @@ -38,7 +38,7 @@ class LazyTypeTest { assertFalse( ModelSchema.fromModelClass(Todo::class.java) .modelClass.getAnnotation(ModelConfig::class.java) - ?.hasLazySupport ?: false + ?.hasLazySupport ?: true ) } } From 481f6e7b9fc171115e260a29ec021a396fa7e2e6 Mon Sep 17 00:00:00 2001 From: tjroach Date: Wed, 4 Oct 2023 10:34:23 -0400 Subject: [PATCH 096/100] semaphore to mutex --- .../com/amplifyframework/api/aws/ApiLazyModelReference.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 index 6806a85bec..4ce5302b94 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt @@ -33,8 +33,8 @@ import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock internal class ApiLazyModelReference internal constructor( private val clazz: Class, @@ -45,7 +45,7 @@ internal class ApiLazyModelReference internal constructor( private val apiCategory: ApiCategory = Amplify.API ) : LazyModelReference { private val cachedValue = AtomicReference?>(null) - private val semaphore = Semaphore(1) // prevents multiple fetches + private val mutex = Mutex() // prevents multiple fetches private val callbackScope = CoroutineScope(Dispatchers.IO) init { @@ -88,7 +88,7 @@ internal class ApiLazyModelReference internal constructor( private suspend fun fetchInternal(): M? { // Use Semaphore with 1 permit to only allow 1 execution at a time - semaphore.withPermit { + mutex.withLock { // Quick return if value is already present val cached = cachedValue.get() From 8dc06a41a6004f72c615d6fc9e7943a7dde0d388 Mon Sep 17 00:00:00 2001 From: tjroach Date: Wed, 4 Oct 2023 10:34:57 -0400 Subject: [PATCH 097/100] update comment --- .../java/com/amplifyframework/api/aws/ApiLazyModelReference.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 4ce5302b94..cc7a926f15 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt @@ -87,7 +87,7 @@ internal class ApiLazyModelReference internal constructor( } private suspend fun fetchInternal(): M? { - // Use Semaphore with 1 permit to only allow 1 execution at a time + // Use mutex to only allow 1 execution at a time mutex.withLock { // Quick return if value is already present From 24b8a4d9b8eb85b28acef5e95361f6f8069b2fee Mon Sep 17 00:00:00 2001 From: tjroach Date: Wed, 4 Oct 2023 16:29:36 -0400 Subject: [PATCH 098/100] Pr comments --- .../api/aws/SelectionSet.java | 3 ++ .../api/aws/ApiLazyModelReference.kt | 40 +++------------- .../api/aws/ApiModelListTypes.kt | 28 +---------- .../com/amplifyframework/api/aws/ApiQuery.kt | 48 +++++++++++++++++++ 4 files changed, 58 insertions(+), 61 deletions(-) create mode 100644 aws-api/src/main/java/com/amplifyframework/api/aws/ApiQuery.kt 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 a5ec7d7a9b..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 @@ -322,6 +322,9 @@ private Set getModelFields( String fieldName = field.getName(); if (schema.getAssociations().containsKey(fieldName)) { 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) { 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 index cc7a926f15..bfdecfe049 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiLazyModelReference.kt @@ -19,7 +19,6 @@ 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.Amplify import com.amplifyframework.core.Consumer import com.amplifyframework.core.NullableConsumer @@ -27,9 +26,6 @@ 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 kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -61,9 +57,9 @@ internal class ApiLazyModelReference internal constructor( override suspend fun fetchModel(): M? { val cached = cachedValue.get() - if (cached != null || keyMap.isEmpty()) { + if (cached != null) { // Quick return if value is already present - return cached?.value + return cached.value } return fetchInternal() @@ -71,9 +67,9 @@ internal class ApiLazyModelReference internal constructor( override fun fetchModel(onSuccess: NullableConsumer, onError: Consumer) { val cached = cachedValue.get() - if (cached != null || keyMap.isEmpty()) { + if (cached != null) { // Quick return if value is already present - onSuccess.accept(cached?.value) + onSuccess.accept(cached.value) } callbackScope.launch { @@ -92,8 +88,8 @@ internal class ApiLazyModelReference internal constructor( // Quick return if value is already present val cached = cachedValue.get() - if (cached != null || keyMap.isEmpty()) { - return cached?.value + if (cached != null) { + return cached.value } return try { @@ -132,27 +128,3 @@ internal class ApiLazyModelReference internal constructor( private class LoadedValue(val value: M?) } } - -/* - Duplicating the query Kotlin Facade method so we aren't pulling in Kotlin Core - */ -@Throws(ApiException::class) -private 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/ApiModelListTypes.kt b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt index 537931ffdc..e5b0484e96 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/ApiModelListTypes.kt @@ -17,9 +17,7 @@ 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.Amplify import com.amplifyframework.core.Consumer import com.amplifyframework.core.model.LazyModelList @@ -30,9 +28,6 @@ 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 kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -62,7 +57,7 @@ internal class ApiLazyModelList constructor( override suspend fun fetchPage(paginationToken: PaginationToken?): ModelPage { try { - val response = query(apiCategory, apiName, createRequest(paginationToken)) + val response = query(apiCategory, createRequest(paginationToken), apiName) return response.data } catch (error: AmplifyException) { throw createLazyException(error) @@ -103,27 +98,6 @@ internal class ApiLazyModelList constructor( ) } - @Throws(ApiException::class) - private suspend fun query(apiCategory: ApiCategory, apiName: String?, request: GraphQLRequest): - 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) } - ) - } - } - } - private fun createLazyException(exception: AmplifyException) = AmplifyException("Error lazy loading the model list.", exception, exception.message ?: "") 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) } + ) + } + } +} From e262410ab521aaaa36ee90e5adf1d4c11a0f1f1d Mon Sep 17 00:00:00 2001 From: tjroach Date: Wed, 4 Oct 2023 17:04:46 -0400 Subject: [PATCH 099/100] Additional tests --- .../aws/AppSyncGraphQLRequestFactoryTest.java | 96 +++++++++++++++++++ .../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 + 5 files changed, 100 insertions(+) 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 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/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 From bc193096a6a2853dc30bc7a187c69835b54c785f Mon Sep 17 00:00:00 2001 From: tjroach Date: Tue, 10 Oct 2023 16:11:13 -0400 Subject: [PATCH 100/100] Downgrade androidx orchestrator and runner --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e26d7e20d9..2059cd784a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,8 +12,8 @@ androidx-security = "1.0.0" androidx-sqlite = "2.2.0" androidx-test-core = "1.3.0" androidx-test-junit = "1.1.2" -androidx-test-orchestrator = "1.4.2" -androidx-test-runner = "1.5.2" +androidx-test-orchestrator = "1.3.0" +androidx-test-runner = "1.3.0" androidx-workmanager = "2.7.1" aws-kotlin = "0.29.1-beta" # ensure proper aws-smithy version also set aws-sdk = "2.62.2"