Title: | 'GraphQL' Server in R |
---|---|
Description: | Server implementation of 'GraphQL' <http://graphql.github.io/graphql-spec/>, a query language originally created by Facebook for describing data requirements on complex application data models. Visit <http://graphql.org> to learn more about 'GraphQL'. |
Authors: | Barret Schloerke [aut, cre], Ryan Hafen [ths], GraphQL [cph] (http://graphql.github.io/graphql-spec/) |
Maintainer: | Barret Schloerke <[email protected]> |
License: | MIT + file LICENSE |
Version: | 0.0.2.9000 |
Built: | 2024-10-31 05:05:41 UTC |
Source: | https://github.com/schloerke/gqlr |
Debug method that strips all gqlr classes and assigns the class as 'R6'
as_R6(x)
as_R6(x)
x |
any object. If it inherits |
Int <- getFromNamespace("Int", "gqlr")$clone() print(Int) print(as_R6(Int))
Int <- getFromNamespace("Int", "gqlr")$clone() print(Int) print(as_R6(Int))
Handles all errors that occur during query validation. This object is returned from execute request function (ans <- execute_request(query, schema)
) under the field 'error_list' (ans$error_list
).
answer <- execute_request(my_request, my_schema) answer$error_list
boolean that determines if errors will be printed on occurrence. Defaults to TRUE
$n
count of errors received
$errors
list of error information
$verbose
boolean that determines of errors are printed when received
$has_no_errors()
helper method to determine if there are no errors
$has_any_errors()
helper method to determine if there are any errors
$get_sub_source(loc)
helper method to display a subsection of source text given Location information
$add(rule_code, ...)
add a new error according to the rule_code
provided. Remaining arguments are passed directly to paste(..., sep = "")
with extra error rule information
$.format(...)
formats the error list into user friendly text. Remaining arguments are ignored
$print(...)
prints the error list by calling self$format(...)
error_list <- ErrorList$new() error_list error_list$has_any_errors() # FALSE error_list$has_no_errors() # TRUE error_list$add("3.1.1", "Multiple part", " error about Scalars") error_list error_list$has_any_errors() # TRUE error_list$has_no_errors() # FALSE
error_list <- ErrorList$new() error_list error_list$has_any_errors() # FALSE error_list$has_no_errors() # TRUE error_list$add("3.1.1", "Multiple part", " error about Scalars") error_list error_list$has_any_errors() # TRUE error_list$has_no_errors() # FALSE
Executes a GraphQL server request with the provided request.
execute_request( request, schema, ..., operation_name = NULL, variables = list(), initial_value = NULL, verbose_errors = is_interactive() )
execute_request( request, schema, ..., operation_name = NULL, variables = list(), initial_value = NULL, verbose_errors = is_interactive() )
request |
a valid GraphQL string |
schema |
a character string (to be used along side |
... |
ignored for paramter expansion |
operation_name |
name of request operation to execute. If not value is provided it will use the operation in the request string. If more than one operations exist, an error will be produced. See https://graphql.github.io/graphql-spec/October2016/#GetOperation() |
variables |
a named list containing variable values. https://graphql.github.io/graphql-spec/October2016/#sec-Language.Variables |
initial_value |
default value for executing requests. This value can either be provided and/or combined with the resolve method of the query root type or mutation root type. The value provided should be a named list of the field name (key) and a value matching that field name type. The value may be a function that returns a value of the field name type. |
verbose_errors |
logical to determine if error-like messages should be displayed when processing a request that finds unknown structures. Be default, this is only enabled when |
https://graphql.github.io/graphql-spec/October2016/#sec-Execution
# bare bones schema <- gqlr_schema(" type Person { name: String friends: [Person] } schema { query: Person } ") data <- list( name = "Barret", friends = list( list(name = "Ryan", friends = list(list(name = "Bill"), list(name = "Barret"))), list(name = "Bill", friends = list(list(name = "Ryan"))) ) ) ans <- execute_request("{ name }", schema, initial_value = data) ans$as_json() execute_request(" { name friends { name friends { name friends { name } } } }", schema, initial_value = data )$as_json() # Using resolve method to help with recursion people <- list( "id_Barret" = list(name = "Barret", friends = list("id_Ryan", "id_Bill")), "id_Ryan" = list(name = "Ryan", friends = list("id_Barret", "id_Bill")), "id_Bill" = list(name = "Bill", friends = list("id_Ryan")) ) schema <- gqlr_schema(" type Person { name: String friends: [Person] } schema { query: Person } ", Person = list( resolve = function(name, schema, ...) { if (name %in% names(people)) { people[[name]] } else { NULL } } ) ) ans <- execute_request("{ name }", schema, initial_value = "id_Barret") ans$as_json() execute_request(" { name friends { name friends { name friends { name } } } }", schema, initial_value = "id_Barret" )$as_json()
# bare bones schema <- gqlr_schema(" type Person { name: String friends: [Person] } schema { query: Person } ") data <- list( name = "Barret", friends = list( list(name = "Ryan", friends = list(list(name = "Bill"), list(name = "Barret"))), list(name = "Bill", friends = list(list(name = "Ryan"))) ) ) ans <- execute_request("{ name }", schema, initial_value = data) ans$as_json() execute_request(" { name friends { name friends { name friends { name } } } }", schema, initial_value = data )$as_json() # Using resolve method to help with recursion people <- list( "id_Barret" = list(name = "Barret", friends = list("id_Ryan", "id_Bill")), "id_Ryan" = list(name = "Ryan", friends = list("id_Barret", "id_Bill")), "id_Bill" = list(name = "Bill", friends = list("id_Ryan")) ) schema <- gqlr_schema(" type Person { name: String friends: [Person] } schema { query: Person } ", Person = list( resolve = function(name, schema, ...) { if (name %in% names(people)) { people[[name]] } else { NULL } } ) ) ans <- execute_request("{ name }", schema, initial_value = "id_Barret") ans$as_json() execute_request(" { name friends { name friends { name friends { name } } } }", schema, initial_value = "id_Barret" )$as_json()
Creates a Schema object from the defined GraphQL string and inserts the provided descriptions, resolve methods, resolve_type methods into the appropriate place.
gqlr_schema(schema, ...)
gqlr_schema(schema, ...)
schema |
GraphQL schema string or Schema object |
... |
named lists of information to help produce the schema definition. See Details |
The ... should be named arguments whose values are lists of information. What information is needed for each type is described below.
ScalarTypeDefinition:
function with two
parameters: x
(the raw to be parsed, such as 5.0) and schema
(the full Schema definition). Should return a parsed value
(optional) single character value that describes the Scalar definition
(optional) function with two
parameters: obj
(a GraphQL wrapped raw value, such as an object of
class IntValue with value 5) and schema
(the full Schema
definition). If the function returns NULL
then the AST could not be
parsed.
EnumTypeDefinition:
(optional) function with two
parameters: x
and schema
(the full Schema definition). Should
return the value x
represents, such as the Star Wars Episode enum
value "4" could represent Episode "NEWHOPE". By default,
EnumTypeDefinitions will return the current value.
(optional) single character value that describes the Enum definition
(optional) named list of enum value
descriptions. Such as values = list(ENUMA = "description for ENUMA",
ENUMZ = "description for ENUMZ")
ObjectTypeDefinition:
function with two
parameters: x
(place holder value to be expanded into a named list)
and schema
(the full Schema definition). By using the resolve
method, recursive relationships, such as friends, can easily be handled.
The resolve function should return a fully named list of all the fields the
definition defines. Missing fields are automatically interpreted as
NULL
.
Values in the returned list may be a function of the form
function(obj, args, schema) {...}
. This allows for fields to be
determined dynamically and lazily. See how add_human
makes a field
for totalCredits
, while the add_droid
pre computes the
information.
(optional) single character value that describes the object
(optional) named list of field
descriptions. Such as fields = list(fieldA = "description for field
A", fieldB = "description for field B")
InterfaceTypeDefinition and UnionTypeDefinition:
function with two parameters: x
(a pre-resolved
object value) and schema
(the full Schema definition). This function
is required to determine which object type is being used.
resolve_type
is called before any ObjectTypeDefinition
resolve
methods are called.
(optional) single character value that describes the object
library(magrittr) ## Set up data add_human <- function(human_data, id, name, appear, home, friend) { human <- list(id = id, name = name, appearsIn = appear, friends = friend, homePlanet = home) # set up a function to be calculated if the field totalCredits is required human$totalCredits <- function(obj, args, schema) { length(human$appearsIn) } human_data[[id]] <- human human_data } add_droid <- function(droid_data, id, name, appear, pf, friend) { droid <- list(id = id, name = name, appearsIn = appear, friends = friend, primaryFunction = pf) # set extra fields manually droid$totalCredits <- length(droid$appearsIn) droid_data[[id]] <- droid droid_data } human_data <- list() %>% add_human("1000", "Luke Skywalker", c(4, 5, 6), "Tatooine", c("1002", "1003", "2000", "2001")) %>% add_human("1002", "Han Solo", c(4, 5, 6), "Corellia", c("1000", "1003", "2001")) %>% add_human("1003", "Leia Organa", c(4, 5, 6), "Alderaan", c("1000", "1002", "2000", "2001")) droid_data <- list() %>% add_droid("2000", "C-3PO", c(4, 5, 6), "Protocol", c("1000", "1002", "1003", "2001")) %>% add_droid("2001", "R2-D2", c(4, 5, 6), "Astromech", c("1000", "1002", "1003")) all_characters <- list() %>% append(human_data) %>% append(droid_data) %>% print() ## End data set up # Define the schema using GraphQL code star_wars_schema <- Schema$new() " enum Episode { NEWHOPE, EMPIRE, JEDI } " %>% gqlr_schema( Episode = list( resolve = function(episode_id, schema) { switch(as.character(episode_id), "4" = "NEWHOPE", "5" = "EMPIRE", "6" = "JEDI", "UNKNOWN_EPISODE" ) } ) ) -> episode_schema # display the schema episode_schema$get_schema() # add the episode definitions to the Star Wars schema star_wars_schema$add(episode_schema) " interface Character { id: String! name: String friends: [Character] appearsIn: [Episode] } " %>% gqlr_schema( Character = list( resolve_type = function(id, schema) { if (id %in% names(droid_data)) { "Droid" } else { "Human" } } ) ) -> character_schema # print the Character schema with no extra formatting character_schema$get_schema() %>% format() %>% cat("\n") star_wars_schema$add(character_schema) " type Droid implements Character { id: String! name: String friends: [Character] appearsIn: [Episode] primaryFunction: String } type Human implements Character { id: String! name: String friends: [Character] appearsIn: [Episode] homePlanet: String } " %>% gqlr_schema( Human = list( # Add a resolve method for type Human that takes in an id and returns the human data resolve = function(id, args, schema) { human_data[[id]] } ), Droid = list( # description for Droid description = "A mechanical creature in the Star Wars universe.", # Add a resolve method for type Droid that takes in an id and returns the droid data resolve = function(id, schema) { droid_data[[id]] } ) ) -> human_and_droid_schema human_and_droid_schema$get_schema() star_wars_schema$add(human_and_droid_schema) " type Query { hero(episode: Episode): Character human(id: String!): Human droid(id: String!): Droid } # the schema type must be provided if a query or mutation is to be executed schema { query: Query } " %>% gqlr_schema( Query = function(null, schema) { list( # return a function for key 'hero' # the id will be resolved by the appropriate resolve() method of Droid or Human hero = function(obj, args, schema) { episode <- args$episode if (identical(episode, 5) || identical(episode, "EMPIRE")) { "1000" # Luke Skywalker } else { "2001" # R2-D2 } }, # the id will be resolved by the Human resolve() method human = function(obj, args, schema) { args$id }, # the id will be resolved by the Droid resolve() method droid = function(obj, args, schema) { args$id } ) } ) -> schema_def # print Schema with no extra formatting schema_def$get_schema() %>% format() %>% cat("\n") star_wars_schema$add(schema_def) # view the final schema definitiion star_wars_schema$get_schema()
library(magrittr) ## Set up data add_human <- function(human_data, id, name, appear, home, friend) { human <- list(id = id, name = name, appearsIn = appear, friends = friend, homePlanet = home) # set up a function to be calculated if the field totalCredits is required human$totalCredits <- function(obj, args, schema) { length(human$appearsIn) } human_data[[id]] <- human human_data } add_droid <- function(droid_data, id, name, appear, pf, friend) { droid <- list(id = id, name = name, appearsIn = appear, friends = friend, primaryFunction = pf) # set extra fields manually droid$totalCredits <- length(droid$appearsIn) droid_data[[id]] <- droid droid_data } human_data <- list() %>% add_human("1000", "Luke Skywalker", c(4, 5, 6), "Tatooine", c("1002", "1003", "2000", "2001")) %>% add_human("1002", "Han Solo", c(4, 5, 6), "Corellia", c("1000", "1003", "2001")) %>% add_human("1003", "Leia Organa", c(4, 5, 6), "Alderaan", c("1000", "1002", "2000", "2001")) droid_data <- list() %>% add_droid("2000", "C-3PO", c(4, 5, 6), "Protocol", c("1000", "1002", "1003", "2001")) %>% add_droid("2001", "R2-D2", c(4, 5, 6), "Astromech", c("1000", "1002", "1003")) all_characters <- list() %>% append(human_data) %>% append(droid_data) %>% print() ## End data set up # Define the schema using GraphQL code star_wars_schema <- Schema$new() " enum Episode { NEWHOPE, EMPIRE, JEDI } " %>% gqlr_schema( Episode = list( resolve = function(episode_id, schema) { switch(as.character(episode_id), "4" = "NEWHOPE", "5" = "EMPIRE", "6" = "JEDI", "UNKNOWN_EPISODE" ) } ) ) -> episode_schema # display the schema episode_schema$get_schema() # add the episode definitions to the Star Wars schema star_wars_schema$add(episode_schema) " interface Character { id: String! name: String friends: [Character] appearsIn: [Episode] } " %>% gqlr_schema( Character = list( resolve_type = function(id, schema) { if (id %in% names(droid_data)) { "Droid" } else { "Human" } } ) ) -> character_schema # print the Character schema with no extra formatting character_schema$get_schema() %>% format() %>% cat("\n") star_wars_schema$add(character_schema) " type Droid implements Character { id: String! name: String friends: [Character] appearsIn: [Episode] primaryFunction: String } type Human implements Character { id: String! name: String friends: [Character] appearsIn: [Episode] homePlanet: String } " %>% gqlr_schema( Human = list( # Add a resolve method for type Human that takes in an id and returns the human data resolve = function(id, args, schema) { human_data[[id]] } ), Droid = list( # description for Droid description = "A mechanical creature in the Star Wars universe.", # Add a resolve method for type Droid that takes in an id and returns the droid data resolve = function(id, schema) { droid_data[[id]] } ) ) -> human_and_droid_schema human_and_droid_schema$get_schema() star_wars_schema$add(human_and_droid_schema) " type Query { hero(episode: Episode): Character human(id: String!): Human droid(id: String!): Droid } # the schema type must be provided if a query or mutation is to be executed schema { query: Query } " %>% gqlr_schema( Query = function(null, schema) { list( # return a function for key 'hero' # the id will be resolved by the appropriate resolve() method of Droid or Human hero = function(obj, args, schema) { episode <- args$episode if (identical(episode, 5) || identical(episode, "EMPIRE")) { "1000" # Luke Skywalker } else { "2001" # R2-D2 } }, # the id will be resolved by the Human resolve() method human = function(obj, args, schema) { args$id }, # the id will be resolved by the Droid resolve() method droid = function(obj, args, schema) { args$id } ) } ) -> schema_def # print Schema with no extra formatting schema_def$get_schema() %>% format() %>% cat("\n") star_wars_schema$add(schema_def) # view the final schema definitiion star_wars_schema$get_schema()
This is a helper function for Scalars. Given a particular kind and a resolve function, it produces a function that will only parse values of a particular kind.
parse_ast(kind, resolve)
parse_ast(kind, resolve)
kind |
single character name of a class to parse |
resolve |
function to parse the value if the kind is correct |
Typically, kind
is the same as the class of the Scalar. When making a
new Scalar, parse_ast defaults to use the name of the scalar and the scalar's
parse value function.
This function should only need to be used when defining a schema in
gqlr_schema()
function that takes obj
and schema
that will only parse
the value if the kind
is inherited in the obj
parse_date_value <- function(obj, schema) { as.Date(obj) } parse_ast("Date", parse_date_value) # Example from Int scalar parse_int <- function(value, ...) { MAX_INT <- 2147483647 MIN_INT <- -2147483648 num <- suppressWarnings(as.integer(value)) if (!is.na(num)) { if (num <= MAX_INT && num >= MIN_INT) { return(num) } } return(NULL) } parse_ast("IntValue", parse_int)
parse_date_value <- function(obj, schema) { as.Date(obj) } parse_ast("Date", parse_date_value) # Example from Int scalar parse_int <- function(value, ...) { MAX_INT <- 2147483647 MIN_INT <- -2147483648 num <- suppressWarnings(as.integer(value)) if (!is.na(num)) { if (num <= MAX_INT && num >= MIN_INT) { return(num) } } return(NULL) } parse_ast("IntValue", parse_int)
Manages a GraphQL schema definition. A Schema can add more GraphQL type definitions, assist in determining definition types, retrieve particular definitions, and can combine with other schema definitions.
Typically, Schema class objects are created using gqlr_schema
. Creating a Schema$new()
object should be reserved for when multiple Schema objects are combined.
## using star_wars_schema from # example(gqlr_schema) star_wars_schema$get_schema() star_wars_schema$is_enum("Episode") # TRUE star_wars_schema$is_object("Episode") # FALSE execute_request("{ hero { name } }", star_wars_schema)
Either a character GraphQL definition of a schema or another Schema object. Extending methods and descriptions should be added with gqlr_schema
.
The initialize function will automatically add
Scalars: Int, Float, String, Boolean
Directives: @skip and @include
Introspection Capabilities
$add(obj)
: function to add either another Schema's definitions or Document of definitions. obj
must inherit class of either 'Schema'
or 'Document'
$is_scalar(name)
, $is_enum(name)
, $is_object(name)
, $is_interface(name)
, $is_union(name)
, $is_input_object(name)
, $is_directive(name)
, $is_value(name)
: methods to determine if there is a definition of the corresponding definition type for the provided name.
$get_scalar(name)
, $get_enum(name)
, $get_object(name)
, $get_interface(name)
, $get_union(name)
, $get_input_object(name)
, $get_directive(name)
, $get_value(name)
: methods to retrieve a definition of the corresponding definition type for the provided name. If the object can't be found, NULL
is returned. When printed, it quickly conveys all known information of the definition. Due to the nature of R6 objects, definitions may be retrieved and altered after retrieval. This is helpful for adding descriptions or resolve after the initialization.
$get_scalars(name)
, $get_enums(name)
, $get_objects(name)
, $get_interfaces(name)
, $get_unions(name)
, $get_input_objects(name)
, $get_directives(name)
, $get_values(name)
: methods to retrieve all definitions of the corresponding definition type.
$get_type(name)
: method to retrieve an object of unknown type. If the object can't be found, NULL
is returned. When printed, it quickly conveys all known information of the definition.
$get_type(name)
: method to retrieve an object of unknown type. If the object can't be found, NULL
is returned.
$get_schema()
: method to retrieve full definition of schema. When printed, it quickly conveys all types in the schema.
$get_query_object()
, $get_mutation_object()
: helper method to retrieve the schema definition query or mutation object.
$implements_interface()
: helper method to retrieve all objects who implement a particular interface.
$is_valid
: boolean that determines if a Schema object has been validated. All Schema objects are validated at the time of request execution. The Schema will remain valid until new definitions are added.
Run a basic GraphQL server using plumber. This server is provided to show basic interaction with GraphQL. The server will run until the function execution is canceled.
server( schema, port = 8000L, ..., graphiql = interactive(), log = TRUE, initial_value = NULL )
server( schema, port = 8000L, ..., graphiql = interactive(), log = TRUE, initial_value = NULL )
schema |
Schema object to use execute requests |
port |
web port to serve the server from. Set port to |
... |
ignored for paramter expansion |
graphiql |
logical to determine if the GraphiQL interface should be enabled. By default, this route is only available when running the server interactively. |
log |
boolean that determines if server logging is done. Defaults to TRUE |
initial_value |
default value to use in |
To view the GraphiQL user interface, navigate to the URL provided when the server is started. The default location is http://localhost:8000/graphiql/
. By default, this route is only available when running the server interactively (graphiql = rlang::is_interactive()
).
server()
implements the basic necessities described in http://graphql.org/learn/serving-over-http/. There are four routes implemented:
'/'
GET. If run interactively, forwards to /graphiql
for user interaction with the GraphQL server. This route is diabled if graphiql = rlang::is_interactive()
is not TRUE
.
'/graphiql/'
GET. Returns a GraphiQL formatted schema definition interface to manually interact with the GraphQL server. By default this route is disabled if graphiql = rlang::is_interactive()
is not TRUE
.
'/graphql'
GET. Executes a query. The parameter 'query'
(which contains a GraphQL formatted query string) must be included. Optional parameters include: 'variables'
a JSON string containing a dictionary of variables (defaults to an empty named list), 'operationName'
name of the particular query operation to execute (defaults to NULL), and 'pretty'
boolean to determine if the response should be compact (FALSE, default) or expanded (TRUE)
'/graphql'
POST. Executes a query. Must provide Content-Type of either 'application/json' or 'application/graphql'.
If 'application/json' is provided, a named JSON list containing 'query', 'operationName' (optional, default = NULL
), 'variables' (optional, default = list()) and 'pretty' (optional, default = TRUE
). The information will used just the same as the GET-'/graphql' route.
If 'application/graphql' is provided, the POST body will be interpreted as the query string. All other possible parameters will take on their default value.
Using bash's curl, we can ask the server questions:
#R # load Star Wars schema from 'execute_request' example example(gqlr_schema) # run server server(star_wars_schema, port = 8000)
#bash # GET Schema definition curl '127.0.0.1:8000/' ## POST for R2-D2 and his friends' names # defaults to parse as JSON curl --data '{"query":"{hero{name, friends { name }}}", "pretty": true}' '127.0.0.1:8000/graphql' # send json header curl --data '{"query":"{hero{name, friends { name }}}"}' '127.0.0.1:8000/graphql' --header "Content-Type:application/json" # send graphql header curl --data '{hero{name, friends { name }}}' '127.0.0.1:8000/graphql' --header "Content-Type:application/graphql" # use variables curl --data '{"query":"query Droid($someId: String!) {droid(id: $someId) {name, friends { name }}}", "variables": {"someId": "2001"}}' '127.0.0.1:8000/graphql' # GET R2-D2 and his friends' names curl '127.0.0.1:8000/graphql?query= # ... using a variable curl '127.0.0.1:8000/graphql?query=query