-
Notifications
You must be signed in to change notification settings - Fork 4
Handle @typePolicy in unions interfaces #156
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
Conversation
override fun foreignSchemas(): List<ForeignSchema> { | ||
return listOf(ForeignSchema("cache", "v0.1", cacheControlGQLDefinitions)) | ||
} | ||
|
||
override fun schemaListener(): SchemaListener { | ||
return CacheSchemaListener(environment) | ||
} | ||
|
||
override fun documentTransform(): DocumentTransform { | ||
return AddKeyFieldsDocumentTransform() | ||
} |
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.
That unfolds nicely 👍
@@ -55,6 +58,7 @@ apollo { | |||
packageName.set("donotstore") | |||
srcDir("src/commonMain/graphql/doNotStore") | |||
|
|||
@OptIn(ApolloExperimental::class) |
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.
Reminder to remove the @ApolloExperimental
from v5
return this | ||
} | ||
val keyFields = schema.keyFields(parentType) | ||
val fieldNames = parentFields + this.filterIsInstance<GQLField>().map { it.responseName() } |
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.
Do we really want to use responseName()
here?
{
product {
id: price
}
}
Because responseName()
is id
, we'll skip adding the id
keyfield below? Which doesn't look like what we want?
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.
Ah interesting - that was to handle that case:
{
product {
myId: id
}
}
here we still want to add id
.
There is a check below to disallow using aliases that clash with key fields, but this is broken. We can make this check first. Also this needs a unit test 😅
Good catch! 👍
.../kotlin/com/apollographql/cache/apollocompilerplugin/internal/validateAndComputeKeyFields.kt
Outdated
Show resolved
Hide resolved
typeDefinitions.values.filter { it.canHaveKeyFields() }.forEach { | ||
keyFields(it, keyFieldsCache) |
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.
Since the map is going to be used with object __typename
, there is no need to codegen the interfaces key fields?
Which yields the question that new types (unknown at build time) will now have a bad cache key I think? (because the lookup will be empty?)
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.
I figured we need the interface key fields for that case:
union Union1 = Type1 | Type2
interface Node @typePolicy(keyFields: "id") {
id: ID!
}
type Type1 implements Node {
id: ID!
}
type Type2 @typePolicy(keyFields: "serialNumber") {
serialNumber: String!
}
When union1 returns a Type1
we need to know its key field inherited from Node
.
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.
Right but there is no need to add the interfaces inside the codegened map, or is there?
object Schema {
val keyFields = mapOf(
"Type1" to setOf("id"),
"Type2" to setOf("serialNumber"),
// no need to codegen "Node" here?
)
}
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.
Ah! No you're right, only types are needed.
class KeyFieldsCacheKeyGenerator(private val keyFields: Map<String, Set<String>>) : CacheKeyGenerator { | ||
override fun cacheKeyForObject(obj: Map<String, Any?>, context: CacheKeyGeneratorContext): CacheKey? { | ||
val typename = obj["__typename"].toString() | ||
val keyFields = keyFields[typename] ?: return null |
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.
Here, if the object isn't known at compile time, I believe we get a bad cache key even though it would be technically possible?
{
animal {
__typename # even if typename is not a known animal, we can still compute a cache key
id
species
}
}
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.
Ah good point. So we should fallback to the schema type and its interface(s), found in the compiled field.
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.
If we want to support this, I think so. I think this is what iOS is doing too.
…ollographql/cache/apollocompilerplugin/internal/validateAndComputeKeyFields.kt Co-authored-by: Martin Bonnin <martin@mbonnin.net>
a89f5b1
to
7ba5909
Compare
newSelections = if (isRoot) { | ||
// Remove the __typename if it exists and add it again at the top, so we're guaranteed to have it at the beginning of json parsing. | ||
// Also remove any @include/@skip directive on __typename. | ||
listOf(buildField("__typename")) + newSelections.filter { (it as? GQLField)?.name != "__typename" } | ||
} else { | ||
newSelections | ||
} |
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.
Should required fields be added in the !isRoot
case? I think isRoot
is already adding everything needed?
- for concrete types, it adds
__typename
and keyfields. - for abstract types, it adds
__typename
and inline fragments for all possible types.
I think that coveres it all? So probably we don't even need to track parentTypes
?
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.
👀 Yeah I think that's right. I'll open a fix.
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.
I remember now, it was to not add new fragments and instead re-use existing ones.
original:
query GetUser {
user {
... on Person {
name
}
}
}
if we only add fields in isRoot
:
query GetUser {
user {
__typename
... on Person {
name
}
... on Robot {
serialNumber
}
... on Person {
id
}
}
}
if we also do it in !isRoot
:
query GetUser {
user {
__typename
... on Person {
name
id
}
... on Robot {
serialNumber
}
}
}
In the end in the generated code the fragments are collapsed, so we'll still get only one onPerson
, right? But this removes a bit of duplication in the sent document. Not sure if it's better to be 'clever' or simple here :)
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.
Aren't the inline fragments unconditionally added in the root of each field?
// Unions and interfaces without key fields: add key fields of all possible types in inline fragments
val inlineFragmentsToAdd = if (isRoot && keyFields.isEmpty()) {
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.
I like the consistency of adding the inline fragments always.
PS: it would be nice to have a comment this field was automatically added by the cache compiler plugin
(but I'm not sure we can do it at the moment because our AST doesn't support comments)
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.
Aren't the inline fragments unconditionally added
alreadyHandledTypes
(maybe badly named 😬) is the trick
consistency
Yeah that may be a bit more obvious for people looking at the resulting document 👍 . And agreed about a comment would be nice!
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.
alreadyHandledTypes is the trick
Ah! I see it now, thanks! I would vote for consistent/simple. We could in the future, introduce another transform that merges fields/inline fragments to reduce the document size (although I believe the "good" solution there is probably PQs)
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.
-> #166
In this PR:
__typename
(including fragments for possible types) are added by the Apollo compiler pluginCache.keyFields
map is generated containing the key fields of all typesKeyFieldsCacheKeyGenerator
which looks up the__typename
in that mapTypePolicyCacheKeyGenerator
is deprecated but still the default - a tradeoff to avoid a breaking change now that we're alpha, and since using the compiled fields still works in most cases.