diff --git a/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/project.pbxproj b/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/project.pbxproj index e44cd6e59a..bf22901ba9 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/project.pbxproj +++ b/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/project.pbxproj @@ -353,6 +353,17 @@ 97914C1E29558AF2002000EA /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 97914C1D29558AF2002000EA /* README.md */; }; 97D4946D2981AF9900397C75 /* AuthSignInHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97D4946B2981AF9900397C75 /* AuthSignInHelper.swift */; }; 97D4946E2981AF9900397C75 /* TestConfigHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97D4946C2981AF9900397C75 /* TestConfigHelper.swift */; }; + D86FBA2B2B95FEBA00024BAC /* GraphQLSubscriptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86FBA2A2B95FEBA00024BAC /* GraphQLSubscriptionsTests.swift */; }; + D86FBA362B96216000024BAC /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = D86FBA352B96216000024BAC /* README.md */; }; + D86FBA382B9624B800024BAC /* schema.graphql in Resources */ = {isa = PBXBuildFile; fileRef = D86FBA372B9624B800024BAC /* schema.graphql */; }; + D86FBA562B9634D400024BAC /* TestConfigHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86FBA542B9634D300024BAC /* TestConfigHelper.swift */; }; + D86FBA5E2B96350D00024BAC /* Comment+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86FBA572B96350D00024BAC /* Comment+Schema.swift */; }; + D86FBA5F2B96350D00024BAC /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86FBA582B96350D00024BAC /* Comment.swift */; }; + D86FBA602B96350D00024BAC /* Blog+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86FBA592B96350D00024BAC /* Blog+Schema.swift */; }; + D86FBA612B96350D00024BAC /* Blog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86FBA5A2B96350D00024BAC /* Blog.swift */; }; + D86FBA622B96350D00024BAC /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86FBA5B2B96350D00024BAC /* Post.swift */; }; + D86FBA632B96350D00024BAC /* Post+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86FBA5C2B96350D00024BAC /* Post+Schema.swift */; }; + D86FBA642B96350D00024BAC /* AmplifyModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86FBA5D2B96350D00024BAC /* AmplifyModels.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -440,6 +451,13 @@ remoteGlobalIDString = 21E73E6A28898D7800D7DB7E; remoteInfo = APIHostApp; }; + D86FBA2C2B95FEBA00024BAC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 21E73E6328898D7800D7DB7E /* Project object */; + proxyType = 1; + remoteGlobalIDString = 21E73E6A28898D7800D7DB7E; + remoteInfo = APIHostApp; + }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ @@ -692,6 +710,18 @@ 97914C1D29558AF2002000EA /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 97D4946B2981AF9900397C75 /* AuthSignInHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthSignInHelper.swift; sourceTree = ""; }; 97D4946C2981AF9900397C75 /* TestConfigHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestConfigHelper.swift; sourceTree = ""; }; + D86FBA282B95FEBA00024BAC /* AWSAPIPluginV2Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AWSAPIPluginV2Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + D86FBA2A2B95FEBA00024BAC /* GraphQLSubscriptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLSubscriptionsTests.swift; sourceTree = ""; }; + D86FBA352B96216000024BAC /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + D86FBA372B9624B800024BAC /* schema.graphql */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = schema.graphql; sourceTree = ""; }; + D86FBA542B9634D300024BAC /* TestConfigHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestConfigHelper.swift; sourceTree = ""; }; + D86FBA572B96350D00024BAC /* Comment+Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Comment+Schema.swift"; sourceTree = ""; }; + D86FBA582B96350D00024BAC /* Comment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; + D86FBA592B96350D00024BAC /* Blog+Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Blog+Schema.swift"; sourceTree = ""; }; + D86FBA5A2B96350D00024BAC /* Blog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Blog.swift; sourceTree = ""; }; + D86FBA5B2B96350D00024BAC /* Post.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; + D86FBA5C2B96350D00024BAC /* Post+Schema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Post+Schema.swift"; sourceTree = ""; }; + D86FBA5D2B96350D00024BAC /* AmplifyModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmplifyModels.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -810,6 +840,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D86FBA252B95FEBA00024BAC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -1164,8 +1201,10 @@ 395906C128AC63A9004B96B1 /* AWSAPIPluginRESTUserPoolTests */, 97914BC429558714002000EA /* GraphQLAPIStressTests */, 21EA887428F9BC600000BA75 /* AWSAPIPluginLazyLoadTests */, + D86FBA292B95FEBA00024BAC /* AWSAPIPluginV2Tests */, 21E73E6C28898D7900D7DB7E /* Products */, 21698BD728899EBB004BD994 /* Frameworks */, + 39AC502FEC3F482AF1545BE2 /* AmplifyConfig */, ); sourceTree = ""; }; @@ -1186,6 +1225,7 @@ 681B35892A43962D0074F369 /* AWSAPIPluginFunctionalTestsWatch.xctest */, 681B35A12A4396CF0074F369 /* AWSAPIPluginGraphQLLambdaAuthTestsWatch.xctest */, 681B35C52A43970A0074F369 /* AWSAPIPluginRESTIAMTestsWatch.xctest */, + D86FBA282B95FEBA00024BAC /* AWSAPIPluginV2Tests.xctest */, ); name = Products; sourceTree = ""; @@ -1405,6 +1445,40 @@ path = Base; sourceTree = ""; }; + D86FBA292B95FEBA00024BAC /* AWSAPIPluginV2Tests */ = { + isa = PBXGroup; + children = ( + D86FBA532B9634D300024BAC /* Base */, + D86FBA552B9634D400024BAC /* Models */, + D86FBA372B9624B800024BAC /* schema.graphql */, + D86FBA352B96216000024BAC /* README.md */, + D86FBA2A2B95FEBA00024BAC /* GraphQLSubscriptionsTests.swift */, + ); + path = AWSAPIPluginV2Tests; + sourceTree = ""; + }; + D86FBA532B9634D300024BAC /* Base */ = { + isa = PBXGroup; + children = ( + D86FBA542B9634D300024BAC /* TestConfigHelper.swift */, + ); + path = Base; + sourceTree = ""; + }; + D86FBA552B9634D400024BAC /* Models */ = { + isa = PBXGroup; + children = ( + D86FBA5D2B96350D00024BAC /* AmplifyModels.swift */, + D86FBA5A2B96350D00024BAC /* Blog.swift */, + D86FBA592B96350D00024BAC /* Blog+Schema.swift */, + D86FBA582B96350D00024BAC /* Comment.swift */, + D86FBA572B96350D00024BAC /* Comment+Schema.swift */, + D86FBA5B2B96350D00024BAC /* Post.swift */, + D86FBA5C2B96350D00024BAC /* Post+Schema.swift */, + ); + path = Models; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -1696,6 +1770,24 @@ productReference = 97914C182955872A002000EA /* GraphQLAPIStressTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + D86FBA272B95FEBA00024BAC /* AWSAPIPluginV2Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = D86FBA302B95FEBA00024BAC /* Build configuration list for PBXNativeTarget "AWSAPIPluginV2Tests" */; + buildPhases = ( + D86FBA242B95FEBA00024BAC /* Sources */, + D86FBA252B95FEBA00024BAC /* Frameworks */, + D86FBA262B95FEBA00024BAC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D86FBA2D2B95FEBA00024BAC /* PBXTargetDependency */, + ); + name = AWSAPIPluginV2Tests; + productName = AWSAPIPluginV2Tests; + productReference = D86FBA282B95FEBA00024BAC /* AWSAPIPluginV2Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -1703,7 +1795,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1430; + LastSwiftUpdateCheck = 1520; LastUpgradeCheck = 1340; TargetAttributes = { 213DBC7428A6C47000B30280 = { @@ -1755,6 +1847,10 @@ 681B35B62A43970A0074F369 = { TestTargetID = 681B35282A4395730074F369; }; + D86FBA272B95FEBA00024BAC = { + CreatedOnToolsVersion = 15.2; + TestTargetID = 21E73E6A28898D7800D7DB7E; + }; }; }; buildConfigurationList = 21E73E6628898D7800D7DB7E /* Build configuration list for PBXProject "APIHostApp" */; @@ -1787,6 +1883,7 @@ 681B353E2A43962D0074F369 /* AWSAPIPluginFunctionalTestsWatch */, 681B35912A4396CF0074F369 /* AWSAPIPluginGraphQLLambdaAuthTestsWatch */, 681B35B62A43970A0074F369 /* AWSAPIPluginRESTIAMTestsWatch */, + D86FBA272B95FEBA00024BAC /* AWSAPIPluginV2Tests */, ); }; /* End PBXProject section */ @@ -1893,6 +1990,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D86FBA262B95FEBA00024BAC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D86FBA382B9624B800024BAC /* schema.graphql in Resources */, + D86FBA362B96216000024BAC /* README.md in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -2539,6 +2645,22 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D86FBA242B95FEBA00024BAC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D86FBA2B2B95FEBA00024BAC /* GraphQLSubscriptionsTests.swift in Sources */, + D86FBA562B9634D400024BAC /* TestConfigHelper.swift in Sources */, + D86FBA602B96350D00024BAC /* Blog+Schema.swift in Sources */, + D86FBA612B96350D00024BAC /* Blog.swift in Sources */, + D86FBA642B96350D00024BAC /* AmplifyModels.swift in Sources */, + D86FBA5E2B96350D00024BAC /* Comment+Schema.swift in Sources */, + D86FBA622B96350D00024BAC /* Post.swift in Sources */, + D86FBA632B96350D00024BAC /* Post+Schema.swift in Sources */, + D86FBA5F2B96350D00024BAC /* Comment.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -2602,6 +2724,11 @@ target = 21E73E6A28898D7800D7DB7E /* APIHostApp */; targetProxy = 97914BCE2955872A002000EA /* PBXContainerItemProxy */; }; + D86FBA2D2B95FEBA00024BAC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 21E73E6A28898D7800D7DB7E /* APIHostApp */; + targetProxy = D86FBA2C2B95FEBA00024BAC /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -3409,6 +3536,53 @@ }; name = Release; }; + D86FBA2E2B95FEBA00024BAC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.aws.amplify.api.AWSAPIPluginV2Tests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/APIHostApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/APIHostApp"; + }; + name = Debug; + }; + D86FBA2F2B95FEBA00024BAC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.aws.amplify.api.AWSAPIPluginV2Tests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/APIHostApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/APIHostApp"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -3547,6 +3721,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + D86FBA302B95FEBA00024BAC /* Build configuration list for PBXNativeTarget "AWSAPIPluginV2Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D86FBA2E2B95FEBA00024BAC /* Debug */, + D86FBA2F2B95FEBA00024BAC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ diff --git a/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/xcshareddata/xcschemes/APIHostApp.xcscheme b/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/xcshareddata/xcschemes/APIHostApp.xcscheme index 5e6d13047d..9aad6b919d 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/xcshareddata/xcschemes/APIHostApp.xcscheme +++ b/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/xcshareddata/xcschemes/APIHostApp.xcscheme @@ -49,6 +49,17 @@ ReferencedContainer = "container:APIHostApp.xcodeproj"> + + + + AmplifyConfiguration { + + let data = try retrieve(forResource: forResource) + return try AmplifyConfiguration.decodeAmplifyConfiguration(from: data) + } + + static func retrieveCredentials(forResource: String) throws -> [String: String] { + let data = try retrieve(forResource: forResource) + + let jsonOptional = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] + guard let json = jsonOptional else { + throw "Could not deserialize `\(forResource)` into JSON object" + } + + return json + } + + static func retrieve(forResource: String) throws -> Data { + guard let path = Bundle(for: self).path(forResource: forResource, ofType: "json") else { + throw "Could not retrieve configuration file: \(forResource)" + } + + let url = URL(fileURLWithPath: path) + return try Data(contentsOf: url) + } +} + +extension String { + var withUUID: String { + "\(self)-\(UUID().uuidString)" + } +} diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/GraphQLSubscriptionsTests.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/GraphQLSubscriptionsTests.swift new file mode 100644 index 0000000000..312e2fb05d --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/GraphQLSubscriptionsTests.swift @@ -0,0 +1,211 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +@testable import AWSAPIPlugin +import AWSPluginsCore +@testable import Amplify +@testable import APIHostApp + +final class GraphQLSubscriptionsTests: XCTestCase { + static let amplifyConfiguration = "testconfiguration/AWSAPIPluginV2Tests-amplifyconfiguration" + + override func setUp() async throws { + await Amplify.reset() + Amplify.Logging.logLevel = .verbose + + let plugin = AWSAPIPlugin(modelRegistration: AmplifyModels()) + + do { + try Amplify.add(plugin: plugin) + + let amplifyConfig = try TestConfigHelper.retrieveAmplifyConfiguration( + forResource: GraphQLSubscriptionsTests.amplifyConfiguration) + try Amplify.configure(amplifyConfig) + + } catch { + XCTFail("Error during setup: \(error)") + } + } + + override func tearDown() async throws { + await Amplify.reset() + } + + /// Given: GraphQL onCreate subscription request with filter + /// When: Adding models - one not matching and two matching the filter + /// Then: Receive mutation syncs only for matching models + func testOnCreatePostSubscriptionWithFilter() async throws { + let incorrectTitle = "other_title" + let incorrectPost1Id = UUID().uuidString + + let correctTitle = "correct" + let correctPost1Id = UUID().uuidString + let correctPost2Id = UUID().uuidString + + let connectedInvoked = expectation(description: "Connection established") + let onCreateCorrectPost1 = expectation(description: "Receioved onCreate for correctPost1") + let onCreateCorrectPost2 = expectation(description: "Receioved onCreate for correctPost2") + + let modelType = Post.self + let filter: QueryPredicate = modelType.keys.title.eq(correctTitle) + let request = GraphQLRequest.subscription(to: modelType, where: filter, subscriptionType: .onCreate) + + let subscription = Amplify.API.subscribe(request: request) + Task { + do { + for try await subscriptionEvent in subscription { + switch subscriptionEvent { + case .connection(let state): + switch state { + case .connected: + connectedInvoked.fulfill() + + case .connecting, .disconnected: + break + } + + case .data(let graphQLResponse): + switch graphQLResponse { + case .success(let mutationSync): + if mutationSync.model.id == correctPost1Id { + onCreateCorrectPost1.fulfill() + + } else if mutationSync.model.id == correctPost2Id { + onCreateCorrectPost2.fulfill() + + } else if mutationSync.model.id == incorrectPost1Id { + XCTFail("We should not receive onCreate for filtered out model!") + } + + case .failure(let error): + XCTFail(error.errorDescription) + } + } + } + + } catch { + XCTFail("Unexpected subscription failure: \(error)") + } + } + + await fulfillment(of: [connectedInvoked], timeout: TestCommonConstants.networkTimeout) + + guard try await createPost(id: incorrectPost1Id, title: incorrectTitle) != nil else { + XCTFail("Failed to create post"); return + } + + guard try await createPost(id: correctPost1Id, title: correctTitle) != nil else { + XCTFail("Failed to create post"); return + } + + guard try await createPost(id: correctPost2Id, title: correctTitle) != nil else { + XCTFail("Failed to create post"); return + } + + await fulfillment(of: [onCreateCorrectPost1], timeout: TestCommonConstants.networkTimeout) + await fulfillment(of: [onCreateCorrectPost2], timeout: TestCommonConstants.networkTimeout) + + subscription.cancel() + } + + func testOnCreatePostSubscriptionWithTooManyFiltersFallbackToNoFilter() async throws { + let incorrectTitle = "other_title" + let incorrectPost1Id = UUID().uuidString + + let correctTitle = "correct" + let correctPost1Id = UUID().uuidString + let correctPost2Id = UUID().uuidString + + let connectedInvoked = expectation(description: "Connection established") + let onCreateCorrectPost1 = expectation(description: "Receioved onCreate for correctPost1") + let onCreateCorrectPost2 = expectation(description: "Receioved onCreate for correctPost2") + + let modelType = Post.self + let filter: QueryPredicate = QueryPredicateGroup(type: .or, predicates: + (0...20).map { + modelType.keys.title.eq("\($0)") + } + ) + + let request = GraphQLRequest.subscription(to: modelType, where: filter, subscriptionType: .onCreate) + + let subscription = Amplify.API.subscribe(request: request) + Task { + do { + for try await subscriptionEvent in subscription { + switch subscriptionEvent { + case .connection(let state): + switch state { + case .connected: + connectedInvoked.fulfill() + + case .connecting, .disconnected: + break + } + + case .data(let graphQLResponse): + switch graphQLResponse { + case .success(let mutationSync): + if mutationSync.model.id == correctPost1Id { + onCreateCorrectPost1.fulfill() + + } else if mutationSync.model.id == correctPost2Id { + onCreateCorrectPost2.fulfill() + + } else if mutationSync.model.id == incorrectPost1Id { + XCTFail("We should not receive onCreate for filtered out model!") + } + + case .failure(let error): + XCTFail(error.errorDescription) + } + } + } + + } catch { + XCTFail("Unexpected subscription failure: \(error)") + } + } + + await fulfillment(of: [connectedInvoked], timeout: TestCommonConstants.networkTimeout) + + guard try await createPost(id: incorrectPost1Id, title: incorrectTitle) != nil else { + XCTFail("Failed to create post"); return + } + + guard try await createPost(id: correctPost1Id, title: correctTitle) != nil else { + XCTFail("Failed to create post"); return + } + + guard try await createPost(id: correctPost2Id, title: correctTitle) != nil else { + XCTFail("Failed to create post"); return + } + + await fulfillment(of: [onCreateCorrectPost1], timeout: TestCommonConstants.networkTimeout) + await fulfillment(of: [onCreateCorrectPost2], timeout: TestCommonConstants.networkTimeout) + + subscription.cancel() + } + + // MARK: Helpers + + func createPost(id: String, title: String) async throws -> Post? { + let post = Post(id: id, title: title, createdAt: .now()) + return try await createPost(post: post) + } + + func createPost(post: Post) async throws -> Post? { + let data = try await Amplify.API.mutate(request: .createMutation(of: post, version: 0)) + switch data { + case .success(let post): + return post.model.instance as? Post + case .failure(let error): + throw error + } + } +} diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/AmplifyModels.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/AmplifyModels.swift new file mode 100644 index 0000000000..8a7eaae2e4 --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/AmplifyModels.swift @@ -0,0 +1,15 @@ +// swiftlint:disable all +import Amplify +import Foundation + +// Contains the set of classes that conforms to the `Model` protocol. + +final public class AmplifyModels: AmplifyModelRegistration { + public let version: String = "165944a36979cd395e3b22145bbfeff0" + + public func registerModels(registry: ModelRegistry.Type) { + ModelRegistry.register(modelType: Blog.self) + ModelRegistry.register(modelType: Post.self) + ModelRegistry.register(modelType: Comment.self) + } +} diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/Blog+Schema.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/Blog+Schema.swift new file mode 100644 index 0000000000..154bee9921 --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/Blog+Schema.swift @@ -0,0 +1,40 @@ +// swiftlint:disable all +import Amplify +import Foundation + +extension Blog { + // MARK: - CodingKeys + public enum CodingKeys: String, ModelKey { + case id + case name + case posts + case createdAt + case updatedAt + } + + public static let keys = CodingKeys.self + // MARK: - ModelSchema + + public static let schema = defineSchema { model in + let blog = Blog.keys + + model.pluralName = "Blogs" + + model.attributes( + .primaryKey(fields: [blog.id]) + ) + + model.fields( + .field(blog.id, is: .required, ofType: .string), + .field(blog.name, is: .required, ofType: .string), + .hasMany(blog.posts, is: .optional, ofType: Post.self, associatedWith: Post.keys.blog), + .field(blog.createdAt, is: .optional, isReadOnly: true, ofType: .dateTime), + .field(blog.updatedAt, is: .optional, isReadOnly: true, ofType: .dateTime) + ) + } +} + +extension Blog: ModelIdentifiable { + public typealias IdentifierFormat = ModelIdentifierFormat.Default + public typealias IdentifierProtocol = DefaultModelIdentifier +} diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/Blog.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/Blog.swift new file mode 100644 index 0000000000..0f4b439bfd --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/Blog.swift @@ -0,0 +1,32 @@ +// swiftlint:disable all +import Amplify +import Foundation + +public struct Blog: Model { + public let id: String + public var name: String + public var posts: List? + public var createdAt: Temporal.DateTime? + public var updatedAt: Temporal.DateTime? + + public init(id: String = UUID().uuidString, + name: String, + posts: List? = []) { + self.init(id: id, + name: name, + posts: posts, + createdAt: nil, + updatedAt: nil) + } + internal init(id: String = UUID().uuidString, + name: String, + posts: List? = [], + createdAt: Temporal.DateTime? = nil, + updatedAt: Temporal.DateTime? = nil) { + self.id = id + self.name = name + self.posts = posts + self.createdAt = createdAt + self.updatedAt = updatedAt + } +} diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/Comment+Schema.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/Comment+Schema.swift new file mode 100644 index 0000000000..a13f3b9803 --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/Comment+Schema.swift @@ -0,0 +1,40 @@ +// swiftlint:disable all +import Amplify +import Foundation + +extension Comment { + // MARK: - CodingKeys + public enum CodingKeys: String, ModelKey { + case id + case post + case content + case createdAt + case updatedAt + } + + public static let keys = CodingKeys.self + // MARK: - ModelSchema + + public static let schema = defineSchema { model in + let comment = Comment.keys + + model.pluralName = "Comments" + + model.attributes( + .primaryKey(fields: [comment.id]) + ) + + model.fields( + .field(comment.id, is: .required, ofType: .string), + .belongsTo(comment.post, is: .optional, ofType: Post.self, targetNames: ["postCommentsId"]), + .field(comment.content, is: .required, ofType: .string), + .field(comment.createdAt, is: .optional, isReadOnly: true, ofType: .dateTime), + .field(comment.updatedAt, is: .optional, isReadOnly: true, ofType: .dateTime) + ) + } +} + +extension Comment: ModelIdentifiable { + public typealias IdentifierFormat = ModelIdentifierFormat.Default + public typealias IdentifierProtocol = DefaultModelIdentifier +} diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/Comment.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/Comment.swift new file mode 100644 index 0000000000..eb85015113 --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/Comment.swift @@ -0,0 +1,32 @@ +// swiftlint:disable all +import Amplify +import Foundation + +public struct Comment: Model { + public let id: String + public var post: Post? + public var content: String + public var createdAt: Temporal.DateTime? + public var updatedAt: Temporal.DateTime? + + public init(id: String = UUID().uuidString, + post: Post? = nil, + content: String) { + self.init(id: id, + post: post, + content: content, + createdAt: nil, + updatedAt: nil) + } + internal init(id: String = UUID().uuidString, + post: Post? = nil, + content: String, + createdAt: Temporal.DateTime? = nil, + updatedAt: Temporal.DateTime? = nil) { + self.id = id + self.post = post + self.content = content + self.createdAt = createdAt + self.updatedAt = updatedAt + } +} diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/Post+Schema.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/Post+Schema.swift new file mode 100644 index 0000000000..a2522fd899 --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/Post+Schema.swift @@ -0,0 +1,42 @@ +// swiftlint:disable all +import Amplify +import Foundation + +extension Post { + // MARK: - CodingKeys + public enum CodingKeys: String, ModelKey { + case id + case title + case blog + case comments + case createdAt + case updatedAt + } + + public static let keys = CodingKeys.self + // MARK: - ModelSchema + + public static let schema = defineSchema { model in + let post = Post.keys + + model.pluralName = "Posts" + + model.attributes( + .primaryKey(fields: [post.id]) + ) + + model.fields( + .field(post.id, is: .required, ofType: .string), + .field(post.title, is: .required, ofType: .string), + .belongsTo(post.blog, is: .optional, ofType: Blog.self, targetNames: ["blogPostsId"]), + .hasMany(post.comments, is: .optional, ofType: Comment.self, associatedWith: Comment.keys.post), + .field(post.createdAt, is: .optional, isReadOnly: true, ofType: .dateTime), + .field(post.updatedAt, is: .optional, isReadOnly: true, ofType: .dateTime) + ) + } +} + +extension Post: ModelIdentifiable { + public typealias IdentifierFormat = ModelIdentifierFormat.Default + public typealias IdentifierProtocol = DefaultModelIdentifier +} diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/Post.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/Post.swift new file mode 100644 index 0000000000..8f3c33670d --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/Models/Post.swift @@ -0,0 +1,37 @@ +// swiftlint:disable all +import Amplify +import Foundation + +public struct Post: Model { + public let id: String + public var title: String + public var blog: Blog? + public var comments: List? + public var createdAt: Temporal.DateTime? + public var updatedAt: Temporal.DateTime? + + public init(id: String = UUID().uuidString, + title: String, + blog: Blog? = nil, + comments: List? = []) { + self.init(id: id, + title: title, + blog: blog, + comments: comments, + createdAt: nil, + updatedAt: nil) + } + internal init(id: String = UUID().uuidString, + title: String, + blog: Blog? = nil, + comments: List? = [], + createdAt: Temporal.DateTime? = nil, + updatedAt: Temporal.DateTime? = nil) { + self.id = id + self.title = title + self.blog = blog + self.comments = comments + self.createdAt = createdAt + self.updatedAt = updatedAt + } +} diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/README.md b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/README.md new file mode 100644 index 0000000000..e67ee2fa73 --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/README.md @@ -0,0 +1,40 @@ +## GraphQL API Integration V2 Tests + +### Prerequisites +- AWS CLI +- Version used: `amplify -v` => `10.3.1` + +### Set-up + +1. `amplify init` and choose `iOS` for type of app you are building + +2. `amplify add api` + +```perl +? Select from one of the below mentioned services: `GraphQL` +? Enable conflict detection? Yes +? Select the default resolution strategy `Optimistic Concurrency` +? Here is the GraphQL API that we will create. Select a setting to edit or continue Authorization modes: `API key (default, expiration time: 7 days from now)` +? Choose the default authorization type for the API API key +? Enter a description for the API key: +? After how many days from now the API key should expire (1-365): `365` +? Configure additional auth types? `No` +? Here is the GraphQL API that we will create. Select a setting to edit or continue Continue +? Choose a schema template: `Single object with fields (e.g., “Todo” with ID, name, description)` + +Then edit your schema and replace it with **amplify-swift/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/schema.graphql** + +3. `amplify push` + +4. Verify that the changes were pushed with the transformer V2 feature flags enabled. In `amplify/cli.json`, the feature flags values should be the following +``` +features.graphqltransformer.transformerversion: 2 +features.graphqltransformer.useexperimentalpipelinedtransformer: true +``` + +5. Copy `amplifyconfiguration.json` to a new file named `AWSAPIPluginV2Tests-amplifyconfiguration.json` inside `~/.aws-amplify/amplify-ios/testconfiguration/` +``` +cp amplifyconfiguration.json ~/.aws-amplify/amplify-ios/testconfiguration/AWSAPIPluginV2Tests-amplifyconfiguration.json +``` + +You should now be able to run all of the tests under the AWSAPIPluginV2Tests folder diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/schema.graphql b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/schema.graphql new file mode 100644 index 0000000000..9f1985e3bb --- /dev/null +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginV2Tests/schema.graphql @@ -0,0 +1,22 @@ +# 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 { + id: ID! + name: String! + posts: [Post] @hasMany +} + +type Post @model { + id: ID! + title: String! + blog: Blog @belongsTo + comments: [Comment] @hasMany +} + +type Comment @model { + id: ID! + post: Post @belongsTo + content: String! +} diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Model/Decorator/FilterDecorator.swift b/AmplifyPlugins/Core/AWSPluginsCore/Model/Decorator/FilterDecorator.swift index bd78aad37f..ad7cc770c9 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Model/Decorator/FilterDecorator.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Model/Decorator/FilterDecorator.swift @@ -37,6 +37,9 @@ public struct FilterDecorator: ModelBasedGraphQLDocumentDecorator { } else if case .query = document.operationType { inputs["filter"] = GraphQLDocumentInput(type: "Model\(modelName)FilterInput", value: .object(filter)) + } else if case .subscription = document.operationType { + inputs["filter"] = GraphQLDocumentInput(type: "ModelSubscription\(modelName)FilterInput", + value: .object(filter)) } return document.copy(inputs: inputs) diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Model/GraphQLRequest/GraphQLRequest+AnyModelWithSync.swift b/AmplifyPlugins/Core/AWSPluginsCore/Model/GraphQLRequest/GraphQLRequest+AnyModelWithSync.swift index 44af846765..f4e8ca03b3 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Model/GraphQLRequest/GraphQLRequest+AnyModelWithSync.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Model/GraphQLRequest/GraphQLRequest+AnyModelWithSync.swift @@ -37,10 +37,12 @@ protocol ModelSyncGraphQLRequestFactory { authType: AWSAuthorizationType?) -> GraphQLRequest static func subscription(to modelSchema: ModelSchema, + where predicate: QueryPredicate?, subscriptionType: GraphQLSubscriptionType, authType: AWSAuthorizationType?) -> GraphQLRequest static func subscription(to modelSchema: ModelSchema, + where predicate: QueryPredicate?, subscriptionType: GraphQLSubscriptionType, claims: IdentityClaimsDictionary, authType: AWSAuthorizationType?) -> GraphQLRequest @@ -94,16 +96,18 @@ extension GraphQLRequest: ModelSyncGraphQLRequestFactory { } public static func subscription(to modelType: Model.Type, + where predicate: QueryPredicate? = nil, subscriptionType: GraphQLSubscriptionType, authType: AWSAuthorizationType? = nil) -> GraphQLRequest { - subscription(to: modelType.schema, subscriptionType: subscriptionType, authType: authType) + subscription(to: modelType.schema, where: predicate, subscriptionType: subscriptionType, authType: authType) } public static func subscription(to modelType: Model.Type, + where predicate: QueryPredicate? = nil, subscriptionType: GraphQLSubscriptionType, claims: IdentityClaimsDictionary, authType: AWSAuthorizationType? = nil) -> GraphQLRequest { - subscription(to: modelType.schema, subscriptionType: subscriptionType, claims: claims, authType: authType) + subscription(to: modelType.schema, where: predicate, subscriptionType: subscriptionType, claims: claims, authType: authType) } public static func syncQuery(modelType: Model.Type, @@ -169,13 +173,18 @@ extension GraphQLRequest: ModelSyncGraphQLRequestFactory { } public static func subscription(to modelSchema: ModelSchema, + where predicate: QueryPredicate? = nil, subscriptionType: GraphQLSubscriptionType, authType: AWSAuthorizationType? = nil) -> GraphQLRequest { var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelSchema: modelSchema, operationType: .subscription, primaryKeysOnly: true) + documentBuilder.add(decorator: DirectiveNameDecorator(type: subscriptionType)) + if let predicate = optimizePredicate(predicate) { + documentBuilder.add(decorator: FilterDecorator(filter: predicate.graphQLFilter(for: modelSchema))) + } documentBuilder.add(decorator: ConflictResolutionDecorator(graphQLType: .subscription, primaryKeysOnly: true)) documentBuilder.add(decorator: AuthRuleDecorator(.subscription(subscriptionType, nil), authType: authType)) let document = documentBuilder.build() @@ -190,6 +199,7 @@ extension GraphQLRequest: ModelSyncGraphQLRequestFactory { } public static func subscription(to modelSchema: ModelSchema, + where predicate: QueryPredicate? = nil, subscriptionType: GraphQLSubscriptionType, claims: IdentityClaimsDictionary, authType: AWSAuthorizationType? = nil) -> GraphQLRequest { @@ -197,7 +207,11 @@ extension GraphQLRequest: ModelSyncGraphQLRequestFactory { var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelSchema: modelSchema, operationType: .subscription, primaryKeysOnly: true) + documentBuilder.add(decorator: DirectiveNameDecorator(type: subscriptionType)) + if let predicate = optimizePredicate(predicate) { + documentBuilder.add(decorator: FilterDecorator(filter: predicate.graphQLFilter(for: modelSchema))) + } documentBuilder.add(decorator: ConflictResolutionDecorator(graphQLType: .subscription, primaryKeysOnly: true)) documentBuilder.add(decorator: AuthRuleDecorator(.subscription(subscriptionType, claims), authType: authType)) let document = documentBuilder.build() diff --git a/AmplifyPlugins/Core/AWSPluginsCoreTests/Model/GraphQLRequest/GraphQLRequestAnyModelWithSyncTests.swift b/AmplifyPlugins/Core/AWSPluginsCoreTests/Model/GraphQLRequest/GraphQLRequestAnyModelWithSyncTests.swift index 7af997bad4..bca410e82d 100644 --- a/AmplifyPlugins/Core/AWSPluginsCoreTests/Model/GraphQLRequest/GraphQLRequestAnyModelWithSyncTests.swift +++ b/AmplifyPlugins/Core/AWSPluginsCoreTests/Model/GraphQLRequest/GraphQLRequestAnyModelWithSyncTests.swift @@ -16,7 +16,7 @@ class GraphQLRequestAnyModelWithSyncTests: XCTestCase { override func setUp() { ModelRegistry.register(modelType: Comment.self) ModelRegistry.register(modelType: Post.self) - + ModelRegistry.register(modelType: ModelWithOwnerField.self) } override func tearDown() { @@ -419,4 +419,122 @@ class GraphQLRequestAnyModelWithSyncTests: XCTestCase { } XCTAssertEqual(conditionValue["eq"], "myTitle") } + + func testCreateSubscriptionGraphQLRequestWithFilter() throws { + let modelType = Post.self as Model.Type + let modelSchema = modelType.schema + let predicate: QueryPredicate = Post.keys.rating > 0 + let filter = QueryPredicateGroup(type: .and, predicates: [predicate]).graphQLFilter(for: modelSchema) + + var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelSchema: modelSchema, + operationType: .subscription) + + documentBuilder.add(decorator: DirectiveNameDecorator(type: .onCreate)) + documentBuilder.add(decorator: FilterDecorator(filter: filter)) + documentBuilder.add(decorator: ConflictResolutionDecorator(graphQLType: .subscription)) + let document = documentBuilder.build() + + let documentStringValue = """ + subscription OnCreatePost($filter: ModelSubscriptionPostFilterInput) { + onCreatePost(filter: $filter) { + id + content + createdAt + draft + rating + status + title + updatedAt + __typename + _version + _deleted + _lastChangedAt + } + } + """ + let request = GraphQLRequest.subscription(to: modelSchema, + where: predicate, + subscriptionType: .onCreate) + + XCTAssertEqual(document.stringValue, request.document) + XCTAssertEqual(documentStringValue, request.document) + XCTAssert(request.responseType == MutationSyncResult.self) + + guard let variables = request.variables else { + XCTFail("The request doesn't contain variables") + return + } + guard + let filter = variables["filter"] as? [String: [[String: [String: Int]]]], + filter == ["and": [["rating": ["gt": 0]]]] + else { + XCTFail("The document variables property doesn't contain a valid filter") + return + } + } + + func testCreateSubscriptionGraphQLRequestWithFilterAndClaims() throws { + let modelType = ModelWithOwnerField.self as Model.Type + let modelSchema = modelType.schema + let author = "MuniekMg" + let username = "user1" + let predicate: QueryPredicate = ModelWithOwnerField.keys.author.eq(author) + let filter = QueryPredicateGroup(type: .and, predicates: [predicate]).graphQLFilter(for: modelSchema) + let claims = [ + "username": username, + "sub": "123e4567-dead-beef-a456-426614174000" + ] as IdentityClaimsDictionary + + var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelSchema: modelSchema, + operationType: .subscription) + + documentBuilder.add(decorator: DirectiveNameDecorator(type: .onCreate)) + documentBuilder.add(decorator: FilterDecorator(filter: filter)) + documentBuilder.add(decorator: ConflictResolutionDecorator(graphQLType: .subscription)) + documentBuilder.add(decorator: AuthRuleDecorator(.subscription(.onCreate, claims))) + let document = documentBuilder.build() + + let documentStringValue = """ + subscription OnCreateModelWithOwnerField($author: String!, $filter: ModelSubscriptionModelWithOwnerFieldFilterInput) { + onCreateModelWithOwnerField(author: $author, filter: $filter) { + id + author + content + __typename + _version + _deleted + _lastChangedAt + } + } + """ + let request = GraphQLRequest.subscription(to: modelSchema, + where: predicate, + subscriptionType: .onCreate, + claims: claims) + + XCTAssertEqual(document.stringValue, request.document) + XCTAssertEqual(documentStringValue, request.document) + XCTAssert(request.responseType == MutationSyncResult.self) + + guard let variables = request.variables else { + XCTFail("The request doesn't contain variables") + return + } + + guard + let filter = variables["filter"] as? [String: [[String: [String: String]]]], + filter == ["and": [["author": ["eq": author]]]] + else { + XCTFail("The document variables property doesn't contain a valid filter") + return + } + + guard + let author = variables["author"] as? String, + author == username + else { + XCTFail("The document variables property doesn't contain a valid claims") + return + } + } } diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift index d5dae69b37..0066b7ab3a 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift @@ -78,7 +78,8 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { self.onCreateValueListener = onCreateValueListener self.onCreateOperation = RetryableGraphQLSubscriptionOperation( requestFactory: IncomingAsyncSubscriptionEventPublisher.apiRequestFactoryFor( - for: modelSchema, + for: modelSchema, + where: modelPredicate, subscriptionType: .onCreate, api: api, auth: auth, @@ -100,6 +101,7 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { self.onUpdateOperation = RetryableGraphQLSubscriptionOperation( requestFactory: IncomingAsyncSubscriptionEventPublisher.apiRequestFactoryFor( for: modelSchema, + where: modelPredicate, subscriptionType: .onUpdate, api: api, auth: auth, @@ -120,7 +122,8 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { self.onDeleteValueListener = onDeleteValueListener self.onDeleteOperation = RetryableGraphQLSubscriptionOperation( requestFactory: IncomingAsyncSubscriptionEventPublisher.apiRequestFactoryFor( - for: modelSchema, + for: modelSchema, + where: modelPredicate, subscriptionType: .onDelete, api: api, auth: auth, @@ -195,6 +198,7 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { } static func makeAPIRequest(for modelSchema: ModelSchema, + where predicate: QueryPredicate?, subscriptionType: GraphQLSubscriptionType, api: APICategoryGraphQLBehaviorExtended, auth: AuthCategoryBehavior?, @@ -205,7 +209,8 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { let _ = auth, let tokenString = try? await awsAuthService.getUserPoolAccessToken(), case .success(let claims) = awsAuthService.getTokenClaims(tokenString: tokenString) { - request = GraphQLRequest.subscription(to: modelSchema, + request = GraphQLRequest.subscription(to: modelSchema, + where: predicate, subscriptionType: subscriptionType, claims: claims, authType: authType) @@ -213,12 +218,14 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { let oidcAuthProvider = hasOIDCAuthProviderAvailable(api: api), let tokenString = try? await oidcAuthProvider.getLatestAuthToken(), case .success(let claims) = awsAuthService.getTokenClaims(tokenString: tokenString) { - request = GraphQLRequest.subscription(to: modelSchema, + request = GraphQLRequest.subscription(to: modelSchema, + where: predicate, subscriptionType: subscriptionType, claims: claims, authType: authType) } else { request = GraphQLRequest.subscription(to: modelSchema, + where: predicate, subscriptionType: subscriptionType, authType: authType) } @@ -296,6 +303,7 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { // MARK: - IncomingAsyncSubscriptionEventPublisher + API request factory extension IncomingAsyncSubscriptionEventPublisher { static func apiRequestFactoryFor(for modelSchema: ModelSchema, + where predicate: QueryPredicate?, subscriptionType: GraphQLSubscriptionType, api: APICategoryGraphQLBehaviorExtended, auth: AuthCategoryBehavior?, @@ -303,7 +311,8 @@ extension IncomingAsyncSubscriptionEventPublisher { authTypeProvider: AWSAuthorizationTypeIterator) -> RetryableGraphQLOperation.RequestFactory { var authTypes = authTypeProvider return { - return await IncomingAsyncSubscriptionEventPublisher.makeAPIRequest(for: modelSchema, + return await IncomingAsyncSubscriptionEventPublisher.makeAPIRequest(for: modelSchema, + where: predicate, subscriptionType: subscriptionType, api: api, auth: auth, diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisherTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisherTests.swift index 48100ba687..4f5d5a0f46 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisherTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisherTests.swift @@ -106,4 +106,76 @@ final class IncomingAsyncSubscriptionEventPublisherTests: XCTestCase { XCTAssertEqual(expectedOrder.get(), actualOrder.get()) sink.cancel() } + + /// Given: IncomingAsyncSubscriptionEventPublisher initilized with modelPredicate + /// When: IncomingAsyncSubscriptionEventPublisher subscribes to onCreate, onUpdate, onDelete events + /// Then: IncomingAsyncSubscriptionEventPublisher provides correct filters in subscriptions request + func testModelPredicateAsSubscribtionsFilter() async throws { + + let id1 = UUID().uuidString + let id2 = UUID().uuidString + + let correctFilterOnCreate = expectation(description: "Correct filter in onCreate request") + let correctFilterOnUpdate = expectation(description: "Correct filter in onUpdate request") + let correctFilterOnDelete = expectation(description: "Correct filter in onDelete request") + + func validateVariables(_ variables: [String: Any]?) -> Bool { + guard let variables = variables else { + XCTFail("The request doesn't contain variables") + return false + } + + guard + let filter = variables["filter"] as? [String: [[String: [String: String]]]], + filter == ["or": [ + ["id": ["eq": id1]], + ["id": ["eq": id2]] + ]] + + else { + XCTFail("The document variables property doesn't contain a valid filter") + return false + } + + return true + } + + let responder = SubscribeRequestListenerResponder> { request, _, _ in + if request.document.contains("onCreatePost") { + if validateVariables(request.variables) { + correctFilterOnCreate.fulfill() + } + + } else if request.document.contains("onUpdatePost") { + if validateVariables(request.variables) { + correctFilterOnUpdate.fulfill() + } + + } else if request.document.contains("onDeletePost") { + if validateVariables(request.variables) { + correctFilterOnDelete.fulfill() + } + + } else { + XCTFail("Unexpected request: \(request.document)") + } + + return nil + } + + apiPlugin.responders[.subscribeRequestListener] = responder + + _ = await IncomingAsyncSubscriptionEventPublisher( + modelSchema: Post.schema, + api: apiPlugin, + modelPredicate: QueryPredicateGroup(type: .or, predicates: [ + Post.keys.id.eq(id1), + Post.keys.id.eq(id2) + ]), + auth: nil, + authModeStrategy: AWSDefaultAuthModeStrategy(), + awsAuthService: nil) + + await fulfillment(of: [correctFilterOnCreate, correctFilterOnUpdate, correctFilterOnDelete], timeout: 1) + } }