-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Add context to relay resolvers #4704
Conversation
Hi @Markionium! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
ce6d88f
to
9bd0d55
Compare
Hey @Markionium thanks for picking this up! I agree that attaching the context to the store is the right choice. However, rather than accessing the context via a magic function, my preference would be that the context object get passed in as the third argument to the resolver function. This would keep alignment with a traditional /**
* @RelayResolver User.bestFriend: User
*/
export function myResolver(user, _, ctx) {
return ctx.userById(user.bestFriendId);
} In terms of type safety, I think we can allow users to provide a module/export name in the compiler config for the project. We can then include that type in our generated type assertions (note that these are not yet built for TypeScript, but it should be pretty easy to add if you are interested in another project). You still need to ensure your compiler config and the store you create actually match, but that's something you can get right once and in one place so probably an okay end state. Beyond that we are also exploring a more light-weight way to define resolvers using an approach similar to Grats. In that world it could be even simpler, and work similar to how it works in Grats (see the docs here). |
Connecting the dots. @alloy and I have been discussing this a bit more on this Gist: https://gist.github.com/alloy/f0c9c90ff7a28f3b17850021488979d5 |
9bd0d55
to
62a5c7f
Compare
49699d5
to
fa4660d
Compare
fa4660d
to
8f8eb58
Compare
Looks like this is getting close, but still in draft. Give me a ping when this is ready for review! Excited to see this! |
Hey @captbaritone! We're ready for your review here. @Markionium can you please un-mark as draft? |
_: void, | ||
_: void, | ||
context: LiveResolverContextType, | ||
) => LiveState<?number>); |
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.
let context_import = if resolver_metadata.live { | ||
match &typegen_context | ||
.project_config | ||
.typegen_config | ||
.live_resolver_context_type | ||
{ | ||
Some(LiveResolverContextTypeInput::Path(context_import)) => { | ||
Some(LiveResolverContextType { | ||
name: context_import.name.clone().intern(), | ||
import_path: typegen_context.project_config.js_module_import_identifier( | ||
&typegen_context.project_config.artifact_path_for_definition( | ||
typegen_context.definition_source_location, | ||
), | ||
&PathBuf::from(&context_import.path), | ||
), | ||
}) | ||
} | ||
Some(LiveResolverContextTypeInput::Package(context_import)) => { | ||
Some(LiveResolverContextType { | ||
name: context_import.name.clone().intern(), | ||
import_path: context_import.package.clone().intern(), | ||
}) | ||
} | ||
None => None, | ||
} | ||
} else { | ||
None | ||
}; |
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 only the import_path
is different we could do something like this (Note I didn't run this)
I think it doesn't matter too much but it might make it a bit clearer.
let context_import = if resolver_metadata.live { | |
match &typegen_context | |
.project_config | |
.typegen_config | |
.live_resolver_context_type | |
{ | |
Some(LiveResolverContextTypeInput::Path(context_import)) => { | |
Some(LiveResolverContextType { | |
name: context_import.name.clone().intern(), | |
import_path: typegen_context.project_config.js_module_import_identifier( | |
&typegen_context.project_config.artifact_path_for_definition( | |
typegen_context.definition_source_location, | |
), | |
&PathBuf::from(&context_import.path), | |
), | |
}) | |
} | |
Some(LiveResolverContextTypeInput::Package(context_import)) => { | |
Some(LiveResolverContextType { | |
name: context_import.name.clone().intern(), | |
import_path: context_import.package.clone().intern(), | |
}) | |
} | |
None => None, | |
} | |
} else { | |
None | |
}; | |
let context_import = if resolver_metadata.live { | |
match &typegen_context | |
.project_config | |
.typegen_config | |
.live_resolver_context_type | |
{ | |
Some(context_type) => { | |
let import_path = match context_type { | |
LiveResolverContextTypeInput::Path(context_import) => { | |
typegen_context.project_config.js_module_import_identifier( | |
&typegen_context.project_config.artifact_path_for_definition( | |
typegen_context.definition_source_location, | |
), | |
&PathBuf::from(&context_import.path), | |
) | |
} | |
LiveResolverContextTypeInput::Package(context_import) => { | |
context_import.package.clone().intern() | |
} | |
}; | |
Some(LiveResolverContextType { | |
name: context_type.name.clone().intern(), | |
import_path, | |
}) | |
}, | |
None => None, | |
} | |
} else { | |
None | |
}; |
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 don't think this work because in the Some(LiveResolverContextType {
I get an error no field
nameon type
&LiveResolverContextTypeInputbecause at that point the type isn't specific enough to know that both types share the
name` field
let void_type = match typegen_context.project_config.typegen_config.language { | ||
TypegenLanguage::Flow | TypegenLanguage::JavaScript => AST::RawType(intern!("void")), | ||
TypegenLanguage::TypeScript => AST::RawType(intern!("undefined")), | ||
}; | ||
|
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.
In javascript, we probably don't want to show any types at all? Or we could have TS docblock types, but in that case perhaps it would make more sense to align JS with TS?
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.
Good catch. This might be caught later in the writer for JS also just by never writing any types but I'm not sure about that.
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.
Would be good to add a test which confirms what happens here. Could be an integration test or a typegen test
store: new LiveResolverStore(RelayRecordSource.create(), { | ||
gcReleaseBufferSize: 0, | ||
liveResolverContext: { | ||
greeting: {myHello: 'Hello Allemaal!'}, |
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.
@Markionium I zie you 😄
try { | ||
const resolverFunctionArgs = []; | ||
if (field.fragment != null) { | ||
resolverFunctionArgs.push(fragmentKey); | ||
} else { | ||
// Set first argument to `null` in case we have resolver context |
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.
Nit: The code is pushing undefined
, not null
. What gives?
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 forgot to change the comment, but I changed it to undefined
to keep consistent with the : undefined
we will push for the args value
*/ | ||
export type LiveResolverContext = { | ||
[key: string]: mixed, | ||
}; |
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 need to insist on this being of any particular type? Unsure of the Flow semantics, perhaps this typing matches any form of JS object?
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.
Agreed. I think context should be able to be whatever value the user wants to provide. I don't think Relay needs to have an opinion. A common case might be a Redux store.
|
||
'use strict'; | ||
|
||
import type {LiveResolverContextType} from '../../../mutations/__tests__/LiveResolverContextType'; |
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.
Nit: For clarity sake, I think it would be good to rename this type to self-document that it's a test fixture representing the host's context typing, not a generic Relay provided context typing.
scripts/config.tests.json
Outdated
@@ -57,7 +57,8 @@ | |||
"relay_resolver_enable_interface_output_type": { "kind": "enabled" } | |||
}, | |||
"language": "flow", | |||
"experimentalEmitSemanticNullabilityTypes": true | |||
"experimentalEmitSemanticNullabilityTypes": true, | |||
"liveResolverContextType": { "name": "LiveResolverContextType", "path": "packages/relay-runtime/mutations/__tests__/LiveResolverContextType" } |
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 need another project that exercises the package
module specifier variant?
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.
Possibly? As far as I could find there was only one project config for all tests
yarn-error.log
Outdated
@@ -0,0 +1,7442 @@ | |||
Arguments: |
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.
Whoopsie, accidental check-in
@@ -178,7 +178,10 @@ pub(crate) fn write_operation_type_exports_section( | |||
write_import_actor_change_point(actor_change_status, writer)?; | |||
runtime_imports.write_runtime_imports(writer)?; | |||
write_fragment_imports(typegen_context, None, encountered_fragments, writer)?; | |||
write_relay_resolver_imports(imported_resolvers, writer)?; | |||
// TODO: Add proper support for Resolver type generation in typescript |
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.
This is follow-up work for us, right? Can you link a WI/issue?
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.
#4772 Added
@@ -449,7 +452,11 @@ pub(crate) fn write_fragment_type_exports_section( | |||
write_custom_scalar_imports(custom_scalars, writer)?; | |||
|
|||
runtime_imports.write_runtime_imports(writer)?; | |||
write_relay_resolver_imports(imported_resolvers, writer)?; | |||
|
|||
// TODO: Add proper support for Resolver type generation in typescript |
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.
Ditto
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.
#4772 Added
@captbaritone @Markionium @alloy Ready for another look here. Addressed all comments and added some fixture tests as well. Feel free to suggest any additional tests you'd like to see. All rust tests are passing on my local, seems to be failing on |
@captbaritone Gentle Ping :) |
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.
Everything looks good! Just a few minor nits/fixups and we can land this!
@@ -136,6 +136,13 @@ pub(crate) struct ImportedResolver { | |||
pub resolver_name: ImportedResolverName, | |||
pub resolver_type: AST, | |||
pub import_path: StringKey, | |||
pub context_import: Option<LiveResolverContextType>, |
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.
pub context_import: Option<LiveResolverContextType>, | |
pub context_import: Option<ResolverContextType>, |
} | ||
|
||
#[derive(Clone, Copy)] | ||
pub(crate) struct LiveResolverContextType { |
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.
pub(crate) struct LiveResolverContextType { | |
pub(crate) struct ResolverContextType { |
source, | ||
snapshot.selector, | ||
this._resolverCache, | ||
this._resolverContext || undefined, |
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.
this._resolverContext || undefined, | |
this._resolverContext || undefined, |
this._resolverContext || undefined, | |
this._resolverContext, |
* `resolverContext` is set on the Relay Store. | ||
* This context will be passed as the third argument to the live resolver | ||
*/ | ||
export type ResolverContext = Object; |
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.
export type ResolverContext = Object; | |
export type ResolverContext = mixed; |
I don't think we need to be opinionated at all as to what your context is.
|
||
</TabItem> | ||
|
||
<TabItem value="Flow"> |
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.
Context won't work with the Flow syntax until we do some additional work. Can you replace these Flow tabs with a note saying it's not supported for now?
import Tabs from '@theme/Tabs'; | ||
import TabItem from '@theme/TabItem'; | ||
|
||
In order to pass a service, or other values to be shared with all resolvers, the `LiveResolverStore` provides a means of passing context. This gets passed to the third argument of all resolvers (live and non-live). |
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.
Maybe we could call out that this is analogous to the context argument used on the server which usually holds things like the database connection: https://graphql.org/learn/execution/#root-fields--resolvers
|
||
In a full GraphQL implementation, resolvers would have access to a `context` and `info` argument. These arguments are not available in Relay Resolvers today. Supporting context is something we would like to do in the future, but have not yet implemented. | ||
In a full GraphQL implementation, resolvers would have access to an `info` argument. This argument is not available in Relay Resolvers today. |
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.
Yes!!
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.
This was a fun change :)
@@ -129,6 +148,10 @@ pub struct TypegenConfig { | |||
/// A map from GraphQL error name to import path, example: | |||
/// {"name:: "MyErrorName", "path": "../src/MyError"} | |||
pub custom_error_type: Option<CustomTypeImport>, | |||
|
|||
// Indicates the type to import and use as the context for live resolvers. |
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 you use ///
here instead of //
it will get picked up as part of the description in the json schema we generate. Can use that here and add similar comments to the ResolverContextTypeInput
type and its variants?
Rust test failures on CI are unrelated. We have an ongoing issue where it is running out of disk space. |
@captbaritone has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
@@ -1468,6 +1468,48 @@ | |||
"default": false, | |||
"type": "boolean" | |||
}, | |||
"resolverContextType": { |
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 you rerun the Rust tests with the flag to update snapshots this should update to include the description
#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] | ||
pub struct ResolverContextTypeInputPath { | ||
pub name: StringKey, | ||
pub path: PathBuf, | ||
} |
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.
#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] | |
pub struct ResolverContextTypeInputPath { | |
pub name: StringKey, | |
pub path: PathBuf, | |
} | |
/// Specifies how Relay can import the Resolver context type from a path | |
#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] | |
pub struct ResolverContextTypeInputPath { | |
/// The name under which the type is exported from the module | |
pub name: StringKey, | |
/// The path to the module | |
pub path: PathBuf, | |
} |
Can we also clarify what the path is relative to?
#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] | ||
pub struct ResolverContextTypeInputPackage { | ||
pub name: StringKey, | ||
pub package: StringKey, | ||
} |
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.
#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] | |
pub struct ResolverContextTypeInputPackage { | |
pub name: StringKey, | |
pub package: StringKey, | |
} | |
/// Specifies how Relay can import the Resolver context type from a named package | |
#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] | |
pub struct ResolverContextTypeInputPackage { | |
/// The name under which the type is exported from the package | |
pub name: StringKey, | |
/// The name of the package | |
pub package: StringKey, | |
} |
value: AST::RawType(format!("{fragment_name}$key").intern()), | ||
read_only: false, | ||
optional: false, | ||
}); | ||
} | ||
} else { | ||
resolver_arguments.push(KeyValuePairProp { | ||
key: "key".intern(), |
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.
key: "key".intern(), | |
key: intern!("key".intern), |
Package(ResolverContextTypeInputPackage), | ||
} | ||
|
||
/// Specifies how Relay can import the Resolver context type from a path |
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.
❤️
@captbaritone has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
When trying to land this internally I found that it was going to be very difficult to rollout this change safely if we change how resolvers that don't read a parent object or rootFragment get called. So, I've updated the diff a bit internally to go back to the previous behavior where the first argument is omitted if there is no rootFragment and the resolver is not on a model type. I've updated the docs to match. |
@captbaritone merged this pull request in 2973a0f. |
This is a draft PR to implement an approach mentioned in #4000.
I've currently put the
resolverContext
on theStore
instead of theEnvironment
. It seems that theStore
might be a better place than theEnvironment
?If defined on the environment the context would need to go from the environment into the store down to the reader? Since a store could be used by different environments if the resolverContext would be on the environment that might cause more issues than it being on the store?
The following is an example of how this would be used.
One would provide the context as follows.
Then the resolvers could use the context similar to
readFragment