Skip to content

Commit 127c8cc

Browse files
calvincestarigh-action-runner
authored andcommitted
fix: Generate local cache mutation referenced fragments as mutable (#659)
1 parent 62f24d6 commit 127c8cc

File tree

8 files changed

+276
-23
lines changed

8 files changed

+276
-23
lines changed

Tests/ApolloCodegenInternalTestHelpers/MockCompilationResult.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ public extension CompilationResult.FragmentDefinition {
149149
directives: nil,
150150
referencedFragments: [],
151151
source: source,
152-
filePath: path
152+
filePath: path,
153+
overrideAsLocalCacheMutation: false
153154
)
154155
}
155156
}

Tests/ApolloCodegenTests/CodeGeneration/Templates/LocalCacheMutationDefinitionTemplateTests.swift

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase {
1111
var document: String!
1212
var ir: IRBuilderTestWrapper!
1313
var operation: IRTestWrapper<IR.Operation>!
14+
var fragment: IRTestWrapper<IR.NamedFragment>!
1415
var config: ApolloCodegenConfiguration!
15-
var subject: LocalCacheMutationDefinitionTemplate!
16+
var subject: (any TemplateRenderer)!
1617

1718
override func setUp() {
1819
super.setUp()
@@ -42,6 +43,7 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase {
4243
document = nil
4344
ir = nil
4445
operation = nil
46+
fragment = nil
4547
config = nil
4648
subject = nil
4749
super.tearDown()
@@ -59,6 +61,16 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase {
5961
)
6062
}
6163

64+
private func buildSubjectAndFragment(named fragmentName: String = "TestFragment") async throws {
65+
ir = try await IRBuilderTestWrapper(.mock(schema: schemaSDL, document: document))
66+
let fragmentDefinition = try XCTUnwrap(ir.compilationResult[fragment: fragmentName])
67+
fragment = await ir.build(fragment: fragmentDefinition)
68+
subject = FragmentTemplate(
69+
fragment: fragment.irObject,
70+
config: ApolloCodegen.ConfigurationContext(config: config)
71+
)
72+
}
73+
6274
private func renderSubject() -> String {
6375
subject.renderBodyTemplate(nonFatalErrorRecorder: .init()).description
6476
}
@@ -270,7 +282,96 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase {
270282
expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true))
271283
}
272284

273-
func test__generate_givenQuery_configIncludesMarkOperationDefinitionsAsFinal_generatesFinalLocalCacheMutation() async throws {
285+
func test__generate__givenQueryWithReferencedFragment_generatesReferencedFragmentAsMutable() async throws {
286+
document = """
287+
query TestOperation @apollo_client_ios_localCacheMutation {
288+
allAnimals {
289+
...SpeciesFragment
290+
}
291+
}
292+
293+
fragment SpeciesFragment on Animal {
294+
species
295+
}
296+
"""
297+
298+
let expected = """
299+
struct SpeciesFragment: TestSchema.MutableSelectionSet, Fragment {
300+
"""
301+
302+
try await buildSubjectAndFragment(named: "SpeciesFragment")
303+
304+
let actual = renderSubject()
305+
306+
// then
307+
expect(actual).to(equalLineByLine(expected, atLine: 1, ignoringExtraLines: true))
308+
}
309+
310+
func test__generate__givenFragmentWithReferencedFragment_generatesReferencedFragmentAsMutable() async throws {
311+
schemaSDL = """
312+
type Query {
313+
allAnimals: [Animal!]
314+
}
315+
316+
type Animal {
317+
name: String!
318+
species: String!
319+
friend: Animal!
320+
}
321+
"""
322+
323+
document = """
324+
query TestOperation {
325+
allAnimals {
326+
...NameFragment
327+
...SpeciesFragment
328+
}
329+
}
330+
331+
fragment NameFragment on Animal {
332+
name
333+
}
334+
335+
fragment SpeciesFragment on Animal @apollo_client_ios_localCacheMutation {
336+
species
337+
...FriendFragment
338+
}
339+
340+
fragment FriendFragment on Animal {
341+
friend {
342+
name
343+
}
344+
}
345+
"""
346+
347+
let expectedNameFragment = """
348+
struct NameFragment: TestSchema.SelectionSet, Fragment {
349+
"""
350+
351+
let expectedSpeciesFragment = """
352+
struct SpeciesFragment: TestSchema.MutableSelectionSet, Fragment {
353+
"""
354+
355+
let expectedFriendFragment = """
356+
struct FriendFragment: TestSchema.MutableSelectionSet, Fragment {
357+
"""
358+
359+
try await buildSubjectAndFragment(named: "NameFragment")
360+
let renderedNameFragment = renderSubject()
361+
362+
try await buildSubjectAndFragment(named: "SpeciesFragment")
363+
let renderedSpeciesFragment = renderSubject()
364+
365+
try await buildSubjectAndFragment(named: "FriendFragment")
366+
let renderedFriendFragment = renderSubject()
367+
368+
// then
369+
expect(renderedNameFragment).to(equalLineByLine(expectedNameFragment, atLine: 1, ignoringExtraLines: true))
370+
expect(renderedSpeciesFragment).to(equalLineByLine(expectedSpeciesFragment, atLine: 1, ignoringExtraLines: true))
371+
expect(renderedFriendFragment).to(equalLineByLine(expectedFriendFragment, atLine: 1, ignoringExtraLines: true))
372+
}
373+
374+
func test__generate__givenQuery_configIncludesMarkOperationDefinitionsAsFinal_generatesFinalLocalCacheMutation() async throws {
274375
// given
275376
let expected =
276377
"""
@@ -289,7 +390,6 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase {
289390
expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true))
290391
}
291392

292-
293393
func test__generate__givenQueryWithLowercasing_generatesCorrectlyCasedLocalCacheMutation() async throws {
294394
// given
295395
schemaSDL = """

apollo-ios-codegen/Sources/GraphQLCompiler/ApolloCodegenFrontendBundle.swift

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apollo-ios-codegen/Sources/GraphQLCompiler/CompilationResult.swift

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -230,11 +230,15 @@ public final class CompilationResult: JavaScriptObjectDecodable {
230230
public let filePath: String
231231

232232
public var isLocalCacheMutation: Bool {
233-
directives?.contains { $0.name == Constants.DirectiveNames.LocalCacheMutation } ?? false
233+
overrideAsLocalCacheMutation || directives?.contains {
234+
$0.name == Constants.DirectiveNames.LocalCacheMutation
235+
} ?? false
234236
}
235237

236238
public let moduleImports: OrderedSet<String>
237239

240+
public let overrideAsLocalCacheMutation: Bool
241+
238242
init(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) {
239243
self.name = jsValue["name"]
240244
self.directives = .fromJSValue(jsValue["directives"], bridge: bridge)
@@ -243,8 +247,11 @@ public final class CompilationResult: JavaScriptObjectDecodable {
243247
self.referencedFragments = .fromJSValue(jsValue["referencedFragments"], bridge: bridge)
244248
self.source = jsValue["source"]
245249
self.filePath = jsValue["filePath"]
246-
self.moduleImports = FragmentDefinition.getImportModuleNames(directives: directives,
247-
referencedFragments: referencedFragments)
250+
self.moduleImports = FragmentDefinition.getImportModuleNames(
251+
directives: directives,
252+
referencedFragments: referencedFragments
253+
)
254+
self.overrideAsLocalCacheMutation = jsValue["overrideAsLocalCacheMutation"]
248255
}
249256

250257
/// Initializer to be used for creating mock objects in tests only.
@@ -255,7 +262,8 @@ public final class CompilationResult: JavaScriptObjectDecodable {
255262
directives: [Directive]?,
256263
referencedFragments: [FragmentDefinition],
257264
source: String,
258-
filePath: String
265+
filePath: String,
266+
overrideAsLocalCacheMutation: Bool
259267
) {
260268
self.name = name
261269
self.type = type
@@ -264,8 +272,11 @@ public final class CompilationResult: JavaScriptObjectDecodable {
264272
self.referencedFragments = referencedFragments
265273
self.source = source
266274
self.filePath = filePath
267-
self.moduleImports = FragmentDefinition.getImportModuleNames(directives: directives,
268-
referencedFragments: referencedFragments)
275+
self.moduleImports = FragmentDefinition.getImportModuleNames(
276+
directives: directives,
277+
referencedFragments: referencedFragments
278+
)
279+
self.overrideAsLocalCacheMutation = overrideAsLocalCacheMutation
269280
}
270281

271282
public var debugDescription: String {

apollo-ios-codegen/Sources/GraphQLCompiler/JavaScript/src/__tests__/referencedFragmentsTests.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,114 @@ describe("operation with referencedFragments on child entity selection sets", ()
184184
});
185185

186186
});
187+
188+
describe("local cache mutation operation with referenced fragments", () => {
189+
const schemaSDL: string =
190+
`type Query {
191+
allAnimals: [Animal!]
192+
}
193+
194+
interface Animal {
195+
species: String!
196+
friend: Animal!
197+
}`;
198+
199+
const schema: GraphQLSchema = loadSchemaFromSources([new Source(schemaSDL, "Test Schema", { line: 1, column: 1 })]);
200+
201+
const documentString: string =
202+
`query Test @apollo_client_ios_localCacheMutation {
203+
allAnimals {
204+
...SpeciesFragment
205+
}
206+
}
207+
208+
fragment SpeciesFragment on Animal {
209+
species
210+
...FriendFragment
211+
}
212+
213+
fragment FriendFragment on Animal {
214+
friend {
215+
species
216+
}
217+
}`;
218+
219+
const document: DocumentNode = parseOperationDocument(
220+
new Source(documentString, "Test Query", { line: 1, column: 1 })
221+
);
222+
223+
it("should flag the referenced fragments as being local cache mutations too.", () => {
224+
const compilationResult: CompilationResult = compileDocument(schema, document, false, false, emptyValidationOptions);
225+
226+
const speciesFragment: FragmentDefinition = compilationResult.fragments.find(function (element) {
227+
return element.name == 'SpeciesFragment'
228+
}) as FragmentDefinition
229+
const friendFragment: FragmentDefinition = compilationResult.fragments.find(function (element) {
230+
return element.name == 'FriendFragment'
231+
}) as FragmentDefinition
232+
233+
expect(speciesFragment.overrideAsLocalCacheMutation).toBeTruthy();
234+
expect(friendFragment.overrideAsLocalCacheMutation).toBeTruthy();
235+
});
236+
});
237+
238+
describe("local cache mutation fragment with referenced fragments", () => {
239+
const schemaSDL: string =
240+
`type Query {
241+
allAnimals: [Animal!]
242+
}
243+
244+
interface Animal {
245+
name: String!
246+
species: String!
247+
friend: Animal!
248+
}`;
249+
250+
const schema: GraphQLSchema = loadSchemaFromSources([new Source(schemaSDL, "Test Schema", { line: 1, column: 1 })]);
251+
252+
const documentString: string =
253+
`query Test {
254+
allAnimals {
255+
...NameFragment
256+
...SpeciesFragment
257+
}
258+
}
259+
260+
fragment NameFragment on Animal {
261+
name
262+
}
263+
264+
fragment SpeciesFragment on Animal @apollo_client_ios_localCacheMutation {
265+
species
266+
...FriendFragment
267+
}
268+
269+
fragment FriendFragment on Animal {
270+
friend {
271+
species
272+
}
273+
}`;
274+
275+
const document: DocumentNode = parseOperationDocument(
276+
new Source(documentString, "Test Query", { line: 1, column: 1 })
277+
);
278+
279+
it("should only flag the cache mutation referenced fragment as being local cache mutation.", () => {
280+
const compilationResult: CompilationResult = compileDocument(schema, document, false, false, emptyValidationOptions);
281+
282+
const nameFragment: FragmentDefinition = compilationResult.fragments.find(function (element) {
283+
return element.name == 'NameFragment'
284+
}) as FragmentDefinition
285+
const speciesFragment: FragmentDefinition = compilationResult.fragments.find(function (element) {
286+
return element.name == 'SpeciesFragment'
287+
}) as FragmentDefinition
288+
const friendFragment: FragmentDefinition = compilationResult.fragments.find(function (element) {
289+
return element.name == 'FriendFragment'
290+
}) as FragmentDefinition
291+
292+
expect(nameFragment.overrideAsLocalCacheMutation).toBeFalsy();
293+
expect(speciesFragment.overrideAsLocalCacheMutation).toBeFalsy();
294+
295+
expect(friendFragment.overrideAsLocalCacheMutation).toBeTruthy();
296+
});
297+
});

apollo-ios-codegen/Sources/GraphQLCompiler/JavaScript/src/compiler/index.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
containsLocalCacheMutationDirective,
23
getFieldDef,
34
isMetaFieldName,
45
isNotNullOrUndefined,
@@ -229,18 +230,21 @@ export function compileToIR(
229230

230231
addReferencedType(rootType)
231232

233+
const selectionSet = compileSelectionSet(operationDefinition.selectionSet, rootType, referencedFragments)
234+
const referencedFragmentsArray = Array.from(referencedFragments.values())
235+
236+
if (containsLocalCacheMutationDirective(operationDefinition.directives)) {
237+
overrideAsLocalCacheMutation(referencedFragmentsArray);
238+
}
239+
232240
return {
233241
name,
234242
operationType,
235243
variables,
236244
rootType,
237-
selectionSet: compileSelectionSet(
238-
operationDefinition.selectionSet,
239-
rootType,
240-
referencedFragments
241-
),
245+
selectionSet: selectionSet,
242246
directives: directives,
243-
referencedFragments: Array.from(referencedFragments.values()),
247+
referencedFragments: referencedFragmentsArray,
244248
source,
245249
filePath
246250
};
@@ -267,21 +271,34 @@ export function compileToIR(
267271

268272
addReferencedType(getNamedType(typeCondition));
269273

274+
const selectionSet = compileSelectionSet(fragmentDefinition.selectionSet, typeCondition, referencedFragments)
275+
const referencedFragmentsArray = Array.from(referencedFragments.values())
276+
277+
if (containsLocalCacheMutationDirective(fragmentDefinition.directives)) {
278+
overrideAsLocalCacheMutation(referencedFragmentsArray);
279+
}
280+
270281
return {
271282
name,
272283
filePath,
273284
source,
274285
typeCondition,
275-
selectionSet: compileSelectionSet(
276-
fragmentDefinition.selectionSet,
277-
typeCondition,
278-
referencedFragments
279-
),
286+
selectionSet: selectionSet,
280287
directives: directives,
281-
referencedFragments: Array.from(referencedFragments.values()),
288+
referencedFragments: referencedFragmentsArray,
289+
overrideAsLocalCacheMutation: false
282290
};
283291
}
284292

293+
function overrideAsLocalCacheMutation(
294+
fragments: ir.FragmentDefinition[]
295+
) {
296+
fragments.forEach(element => {
297+
element.overrideAsLocalCacheMutation = true
298+
overrideAsLocalCacheMutation(element.referencedFragments)
299+
});
300+
}
301+
285302
function compileSelectionSet(
286303
selectionSetNode: SelectionSetNode,
287304
parentType: GraphQLCompositeType,

0 commit comments

Comments
 (0)