Replace options.isReference(ref, true) with options.canRead(ref).#6425
Merged
Replace options.isReference(ref, true) with options.canRead(ref).#6425
Conversation
benjamn
commented
Jun 10, 2020
Comment on lines
-926
to
+929
| children(offspring: Reference[], { isReference }) { | ||
| return offspring ? offspring.filter( | ||
| // The true argument here makes isReference return true | ||
| // only if child is a Reference object that points to | ||
| // valid entity data in the EntityStore (that is, not a | ||
| // dangling reference). | ||
| child => isReference(child, true) | ||
| ) : []; | ||
| children(offspring: Reference[], { canRead }) { | ||
| // Automatically filter out any dangling references, and | ||
| // supply a default empty array if !offspring. | ||
| return offspring ? offspring.filter(canRead) : []; |
Member
Author
There was a problem hiding this comment.
This seems like a clear improvement, to me.
After thinking more about PR #6413, which has been merged but not yet released, I think it makes more sense to have a separate helper function for read, merge, and modify functions that determines if a given object can be read with options.readField. It was tempting to call this helper function options.isReadable, but options.canRead is shorter and less easily confused with options.isReference (which matters for auto-completion and dyslexic programmers), so options.canRead is what I settled on. This new helper makes it easy to filter out dangling references from lists of data correctly, without having to know if the list elements are normalized Reference objects, or non-normalized StoreObjects: new InMemoryCache({ typePolicies: { Person: { fields: { friends(existing, { canRead }) { return existing ? existing.filter(canRead) : []; }, }, }, }, }) Using isReference(friend, true) here would have worked only if isReference(friend) was true, whereas canRead(friend) also returns true when friend is a non-Reference object with readable fields, which can happen if the Friend type has no ID and/or keyFields:false. Now that we have options.canRead and options.readField, I think the use cases for options.isReference are probably pretty scarce, but I don't want to yank isReference away from folks who have been using it in the betas/RCs. Please let us know if you have a specific use case for options.isReference that is not covered by options.canRead, so we can decide how much documentation to give the various helpers.
benjamn
commented
Jun 10, 2020
Comment on lines
+359
to
+361
| return isReference(objOrRef) | ||
| ? this.has(objOrRef.__ref) | ||
| : typeof objOrRef === "object"; |
Member
Author
There was a problem hiding this comment.
Intentionally allowing canRead(null) to return true, since null is a valid placeholder value in GraphQL lists.
Member
Author
|
If we wanted to do this filtering automatically, I think this is the only change we need: commit 6fe8075f53ae33ce28a0aecc1b2e888747b2d166
Author: Ben Newman <[email protected]>
Date: Wed Jun 10 15:04:55 2020 -0400
Automatically filter out dangling references from arrays.
diff --git a/src/cache/inmemory/readFromStore.ts b/src/cache/inmemory/readFromStore.ts
index 4143f72d..a0a0687b 100644
--- a/src/cache/inmemory/readFromStore.ts
+++ b/src/cache/inmemory/readFromStore.ts
@@ -377,40 +377,44 @@ export class StoreReader {
// Uncached version of executeSubSelectedArray.
private execSubSelectedArrayImpl({
field,
array,
context,
}: ExecSubSelectedArrayOptions): ExecResult {
let missing: MissingFieldError[] | undefined;
function handleMissing<T>(childResult: ExecResult<T>, i: number): T {
if (childResult.missing) {
missing = missing || [];
missing.push(...childResult.missing);
}
invariant(context.path.pop() === i);
return childResult.result;
}
+ if (field.selectionSet) {
+ array = array.filter(context.store.canRead);
+ }
+
array = array.map((item, i) => {
// null value in array
if (item === null) {
return null;
}
context.path.push(i);
// This is a nested array, recurse
if (Array.isArray(item)) {
return handleMissing(this.executeSubSelectedArray({
field,
array: item,
context,
}), i);
}
// This is an object, run the selection set on it
if (field.selectionSet) {
return handleMissing(this.executeSelectionSet({With this change, you would not need to define a |
jcreighton
approved these changes
Jun 10, 2020
Contributor
jcreighton
left a comment
There was a problem hiding this comment.
I love how much clearer this is!
benjamn
added a commit
that referenced
this pull request
Jun 17, 2020
This commit implements the proposal for automatic filtering of dangling references that I described in #6425 (comment). The filtering functionality demonstrated by #6412 (and updated by #6425) seems useful enough that we might as well make it the default behavior for any array-valued field consumed by a selection set. Note: the presence of field.selectionSet implies the author of the query expects the elements to be objects (or references) rather than scalar values. By making .filter(canRead) automatic, we free developers from having to worry about manually removing any references after evicting entities from the cache. Instead, those dangling references will simply (appear to) disappear from cache results, which is almost always the desired behavior. In case this automatic filtering is not desired, a custom read function can be used to override the filtering, since read functions run before this filtering happens. This commit includes tests demonstrating several options for replacing/filtering dangling references in non-default ways.
benjamn
added a commit
that referenced
this pull request
Jun 17, 2020
This commit implements the proposal for automatic filtering of dangling references that I described in #6425 (comment). The filtering functionality demonstrated by #6412 (and updated by #6425) seems useful enough that we might as well make it the default behavior for any array-valued field consumed by a selection set. Note: the presence of field.selectionSet implies the author of the query expects the elements to be objects (or references) rather than scalar values. A list of scalar values should not be filtered, since it cannot contain dangling references. By making .filter(canRead) automatic, we free developers from having to worry about manually removing any references after evicting entities from the cache. Instead, those dangling references will simply (appear to) disappear from cache results, which is almost always the desired behavior. Fields whose values hold single (non-list) dangling references cannot be automatically filtered in the same way, but you can always write a custom read function for the field, and it's somewhat more likely that a refetch will fix those fields correctly. In case this automatic filtering is not desired, a custom read function can be used to override the filtering, since read functions run before this filtering happens. This commit includes tests demonstrating several options for replacing/filtering dangling references in non-default ways.
This was referenced Jul 10, 2020
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
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
After thinking more about PR #6413, which has been merged but not yet released, I think it makes more sense to have a separate helper function for
read,merge, andmodifyfunctions that determines if a given object can be read withoptions.readField.It was tempting to call this helper function
options.isReadable, butoptions.canReadis shorter and less easily confused withoptions.isReference(which matters for auto-completion and dyslexic programmers), sooptions.canReadis what I settled on.This new helper makes it easy to filter out dangling references from lists of data correctly, without having to know if the list elements are normalized
Referenceobjects, or non-normalizedStoreObjects:Using
isReference(friend, true)here would have worked only ifisReference(friend)wastrue, whereascanRead(friend)also returnstruewhen friend is a non-Referenceobject with readable fields, which can happen if theFriendtype has no ID and/orkeyFields: false.Now that we have
options.canReadandoptions.readField, I think the use cases foroptions.isReferenceare probably pretty scarce, but I don't want to yankisReferenceaway from folks who have been using it in the betas/RCs.Please let us know if you have a specific use case for
options.isReferencethat is not covered byoptions.canRead, so we can decide how much documentation to give the various helpers.