-
Notifications
You must be signed in to change notification settings - Fork 236
feat(api): add appsync client interface #4217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
mattcreaser
merged 2 commits into
feat/appsync-client
from
jv/appsync-client-add-interface
Aug 18, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
167 changes: 167 additions & 0 deletions
167
AmplifyClients/AmplifyAppSyncClient/Sources/AmplifyAppSyncClient.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| // | ||
| // Copyright Amazon.com Inc. or its affiliates. | ||
| // All Rights Reserved. | ||
| // | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // | ||
|
|
||
| import Amplify | ||
| import Foundation | ||
|
|
||
| /// 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. | ||
| /// | ||
| /// ```swift | ||
| /// let client = try AmplifyAppSyncClient( | ||
| /// configuration: .init( | ||
| /// endpoint: "https://xxx.appsync-api.us-east-1.amazonaws.com/graphql", | ||
| /// authorization: .single(.apiKey("da2-xxx")) | ||
| /// ) | ||
| /// ) | ||
| /// | ||
| /// let response = try await client.query(request) | ||
| /// ``` | ||
| public final class AmplifyAppSyncClient: Sendable { | ||
|
|
||
| /// The configuration this client was created with. | ||
| public let configuration: Configuration | ||
|
|
||
| /// Creates a new AppSync client. | ||
| /// | ||
| /// - Parameter configuration: The client configuration including endpoint and authorization. | ||
| /// - Throws: If the configuration is invalid (e.g., region cannot be inferred). | ||
| public init(configuration: Configuration) throws { | ||
| self.configuration = configuration | ||
| // TODO: Initialize internal HTTP and WebSocket transports | ||
| } | ||
|
|
||
| // MARK: - Query | ||
|
|
||
| /// Execute a GraphQL query. | ||
| /// | ||
| /// - Parameter request: The GraphQL request. | ||
| /// - Returns: The typed GraphQL response. | ||
| /// - Throws: `AppSyncError` or `AppSyncAuthError` on failure. | ||
| public func query<T: Decodable & Sendable>( | ||
| _ request: GraphQLRequest<T> | ||
| ) async throws -> GraphQLResponse<T> { | ||
| fatalError("Not yet implemented") | ||
| } | ||
|
|
||
| // MARK: - Mutation | ||
|
|
||
| /// Execute a GraphQL mutation. | ||
| /// | ||
| /// - Parameter request: The GraphQL request. | ||
| /// - Returns: The typed GraphQL response. | ||
| /// - Throws: `AppSyncError` or `AppSyncAuthError` on failure. | ||
| public func mutate<T: Decodable & Sendable>( | ||
| _ request: GraphQLRequest<T> | ||
| ) async throws -> GraphQLResponse<T> { | ||
| fatalError("Not yet implemented") | ||
| } | ||
|
|
||
| // MARK: - Subscription | ||
|
|
||
| /// Subscribe to a GraphQL subscription. | ||
| /// | ||
| /// The WebSocket connection is lazy (established on first subscribe) and shared across | ||
| /// all subscriptions on this client. Cancelling the task terminates the subscription. | ||
| /// | ||
| /// - Parameter request: The GraphQL subscription request. | ||
| /// - Returns: An async stream of subscription events. | ||
| public func subscribe<T: Decodable & Sendable>( | ||
| _ request: GraphQLRequest<T> | ||
| ) -> AsyncThrowingStream<SubscriptionEvent<T>, Error> { | ||
| fatalError("Not yet implemented") | ||
| } | ||
|
|
||
| // MARK: - Connection State | ||
|
|
||
| /// Per-client WebSocket connection state. | ||
| /// Emits `ConnectionState` changes for the shared WebSocket connection. | ||
| public var connectionState: AsyncStream<ConnectionState> { | ||
| fatalError("Not yet implemented") | ||
| } | ||
|
|
||
| // MARK: - Lifecycle | ||
|
|
||
| /// Close the client. Terminates all active subscriptions and releases resources. | ||
| /// The client cannot be reused after closing. | ||
| public func close() { | ||
| // TODO: Close HTTP and WebSocket transports | ||
| } | ||
|
|
||
| // MARK: - Configuration | ||
|
|
||
| /// Configuration for `AmplifyAppSyncClient`. | ||
| public struct Configuration: @unchecked Sendable { | ||
| /// The AppSync GraphQL endpoint URL. | ||
| public let endpoint: URL | ||
|
|
||
| /// Auth configuration for the client. | ||
| public let authorization: AppSyncAuthorization | ||
|
|
||
| /// AWS region. Inferred from the endpoint URL if not provided. | ||
| public let region: String | ||
|
|
||
| /// Optional configurator for the URLSession used for HTTP requests. | ||
| public let urlSessionConfiguration: URLSessionConfiguration? | ||
|
|
||
| /// Creates a new configuration. | ||
| /// | ||
| /// - Parameters: | ||
| /// - endpoint: The AppSync GraphQL endpoint URL string. | ||
| /// - authorization: Auth configuration for the client. | ||
| /// - region: AWS region. If nil, inferred from the endpoint URL. | ||
| /// - urlSessionConfiguration: Optional URLSession configuration for HTTP requests. | ||
| /// - Throws: If the endpoint is not a valid URL or region cannot be inferred. | ||
| public init( | ||
| endpoint: String, | ||
| authorization: AppSyncAuthorization, | ||
| region: String? = nil, | ||
| urlSessionConfiguration: URLSessionConfiguration? = nil | ||
| ) throws { | ||
| guard let url = URL(string: endpoint) else { | ||
| throw ConfigurationError.invalidEndpoint(endpoint) | ||
| } | ||
| let resolvedRegion = region ?? Self.inferRegion(from: endpoint) | ||
| guard let resolvedRegion else { | ||
| throw ConfigurationError.regionRequired | ||
| } | ||
| self.endpoint = url | ||
| self.authorization = authorization | ||
| self.region = resolvedRegion | ||
| self.urlSessionConfiguration = urlSessionConfiguration | ||
| } | ||
|
|
||
| /// Infer the AWS region from an AppSync endpoint URL. | ||
| /// Expected format: `https://{id}.appsync-api.{region}.amazonaws.com/graphql` | ||
| static func inferRegion(from endpoint: String) -> String? { | ||
| let pattern = #"\.appsync-api\.([a-z0-9-]+)\.amazonaws\.com"# | ||
| guard let regex = try? NSRegularExpression(pattern: pattern), | ||
| let match = regex.firstMatch( | ||
| in: endpoint, | ||
| range: NSRange(endpoint.startIndex..., in: endpoint) | ||
| ), | ||
| let range = Range(match.range(at: 1), in: endpoint) else { | ||
| return nil | ||
| } | ||
| return String(endpoint[range]) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Configuration Errors | ||
|
|
||
| public extension AmplifyAppSyncClient { | ||
| /// Errors thrown during configuration validation. | ||
| enum ConfigurationError: Error, Sendable { | ||
| /// The endpoint string is not a valid URL. | ||
| case invalidEndpoint(String) | ||
| /// Region could not be inferred and was not provided. | ||
| case regionRequired | ||
| } | ||
| } | ||
22 changes: 22 additions & 0 deletions
22
AmplifyClients/AmplifyAppSyncClient/Sources/AppSyncAuthMode.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| // | ||
| // Copyright Amazon.com Inc. or its affiliates. | ||
| // All Rights Reserved. | ||
| // | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| /// The authorization modes supported by AWS AppSync. | ||
| public enum AppSyncAuthMode: Sendable { | ||
| /// API Key authorization. | ||
| case apiKey | ||
| /// Amazon Cognito User Pools authorization. | ||
| case userPools | ||
| /// OpenID Connect authorization. | ||
| case oidc | ||
| /// AWS IAM authorization (SigV4 signing). | ||
| case iam | ||
| /// AWS Lambda custom authorization. | ||
| case lambda | ||
| } |
67 changes: 67 additions & 0 deletions
67
AmplifyClients/AmplifyAppSyncClient/Sources/AppSyncAuthorization.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| // | ||
| // Copyright Amazon.com Inc. or its affiliates. | ||
| // All Rights Reserved. | ||
| // | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| /// 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). | ||
| public enum AppSyncAuthorization: Sendable { | ||
|
|
||
| /// Single authorizer used for all requests. | ||
| case single(AppSyncAuthorizer) | ||
|
|
||
| /// 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. | ||
| /// | ||
| /// - Parameters: | ||
| /// - defaultAuthMode: The auth mode to use when no per-request override or model rule applies. | ||
| /// - authorizers: The list of authorizers. Duplicate auth modes are not allowed. | ||
| case multi(defaultAuthMode: AppSyncAuthMode, authorizers: [AppSyncAuthorizer]) | ||
| } | ||
|
|
||
| extension AppSyncAuthorization { | ||
|
|
||
| /// Resolves the authorizer for a given auth mode. | ||
| /// - Returns: The matching authorizer, or nil if not found. | ||
| func authorizer(for mode: AppSyncAuthMode) -> AppSyncAuthorizer? { | ||
| switch self { | ||
| case .single(let authorizer): | ||
| return authorizer.authMode == mode ? authorizer : nil | ||
| case .multi(_, let authorizers): | ||
| return authorizers.first { $0.authMode == mode } | ||
| } | ||
| } | ||
|
|
||
| /// The default authorizer. | ||
| var defaultAuthorizer: AppSyncAuthorizer { | ||
| switch self { | ||
| case .single(let authorizer): | ||
| return authorizer | ||
| case .multi(let defaultAuthMode, let authorizers): | ||
| guard let authorizer = authorizers.first(where: { $0.authMode == defaultAuthMode }) else { | ||
| preconditionFailure( | ||
| "No authorizer provided for the default auth mode: \(defaultAuthMode). " + | ||
| "Ensure the authorizers list contains an entry matching the defaultAuthMode." | ||
| ) | ||
| } | ||
| return authorizer | ||
| } | ||
| } | ||
|
|
||
| /// The default auth mode. | ||
| var defaultAuthMode: AppSyncAuthMode { | ||
| switch self { | ||
| case .single(let authorizer): | ||
| return authorizer.authMode | ||
| case .multi(let defaultAuthMode, _): | ||
| return defaultAuthMode | ||
| } | ||
| } | ||
| } |
56 changes: 56 additions & 0 deletions
56
AmplifyClients/AmplifyAppSyncClient/Sources/AppSyncAuthorizer.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| // | ||
| // Copyright Amazon.com Inc. or its affiliates. | ||
| // All Rights Reserved. | ||
| // | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // | ||
|
|
||
| import AWSClientRuntime | ||
| import Foundation | ||
| import SmithyIdentity | ||
|
|
||
| /// An authorizer that provides credentials for a specific AppSync auth mode. | ||
| /// | ||
| /// Each case encodes its auth mode and holds the provider needed to produce | ||
| /// authorization credentials for that mode. | ||
| public enum AppSyncAuthorizer: @unchecked Sendable { | ||
|
|
||
| /// API Key authorization. | ||
| /// - Parameter fetchApiKey: Async function that provides the API key. | ||
| case apiKey(_ fetchApiKey: @Sendable () async throws -> String) | ||
|
|
||
| /// Amazon Cognito User Pools authorization. | ||
| /// - Parameter fetchToken: Async function that returns a valid access/ID token. | ||
| case userPools(_ fetchToken: @Sendable () async throws -> String) | ||
|
|
||
| /// OpenID Connect authorization. | ||
| /// - Parameter fetchToken: Async function that returns a valid OIDC token. | ||
| case oidc(_ fetchToken: @Sendable () async throws -> String) | ||
|
|
||
| /// AWS Lambda custom authorization. | ||
| /// - Parameter fetchToken: Async function that returns a valid authorization token. | ||
| case lambda(_ fetchToken: @Sendable () async throws -> String) | ||
|
|
||
| /// IAM (SigV4) authorization. | ||
| /// - Parameter credentialIdentityResolver: Provides IAM credentials for SigV4 signing. | ||
| case iam(_ credentialIdentityResolver: any AWSCredentialIdentityResolver) | ||
|
|
||
| /// The auth mode this authorizer provides. | ||
| public var authMode: AppSyncAuthMode { | ||
| switch self { | ||
| case .apiKey: return .apiKey | ||
| case .userPools: return .userPools | ||
| case .oidc: return .oidc | ||
| case .lambda: return .lambda | ||
| case .iam: return .iam | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Convenience factory for static API key | ||
| public extension AppSyncAuthorizer { | ||
| /// Creates an API Key authorizer with a static key value. | ||
| static func apiKey(_ apiKey: String) -> AppSyncAuthorizer { | ||
| .apiKey { apiKey } | ||
| } | ||
| } |
19 changes: 19 additions & 0 deletions
19
AmplifyClients/AmplifyAppSyncClient/Sources/ConnectionState.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| // | ||
| // Copyright Amazon.com Inc. or its affiliates. | ||
| // All Rights Reserved. | ||
| // | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| /// The connection state of the client's shared WebSocket. | ||
| public enum ConnectionState: Sendable { | ||
| /// A WebSocket connection is being established. | ||
| case connecting | ||
| /// The WebSocket connection is established and ready. | ||
| case connected | ||
| /// No active WebSocket connection. | ||
| /// - Parameter reason: A description of why the connection was lost, or nil for clean shutdown. | ||
| case disconnected(reason: String? = nil) | ||
| } |
23 changes: 23 additions & 0 deletions
23
AmplifyClients/AmplifyAppSyncClient/Sources/SubscriptionEvent.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| // | ||
| // Copyright Amazon.com Inc. or its affiliates. | ||
| // All Rights Reserved. | ||
| // | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // | ||
|
|
||
| import Amplify | ||
| import Foundation | ||
|
|
||
| /// Events emitted by a GraphQL subscription stream. | ||
| /// | ||
| /// Lifecycle: the stream emits `.connecting` → `.connected` → `.data(...)` repeatedly, | ||
| /// then either completes normally (user cancel, server complete, client close) or | ||
| /// throws an error (network, auth, timeout, etc.). | ||
| public enum SubscriptionEvent<T: Decodable & Sendable>: Sendable { | ||
| /// A data message received from the subscription. | ||
| case data(GraphQLResponse<T>) | ||
| /// The subscription is being established (WebSocket connecting + registration in progress). | ||
| case connecting | ||
| /// The subscription is established and receiving data. | ||
| case connected | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just FYI, china regions are slightly different example appsync-api.cn-north-1.amazonaws.com.cn