diff --git a/Tests/ApolloCodegenTests/ApolloCodegenTests.swift b/Tests/ApolloCodegenTests/ApolloCodegenTests.swift index a369c3c7c..1855c2f48 100644 --- a/Tests/ApolloCodegenTests/ApolloCodegenTests.swift +++ b/Tests/ApolloCodegenTests/ApolloCodegenTests.swift @@ -2904,4 +2904,172 @@ class ApolloCodegenTests: XCTestCase { } + // MARK: - Local Cache Mutation + Field Merging Integration Tests + // + // These are integration tests because the codegen test wrapper infrastructure does not support overriding config + // values during the test. + + func test__fileRendering__givenLocalCacheMutationQuery_whenSelectionSetInitializersEmpty_andFileMergingNone_shouldGenerateFullSelectionSetInitializers() async throws { + // given + try createFile( + body: """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + } + """, + filename: "schema.graphqls" + ) + + try createFile( + body: """ + query TestOperation @apollo_client_ios_localCacheMutation { + allAnimals { + species + } + } + """, + filename: "operation.graphql" + ) + + let fileManager = MockApolloFileManager(strict: false) + let expectation = expectation(description: "Received local cache mutation file data.") + + fileManager.mock(closure: .createFile({ path, data, attributes in + if path.hasSuffix("TestOperationLocalCacheMutation.graphql.swift") { + expect(data?.asString).to(equalLineByLine(""" + init( + allAnimals: [AllAnimal]? = nil + ) { + """, atLine: 26, ignoringExtraLines: true)) + + expectation.fulfill() + } + + return true + })) + + // when + let config = ApolloCodegen.ConfigurationContext( + config: ApolloCodegenConfiguration.mock( + input: .init( + schemaSearchPaths: [directoryURL.appendingPathComponent("schema.graphqls").path], + operationSearchPaths: [directoryURL.appendingPathComponent("operation.graphql").path] + ), + // Apollo codegen should override the next two value to force the generation of selection set initializers + // and perform all file merging for the local cache mutation. + options: .init(selectionSetInitializers: []), + experimentalFeatures: .init(fieldMerging: .none) + ), + rootURL: nil + ) + + let subject = ApolloCodegen( + config: config, + operationIdentifierFactory: OperationIdentifierFactory(), + itemsToGenerate: .code + ) + + let compilationResult = try await subject.compileGraphQLResult() + let ir = IRBuilder(compilationResult: compilationResult) + + try await subject.generateFiles( + compilationResult: compilationResult, + ir: ir, + fileManager: fileManager + ) + + // then + expect(fileManager.allClosuresCalled).to(beTrue()) + + await fulfillment(of: [expectation], timeout: 1) + } + + func test__fileRendering__givenLocalCacheMutationFragment_whenSelectionSetInitializersEmpty_andFileMergingNone_shouldGenerateFullSelectionSetInitializers() async throws { + // given + try createFile( + body: """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + } + """, + filename: "schema.graphqls" + ) + + try createFile( + body: """ + query TestOperation { + allAnimals { + ...PredatorFragment + } + } + + fragment PredatorFragment on Animal @apollo_client_ios_localCacheMutation { + species + } + """, + filename: "operation.graphql" + ) + + let fileManager = MockApolloFileManager(strict: false) + let expectation = expectation(description: "Received local cache mutation file data.") + + fileManager.mock(closure: .createFile({ path, data, attributes in + if path.hasSuffix("PredatorFragment.graphql.swift") { + expect(data?.asString).to(equalLineByLine(""" + init( + __typename: String, + species: String? = nil + ) { + """, atLine: 26, ignoringExtraLines: true)) + + expectation.fulfill() + } + + return true + })) + + // when + let config = ApolloCodegen.ConfigurationContext( + config: ApolloCodegenConfiguration.mock( + input: .init( + schemaSearchPaths: [directoryURL.appendingPathComponent("schema.graphqls").path], + operationSearchPaths: [directoryURL.appendingPathComponent("operation.graphql").path] + ), + // Apollo codegen should override the next two value to force the generation of selection set initializers + // and perform all file merging for the local cache mutation. + options: .init(selectionSetInitializers: []), + experimentalFeatures: .init(fieldMerging: .none) + ), + rootURL: nil + ) + + let subject = ApolloCodegen( + config: config, + operationIdentifierFactory: OperationIdentifierFactory(), + itemsToGenerate: .code + ) + + let compilationResult = try await subject.compileGraphQLResult() + let ir = IRBuilder(compilationResult: compilationResult) + + try await subject.generateFiles( + compilationResult: compilationResult, + ir: ir, + fileManager: fileManager + ) + + // then + expect(fileManager.allClosuresCalled).to(beTrue()) + + await fulfillment(of: [expectation], timeout: 1) + } + } diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegen.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegen.swift index 542de62a2..f08338334 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegen.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegen.swift @@ -299,34 +299,68 @@ public class ApolloCodegen { let mergeNamedFragmentFields = config.experimentalFeatures.fieldMerging.options .contains(.namedFragments) + /// A `ConfigurationContext` to use when generated local cache mutations. + /// + /// Local cache mutations require some codegen options to be overridden to generate valid objects. + /// This context overrides only the necessary properties, copying all other values from the user-provided `context`. + lazy var cacheMutationContext: ConfigurationContext = { + ConfigurationContext( + config: ApolloCodegenConfiguration( + schemaNamespace: self.config.schemaNamespace, + input: self.config.input, + output: self.config.output, + options: self.config.options, + experimentalFeatures: ApolloCodegenConfiguration.ExperimentalFeatures( + fieldMerging: .all, + legacySafelistingCompatibleOperations: self.config.experimentalFeatures.legacySafelistingCompatibleOperations + ), + schemaDownload: self.config.schemaDownload, + operationManifest: self.config.operationManifest + ), + rootURL: self.config.rootURL + ) + }() + return try await nonFatalErrorCollectingTaskGroup() { group in for fragment in fragments { + let fragmentConfig = fragment.isLocalCacheMutation ? cacheMutationContext : self.config + group.addTask { let irFragment = await ir.build( fragment: fragment, - mergingNamedFragmentFields: mergeNamedFragmentFields + mergingNamedFragmentFields: fragment.isLocalCacheMutation ? true : mergeNamedFragmentFields ) - let errors = try await FragmentFileGenerator(irFragment: irFragment, config: self.config) - .generate(forConfig: self.config, fileManager: fileManager) + let errors = try await FragmentFileGenerator( + irFragment: irFragment, + config: fragmentConfig + ).generate( + forConfig: fragmentConfig, + fileManager: fileManager + ) return (irFragment.name, errors) } } for operation in operations { + let operationConfig = operation.isLocalCacheMutation ? cacheMutationContext : self.config + group.addTask { async let identifier = self.operationIdentifierFactory.identifier(for: operation) let irOperation = await ir.build( operation: operation, - mergingNamedFragmentFields: mergeNamedFragmentFields + mergingNamedFragmentFields: operation.isLocalCacheMutation ? true : mergeNamedFragmentFields ) let errors = try await OperationFileGenerator( irOperation: irOperation, operationIdentifier: await identifier, - config: self.config - ).generate(forConfig: self.config, fileManager: fileManager) + config: operationConfig + ).generate( + forConfig: operationConfig, + fileManager: fileManager + ) return (irOperation.name, errors) } } diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift index 130489306..b9cf9795b 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift @@ -1434,30 +1434,35 @@ extension ApolloCodegenConfiguration.OperationsFileOutput { extension ApolloCodegenConfiguration { /// Determine whether the operations files are output to the schema types module. func shouldGenerateSelectionSetInitializers(for operation: IR.Operation) -> Bool { - guard experimentalFeatures.fieldMerging == .all else { return false } - if operation.definition.isLocalCacheMutation { return true - } else if options.selectionSetInitializers.contains(.operations) { - return true - } else { - return options.selectionSetInitializers.contains(definitionNamed: operation.definition.name) + guard experimentalFeatures.fieldMerging == .all else { return false } + + if options.selectionSetInitializers.contains(.operations) { + return true + + } else { + return options.selectionSetInitializers.contains(definitionNamed: operation.definition.name) + } } } /// Determine whether the operations files are output to the schema types module. func shouldGenerateSelectionSetInitializers(for fragment: IR.NamedFragment) -> Bool { - guard experimentalFeatures.fieldMerging == .all else { return false } - - if options.selectionSetInitializers.contains(.namedFragments) { return true } - if fragment.definition.isLocalCacheMutation { return true - } - return options.selectionSetInitializers.contains(definitionNamed: fragment.definition.name) + } else { + guard experimentalFeatures.fieldMerging == .all else { return false } + + if options.selectionSetInitializers.contains(.namedFragments) { + return true + } else { + return options.selectionSetInitializers.contains(definitionNamed: fragment.definition.name) + } + } } }