diff --git a/appsync/aws-appsync/.gitignore b/appsync/aws-appsync/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/appsync/aws-appsync/.gitignore @@ -0,0 +1 @@ +/build diff --git a/appsync/aws-appsync/build.gradle.kts b/appsync/aws-appsync/build.gradle.kts new file mode 100644 index 0000000000..df45716d23 --- /dev/null +++ b/appsync/aws-appsync/build.gradle.kts @@ -0,0 +1,51 @@ +/* + * Copyright 2025 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 java.util.Properties + +plugins { + alias(libs.plugins.amplify.android.library) + alias(libs.plugins.amplify.publishing) +} + +fun readVersion() = Properties().run { + file("../version.properties").inputStream().use { load(it) } + get("VERSION_NAME").toString() +} + +project.setProperty("VERSION_NAME", readVersion()) + +android { + namespace = "com.amazonaws.appsync" +} + +dependencies { + api(project(":core")) + implementation(project(":aws-api")) + implementation(project(":aws-api-appsync")) + + implementation(libs.okhttp) + implementation(libs.gson) + implementation(libs.kotlin.coroutines) + api(project(":foundation")) + implementation(project(":foundation-bridge")) + + testImplementation(libs.test.junit) + testImplementation(libs.test.mockk) + testImplementation(libs.test.kotlin.coroutines) + testImplementation(libs.test.kotest.assertions) + testImplementation(libs.test.mockwebserver) + testImplementation(libs.test.turbine) +} diff --git a/appsync/aws-appsync/gradle.properties b/appsync/aws-appsync/gradle.properties new file mode 100644 index 0000000000..a6770d5687 --- /dev/null +++ b/appsync/aws-appsync/gradle.properties @@ -0,0 +1,5 @@ +POM_GROUP=com.amazonaws +POM_ARTIFACT_ID=aws-appsync +POM_NAME=AWS AppSync Client for Android +POM_DESCRIPTION=Standalone AppSync GraphQL Client for Android +POM_PACKAGING=aar diff --git a/appsync/aws-appsync/src/main/java/com/amazonaws/appsync/AmplifyAppSyncClient.kt b/appsync/aws-appsync/src/main/java/com/amazonaws/appsync/AmplifyAppSyncClient.kt new file mode 100644 index 0000000000..d29b30aa9c --- /dev/null +++ b/appsync/aws-appsync/src/main/java/com/amazonaws/appsync/AmplifyAppSyncClient.kt @@ -0,0 +1,177 @@ +/* + * Copyright 2025 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.amazonaws.appsync + +import com.amplifyframework.annotations.ExperimentalAmplifyApi +import com.amplifyframework.api.ApiException +import com.amplifyframework.api.graphql.GraphQLRequest +import com.amplifyframework.api.graphql.GraphQLResponse +import com.amplifyframework.foundation.result.Result +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow +import okhttp3.OkHttpClient + +/** + * Standalone, instantiable AppSync GraphQL client. Supports queries, mutations, and + * subscriptions with typed auth and per-client connection state. + * + * Not a singleton — create multiple instances for multi-tenant / multi-API scenarios. + * + * ```kotlin + * val client = AmplifyAppSyncClient( + * AmplifyAppSyncClient.Configuration { + * endpoint = "https://xxx.appsync-api.us-east-1.amazonaws.com/graphql" + * authorization = AppSyncAuthorization.Single( + * AppSyncClientAuthorizer.ApiKey("da2-xxx") + * ) + * } + * ) + * + * when (val result = client.query(ModelQuery.get(Todo::class.java, "id-123"))) { + * is Result.Success -> use(result.data) + * is Result.Failure -> handleError(result.error) + * } + * ``` + */ +@ExperimentalAmplifyApi +class AmplifyAppSyncClient(val configuration: Configuration) { + + /** + * Per-client connection state flow. + * Emits [ConnectionState] changes for the shared WebSocket connection. + */ + val events: SharedFlow + get() = TODO("Connection state will be implemented with subscriptions") + + /** + * Execute a GraphQL query. + * + * @param request The GraphQL request. Use model helpers or construct manually. + * @return [Result.Success] with the typed GraphQL response, or [Result.Failure] with an [ApiException]. + */ + suspend fun query(request: GraphQLRequest): Result, ApiException> = + TODO("Query implementation will be added in a follow-up PR") + + /** + * Execute a GraphQL mutation. + * + * @param request The GraphQL request. Use model helpers or construct manually. + * @return [Result.Success] with the typed GraphQL response, or [Result.Failure] with an [ApiException]. + */ + suspend fun mutate(request: GraphQLRequest): Result, ApiException> = + TODO("Mutation implementation will be added in a follow-up PR") + + /** + * Subscribe to a GraphQL subscription. Returns a [Flow] of [SubscriptionEvent]. + * + * The WebSocket connection is lazy (established on first subscribe) and shared across + * all subscriptions on this client. Cancelling the collecting coroutine sends an + * unsubscribe message and releases the subscription. + * + * @param request The GraphQL subscription request. Use model helpers or construct manually. + * @return A cold [Flow] of [SubscriptionEvent]. + */ + fun subscribe(request: GraphQLRequest): Flow> = + TODO("Subscription implementation will be added in a follow-up PR") + + /** + * Close the client. Terminates all active subscriptions and releases resources. + * The client cannot be reused after closing. + */ + fun close() { + TODO("Close implementation will be added in a follow-up PR") + } + + // ── Configuration ─────────────────────────────────────────────────── + + /** + * Configuration for [AmplifyAppSyncClient]. + * + * Use the builder DSL: + * ```kotlin + * AmplifyAppSyncClient.Configuration { + * endpoint = "https://xxx.appsync-api.us-east-1.amazonaws.com/graphql" + * authorization = AppSyncAuthorization.Single( + * AppSyncClientAuthorizer.ApiKey("da2-xxx") + * ) + * } + * ``` + */ + data class Configuration internal constructor( + /** The AppSync GraphQL endpoint URL. */ + val endpoint: String, + /** Auth configuration for the client. */ + val authorization: AppSyncAuthorization, + /** AWS region. Inferred from the endpoint URL or set explicitly. */ + val region: String, + /** Optional configurator for the OkHttp client used for HTTP requests. */ + val httpClientConfigurator: ((OkHttpClient.Builder) -> Unit)? = null, + /** Optional configurator for the OkHttp client used for WebSocket connections. */ + val webSocketClientConfigurator: ((OkHttpClient.Builder) -> Unit)? = null + ) { + /** + * Builder for [Configuration]. Required fields: [endpoint] and [authorization]. + */ + class Builder internal constructor() { + /** The AppSync GraphQL endpoint URL. Required. */ + lateinit var endpoint: String + + /** Auth configuration. Required. */ + lateinit var authorization: AppSyncAuthorization + + /** AWS region. Defaults to inferred from the endpoint URL. */ + var region: String? = null + + /** Optional configurator for the HTTP OkHttp client. */ + var httpClientConfigurator: ((OkHttpClient.Builder) -> Unit)? = null + + /** Optional configurator for the WebSocket OkHttp client. */ + var webSocketClientConfigurator: ((OkHttpClient.Builder) -> Unit)? = null + + internal fun build(): Configuration { + require(::endpoint.isInitialized) { "endpoint is required" } + require(::authorization.isInitialized) { "authorization is required" } + val resolvedRegion = region ?: inferRegion(endpoint) + requireNotNull(resolvedRegion) { + "region is required. Either set it explicitly or use a standard AppSync endpoint URL." + } + return Configuration( + endpoint = endpoint, + authorization = authorization, + region = resolvedRegion, + httpClientConfigurator = httpClientConfigurator, + webSocketClientConfigurator = webSocketClientConfigurator + ) + } + } + + companion object { + /** + * Create a [Configuration] using the builder DSL. + */ + operator fun invoke(block: Builder.() -> Unit): Configuration = + Builder().apply(block).build() + + /** + * Infer the AWS region from an AppSync endpoint URL. + * Expected format: `https://{id}.appsync-api.{region}.amazonaws.com/graphql` + */ + internal fun inferRegion(endpoint: String): String? { + val regex = Regex("""\.appsync-api\.([a-z0-9-]+)\.amazonaws\.com""") + return regex.find(endpoint)?.groupValues?.get(1) + } + } + } +} diff --git a/appsync/aws-appsync/src/main/java/com/amazonaws/appsync/AppSyncAuthMode.kt b/appsync/aws-appsync/src/main/java/com/amazonaws/appsync/AppSyncAuthMode.kt new file mode 100644 index 0000000000..55fc323922 --- /dev/null +++ b/appsync/aws-appsync/src/main/java/com/amazonaws/appsync/AppSyncAuthMode.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 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.amazonaws.appsync + +import com.amplifyframework.annotations.ExperimentalAmplifyApi + +/** + * The authorization modes supported by AWS AppSync. + */ +@ExperimentalAmplifyApi +enum class AppSyncAuthMode { + /** API Key authorization. */ + API_KEY, + + /** Amazon Cognito User Pools authorization. */ + USER_POOLS, + + /** OpenID Connect authorization. */ + OIDC, + + /** AWS IAM authorization. */ + IAM, + + /** AWS Lambda custom authorization. */ + LAMBDA +} diff --git a/appsync/aws-appsync/src/main/java/com/amazonaws/appsync/AppSyncAuthorization.kt b/appsync/aws-appsync/src/main/java/com/amazonaws/appsync/AppSyncAuthorization.kt new file mode 100644 index 0000000000..11961500a7 --- /dev/null +++ b/appsync/aws-appsync/src/main/java/com/amazonaws/appsync/AppSyncAuthorization.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2025 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.amazonaws.appsync + +import com.amplifyframework.annotations.ExperimentalAmplifyApi + +/** + * Wraps the authorizer(s) that the client uses. Supports both single-auth (one authorizer for + * all requests) and multi-auth (multiple authorizers, selected based on model `@auth` rules + * or per-request overrides). + */ +@ExperimentalAmplifyApi +sealed class AppSyncAuthorization { + + /** + * Single authorizer used for all requests. + * @param authorizer The authorizer to use. + */ + data class Single( + val authorizer: AppSyncClientAuthorizer + ) : AppSyncAuthorization() + + /** + * Multiple authorizers. The client selects the appropriate one based on model `@auth` rules + * or per-request auth mode overrides. Falls back to [defaultAuthMode] when no rule matches. + * + * @param defaultAuthMode The auth mode to use when no per-request override or model rule applies. + * @param authorizers The list of authorizers. Each authorizer's [AppSyncClientAuthorizer.authMode] + * determines which auth mode it handles. Duplicate auth modes are not allowed. + */ + data class Multi( + val defaultAuthMode: AppSyncAuthMode, + val authorizers: List + ) : AppSyncAuthorization() { + init { + val modes = authorizers.map { it.authMode } + require(modes.distinct().size == modes.size) { + "Duplicate auth modes in authorizers list: ${modes.groupBy { it }.filter { it.value.size > 1 }.keys}" + } + require(authorizers.any { it.authMode == defaultAuthMode }) { + "No authorizer provided for the default auth mode: $defaultAuthMode" + } + } + } + + /** + * Resolves the authorizer for a given [AppSyncAuthMode]. + * @return The matching authorizer, or null if not found. + */ + internal fun authorizerFor(mode: AppSyncAuthMode): AppSyncClientAuthorizer? = when (this) { + is Single -> authorizer.takeIf { it.authMode == mode } + is Multi -> authorizers.firstOrNull { it.authMode == mode } + } + + /** + * Returns the default authorizer. + */ + internal val defaultAuthorizer: AppSyncClientAuthorizer + get() = when (this) { + is Single -> authorizer + is Multi -> authorizers.first { it.authMode == this.defaultAuthMode } + } + + /** + * Returns the default auth mode. + */ + internal fun resolveDefaultAuthMode(): AppSyncAuthMode = when (this) { + is Single -> authorizer.authMode + is Multi -> defaultAuthMode + } +} diff --git a/appsync/aws-appsync/src/main/java/com/amazonaws/appsync/AppSyncClientAuthorizer.kt b/appsync/aws-appsync/src/main/java/com/amazonaws/appsync/AppSyncClientAuthorizer.kt new file mode 100644 index 0000000000..8c67fac9f4 --- /dev/null +++ b/appsync/aws-appsync/src/main/java/com/amazonaws/appsync/AppSyncClientAuthorizer.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2025 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.amazonaws.appsync + +import com.amplifyframework.annotations.ExperimentalAmplifyApi +import com.amplifyframework.foundation.credentials.AwsCredentialsProvider + +/** + * Sealed hierarchy of authorizer configurations for the AppSync GraphQL client. + * Each subtype encodes its [AppSyncAuthMode] and holds the provider needed to + * produce authorization credentials for that mode. + * + * These are configuration objects — the actual header generation and request signing + * is handled internally by the plugin infrastructure via [AuthProviderBridge]. + */ +@ExperimentalAmplifyApi +sealed class AppSyncClientAuthorizer( + /** The auth mode this authorizer provides. */ + val authMode: AppSyncAuthMode +) { + /** + * API Key authorization. + * @param fetchApiKey Suspend function that provides the API key. + */ + class ApiKey( + internal val fetchApiKey: suspend () -> String + ) : AppSyncClientAuthorizer(AppSyncAuthMode.API_KEY) { + /** Convenience constructor for a static API key. */ + constructor(apiKey: String) : this({ apiKey }) + } + + /** + * Amazon Cognito User Pools authorization. + * @param fetchToken Suspend function that returns a valid access/ID token. + */ + class UserPools( + internal val fetchToken: suspend () -> String + ) : AppSyncClientAuthorizer(AppSyncAuthMode.USER_POOLS) + + /** + * OpenID Connect authorization. + * @param fetchToken Suspend function that returns a valid OIDC token. + */ + class Oidc( + internal val fetchToken: suspend () -> String + ) : AppSyncClientAuthorizer(AppSyncAuthMode.OIDC) + + /** + * AWS Lambda custom authorization. + * @param fetchToken Suspend function that returns a valid authorization token. + */ + class Lambda( + internal val fetchToken: suspend () -> String + ) : AppSyncClientAuthorizer(AppSyncAuthMode.LAMBDA) + + /** + * IAM (SigV4) authorization. + * @param credentialsProvider An [AwsCredentialsProvider] that supplies IAM credentials + * for SigV4 signing. + */ + class Iam( + internal val credentialsProvider: AwsCredentialsProvider<*> + ) : AppSyncClientAuthorizer(AppSyncAuthMode.IAM) +} diff --git a/appsync/aws-appsync/src/main/java/com/amazonaws/appsync/ConnectionState.kt b/appsync/aws-appsync/src/main/java/com/amazonaws/appsync/ConnectionState.kt new file mode 100644 index 0000000000..68887da2a0 --- /dev/null +++ b/appsync/aws-appsync/src/main/java/com/amazonaws/appsync/ConnectionState.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 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.amazonaws.appsync + +import com.amplifyframework.annotations.ExperimentalAmplifyApi +import com.amplifyframework.api.ApiException + +/** + * The connection state of the client's shared WebSocket. Replaces Hub events from V2. + * Exposed via [AmplifyAppSyncClient.events]. + */ +@ExperimentalAmplifyApi +sealed class ConnectionState { + /** A WebSocket connection is being established. */ + data object Connecting : ConnectionState() + + /** The WebSocket connection is established and ready. */ + data object Connected : ConnectionState() + + /** + * No active WebSocket connection. + * @param cause The reason for disconnection, or null if this is a clean shutdown + * (no subscriptions, client closed, or not yet connected). + */ + data class Disconnected(val cause: ApiException? = null) : ConnectionState() +} diff --git a/appsync/aws-appsync/src/main/java/com/amazonaws/appsync/SubscriptionEvent.kt b/appsync/aws-appsync/src/main/java/com/amazonaws/appsync/SubscriptionEvent.kt new file mode 100644 index 0000000000..e5f1345a81 --- /dev/null +++ b/appsync/aws-appsync/src/main/java/com/amazonaws/appsync/SubscriptionEvent.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2025 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.amazonaws.appsync + +import com.amplifyframework.annotations.ExperimentalAmplifyApi +import com.amplifyframework.api.graphql.GraphQLResponse + +/** + * Events emitted by a GraphQL subscription flow. + * + * Lifecycle: the flow emits [Connection.Connecting] → [Connection.Connected] → [Data]* + * and then either completes normally (user cancel, server complete, client close) or + * throws an [ApiException] (network, auth, timeout, etc.). + * + * For client-wide WebSocket connection state, observe [AmplifyAppSyncClient.events]. + */ +@ExperimentalAmplifyApi +sealed class SubscriptionEvent { + + /** + * A data message received from the subscription. + * @param response The GraphQL response, which may contain data, errors, or both (partial success). + */ + data class Data(val response: GraphQLResponse) : SubscriptionEvent() + + /** + * Subscription establishment lifecycle events. + */ + sealed class Connection : SubscriptionEvent() { + /** The subscription is being established (WebSocket connecting + registration in progress). */ + data object Connecting : Connection() + + /** The subscription is established and receiving data. */ + data object Connected : Connection() + } +} 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 e286d27046..cdf85f4696 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 @@ -26,10 +26,13 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.amplifyframework.annotations.InternalAmplifyApi; + /** * Creates a {@link Gson} instance which may be used around the API plugin. */ -final class GsonFactory { +@InternalAmplifyApi +public final class GsonFactory { private static Gson gson = null; private GsonFactory() {} 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 103938549f..938d66dde3 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 @@ -42,20 +42,25 @@ import java.util.ArrayList; import java.util.List; +import com.amplifyframework.annotations.InternalAmplifyApi; + /** * Converts JSON strings into models of a given type, using Gson. */ -final class GsonGraphQLResponseFactory implements GraphQLResponse.Factory { +@InternalAmplifyApi +public final class GsonGraphQLResponseFactory implements GraphQLResponse.Factory { private final Gson gson; private final AWSApiSchemaRegistry schemaRegistry = new AWSApiSchemaRegistry(); - GsonGraphQLResponseFactory() { + @InternalAmplifyApi + public GsonGraphQLResponseFactory() { this(GsonFactory.instance()); } @VisibleForTesting - GsonGraphQLResponseFactory(Gson gson) { + @InternalAmplifyApi + public GsonGraphQLResponseFactory(Gson gson) { this.gson = gson; } diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/Iso8601Timestamp.java b/aws-api/src/main/java/com/amplifyframework/api/aws/Iso8601Timestamp.java index a45a7e33cd..cc303a8a50 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/Iso8601Timestamp.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/Iso8601Timestamp.java @@ -19,6 +19,8 @@ import java.util.Date; import java.util.Locale; +import com.amplifyframework.annotations.InternalAmplifyApi; + /** * Utility to create a ISO 8601 compliant timestamps. * This utility only created US-locale timestamps. It is intended for @@ -26,10 +28,12 @@ * timestamp returned by this utility should not be displayed to end * users in a UI, as it is not localized. */ -final class Iso8601Timestamp { +@InternalAmplifyApi +public final class Iso8601Timestamp { private Iso8601Timestamp() {} - static String now() { + @InternalAmplifyApi + public static String now() { SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US); return formatter.format(new Date()); } 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 fb13784113..d59dbf9831 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 @@ -19,6 +19,7 @@ import androidx.annotation.NonNull; import com.amplifyframework.AmplifyException; +import com.amplifyframework.annotations.InternalAmplifyApi; import com.amplifyframework.api.ApiException; import com.amplifyframework.api.ApiException.ApiAuthException; import com.amplifyframework.api.aws.auth.ApiRequestDecoratorFactory; @@ -152,7 +153,8 @@ private boolean hasAuthRelatedErrors(GraphQLResponse response) { return false; } - static Builder builder() { + @InternalAmplifyApi + public static Builder builder() { return new Builder<>(); } @@ -202,7 +204,8 @@ public void onFailure(@NonNull Call call, @NonNull IOException exception) { } } - static final class Builder { + @InternalAmplifyApi + public static final class Builder { private String endpoint; private OkHttpClient client; private GraphQLRequest request; @@ -213,53 +216,63 @@ static final class Builder { private ExecutorService executorService; private String apiName; - Builder endpoint(@NonNull String endpoint) { + @InternalAmplifyApi + public Builder endpoint(@NonNull String endpoint) { this.endpoint = Objects.requireNonNull(endpoint); return this; } - Builder client(@NonNull OkHttpClient client) { + @InternalAmplifyApi + public Builder client(@NonNull OkHttpClient client) { this.client = Objects.requireNonNull(client); return this; } - Builder request(@NonNull GraphQLRequest request) { + @InternalAmplifyApi + public Builder request(@NonNull GraphQLRequest request) { this.request = Objects.requireNonNull(request); return this; } - Builder responseFactory(@NonNull GraphQLResponse.Factory responseFactory) { + @InternalAmplifyApi + public Builder responseFactory(@NonNull GraphQLResponse.Factory responseFactory) { this.responseFactory = Objects.requireNonNull(responseFactory); return this; } - Builder onResponse(@NonNull Consumer> onResponse) { + @InternalAmplifyApi + public Builder onResponse(@NonNull Consumer> onResponse) { this.onResponse = Objects.requireNonNull(onResponse); return this; } - Builder onFailure(@NonNull Consumer onFailure) { + @InternalAmplifyApi + public Builder onFailure(@NonNull Consumer onFailure) { this.onFailure = Objects.requireNonNull(onFailure); return this; } - Builder apiRequestDecoratorFactory(ApiRequestDecoratorFactory apiRequestDecoratorFactory) { + @InternalAmplifyApi + public Builder apiRequestDecoratorFactory(ApiRequestDecoratorFactory apiRequestDecoratorFactory) { this.apiRequestDecoratorFactory = apiRequestDecoratorFactory; return this; } - Builder executorService(ExecutorService executorService) { + @InternalAmplifyApi + public Builder executorService(ExecutorService executorService) { this.executorService = executorService; return this; } - Builder apiName(String apiName) { + @InternalAmplifyApi + public Builder apiName(String apiName) { this.apiName = apiName; return this; } + @InternalAmplifyApi @SuppressLint("SyntheticAccessor") - MultiAuthAppSyncGraphQLOperation build() { + public MultiAuthAppSyncGraphQLOperation build() { return new MultiAuthAppSyncGraphQLOperation<>(this); } diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/MultiAuthSubscriptionOperation.java b/aws-api/src/main/java/com/amplifyframework/api/aws/MultiAuthSubscriptionOperation.java index a904fb139a..ffbbccd2fa 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/MultiAuthSubscriptionOperation.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/MultiAuthSubscriptionOperation.java @@ -18,6 +18,7 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import com.amplifyframework.annotations.InternalAmplifyApi; import com.amplifyframework.api.ApiException; import com.amplifyframework.api.ApiException.ApiAuthException; import com.amplifyframework.api.aws.auth.AuthRuleRequestDecorator; @@ -37,7 +38,8 @@ import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicBoolean; -final class MultiAuthSubscriptionOperation extends AWSGraphQLOperation { +@InternalAmplifyApi +public final class MultiAuthSubscriptionOperation extends AWSGraphQLOperation { private static final Logger LOG = Amplify.Logging.logger(CategoryType.API, "amplify:aws-api"); private final SubscriptionEndpoint subscriptionEndpoint; @@ -68,7 +70,8 @@ private MultiAuthSubscriptionOperation(Builder builder) { } @NonNull - static Builder builder() { + @InternalAmplifyApi + public static Builder builder() { return new Builder<>(); } @@ -197,7 +200,8 @@ Future getSubscriptionFuture() { return subscriptionFuture; } - static final class Builder { + @InternalAmplifyApi + public static final class Builder { private SubscriptionEndpoint subscriptionEndpoint; private AppSyncGraphQLRequest graphQlRequest; private GraphQLResponse.Factory responseFactory; @@ -210,65 +214,76 @@ static final class Builder { private String apiName; @NonNull + @InternalAmplifyApi public Builder subscriptionEndpoint(@NonNull SubscriptionEndpoint subscriptionEndpoint) { this.subscriptionEndpoint = Objects.requireNonNull(subscriptionEndpoint); return this; } @NonNull + @InternalAmplifyApi public Builder graphQlRequest(@NonNull AppSyncGraphQLRequest graphQlRequest) { this.graphQlRequest = Objects.requireNonNull(graphQlRequest); return this; } @NonNull + @InternalAmplifyApi public Builder responseFactory(@NonNull GraphQLResponse.Factory responseFactory) { this.responseFactory = Objects.requireNonNull(responseFactory); return this; } @NonNull + @InternalAmplifyApi public Builder executorService(@NonNull ExecutorService executorService) { this.executorService = Objects.requireNonNull(executorService); return this; } @NonNull + @InternalAmplifyApi public Builder onSubscriptionStart(@NonNull Consumer onSubscriptionStart) { this.onSubscriptionStart = Objects.requireNonNull(onSubscriptionStart); return this; } @NonNull + @InternalAmplifyApi public Builder onNextItem(@NonNull Consumer> onNextItem) { this.onNextItem = Objects.requireNonNull(onNextItem); return this; } @NonNull + @InternalAmplifyApi public Builder onSubscriptionError(@NonNull Consumer onSubscriptionError) { this.onSubscriptionError = Objects.requireNonNull(onSubscriptionError); return this; } @NonNull + @InternalAmplifyApi public Builder onSubscriptionComplete(@NonNull Action onSubscriptionComplete) { this.onSubscriptionComplete = Objects.requireNonNull(onSubscriptionComplete); return this; } + @InternalAmplifyApi public Builder requestDecorator(AuthRuleRequestDecorator requestDecorator) { this.requestDecorator = requestDecorator; return this; } @NonNull + @InternalAmplifyApi public Builder apiName(String apiName) { this.apiName = apiName; return this; } @NonNull + @InternalAmplifyApi public MultiAuthSubscriptionOperation build() { return new MultiAuthSubscriptionOperation<>(this); } diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/SubscriptionEndpoint.java b/aws-api/src/main/java/com/amplifyframework/api/aws/SubscriptionEndpoint.java index 5e883325cc..6c20542eca 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 @@ -57,11 +57,14 @@ import okhttp3.WebSocket; import okhttp3.WebSocketListener; +import com.amplifyframework.annotations.InternalAmplifyApi; + /** * Manages the lifecycle of a single WebSocket connection, * and multiple GraphQL subscriptions that work on top of it. */ -final class SubscriptionEndpoint { +@InternalAmplifyApi +public final class SubscriptionEndpoint { private static final Logger LOG = Amplify.Logging.logger(CategoryType.API, "amplify:aws-api"); private static final int CONNECTION_ACKNOWLEDGEMENT_TIMEOUT = 30 /* seconds */; private static final int NORMAL_CLOSURE_STATUS = 1000; @@ -104,7 +107,54 @@ final class SubscriptionEndpoint { this.okHttpClient = okHttpClientBuilder.build(); } - void requestSubscription( + /** + * Convenience constructor for standalone use outside the plugin. Builds the internal + * configuration and authorizer from raw parameters. + * + * @param endpoint The AppSync GraphQL endpoint URL. + * @param region The AWS region. + * @param authorizationType The default authorization type. + * @param apiKey The API key (required when authorizationType is API_KEY, null otherwise). + * @param configurator Optional OkHttp client configurator. + * @param responseFactory Factory for deserializing GraphQL responses. + * @param authProviders Optional auth providers for token/credential resolution. + */ + @InternalAmplifyApi + public SubscriptionEndpoint( + @NonNull String endpoint, + @NonNull String region, + @NonNull AuthorizationType authorizationType, + @Nullable String apiKey, + @Nullable OkHttpConfigurator configurator, + @NonNull GraphQLResponse.Factory responseFactory, + @Nullable ApiAuthProviders authProviders + ) { + this( + ApiConfiguration.builder() + .endpoint(endpoint) + .region(region) + .endpointType(EndpointType.GRAPHQL) + .authorizationType(authorizationType) + .apiKey(apiKey) + .build(), + configurator, + responseFactory, + new SubscriptionAuthorizer( + ApiConfiguration.builder() + .endpoint(endpoint) + .region(region) + .endpointType(EndpointType.GRAPHQL) + .authorizationType(authorizationType) + .apiKey(apiKey) + .build(), + authProviders != null ? authProviders : ApiAuthProviders.noProviderOverrides() + ), + null + ); + } + + @InternalAmplifyApi + public void requestSubscription( @NonNull GraphQLRequest request, @NonNull AuthorizationType authType, @NonNull Consumer onSubscriptionStarted, @@ -261,7 +311,8 @@ private void notifySubscriptionData(String subscriptionId, String data) throws A dispatcher.dispatchNextMessage(data); } - void releaseSubscription(String subscriptionId) throws ApiException { + @InternalAmplifyApi + public void releaseSubscription(String subscriptionId) throws ApiException { // First thing we should do is remove it from the pending subscription collection so // the other methods can't grab a hold of the subscription. final Subscription subscription = subscriptions.get(subscriptionId); diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/SubscriptionMessageType.java b/aws-api/src/main/java/com/amplifyframework/api/aws/SubscriptionMessageType.java index ec9a2e43b1..3212bd2baf 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/SubscriptionMessageType.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/SubscriptionMessageType.java @@ -17,13 +17,16 @@ import androidx.annotation.NonNull; +import com.amplifyframework.annotations.InternalAmplifyApi; + /** * An enumeration of the values that are possible in the "type" field * of a subscription message. * @see GraphQL Over WebSocket Message Types * @see GraphQL Over WebSocket Protocol */ -enum SubscriptionMessageType { +@InternalAmplifyApi +public enum SubscriptionMessageType { /** * Client requests initialization of a connection, to the server. diff --git a/aws-api/src/main/java/com/amplifyframework/api/aws/TimeoutWatchdog.java b/aws-api/src/main/java/com/amplifyframework/api/aws/TimeoutWatchdog.java index c56fa8c723..e29fa6d043 100644 --- a/aws-api/src/main/java/com/amplifyframework/api/aws/TimeoutWatchdog.java +++ b/aws-api/src/main/java/com/amplifyframework/api/aws/TimeoutWatchdog.java @@ -21,11 +21,14 @@ import com.amplifyframework.AmplifyException; import com.amplifyframework.api.ApiException; +import com.amplifyframework.annotations.InternalAmplifyApi; + /** * Closes the WebSocket connection if the time remaining has elapsed. * Enables resetting of the watchdog remaining time. */ -final class TimeoutWatchdog { +@InternalAmplifyApi +public final class TimeoutWatchdog { private final Handler handler; private Runnable timeoutAction; diff --git a/settings.gradle.kts b/settings.gradle.kts index 0e501cdfa7..e90a2dd88e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -64,13 +64,15 @@ include(":maplibre-adapter") include(":aws-pinpoint-core") include(":aws-push-notifications-pinpoint-common") -// Events API +// AppSync Libraries include(":aws-sdk-appsync-core") include(":aws-sdk-appsync-amplify") include(":aws-sdk-appsync-events") +include(":aws-appsync") project(":aws-sdk-appsync-core").projectDir = file("appsync/aws-sdk-appsync-core") project(":aws-sdk-appsync-amplify").projectDir = file("appsync/aws-sdk-appsync-amplify") project(":aws-sdk-appsync-events").projectDir = file("appsync/aws-sdk-appsync-events") +project(":aws-appsync").projectDir = file("appsync/aws-appsync") // Apollo Extensions