diff --git a/compiler/crates/relay-compiler/src/build_project/rescript_generate_extra_files.rs b/compiler/crates/relay-compiler/src/build_project/rescript_generate_extra_files.rs index f3d4e9b6e9fb9..3b91d286d2b84 100644 --- a/compiler/crates/relay-compiler/src/build_project/rescript_generate_extra_files.rs +++ b/compiler/crates/relay-compiler/src/build_project/rescript_generate_extra_files.rs @@ -1,14 +1,18 @@ use std::fmt::Write; - use common::SourceLocationKey; +use docblock_shared::RELAY_RESOLVER_WEAK_OBJECT_DIRECTIVE; use graphql_ir::reexport::Intern; use relay_config::ProjectConfig; +use relay_transforms::relay_resolvers::get_resolver_info; +use common::NamedItem; use relay_transforms::Programs; use relay_transforms::is_operation_preloadable; +use relay_transforms::RESOLVER_BELONGS_TO_BASE_SCHEMA_DIRECTIVE; use relay_typegen::rescript::NullabilityMode; use relay_typegen::rescript_utils::capitalize_string; use relay_typegen::rescript_utils::get_safe_key; use relay_typegen::rescript_utils::print_type_reference; +use relay_typegen::rescript_utils::uncapitalize_string; use schema::SDLSchema; use schema::Schema; use schema::TypeReference; @@ -53,6 +57,7 @@ pub(crate) fn rescript_generate_extra_artifacts( .filter_map(|artifact| artifact) .collect(); + // Schema assets file let dummy_source_file = SourceLocationKey::Generated; let mut content = String::from("/* @generated */\n@@warning(\"-30\")\n\n"); @@ -290,5 +295,101 @@ pub(crate) fn rescript_generate_extra_artifacts( extra_artifacts.push(schema_assets_artifact); + // Relay Resolvers + for object in schema.get_objects() { + let mut has_resolvers = false; + let mut c = String::from("/* @generated */\n@@warning(\"-30\")\n\n"); + + for field in object.fields.iter().map(|field_id| schema.field(*field_id)) { + if let Some(Ok(resolver_info)) = get_resolver_info(schema, field, field.name.location) + { + if field + .directives + .named(*RESOLVER_BELONGS_TO_BASE_SCHEMA_DIRECTIVE) + .is_some() || field.name.item.to_string().starts_with("__relay_") + { + continue; + } + + has_resolvers = true; + + if !&field.arguments.is_empty() { + // Write args type + write!(c, "type {}ResolverArgs = {{\n", uncapitalize_string(&field.name.item.to_string())).unwrap(); + field.arguments.iter().for_each(|argument| { + let (key, maybe_original_key) = get_safe_key(&argument.name.item.to_string()); + + writeln!( + c, + " {}{}: {},", + (match maybe_original_key { + Some(original_key) => format!("@as(\"{}\") ", original_key), + None => String::from(""), + }), + key, + print_type_reference( + &argument.type_, + &schema, + &project_config.typegen_config.custom_scalar_types, + true, + false, + &NullabilityMode::Option, + true + ) + ) + .unwrap(); + }); + write!(c, "}}\n").unwrap(); + } + + write!(c, "type {}Resolver = (", uncapitalize_string(&field.name.item.to_string())).unwrap(); + + // Case when the field is on a client extension type + if object.is_extension { + write!(c, "Relay{}Model.t, ", &object.name.item.to_string()).unwrap(); + + // Case @weak object + let _is_weak_object = object.directives.named(*RELAY_RESOLVER_WEAK_OBJECT_DIRECTIVE).is_some(); + } + + if !&field.arguments.is_empty() { + write!(c, "{}ResolverArgs", uncapitalize_string(&field.name.item.to_string())).unwrap(); + } + + let is_live = resolver_info.live; + let return_type = print_type_reference( + &field.type_, + &schema, + &project_config.typegen_config.custom_scalar_types, + true, + false, + &NullabilityMode::Option, + true + ); + write!( + c, ") => {}\n\n", + if is_live { + format!("RescriptRelay.liveState<{}>", return_type) + } else { + return_type + } + ).unwrap(); + } + } + + if has_resolvers { + let relay_resolvers_assets_artifact = Artifact { + artifact_source_keys: vec![], + path: project_config.create_path_for_artifact(dummy_source_file, format!("{}_relayResolvers_graphql.res", object.name.item.0)), + source_file: dummy_source_file, + content: crate::ArtifactContent::Generic { + content: c.as_bytes().to_vec(), + }, + }; + + extra_artifacts.push(relay_resolvers_assets_artifact); + } + } + extra_artifacts } diff --git a/compiler/crates/relay-transforms/src/relay_resolvers.rs b/compiler/crates/relay-transforms/src/relay_resolvers.rs index 9b928df99b8c6..343e667ee72ef 100644 --- a/compiler/crates/relay-transforms/src/relay_resolvers.rs +++ b/compiler/crates/relay-transforms/src/relay_resolvers.rs @@ -113,7 +113,7 @@ pub enum FragmentDataInjectionMode { } #[derive(Clone, Debug, PartialEq, Eq, Hash)] -struct RelayResolverFieldMetadata { +pub struct RelayResolverFieldMetadata { field_parent_type: StringKey, import_path: StringKey, import_name: Option, @@ -610,7 +610,7 @@ pub struct ResolverInfo { fragment_data_injection_mode: Option, pub import_path: StringKey, pub import_name: Option, - live: bool, + pub live: bool, has_output_type: bool, } diff --git a/compiler/crates/relay-typegen/src/rescript.rs b/compiler/crates/relay-typegen/src/rescript.rs index 8b45c2c3b34c0..6207a413f2432 100644 --- a/compiler/crates/relay-typegen/src/rescript.rs +++ b/compiler/crates/relay-typegen/src/rescript.rs @@ -407,6 +407,10 @@ fn ast_to_prop_value( // These are ignored for now, but might be supported in the future. None } + AST::AssertFunctionType(a) => { + log::info!("fn type: {:#?}", a); + None + }, _ => None, } } diff --git a/compiler/crates/relay-typegen/src/rescript_utils.rs b/compiler/crates/relay-typegen/src/rescript_utils.rs index 2a70b56fb5fd6..0992c1b4a953f 100644 --- a/compiler/crates/relay-typegen/src/rescript_utils.rs +++ b/compiler/crates/relay-typegen/src/rescript_utils.rs @@ -4,6 +4,7 @@ use std::ops::Add; use common::ScalarName; use common::WithLocation; +use docblock_shared::RELAY_RESOLVER_WEAK_OBJECT_DIRECTIVE; use graphql_ir::reexport::Intern; use graphql_ir::reexport::StringKey; use graphql_ir::Argument; @@ -510,6 +511,20 @@ pub fn print_type_reference( } } ), + Type::Object(id) => { + let object = schema.object(*id); + let weak = object.directives.iter().find(|d| { + d.name == *RELAY_RESOLVER_WEAK_OBJECT_DIRECTIVE + }).is_some(); + + if weak { + format!("Relay{}Model.t", object.name.item) + } else if object.is_extension { + format!("RescriptRelay.dataIdObject") + } else { + format!("RescriptRelay.dataId") + } + }, _ => String::from("RescriptRelay.any"), }, nullable, diff --git a/compiler/test-project-res/src/RelayUserModel.res b/compiler/test-project-res/src/RelayUserModel.res new file mode 100644 index 0000000000000..364bc0f679c6a --- /dev/null +++ b/compiler/test-project-res/src/RelayUserModel.res @@ -0,0 +1 @@ +type t = {name: string} diff --git a/compiler/test-project-res/src/TestRelayResolverMulti.res b/compiler/test-project-res/src/TestRelayResolverMulti.res new file mode 100644 index 0000000000000..bf95efa64dc7c --- /dev/null +++ b/compiler/test-project-res/src/TestRelayResolverMulti.res @@ -0,0 +1,50 @@ +/** + * @RelayResolver UserMeta + * @weak + */ +type userMeta = { + name: string, + age: int, +} + +/** + * @RelayResolver LocalUser + */ +let localUser = dataId => { + UserService.getById(dataId) +} + +/** + * @RelayResolver Query.time(now: Boolean): String + */ +let time = () => "hello" + +/** + * @RelayResolver LocalUser.bestFriend(from: Timestamp): LocalUser + */ +let bestFriend = () => "hello" + +/** + * @RelayResolver UserMeta.greeting(show: Boolean!): String + */ +let greeting = () => "hello" + +/** + * @RelayResolver User.meta(status: OnlineStatus!): UserMeta + */ +let meta = () => "hello" + +/** + * @RelayResolver LocalUser.favoriteColors: [String!] + */ +let favoriteColors = user => { + [] +} + +/** + * @RelayResolver User.friendCount: Int + * @live + */ +let friendCount = user => { + 1 +} diff --git a/compiler/test-project-res/src/Test_query.res b/compiler/test-project-res/src/Test_query.res index 754fb6b2bf6b7..cb2acdf78a8bf 100644 --- a/compiler/test-project-res/src/Test_query.res +++ b/compiler/test-project-res/src/Test_query.res @@ -1,5 +1,6 @@ module Query = %relay(` query TestQuery($status: OnlineStatus) { + time users(status: $status) { edges { node { diff --git a/compiler/test-project-res/src/__generated__/LocalUser____relay_model_instance_graphql.res b/compiler/test-project-res/src/__generated__/LocalUser____relay_model_instance_graphql.res new file mode 100644 index 0000000000000..2a878646885f0 --- /dev/null +++ b/compiler/test-project-res/src/__generated__/LocalUser____relay_model_instance_graphql.res @@ -0,0 +1,64 @@ +/* @sourceLoc TestRelayResolverMulti.res */ +/* @generated */ +%%raw("/* @generated */") +module Types = { + @@warning("-30") + + type fragment = unit +} + +module Internal = { + @live + type fragmentRaw + @live + let fragmentConverter: Js.Dict.t>> = %raw( + json`{}` + ) + @live + let fragmentConverterMap = () + @live + let convertFragment = v => v->RescriptRelay.convertObj( + fragmentConverter, + fragmentConverterMap, + Js.undefined + ) +} + +type t +type fragmentRef +external getFragmentRef: + RescriptRelay.fragmentRefs<[> | #LocalUser____relay_model_instance]> => fragmentRef = "%identity" + +module Utils = { + @@warning("-33") + open Types +} + +type relayOperationNode +type operationType = RescriptRelay.fragmentNode + + +let node: operationType = %raw(json` { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "LocalUser____relay_model_instance", + "selections": [ + { + "alias": null, + "args": null, + "fragment": { + "args": null, + "kind": "FragmentSpread", + "name": "LocalUser__id" + }, + "kind": "RelayResolver", + "name": "__relay_model_instance", + "resolverModule": relay-runtime/experimental.resolverDataInjector(LocalUser__id.graphql, TestRelayResolverMulti.LocalUser, 'id', true), + "path": "__relay_model_instance" + } + ], + "type": "LocalUser", + "abstractKey": null +} `) + diff --git a/compiler/test-project-res/src/__generated__/LocalUser__id_graphql.res b/compiler/test-project-res/src/__generated__/LocalUser__id_graphql.res new file mode 100644 index 0000000000000..2990e15e06e1a --- /dev/null +++ b/compiler/test-project-res/src/__generated__/LocalUser__id_graphql.res @@ -0,0 +1,65 @@ +/* @sourceLoc TestRelayResolverMulti.res */ +/* @generated */ +%%raw("/* @generated */") +module Types = { + @@warning("-30") + + type fragment = { + @live id: string, + } +} + +module Internal = { + @live + type fragmentRaw + @live + let fragmentConverter: Js.Dict.t>> = %raw( + json`{}` + ) + @live + let fragmentConverterMap = () + @live + let convertFragment = v => v->RescriptRelay.convertObj( + fragmentConverter, + fragmentConverterMap, + Js.undefined + ) +} + +type t +type fragmentRef +external getFragmentRef: + RescriptRelay.fragmentRefs<[> | #LocalUser__id]> => fragmentRef = "%identity" + +module Utils = { + @@warning("-33") + open Types +} + +type relayOperationNode +type operationType = RescriptRelay.fragmentNode + + +let node: operationType = %raw(json` { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "LocalUser__id", + "selections": [ + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ] + } + ], + "type": "LocalUser", + "abstractKey": null +} `) + diff --git a/compiler/test-project-res/src/__generated__/LocalUser_relayResolvers_graphql.res b/compiler/test-project-res/src/__generated__/LocalUser_relayResolvers_graphql.res new file mode 100644 index 0000000000000..a069a1ff237da --- /dev/null +++ b/compiler/test-project-res/src/__generated__/LocalUser_relayResolvers_graphql.res @@ -0,0 +1,10 @@ +/* @generated */ +@@warning("-30") + +type bestFriendResolverArgs = { + from: option, +} +type bestFriendResolver = (RelayLocalUserModel.t, bestFriendResolverArgs) => option + +type favoriteColorsResolver = (RelayLocalUserModel.t, ) => option> + diff --git a/compiler/test-project-res/src/__generated__/Query_relayResolvers_graphql.res b/compiler/test-project-res/src/__generated__/Query_relayResolvers_graphql.res new file mode 100644 index 0000000000000..57f95d4a3efcb --- /dev/null +++ b/compiler/test-project-res/src/__generated__/Query_relayResolvers_graphql.res @@ -0,0 +1,8 @@ +/* @generated */ +@@warning("-30") + +type timeResolverArgs = { + now: option, +} +type timeResolver = (timeResolverArgs) => option + diff --git a/compiler/test-project-res/src/__generated__/TestQuery_graphql.res b/compiler/test-project-res/src/__generated__/TestQuery_graphql.res index e8bba36698d06..448bee9c8f572 100644 --- a/compiler/test-project-res/src/__generated__/TestQuery_graphql.res +++ b/compiler/test-project-res/src/__generated__/TestQuery_graphql.res @@ -16,6 +16,7 @@ module Types = { edges: option>>, } type response = { + time: option, users: option, } @live @@ -113,7 +114,9 @@ type relayOperationNode type operationType = RescriptRelay.queryNode -let node: operationType = %raw(json` (function(){ +%%private(let makeNode = (rescript_module_TestRelayResolverMulti): operationType => { + ignore(rescript_module_TestRelayResolverMulti) + %raw(json`(function(){ var v0 = [ { "defaultValue": null, @@ -121,75 +124,89 @@ var v0 = [ "name": "status" } ], -v1 = [ - { - "alias": null, - "args": [ - { - "kind": "Variable", - "name": "status", - "variableName": "status" - } - ], - "concreteType": "UserConnection", - "kind": "LinkedField", - "name": "users", - "plural": false, +v1 = { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "status", + "variableName": "status" + } + ], + "concreteType": "UserConnection", + "kind": "LinkedField", + "name": "users", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "UserEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "User", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "firstName", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "onlineStatus", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "TestQuery", "selections": [ + (v1/*: any*/), { - "alias": null, - "args": null, - "concreteType": "UserEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, + "kind": "ClientExtension", "selections": [ { "alias": null, - "args": null, - "concreteType": "User", - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "firstName", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "onlineStatus", - "storageKey": null - } - ], - "storageKey": null + "args": [], + "fragment": null, + "kind": "RelayResolver", + "name": "time", + "resolverModule": rescript_module_TestRelayResolverMulti, + "path": "time" } - ], - "storageKey": null + ] } ], - "storageKey": null - } -]; -return { - "fragment": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Fragment", - "metadata": null, - "name": "TestQuery", - "selections": (v1/*: any*/), "type": "Query", "abstractKey": null }, @@ -198,7 +215,22 @@ return { "argumentDefinitions": (v0/*: any*/), "kind": "Operation", "name": "TestQuery", - "selections": (v1/*: any*/) + "selections": [ + (v1/*: any*/), + { + "kind": "ClientExtension", + "selections": [ + { + "name": "time", + "args": null, + "fragment": null, + "kind": "RelayResolver", + "storageKey": null, + "isOutputType": true + } + ] + } + ] }, "params": { "cacheID": "123064f3c998fd5b717ca05be99d7ee1", @@ -209,7 +241,9 @@ return { "text": "query TestQuery(\n $status: OnlineStatus\n) {\n users(status: $status) {\n edges {\n node {\n id\n firstName\n onlineStatus\n }\n }\n }\n}\n" } }; -})() `) +})()`) +}) +let node: operationType = makeNode(TestRelayResolverMulti.default) @live let load: ( ~environment: RescriptRelay.Environment.t, diff --git a/compiler/test-project-res/src/__generated__/UserMeta____relay_model_instance_graphql.res b/compiler/test-project-res/src/__generated__/UserMeta____relay_model_instance_graphql.res new file mode 100644 index 0000000000000..6e895d658651e --- /dev/null +++ b/compiler/test-project-res/src/__generated__/UserMeta____relay_model_instance_graphql.res @@ -0,0 +1,67 @@ +/* @sourceLoc TestRelayResolverMulti.res */ +/* @generated */ +%%raw("/* @generated */") +module Types = { + @@warning("-30") + + type fragment = { + __relay_model_instance: UserMeta.t, + } +} + +module Internal = { + @live + type fragmentRaw + @live + let fragmentConverter: Js.Dict.t>> = %raw( + json`{"__root":{"__relay_model_instance":{"c":"UserMeta"}}}` + ) + @live + let fragmentConverterMap = { + "UserMeta": UserMeta.parse, + } + @live + let convertFragment = v => v->RescriptRelay.convertObj( + fragmentConverter, + fragmentConverterMap, + Js.undefined + ) +} + +type t +type fragmentRef +external getFragmentRef: + RescriptRelay.fragmentRefs<[> | #UserMeta____relay_model_instance]> => fragmentRef = "%identity" + +module Utils = { + @@warning("-33") + open Types +} + +type relayOperationNode +type operationType = RescriptRelay.fragmentNode + + +let node: operationType = %raw(json` { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "UserMeta____relay_model_instance", + "selections": [ + { + "kind": "ClientExtension", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__relay_model_instance", + "storageKey": null + } + ] + } + ], + "type": "UserMeta", + "abstractKey": null +} `) + diff --git a/compiler/test-project-res/src/__generated__/UserMeta_relayResolvers_graphql.res b/compiler/test-project-res/src/__generated__/UserMeta_relayResolvers_graphql.res new file mode 100644 index 0000000000000..b6f3099622334 --- /dev/null +++ b/compiler/test-project-res/src/__generated__/UserMeta_relayResolvers_graphql.res @@ -0,0 +1,8 @@ +/* @generated */ +@@warning("-30") + +type greetingResolverArgs = { + show: bool, +} +type greetingResolver = (RelayUserMetaModel.t, greetingResolverArgs) => option + diff --git a/compiler/test-project-res/src/__generated__/User_relayResolvers_graphql.res b/compiler/test-project-res/src/__generated__/User_relayResolvers_graphql.res new file mode 100644 index 0000000000000..28c0fe9706341 --- /dev/null +++ b/compiler/test-project-res/src/__generated__/User_relayResolvers_graphql.res @@ -0,0 +1,12 @@ +/* @generated */ +@@warning("-30") + +type greetingResolver = () => option + +type metaResolverArgs = { + status: enum_OnlineStatus_input, +} +type metaResolver = (metaResolverArgs) => option + +type friendCountResolver = () => RescriptRelay.liveState> +