├── images ├── .gdignore ├── icon.png └── godot-icon.xcf ├── .gitignore ├── LICENSE ├── tests └── test_gql_query.gd ├── src ├── gql_query.gd ├── gql_query_executer.gd ├── gql_query_subscriber.gd ├── gql_client.gd └── websocket_client.gd └── README.md /images/.gdignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dracks/GodotGraphQL/HEAD/images/icon.png -------------------------------------------------------------------------------- /images/godot-icon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dracks/GodotGraphQL/HEAD/images/godot-icon.xcf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | 4 | # Godot-specific ignores 5 | .import/ 6 | export.cfg 7 | export_presets.cfg 8 | 9 | # Imported translations (automatically generated from CSV files) 10 | *.translation 11 | 12 | # Mono-specific ignores 13 | .mono/ 14 | data_*/ 15 | mono_crash.*.json 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jaume 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_gql_query.gd: -------------------------------------------------------------------------------- 1 | extends GutTest 2 | 3 | func test_simple_prop(): 4 | var subject = GQLQuery.new("prop1") 5 | 6 | assert_eq(subject.serialize(), "prop1") 7 | 8 | func test_prop_with_arguments(): 9 | var subject = GQLQuery.new('prop2').set_args({"variable1":"arg1", "variable2":"arg2"}) 10 | 11 | assert_eq(subject.serialize(), "prop2 (arg1: $variable1, arg2: $variable2)") 12 | 13 | 14 | func test_complex_with_subqueries(): 15 | var subquery = GQLQuery.new('subprop').set_args({"variable3":"arg3"}) 16 | var subject = GQLQuery.new('prop3').set_props([ 17 | "some_prop", 18 | subquery 19 | ]) 20 | 21 | assert_eq(subject.serialize(), "prop3 {\nsome_prop\nsubprop (arg3: $variable3)\n}") 22 | 23 | func test_set_args_v2(): 24 | var subject = GQLQuery.new('subject').set_args_v2({"input": "variable"}) 25 | 26 | assert_eq(subject.serialize(), "subject (input: $variable)") 27 | 28 | func test_more_complex_reusing_names(): 29 | var subsubquery = GQLQuery.new("subsubprop").set_props([ 30 | "prop1" 31 | ]) 32 | var subquery = GQLQuery.new("subprop").set_args_v2({"variable": "$"}).set_props([ 33 | subsubquery, 34 | "extra_prop" 35 | ]) 36 | 37 | var subject = GQLQuery.new('subject').set_props([subquery]) 38 | 39 | assert_eq(subject.serialize(), "subject {\nsubprop (variable: $variable) {\nsubsubprop {\nprop1\n}\nextra_prop\n}\n}") 40 | -------------------------------------------------------------------------------- /src/gql_query.gd: -------------------------------------------------------------------------------- 1 | class_name GQLQuery 2 | 3 | var name: String 4 | var props_list: Array = [] 5 | var args_list: Dictionary = {} 6 | 7 | func _init(_name: String): 8 | name = _name 9 | 10 | func set_args_v2(_args: Dictionary) -> GQLQuery: 11 | args_list = _args 12 | return self 13 | 14 | func set_args(_args: Dictionary) -> GQLQuery: 15 | print("Deprecated set_args, please use set_args_v2 inverting the keys and values of the dict") 16 | var new_args_list = {} 17 | for variable in _args.keys(): 18 | var arg = _args[variable] 19 | new_args_list[arg] = variable 20 | self.args_list = new_args_list 21 | return self 22 | 23 | 24 | func set_props(_props: Array) -> GQLQuery: 25 | props_list = _props 26 | return self 27 | 28 | func add_prop(_prop) -> GQLQuery: 29 | props_list.append(_prop) 30 | return self 31 | 32 | func _serialize_args()->String: 33 | var query = " (" 34 | var sep = "" 35 | for arg in args_list.keys(): 36 | var variable = args_list[arg] 37 | if variable == "$": 38 | variable = arg 39 | query +=sep+arg+": $"+variable 40 | sep = ", " 41 | return query + ")" 42 | 43 | 44 | func serialize() -> String: 45 | var query = name 46 | if args_list.size()>0: 47 | query += self._serialize_args() 48 | 49 | if props_list.size()>0: 50 | query += " {\n" 51 | for prop in props_list: 52 | if typeof(prop)==TYPE_STRING: 53 | query +=prop+"\n" 54 | else: 55 | query +=prop.serialize()+"\n" 56 | query +="}" 57 | return query 58 | -------------------------------------------------------------------------------- /src/gql_query_executer.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | class_name GQLQueryExecuter 4 | 5 | signal graphql_response 6 | 7 | var endpoint: String 8 | var query: GQLQuery 9 | var use_ssl: bool 10 | var query_cached: String = "" 11 | 12 | var headers = ["Content-Type: application/json"] 13 | var request : HTTPRequest 14 | 15 | 16 | func _init(_endpoint:String, _use_ssl: bool, _query: GQLQuery): 17 | endpoint = _endpoint 18 | query = _query 19 | use_ssl = _use_ssl 20 | 21 | func _ready(): 22 | request = HTTPRequest.new() 23 | request.request_completed.connect(self.request_completed) 24 | add_child(request) 25 | 26 | func request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray): 27 | print("Request completed:", result, ",", response_code) 28 | if response_code!=200: 29 | print("Query:"+query_cached) 30 | print("Response:"+body.get_string_from_utf8()) 31 | var json := JSON.new() 32 | json.parse(body.get_string_from_utf8()) 33 | emit_signal('graphql_response', json.data) 34 | 35 | func run(variables: Dictionary): 36 | assert(request!=null, 'You should add this node to the childs') 37 | if query_cached == "": 38 | query_cached = query.serialize() 39 | var data_to_send = { 40 | "query": query_cached, 41 | "variables": variables, 42 | } 43 | print("h:", headers, "use_ssl:", use_ssl) 44 | var body = JSON.new().stringify(data_to_send) 45 | var err=request.request(endpoint, headers, HTTPClient.METHOD_POST, body) 46 | print("Request to: ", endpoint, " return: ", err) 47 | 48 | -------------------------------------------------------------------------------- /src/gql_query_subscriber.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | class_name GQLQuerySubscriber 4 | 5 | signal new_data 6 | signal closed 7 | 8 | var _client := WebSocketClient.new() 9 | var endpoint: String 10 | var query: String 11 | 12 | var subscription_completed:=false 13 | 14 | func _init(_endpoint: String, _query: GQLQuery): 15 | endpoint = _endpoint 16 | query = _query.serialize() 17 | 18 | func _ready(): 19 | _client.connection_closed.connect(_closed) 20 | _client.connected_to_server.connect(_connected) 21 | _client.message_received.connect(_on_data) 22 | _client.supported_protocols = PackedStringArray(["graphql-ws"]) 23 | add_child(_client) 24 | 25 | # Initiate connection to the given URL. 26 | var err = _client.connect_to_url(endpoint) 27 | if err != OK: 28 | print("Unable to connect") 29 | set_process(false) 30 | 31 | func _closed(was_clean = false): 32 | # was_clean will tell you if the disconnection was correctly notified 33 | # by the remote peer before closing the socket. 34 | print("Closed, clean: ", was_clean) 35 | set_process(false) 36 | 37 | func _connected(proto = ""): 38 | # This is called on connection, "proto" will be the selected WebSocket 39 | # sub-protocol (which is optional) 40 | print("Connected with protocol: ", proto) 41 | # You MUST always use get_peer(1).put_packet to send data to server, 42 | # and not put_packet directly when not using the MultiplayerAPI. 43 | _client.send("{\"type\": \"connection_init\",\"payload\": {}}") 44 | 45 | func _on_data(data): 46 | var json := JSON.new() 47 | json.parse(data) 48 | if json.data.type == "data": 49 | new_data.emit(json.data.payload.data) 50 | elif json.data.type =="connection_ack": 51 | var to_complete_subscription = JSON.stringify({ 52 | "type": "start", 53 | "id": "1", 54 | "payload": { 55 | "query": query 56 | } 57 | }) 58 | _client.send(to_complete_subscription) 59 | subscription_completed = true 60 | 61 | func _process(delta): 62 | # Call this in _process or _physics_process. Data transfer, and signals 63 | # emission will only happen when calling this function. 64 | _client.poll() 65 | -------------------------------------------------------------------------------- /src/gql_client.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | class_name GQLClient 4 | 5 | var endpoint: String 6 | var use_ssl: bool 7 | var websocket_endpoint: String 8 | 9 | 10 | class AbstractQuery: 11 | extends GQLQuery 12 | 13 | func _init(name:String): 14 | super(name); 15 | 16 | func set_args(args: Dictionary) -> GQLQuery: 17 | args_list = args 18 | return self 19 | 20 | func _serialize_args()->String: 21 | var query = " (" 22 | var sep = "$" 23 | for variable in args_list.keys(): 24 | query +=sep+variable+": "+args_list[variable] 25 | sep = ", $" 26 | return query + ")" 27 | 28 | 29 | class Query: 30 | extends AbstractQuery 31 | 32 | func _init(name:String): 33 | super("query "+name) 34 | 35 | class Mutation: 36 | extends AbstractQuery 37 | 38 | func _init(name:String): 39 | super("mutation "+name) 40 | 41 | class Subscription: 42 | extends AbstractQuery 43 | 44 | func _init(name: String): 45 | super("subscription "+name) 46 | 47 | func set_endpoint(is_secure: bool, host: String, port: int, path: String): 48 | endpoint = "http://" 49 | websocket_endpoint = "ws://" 50 | use_ssl = is_secure 51 | if is_secure: 52 | endpoint = "https://" 53 | websocket_endpoint = "wss://" 54 | endpoint += host 55 | websocket_endpoint += host 56 | if port!=0: 57 | endpoint += ":{0}".format([port]) 58 | websocket_endpoint +=":{0}".format([port]) 59 | endpoint += path 60 | websocket_endpoint += path 61 | 62 | 63 | func query(name:String, args: Dictionary, query: GQLQuery): 64 | var _query = Query.new(name).set_args(args).add_prop(query) 65 | return GQLQueryExecuter.new(endpoint, use_ssl, _query) 66 | 67 | func mutation(name:String, args: Dictionary, query: GQLQuery): 68 | var _query = Mutation.new(name).set_args(args).add_prop(query) 69 | return GQLQueryExecuter.new(endpoint, use_ssl, _query) 70 | 71 | func subscribe(name: String, args: Dictionary, query: GQLQuery): 72 | var _query = Subscription.new(name).set_args(args).add_prop(query) 73 | return GQLQuerySubscriber.new(websocket_endpoint, _query) 74 | 75 | func raw(query:String): 76 | return GQLQueryExecuter.new(endpoint, use_ssl, Query.new(query)) 77 | -------------------------------------------------------------------------------- /src/websocket_client.gd: -------------------------------------------------------------------------------- 1 | # Extracted from godot samples: https://github.com/godotengine/godot-demo-projects/blob/master/networking/websocket_chat/websocket/WebSocketClient.gd 2 | extends Node 3 | class_name WebSocketClient 4 | 5 | @export var handshake_headers: PackedStringArray 6 | @export var supported_protocols: PackedStringArray 7 | var tls_options: TLSOptions = null 8 | 9 | 10 | var socket = WebSocketPeer.new() 11 | var last_state = WebSocketPeer.STATE_CLOSED 12 | 13 | 14 | signal connected_to_server() 15 | signal connection_closed() 16 | signal message_received(message: Variant) 17 | 18 | 19 | func connect_to_url(url) -> int: 20 | socket.supported_protocols = supported_protocols 21 | socket.handshake_headers = handshake_headers 22 | var err = socket.connect_to_url(url, tls_options) 23 | if err != OK: 24 | return err 25 | last_state = socket.get_ready_state() 26 | return OK 27 | 28 | 29 | func send(message) -> int: 30 | if typeof(message) == TYPE_STRING: 31 | return socket.send_text(message) 32 | return socket.send(var_to_bytes(message)) 33 | 34 | 35 | func get_message() -> Variant: 36 | if socket.get_available_packet_count() < 1: 37 | return null 38 | var pkt = socket.get_packet() 39 | if socket.was_string_packet(): 40 | return pkt.get_string_from_utf8() 41 | return bytes_to_var(pkt) 42 | 43 | 44 | func close(code := 1000, reason := "") -> void: 45 | socket.close(code, reason) 46 | last_state = socket.get_ready_state() 47 | 48 | 49 | func clear() -> void: 50 | socket = WebSocketPeer.new() 51 | last_state = socket.get_ready_state() 52 | 53 | 54 | func get_socket() -> WebSocketPeer: 55 | return socket 56 | 57 | 58 | func poll() -> void: 59 | if socket.get_ready_state() != socket.STATE_CLOSED: 60 | socket.poll() 61 | var state = socket.get_ready_state() 62 | if last_state != state: 63 | last_state = state 64 | if state == socket.STATE_OPEN: 65 | connected_to_server.emit() 66 | elif state == socket.STATE_CLOSED: 67 | connection_closed.emit() 68 | while socket.get_ready_state() == socket.STATE_OPEN and socket.get_available_packet_count(): 69 | message_received.emit(get_message()) 70 | 71 | 72 | func _process(delta): 73 | poll() 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Godot GraphQLClient Addon 2 | 3 | ## Prerequisits 4 | You need to have GUT in your project. 5 | See https://gut.readthedocs.io/en/latest/Install.html 6 | 7 | ## Instalation 8 | 1. download the contents (or clone) and put inside $PROJECT/addons 9 | 2. Create a class extending GQLClient 10 | 3. Implement in a _ready function or in the same _init to configure it calling to set_endpoint(is_secure, host, port, path) 11 | 4. Add this class as an autoload of your godot project. 12 | 13 | ## Usage 14 | This library uses his own objects to create the query. But provide also a raw argument to call with a string. 15 | 16 | 1. create some graphql query: 17 | ```gdscript 18 | var query =GQLQuery.new("someProp").set_args_v2({"arg":"variable"}).set_props([ 19 | "otherProp", 20 | GQLQuery.new("moreComplexProp") 21 | ]) 22 | ``` 23 | 24 | 2. Call to your singleton to the query method and add it to your node_tree: 25 | ```gdscript 26 | var my_query_executer = ServerConfigInstance.query("NameOfTheQuery", {"variable":"HisType"}, query) 27 | ``` 28 | 29 | 3. Connect to graphql_response signal to retrieve the data 30 | 4. Execute the run method with the variables as args 31 | ```gdscript 32 | my_query_executor.run({"variable":42}) 33 | ``` 34 | 35 | You can see the [sample project](https://github.com/Dracks/godot-gql-test) 36 | 37 | ## Features 38 | * Tested with a django-graphene server 39 | * Do queries and mutations 40 | * gql_query tested using [gut](https://github.com/bitwes/Gut) 41 | 42 | 43 | ## Documentation 44 | ### GQLQuery samples 45 | The sample of use in the usage will generate something like this: 46 | ```graphql 47 | someProp(arg:$variable){ 48 | otherProp 49 | moreComplexProp 50 | } 51 | ``` 52 | 53 | As you can see there is no query information or mutation. The query or mutation is added when you call to client.query or client.mutation. The query generated in the point usage 4 is the following: 54 | ```graphql 55 | query NameOfTheQuery(variable: HisType){ 56 | someProp(arg:$variable){ 57 | otherProp 58 | moreComplexProp 59 | } 60 | } 61 | ``` 62 | Adding the variable of variable to 42 63 | 64 | ### Writing the graphql with samples 65 | 1. Query a field with variables 66 | ```gdscript 67 | var gqlClient : GqlClient = get_node('/root/GqlClient') 68 | var subject = GQLQuery.new("prop").set_args_v2({"argument": "input"}).set_props(["sample"]) 69 | var executor = gqlClient.query("queryName", {"input": "String" }, subject) 70 | ``` 71 | 72 | Will generate 73 | ```Gql 74 | query queryName ($input: String) { 75 | prop(argument: $input){ 76 | sample 77 | } 78 | } 79 | ``` 80 | 81 | 2. Using variable shortcut, when the argument name is the same as the variable, we can use a $ to automatically match them 82 | ```gdscript 83 | var gqlClient : GqlClient = get_node('/root/GqlClient') 84 | var subject = GQLQuery.new("prop").set_args_v2({"argument": "$"}).set_props(["sample"]) 85 | var executor = gqlClient.query("queryName", {"argument": "String" }, subject) 86 | ``` 87 | 88 | Will generate 89 | ```Gql 90 | query queryName ($argument: String) { 91 | prop(argument: $argument){ 92 | sample 93 | } 94 | } 95 | ``` 96 | --------------------------------------------------------------------------------