├── .tool-versions ├── lib ├── graphql.ex └── graphql │ ├── local_backend.ex │ ├── client.ex │ ├── variable.ex │ ├── response.ex │ ├── query_registry.ex │ ├── encoder.ex │ ├── query_builder.ex │ ├── query.ex │ └── node.ex ├── test ├── graphql_client_test.exs ├── test_helper.exs └── graphql │ ├── response_test.exs │ ├── node_test.exs │ ├── query_test.exs │ ├── encoder_test.exs │ ├── query_builder_test.exs │ └── query_registry_test.exs ├── .formatter.exs ├── .github ├── pull_request_template.md └── workflows │ └── ci.yml ├── NOTICE ├── .gitignore ├── CHANGELOG.md ├── mix.exs ├── mix.lock ├── README.md ├── CODE_OF_CONDUCT.md └── LICENSE /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.11.4 2 | -------------------------------------------------------------------------------- /lib/graphql.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL do 2 | @moduledoc false 3 | end 4 | -------------------------------------------------------------------------------- /test/graphql_client_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GraphqlClientTest do 2 | end 3 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | GraphQL.LocalBackend.start_link() 3 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Motivation 2 | 3 | 13 | 14 | ## Overview 15 | 16 | 23 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2022 TheRealReal 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | graphql_client-*.tar 24 | 25 | 26 | # Temporary files for e.g. tests 27 | /tmp 28 | 29 | # MacOS stuff 30 | .DS_Store 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for `graphql_client` 2 | 3 | ## v0.2.1 4 | 5 | ### Changes 6 | 7 | - Minor documentation fixes 8 | 9 | ## v0.2.0 10 | 11 | ### Changes 12 | - `GraphQL.Query.merge/3` and `GraphQL.Query.merge_many/2` now return ok/error tuples instead of structs - the new 13 | check for duplicated variables may now invalidate a merge and return an error. 14 | 15 | ### Bugfixes 16 | - Do not allow variables to be added twice, even when declared with different key types 17 | 18 | ## v0.1.2 19 | 20 | ### Bugfixes 21 | - Fix the return value of `GraphQL.LocalBackend.execute_query/2` 22 | 23 | ## v0.1.1 24 | 25 | ### New Features 26 | - Enable recursive expression for variables 27 | - New function `GraphQL.QueryBuilder.enum/1`, to declare enum values so they are rendered without quotes. 28 | ## v0.1.0 29 | 30 | First version! 31 | 32 | ### New Features 33 | - GraphQL query representation using elixir code! 34 | - Merge queries into a single operation 35 | - Testing suppport 36 | -------------------------------------------------------------------------------- /lib/graphql/local_backend.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.LocalBackend do 2 | @moduledoc """ 3 | A `GraphQL.Client` implementation that uses an `Agent` to store data, useful 4 | for tests. 5 | """ 6 | use Agent 7 | 8 | alias GraphQL.Response 9 | 10 | @behaviour GraphQL.Client 11 | 12 | def start_link do 13 | Application.put_env(:graphql_client, :backend, __MODULE__) 14 | Agent.start_link(fn -> nil end, name: __MODULE__) 15 | end 16 | 17 | @doc """ 18 | Stores a response or a function that will be evaluated to the next call 19 | to `execute_query` 20 | """ 21 | def expect(response) do 22 | Agent.update(__MODULE__, fn _ -> response end) 23 | end 24 | 25 | @impl true 26 | def execute_query(query, variables, options) do 27 | response = Agent.get_and_update(__MODULE__, fn state -> {state, nil} end) 28 | 29 | case response do 30 | %Response{} = response -> response 31 | f when is_function(f, 3) -> f.(query, variables, options) 32 | nil -> raise "there is no response" 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/graphql/client.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Client do 2 | @moduledoc """ 3 | Facade client for GraphQL requests 4 | 5 | The real backend must implement the execute/4 callback and must be configured 6 | by the `:therealreal, GraphQL.Client, :backend` application 7 | config. 8 | """ 9 | alias GraphQL.{Query, Response} 10 | 11 | @type query :: Query.t() | {String.t(), reference()} | String.t() 12 | 13 | @doc """ 14 | Callback spec for backend implementation 15 | """ 16 | @callback execute_query( 17 | query :: Query.t(), 18 | variables :: map(), 19 | options :: map() 20 | ) :: Response.t() 21 | 22 | @doc """ 23 | Executes the given query, with the given variables and options. 24 | """ 25 | @spec execute(Query.t(), map(), map()) :: Response.t() 26 | def execute(%Query{} = query, variables, options \\ %{}) do 27 | backend().execute_query(query, variables, options) 28 | end 29 | 30 | defp backend do 31 | Application.fetch_env!(:graphql_client, :backend) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/graphql/variable.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Variable do 2 | @moduledoc """ 3 | A struct to represent GraphQL variables 4 | """ 5 | defstruct name: nil, type: nil, default_value: nil 6 | 7 | @typedoc """ 8 | A GraphQL generic name 9 | """ 10 | @type name() :: String.t() | atom() 11 | 12 | @typedoc """ 13 | A struct that represents the definition of a GraphQL variable. 14 | 15 | A variable definition exists within a query or mutation, and then can be 16 | referenced by the arguments of fields. 17 | """ 18 | @type t :: %__MODULE__{ 19 | name: name(), 20 | type: name(), 21 | default_value: any() 22 | } 23 | 24 | @doc """ 25 | Check if two variables represent the same variable 26 | """ 27 | @spec same?(t(), t()) :: boolean() 28 | def same?(%__MODULE__{} = a, %__MODULE__{} = b) do 29 | name_a = term_as_string(a.name) 30 | name_b = term_as_string(b.name) 31 | 32 | name_a == name_b 33 | end 34 | 35 | defp term_as_string(term) when is_atom(term), do: Atom.to_string(term) 36 | defp term_as_string(term) when is_binary(term), do: term 37 | end 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-20.04 8 | name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | include: 13 | - elixir: 1.11.x 14 | otp: 22 15 | - elixir: 1.11.x 16 | otp: 23 17 | - elixir: 1.11.x 18 | otp: 24 19 | - elixir: 1.12.x 20 | otp: 22 21 | - elixir: 1.12.x 22 | otp: 23 23 | - elixir: 1.12.x 24 | otp: 24 25 | - elixir: 1.13.x 26 | otp: 22 27 | - elixir: 1.13.x 28 | otp: 23 29 | - elixir: 1.13.x 30 | otp: 24 31 | lint: true 32 | env: 33 | MIX_ENV: test 34 | 35 | steps: 36 | - uses: actions/checkout@v2 37 | 38 | - uses: erlef/setup-beam@v1 39 | with: 40 | otp-version: ${{matrix.otp}} 41 | elixir-version: ${{matrix.elixir}} 42 | 43 | - run: mix deps.get 44 | 45 | - run: mix format --check-formatted 46 | if: ${{ matrix.lint }} 47 | 48 | - run: mix deps.unlock --check-unused 49 | if: ${{ matrix.lint }} 50 | 51 | - run: mix deps.compile 52 | 53 | - run: mix compile --warnings-as-errors 54 | if: ${{ matrix.lint }} 55 | 56 | - run: mix test 57 | 58 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule GraphqlClient.MixProject do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/TheRealReal/graphql_client" 5 | @version "0.2.1" 6 | 7 | def project do 8 | [ 9 | app: :graphql_client, 10 | version: @version, 11 | name: "GraphQL Client", 12 | description: " A composable GraphQL client library for Elixir", 13 | elixir: "~> 1.11", 14 | deps: deps(), 15 | docs: docs(), 16 | package: package() 17 | ] 18 | end 19 | 20 | def application do 21 | [ 22 | extra_applications: [] 23 | ] 24 | end 25 | 26 | defp deps do 27 | [ 28 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 29 | ] 30 | end 31 | 32 | defp package do 33 | [ 34 | maintainers: ["TheRealReal"], 35 | licenses: ["Apache-2.0"], 36 | links: %{"GitHub" => @source_url} 37 | ] 38 | end 39 | 40 | defp docs do 41 | [ 42 | source_ref: "v#{@version}", 43 | canonical: "http://hexdocs.pm/graphql_client", 44 | source_url: @source_url, 45 | extras: [ 46 | "README.md", 47 | "CHANGELOG.md": [filename: "changelog", title: "Changelog"], 48 | "CODE_OF_CONDUCT.md": [filename: "code_of_conduct", title: "Code of Conduct"], 49 | LICENSE: [filename: "license", title: "License"], 50 | NOTICE: [filename: "notice", title: "Notice"] 51 | ] 52 | ] 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.20", "89970db71b11b6b89759ce16807e857df154f8df3e807b2920a8c39834a9e5cf", [:mix], [], "hexpm", "1eb0d2dabeeeff200e0d17dc3048a6045aab271f73ebb82e416464832eb57bdd"}, 3 | "ex_doc": {:hex, :ex_doc, "0.26.0", "1922164bac0b18b02f84d6f69cab1b93bc3e870e2ad18d5dacb50a9e06b542a3", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2775d66e494a9a48355db7867478ffd997864c61c65a47d31c4949459281c78d"}, 4 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 5 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, 6 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 7 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.2", "b99ca56bbce410e9d5ee4f9155a212e942e224e259c7ebbf8f2c86ac21d4fa3c", [:mix], [], "hexpm", "98d51bd64d5f6a2a9c6bb7586ee8129e27dfaab1140b5a4753f24dac0ba27d2f"}, 8 | } 9 | -------------------------------------------------------------------------------- /test/graphql/response_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.ResponseTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias GraphQL.Response 5 | 6 | doctest Response, import: true 7 | 8 | describe "success/1" do 9 | test "creates a new Response struct marked as success" do 10 | expected = %Response{ 11 | success?: true, 12 | data: %{ 13 | field: %{ 14 | name: "Blorgon" 15 | } 16 | } 17 | } 18 | 19 | result = 20 | Response.success(%{ 21 | field: %{ 22 | name: "Blorgon" 23 | } 24 | }) 25 | 26 | assert result == expected 27 | end 28 | end 29 | 30 | describe "failure/1" do 31 | test "creates a new Response struct marked as failure" do 32 | expected = %Response{ 33 | success?: false, 34 | errors: [%{message: "Some error", locations: [%{line: 1, column: 1}]}] 35 | } 36 | 37 | result = Response.failure([%{message: "Some error", locations: [%{line: 1, column: 1}]}]) 38 | 39 | assert result == expected 40 | end 41 | end 42 | 43 | describe "partial_success/2" do 44 | test "creates a new Response struct marked as a partial success" do 45 | expected = %Response{ 46 | success?: :partial, 47 | data: %{ 48 | field: %{ 49 | name: "Blorgon" 50 | } 51 | }, 52 | errors: [ 53 | %{message: "Some error", locations: [%{line: 1, column: 1}]} 54 | ] 55 | } 56 | 57 | result = 58 | Response.partial_success( 59 | %{field: %{name: "Blorgon"}}, 60 | [%{message: "Some error", locations: [%{line: 1, column: 1}]}] 61 | ) 62 | 63 | assert result == expected 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/graphql/response.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Response do 2 | @moduledoc """ 3 | Functions to handle GraphQL responses 4 | """ 5 | @enforce_keys [:success?] 6 | defstruct [:data, :errors, :success?] 7 | 8 | @typedoc """ 9 | A struct that contains the GraphQL response data. 10 | """ 11 | @type t :: %__MODULE__{ 12 | data: any(), 13 | errors: any(), 14 | success?: true | false | :partial 15 | } 16 | 17 | @doc """ 18 | Creates a succes response with the given data 19 | 20 | ## Examples 21 | 22 | iex> success(%{field: "value"}) 23 | %GraphQL.Response{success?: true, data: %{field: "value"}} 24 | """ 25 | @spec success(map) :: t() 26 | def success(data) do 27 | %__MODULE__{success?: true, data: data, errors: nil} 28 | end 29 | 30 | @doc """ 31 | Creates a new failure response with the given errors 32 | 33 | ## Examples 34 | 35 | iex> failure([%{message: "some error", locations: [%{line: 2, column: 5}]}]) 36 | %GraphQL.Response{success?: false, errors: [%{message: "some error", locations: [%{line: 2, column: 5}]}]} 37 | """ 38 | @spec failure(map) :: t() 39 | def failure(errors) do 40 | %__MODULE__{success?: false, data: nil, errors: errors} 41 | end 42 | 43 | @doc """ 44 | Create a new partial success response with the given data and errors 45 | 46 | ## Examples 47 | 48 | iex> data = %{field: "value"} 49 | %{field: "value"} 50 | iex> errors = [%{message: "some error", locations: [%{line: 2, column: 5}]}] 51 | [%{message: "some error", locations: [%{line: 2, column: 5}]}] 52 | iex> partial_success(data, errors) 53 | %GraphQL.Response{success?: :partial, data: %{field: "value"}, errors: [%{message: "some error", locations: [%{line: 2, column: 5}]}]} 54 | """ 55 | @spec partial_success(map(), list()) :: t() 56 | def partial_success(data, errors) do 57 | %__MODULE__{success?: :partial, data: data, errors: errors} 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/graphql/node_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.NodeTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias GraphQL.Node 5 | 6 | doctest Node, import: true 7 | 8 | describe "field/1" do 9 | test "creates a Node struct for a field" do 10 | expected = %Node{node_type: :field, name: :field_name} 11 | 12 | result = Node.field(:field_name) 13 | 14 | assert result == expected 15 | end 16 | 17 | test "creates a Node struct for a field with an alias" do 18 | expected = %Node{node_type: :field, name: :field_name, alias: "fieldAlias"} 19 | 20 | result = Node.field({:field_name, "fieldAlias"}) 21 | 22 | assert result == expected 23 | end 24 | end 25 | 26 | describe "field/3" do 27 | test "creates a Node struct for a field with arguments and variables" do 28 | expected = %Node{ 29 | node_type: :field, 30 | name: :field_name, 31 | arguments: %{arg: "value"}, 32 | nodes: [ 33 | %Node{node_type: :field, name: :subfield} 34 | ] 35 | } 36 | 37 | result = 38 | Node.field(:field_name, %{arg: "value"}, [ 39 | %Node{node_type: :field, name: :subfield} 40 | ]) 41 | 42 | assert result == expected 43 | end 44 | 45 | test "creates a Node struct for a field with arguments, variables and an alias" do 46 | expected = %Node{ 47 | node_type: :field, 48 | name: :field_name, 49 | alias: "fieldAlias", 50 | arguments: %{arg: "value"}, 51 | nodes: [ 52 | %Node{node_type: :field, name: :subfield} 53 | ] 54 | } 55 | 56 | result = 57 | Node.field({:field_name, "fieldAlias"}, %{arg: "value"}, [ 58 | %Node{node_type: :field, name: :subfield} 59 | ]) 60 | 61 | assert result == expected 62 | end 63 | end 64 | 65 | describe "fragment/1" do 66 | test "creates a Node struct for a fragment reference" do 67 | expected = %Node{ 68 | node_type: :fragment_ref, 69 | name: "someFields" 70 | } 71 | 72 | result = Node.fragment("someFields") 73 | 74 | assert result == expected 75 | end 76 | end 77 | 78 | describe "fragment/3" do 79 | test "creates a Node struct for a fragment" do 80 | expected = %Node{ 81 | node_type: :fragment, 82 | name: "someFields", 83 | type: "SomeType", 84 | nodes: [ 85 | %Node{node_type: :field, name: :subfield} 86 | ] 87 | } 88 | 89 | result = 90 | Node.fragment("someFields", "SomeType", [ 91 | %Node{node_type: :field, name: :subfield} 92 | ]) 93 | 94 | assert result == expected 95 | end 96 | end 97 | 98 | describe "inline_fragment/2" do 99 | test "creates a Node struct for an inline fragment" do 100 | expected = %Node{ 101 | node_type: :inline_fragment, 102 | type: "SomeType", 103 | nodes: [ 104 | %Node{node_type: :field, name: :subfield} 105 | ] 106 | } 107 | 108 | result = 109 | Node.inline_fragment("SomeType", [ 110 | %Node{node_type: :field, name: :subfield} 111 | ]) 112 | 113 | assert result == expected 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/graphql/query_registry.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.QueryRegistry do 2 | @moduledoc """ 3 | Functions to handle query registries. 4 | 5 | A query registry stores several `GraphQL.Query` structs, so they 6 | can be combined into a single query before the execution. 7 | """ 8 | alias GraphQL.{Client, Query} 9 | 10 | @enforce_keys [:name] 11 | defstruct name: nil, queries: [], variables: [], resolvers: [] 12 | 13 | @typedoc """ 14 | A resolver is a function that must accept two arguments: 15 | - a `GraphQL.Response` struct 16 | - an accumulator, that can be of any type 17 | 18 | It also must return the updated value of the accumulator. 19 | """ 20 | @type resolver :: (Response.t(), any() -> any()) 21 | 22 | @typedoc """ 23 | A struct that keeps the information about several queries, variables and 24 | resolvers. 25 | 26 | The `name` field will be used as the name of the final query or mutation. 27 | 28 | The `queries` field is a list of `GraphQL.Query` structs, that 29 | will be merged before execution. 30 | 31 | The `variables` is a map with all _values_ of variables that will be sent 32 | to the server along with the GraphQL body. 33 | 34 | The `resolver` is a list of `t:resolver()` functions that can be used to 35 | produce the side effects in an accumulator. 36 | """ 37 | @type t :: %__MODULE__{ 38 | name: String.t(), 39 | queries: [Query.t()], 40 | variables: [map()], 41 | resolvers: list() 42 | } 43 | 44 | @doc """ 45 | Creates a new QueryRegistry struct with the given name. 46 | """ 47 | @spec new(String.t()) :: t() 48 | def new(name) do 49 | %__MODULE__{name: name} 50 | end 51 | 52 | @doc """ 53 | Add a query to the a query registry 54 | """ 55 | @spec add_query(t(), Query.t(), map()) :: t() 56 | def add_query(%__MODULE__{} = registry, %Query{} = query, variables \\ nil) do 57 | updated_variables = 58 | if variables == %{} || variables == nil do 59 | registry.variables 60 | else 61 | [variables | registry.variables] 62 | end 63 | 64 | %__MODULE__{registry | queries: [query | registry.queries], variables: updated_variables} 65 | end 66 | 67 | @doc """ 68 | Add a new resolver into a query registry 69 | """ 70 | @spec add_resolver(t(), resolver()) :: t() 71 | def add_resolver(%__MODULE__{} = registry, function) when is_function(function, 2) do 72 | add_resolvers(registry, [function]) 73 | end 74 | 75 | @doc """ 76 | Add a list of resolvers into a query registry 77 | """ 78 | @spec add_resolvers(t(), [resolver()]) :: t() 79 | def add_resolvers(%__MODULE__{} = registry, resolvers) do 80 | %__MODULE__{registry | resolvers: registry.resolvers ++ resolvers} 81 | end 82 | 83 | @doc """ 84 | Executes the given query registry, using the given accumulator `acc` and the given options 85 | """ 86 | @spec execute(t(), any(), Keyword.t()) :: any() 87 | def execute(registry, acc, options \\ []) do 88 | case prepare_query(registry) do 89 | {:ok, {query, variables, resolvers}} -> 90 | result = 91 | query 92 | |> Client.execute(variables, options) 93 | |> resolve(resolvers, acc) 94 | 95 | {:ok, result} 96 | 97 | error -> 98 | error 99 | end 100 | end 101 | 102 | defp prepare_query(%__MODULE__{} = registry) do 103 | case registry.queries do 104 | [] -> 105 | {:error, "no queries available"} 106 | 107 | _not_empty -> 108 | case Query.merge_many(registry.queries, registry.name) do 109 | {:ok, query} -> 110 | variables = merge_variables(registry.variables) 111 | {:ok, {query, variables, registry.resolvers}} 112 | 113 | error -> 114 | error 115 | end 116 | end 117 | end 118 | 119 | defp merge_variables([]), do: %{} 120 | 121 | defp merge_variables(variables) do 122 | Enum.reduce(variables, &Map.merge/2) 123 | end 124 | 125 | defp resolve(response, resolvers, initial_acc) do 126 | Enum.reduce(resolvers, initial_acc, fn resolver, acc -> 127 | resolver.(response, acc) 128 | end) 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /test/graphql/query_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.QueryTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias GraphQL.{Node, Query, Variable} 5 | 6 | import GraphQL.QueryBuilder 7 | 8 | doctest Query, import: true 9 | 10 | @some_query query("SomeQuery", %{userId: "Integer"}, [ 11 | field("recommendation", %{"userId" => :"$userId"}) 12 | ]) 13 | 14 | @user_query query( 15 | "UserQuery", 16 | %{"userId" => "Integer"}, 17 | [ 18 | field("user", %{"userId" => :"$user_id"}, [ 19 | field("id"), 20 | field("email"), 21 | fragment("personFields") 22 | ]) 23 | ], 24 | [ 25 | fragment("personFields", "PersonType", [ 26 | field("name") 27 | ]) 28 | ] 29 | ) 30 | 31 | @product_query query( 32 | "ProductQuery", 33 | %{sku: "String!"}, 34 | [ 35 | field("product", %{"sku" => :"$sku"}, [ 36 | field("id"), 37 | fragment("productFields") 38 | ]) 39 | ], 40 | [ 41 | fragment("productFields", "ProductType", [ 42 | field("title"), 43 | field("description") 44 | ]) 45 | ] 46 | ) 47 | @merged_queries %Query{ 48 | name: "ProductAndUser", 49 | operation: :query, 50 | variables: [ 51 | %Variable{name: "userId", type: "Integer"}, 52 | %Variable{name: :sku, type: "String!"} 53 | ], 54 | fields: [ 55 | %Node{ 56 | node_type: :field, 57 | name: "user", 58 | arguments: %{"userId" => :"$user_id"}, 59 | nodes: [ 60 | %Node{node_type: :field, name: "id"}, 61 | %Node{node_type: :field, name: "email"}, 62 | %Node{node_type: :fragment_ref, name: "personFields"} 63 | ] 64 | }, 65 | %Node{ 66 | node_type: :field, 67 | name: "product", 68 | arguments: %{"sku" => :"$sku"}, 69 | nodes: [ 70 | %Node{node_type: :field, name: "id"}, 71 | %Node{node_type: :fragment_ref, name: "productFields"} 72 | ] 73 | } 74 | ], 75 | fragments: [ 76 | %Node{ 77 | node_type: :fragment, 78 | name: "personFields", 79 | type: "PersonType", 80 | nodes: [ 81 | %Node{node_type: :field, name: "name"} 82 | ] 83 | }, 84 | %Node{ 85 | node_type: :fragment, 86 | name: "productFields", 87 | type: "ProductType", 88 | nodes: [ 89 | %Node{node_type: :field, name: "title"}, 90 | %Node{node_type: :field, name: "description"} 91 | ] 92 | } 93 | ] 94 | } 95 | 96 | describe "query/1" do 97 | test "creates a new query from a keyword list" do 98 | props = @user_query |> Map.drop([:operation, :__struct__]) |> Map.to_list() 99 | result = Query.query(props) 100 | assert result == @user_query 101 | end 102 | end 103 | 104 | describe "mutation/1" do 105 | test "creates a new query from a keyword list" do 106 | props = @user_query |> Map.drop([:operation, :__struct__]) |> Map.to_list() 107 | result = Query.mutation(props) 108 | expected = Map.put(@user_query, :operation, :mutation) 109 | assert result == expected 110 | end 111 | end 112 | 113 | describe "merge/3" do 114 | test "merges two queries" do 115 | {:ok, result} = Query.merge(@user_query, @product_query, "ProductAndUser") 116 | 117 | assert @merged_queries == result 118 | end 119 | 120 | test "returns an error if two queries declare the same variable" do 121 | {:error, message} = Query.merge(@user_query, @some_query, "AQuery") 122 | assert message == "variables declared twice: \"userId\"" 123 | end 124 | end 125 | 126 | describe "merge_many/2" do 127 | test "merges a list of queries" do 128 | {:ok, result} = Query.merge_many([@product_query, @user_query], "ProductAndUser") 129 | 130 | assert @merged_queries == result 131 | end 132 | 133 | test "returns a query if it is the only element on the list" do 134 | {:ok, result} = Query.merge_many([@product_query]) 135 | 136 | assert result == @product_query 137 | end 138 | 139 | test "returns an error if two queries declare the same variable twice" do 140 | {:error, message} = Query.merge_many([@user_query, @some_query], "AQuery") 141 | assert message == "variables declared twice: \"userId\"" 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /test/graphql/encoder_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.EncoderTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias GraphQL.{Encoder, Node, Query, Variable} 5 | 6 | describe "encode/1" do 7 | test "generates a graphql body for a simple query" do 8 | query = %Query{ 9 | operation: :query, 10 | name: "TestQuery", 11 | fields: [ 12 | Node.field("field", %{}, [ 13 | Node.field("subfield") 14 | ]) 15 | ] 16 | } 17 | 18 | expected = 19 | """ 20 | query TestQuery { 21 | field { 22 | subfield 23 | } 24 | } 25 | """ 26 | |> String.trim() 27 | 28 | assert expected == Encoder.encode(query) 29 | end 30 | 31 | test "generates a graphql body for a simple mutation with variables" do 32 | query = %Query{ 33 | operation: :mutation, 34 | name: "TestMutation", 35 | fields: [ 36 | Node.field("field", %{input: :"$input"}, [ 37 | Node.field("subfield") 38 | ]) 39 | ], 40 | variables: [ 41 | %Variable{name: "input", type: "Integer", default_value: 10} 42 | ] 43 | } 44 | 45 | expected = 46 | """ 47 | mutation TestMutation($input: Integer = 10) { 48 | field(input: $input) { 49 | subfield 50 | } 51 | } 52 | """ 53 | |> String.trim() 54 | 55 | assert expected == Encoder.encode(query) 56 | end 57 | 58 | test "generates a graphql query with fragments" do 59 | query = %Query{ 60 | operation: :query, 61 | name: "TestQuery", 62 | fields: [ 63 | Node.field("field", %{}, [ 64 | Node.field("subfield"), 65 | Node.fragment("someFields") 66 | ]) 67 | ], 68 | fragments: [ 69 | Node.fragment("someFields", "SomeType", [ 70 | Node.field("field1"), 71 | Node.field("field2") 72 | ]) 73 | ] 74 | } 75 | 76 | expected = 77 | """ 78 | query TestQuery { 79 | field { 80 | subfield 81 | ...someFields 82 | } 83 | } 84 | fragment someFields on SomeType { 85 | field1 86 | field2 87 | } 88 | """ 89 | |> String.trim() 90 | 91 | assert expected == Encoder.encode(query) 92 | end 93 | 94 | test "generates a graphql query with an inline fragment" do 95 | query = %Query{ 96 | operation: :query, 97 | name: "TestQuery", 98 | fields: [ 99 | Node.field("field", %{}, [ 100 | Node.field("subfield"), 101 | Node.inline_fragment("SomeType", [ 102 | Node.field("field1"), 103 | Node.field("field2") 104 | ]) 105 | ]) 106 | ] 107 | } 108 | 109 | expected = 110 | """ 111 | query TestQuery { 112 | field { 113 | subfield 114 | ... on SomeType { 115 | field1 116 | field2 117 | } 118 | } 119 | } 120 | """ 121 | |> String.trim() 122 | 123 | assert expected == Encoder.encode(query) 124 | end 125 | 126 | test "generates a graphql query with multiple fields and fragments" do 127 | query = %Query{ 128 | operation: :query, 129 | name: "TestQuery", 130 | fields: [ 131 | Node.field({"dog", "theDog"}, %{nick: "Luna"}, [ 132 | Node.fragment("dogFields"), 133 | Node.field({"name", "dogName"}) 134 | ]), 135 | Node.field("field", %{}, [ 136 | Node.inline_fragment("SomeType", [ 137 | Node.field("field1"), 138 | Node.field("field2") 139 | ]), 140 | Node.fragment("otherFields"), 141 | Node.field("subfield") 142 | ]) 143 | ], 144 | fragments: [ 145 | Node.fragment("dogFields", "DogObject", [ 146 | Node.field(:race) 147 | ]), 148 | Node.fragment("otherFields", "OtherObject", [ 149 | Node.field("someField") 150 | ]) 151 | ] 152 | } 153 | 154 | expected = 155 | """ 156 | query TestQuery { 157 | theDog: dog(nick: "Luna") { 158 | ...dogFields 159 | dogName: name 160 | } 161 | field { 162 | ... on SomeType { 163 | field1 164 | field2 165 | } 166 | ...otherFields 167 | subfield 168 | } 169 | } 170 | fragment dogFields on DogObject { 171 | race 172 | } 173 | fragment otherFields on OtherObject { 174 | someField 175 | } 176 | """ 177 | |> String.trim() 178 | 179 | assert expected == Encoder.encode(query) 180 | end 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /lib/graphql/encoder.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Encoder do 2 | @moduledoc """ 3 | Functions to encode `GraphQL.Query` struct into a string 4 | """ 5 | alias GraphQL.{Node, Query, Variable} 6 | 7 | @doc """ 8 | Encodes a `GraphQL.Query` struct into a GraphQL query body 9 | """ 10 | @spec encode(Query.t()) :: String.t() 11 | def encode(%Query{} = query) do 12 | has_fragments? = valid?(query.fragments) 13 | has_variables? = valid?(query.variables) 14 | 15 | identation = 0 16 | 17 | [ 18 | query.operation, 19 | " ", 20 | query.name, 21 | if(has_variables?, do: encode_variables(query.variables)), 22 | " {\n", 23 | encode_nodes(query.fields, identation + 2), 24 | "\n}", 25 | if(has_fragments?, do: "\n"), 26 | if(has_fragments?, do: encode_nodes(query.fragments, identation)) 27 | ] 28 | |> Enum.join() 29 | end 30 | 31 | # Variables 32 | 33 | defp encode_variables(variables) do 34 | [ 35 | "(", 36 | variables |> Enum.map(&encode_variable/1) |> Enum.join(", "), 37 | ")" 38 | ] 39 | |> Enum.join() 40 | end 41 | 42 | defp encode_variable(%Variable{} = var) do 43 | has_default? = var.default_value != nil 44 | 45 | [ 46 | "$", 47 | var.name, 48 | ": ", 49 | var.type, 50 | if(has_default?, do: " = "), 51 | encode_value(var.default_value) 52 | ] 53 | |> Enum.join() 54 | end 55 | 56 | defp encode_nodes(nil, _), do: "" 57 | defp encode_nodes([], _), do: "" 58 | 59 | defp encode_nodes(fields, identation) do 60 | fields 61 | |> Enum.map(&encode_node(&1, identation)) 62 | |> Enum.join("\n") 63 | end 64 | 65 | # Field 66 | defp encode_node(%Node{node_type: :field} = a_node, identation) do 67 | has_arguments? = valid?(a_node.arguments) 68 | has_nodes? = valid?(a_node.nodes) 69 | has_directives? = valid?(a_node.directives) 70 | 71 | [ 72 | String.duplicate(" ", identation), 73 | encode_field_alias(a_node.alias), 74 | encode_name(a_node.name), 75 | if(has_arguments?, do: encode_arguments(a_node.arguments)), 76 | if(has_directives?, do: " "), 77 | if(has_directives?, do: encode_directives(a_node.directives)), 78 | if(has_nodes?, do: " {\n"), 79 | encode_nodes(a_node.nodes, identation + 2), 80 | if(has_nodes?, do: "\n"), 81 | if(has_nodes?, do: String.duplicate(" ", identation)), 82 | if(has_nodes?, do: "}") 83 | ] 84 | |> Enum.join() 85 | end 86 | 87 | # Fragment reference 88 | defp encode_node(%Node{node_type: :fragment_ref} = a_node, identation) do 89 | [ 90 | String.duplicate(" ", identation), 91 | "...", 92 | a_node.name 93 | ] 94 | |> Enum.join() 95 | end 96 | 97 | # Fragment 98 | defp encode_node(%Node{node_type: :fragment} = fragment, identation) do 99 | [ 100 | String.duplicate(" ", identation), 101 | "fragment ", 102 | fragment.name, 103 | " on ", 104 | fragment.type, 105 | " {\n", 106 | fragment.nodes |> Enum.map(&encode_node(&1, identation + 2)) |> Enum.join("\n"), 107 | "\n", 108 | String.duplicate(" ", identation), 109 | "}" 110 | ] 111 | |> Enum.join() 112 | end 113 | 114 | # Inline Fragment 115 | defp encode_node(%Node{node_type: :inline_fragment} = a_node, identation) do 116 | [ 117 | String.duplicate(" ", identation), 118 | "... on ", 119 | a_node.type, 120 | " {\n", 121 | a_node.nodes |> Enum.map(&encode_node(&1, identation + 2)) |> Enum.join("\n"), 122 | "\n", 123 | String.duplicate(" ", identation), 124 | "}" 125 | ] 126 | |> Enum.join() 127 | end 128 | 129 | defp encode_name(name) when is_atom(name), do: Atom.to_string(name) 130 | defp encode_name(name) when is_binary(name), do: name 131 | 132 | defp encode_field_alias(nil), do: "" 133 | defp encode_field_alias(an_alias), do: "#{an_alias}: " 134 | 135 | # Arguments 136 | def encode_arguments(nil), do: "" 137 | 138 | def encode_arguments([]), do: "" 139 | 140 | def encode_arguments(map_or_keyword) do 141 | vars = 142 | map_or_keyword 143 | |> Enum.map(&encode_argument/1) 144 | |> Enum.join(", ") 145 | 146 | "(#{vars})" 147 | end 148 | 149 | def encode_argument({key, value}) do 150 | "#{key}: #{encode_value(value)}" 151 | end 152 | 153 | defp encode_value(v) do 154 | cond do 155 | is_binary(v) -> 156 | "\"#{v}\"" 157 | 158 | is_list(v) -> 159 | v 160 | |> Enum.map(&encode_value/1) 161 | |> Enum.join() 162 | 163 | is_map(v) -> 164 | parsed_v = 165 | v 166 | |> Enum.map(&encode_argument/1) 167 | |> Enum.join(", ") 168 | 169 | Enum.join(["{", parsed_v, "}"]) 170 | 171 | true -> 172 | case v do 173 | {:enum, v} -> v 174 | v -> "#{v}" 175 | end 176 | end 177 | end 178 | 179 | defp encode_directives(directives) do 180 | directives 181 | |> Enum.map(&encode_directive/1) 182 | |> Enum.join(" ") 183 | end 184 | 185 | defp encode_directive({key, arguments}) do 186 | [ 187 | "@", 188 | key, 189 | encode_arguments(arguments) 190 | ] 191 | |> Enum.join() 192 | end 193 | 194 | defp encode_directive(key) do 195 | ["@", key] |> Enum.join() 196 | end 197 | 198 | defp valid?(nil), do: false 199 | defp valid?([]), do: false 200 | defp valid?(a_map) when is_map(a_map), do: a_map != %{} 201 | defp valid?(_), do: true 202 | end 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Tests](https://github.com/TheRealReal/graphql_client/actions/workflows/ci.yml/badge.svg) 2 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) 3 | 4 | # Graphql Client 5 | 6 | A client-side GraphQL library. 7 | 8 | ## Installation 9 | 10 | Add `graphql_client` to you list of dependencies: 11 | 12 | ```elixir 13 | def deps do 14 | [{:graphql_client, "~> 0.1"}] 15 | end 16 | ``` 17 | 18 | **Creating a backend** 19 | 20 | Now, you need to implement the `GraphQL.Client` behaviour: 21 | 22 | ```elixir 23 | defmodule MyClient do 24 | @behaviour GraphQL.Client 25 | 26 | def execute_query(query, variables, options) do 27 | # your implementation 28 | end 29 | end 30 | ``` 31 | 32 | **Configuring the client** 33 | 34 | In your configuration, set it as your backend: 35 | 36 | ```elixir 37 | config :graphql_client, backend: MyClient 38 | ``` 39 | 40 | Now, any call to `GraphQL.Client` will use the configured backend. 41 | 42 | ## Usage 43 | ### GraphQL as code 44 | 45 | To build queries, you can `import` all functions from `GraphQL.QueryBuilder`. 46 | 47 | A simple query, like this one: 48 | 49 | ```graphql 50 | query User($slug: String! = "*") { 51 | user(slug: $slug) { 52 | id 53 | email 54 | } 55 | } 56 | ``` 57 | 58 | Can be built using the following snippet: 59 | 60 | ```elixir 61 | import GraphQL.QueryBuilder 62 | 63 | user_query = query("User", %{slug: {"String!", "*"}}, [ 64 | field(:user, %{slug: :"$slug"}, [ 65 | field(:id), 66 | field(:email) 67 | ]) 68 | ]) 69 | ``` 70 | 71 | Now, the `user_query` variable contains a _representation_ of this GraphQL operation. If you inspect it, you'll see this: 72 | 73 | ```elixir 74 | %GraphQL.Query{ 75 | fields: [ 76 | %GraphQL.Node{ 77 | alias: nil, 78 | arguments: %{slug: :"$slug"}, 79 | name: :user, 80 | node_type: :field, 81 | nodes: [ 82 | %GraphQL.Node{ 83 | alias: nil, 84 | arguments: nil, 85 | name: :id, 86 | node_type: :field, 87 | nodes: nil, 88 | type: nil 89 | }, 90 | %GraphQL.Node{ 91 | alias: nil, 92 | arguments: nil, 93 | name: :email, 94 | node_type: :field, 95 | nodes: nil, 96 | type: nil 97 | } 98 | ], 99 | type: nil 100 | } 101 | ], 102 | fragments: [], 103 | name: "User", 104 | operation: :query, 105 | variables: [ 106 | %GraphQL.Variable{ 107 | default_value: "*", 108 | name: :slug, 109 | type: "String!" 110 | } 111 | ] 112 | } 113 | ``` 114 | 115 | But most of the time you'll not need to handle this directly. 116 | 117 | ### Executing queries 118 | 119 | To execute this query, you can now call the `GraphQL.Client` and use this query directly: 120 | 121 | ```elixir 122 | GraphQL.Client.execute(user_query, %{slug: "some-user"}) 123 | ``` 124 | 125 | From the POV of the code that it's calling, it doesn't know if this client is using HTTP, smoke signals or magic. 126 | 127 | All you know is that this function will always return a `%GraphQL.Response{}` struct. 128 | 129 | To get the actual text body, you can use `GraphQL.Encoder.encode/1` function: 130 | 131 | ``` 132 | iex> user_query |> GraphQL.Encoder.encode() |> IO.puts() 133 | query User($slug: String! = "*") { 134 | user(slug: $slug) { 135 | id 136 | email 137 | } 138 | } 139 | :ok 140 | ``` 141 | 142 | ### The Query Registry 143 | 144 | The end goal is to merge different queries into one operation and the query registry does exactly that. 145 | 146 | It will accumulate queries, variables and resolvers (yes, resolvers!), merge them, and then execute resolvers with an accumulator. 147 | 148 | ```elixir 149 | user_query = query(...) 150 | product_query = query(...) 151 | 152 | user_resolver = fn response, acc -> 153 | # do something with the response and return the updated accumulator 154 | updated_acc 155 | end 156 | 157 | registry = QueryRegistry.new("BigQuery") 158 | 159 | result = 160 | registry 161 | |> QueryRegistry.add_query(user_query, user_variables,[user_resolver]) 162 | |> QueryRegistry.add_query(product_query, product_variables) 163 | |> QueryRegistry.execute(%{}, options) 164 | 165 | ``` 166 | 167 | A resolver function must accept two parameters: a `%GraphQL.Response{}` struct and the accumulator defined by the query registry. 168 | 169 | ### Testing 170 | 171 | The `%GraphQL.Response{}` is the only thing clients must return, and that we can configure the backend via config files. 172 | 173 | Internally, during tests, the backend will be changed to `LocalBackend`, that uses an Agent process to store responses. 174 | 175 | Call `GraphQL.LocalBackend.start_link/0` on your `test_helper.exs` file. 176 | 177 | Now you can use the `GraphQL.LocalBackend.expect/1` function: 178 | 179 | ```elixir 180 | import GraphQL.LocalBackend, only: [expect: 1] 181 | alias GraphQL.Response 182 | 183 | test "my test" do 184 | my_registry = QueryRegistry.new(...) 185 | response = Response.success(%{field: "value"}) 186 | expect(my_registry, response) 187 | assert 1 == 1 188 | end 189 | ``` 190 | 191 | If you need to inspect and assert the query and variables, you can pass a function: 192 | 193 | ```elixir 194 | import GraphQL.LocalBackend, only: [expect: 1] 195 | alias GraphQL.Response 196 | 197 | test "my test" do 198 | my_registry = QueryRegistry.new(...) 199 | 200 | expect(my_registry, fn query, _variables, _options -> 201 | assert query == expected_query 202 | Response.success(%{field: "value"}) 203 | end) 204 | assert 1 == 1 205 | end 206 | ``` 207 | 208 | ## Code of Conduct 209 | 210 | This project uses Contributor Covenant version 2.1. Check [CODE_OF_CONDUCT.md](/CODE_OF_CONDUCT.md) file for more information. 211 | 212 | ## License 213 | 214 | `graphql_client` source code is released under Apache License 2.0. 215 | 216 | Check [NOTICE](/NOTICE) and [LICENSE](/LICENSE) files for more information. 217 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | opensource@therealreal.com. All complaints will be reviewed and investigated 65 | promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /test/graphql/query_builder_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.QueryBuilderTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias GraphQL.{Node, Query, Variable} 5 | alias GraphQL.QueryBuilder 6 | 7 | doctest QueryBuilder, import: true 8 | 9 | describe "query/4" do 10 | import QueryBuilder 11 | 12 | test "creates a Query struct with name, variables, fields and arguments" do 13 | expected = %Query{ 14 | operation: :query, 15 | name: "TestQuery", 16 | variables: [%Variable{name: "term", type: "String", default_value: "*"}], 17 | fields: [ 18 | Node.field("someField") 19 | ], 20 | fragments: [ 21 | Node.fragment("fragmentFields", "SomeType", [ 22 | Node.field("whateverField") 23 | ]) 24 | ] 25 | } 26 | 27 | generated = 28 | query( 29 | "TestQuery", 30 | %{"term" => {"String", "*"}}, 31 | [ 32 | field("someField") 33 | ], 34 | [ 35 | fragment("fragmentFields", "SomeType", [ 36 | field("whateverField") 37 | ]) 38 | ] 39 | ) 40 | 41 | assert expected == generated 42 | end 43 | end 44 | 45 | describe "mutation/4" do 46 | import QueryBuilder 47 | 48 | test "creates a Query struct with name, variables, fields and arguments" do 49 | expected = %Query{ 50 | operation: :mutation, 51 | name: "TestMutation", 52 | variables: [%Variable{name: "term", type: "String", default_value: "*"}], 53 | fields: [ 54 | Node.field("someField") 55 | ], 56 | fragments: [ 57 | Node.fragment("fragmentFields", "SomeType", [ 58 | Node.field("whateverField") 59 | ]) 60 | ] 61 | } 62 | 63 | generated = 64 | mutation( 65 | "TestMutation", 66 | %{"term" => {"String", "*"}}, 67 | [ 68 | field("someField") 69 | ], 70 | [ 71 | fragment("fragmentFields", "SomeType", [ 72 | field("whateverField") 73 | ]) 74 | ] 75 | ) 76 | 77 | assert expected == generated 78 | end 79 | end 80 | 81 | describe "var/3" do 82 | test "creates a Variable struct" do 83 | expected = %Variable{name: "varName", type: "VarType", default_value: 123} 84 | result = QueryBuilder.var("varName", "VarType", 123) 85 | assert result == expected 86 | end 87 | 88 | test "creates a Variable struct with nil as default_value" do 89 | expected = %Variable{name: "varName", type: "VarType"} 90 | result = QueryBuilder.var("varName", "VarType") 91 | assert result == expected 92 | end 93 | end 94 | 95 | describe "field/3" do 96 | test "creates a simple field Node struct" do 97 | expected = %Node{node_type: :field, name: "price"} 98 | 99 | generated = QueryBuilder.field("price") 100 | 101 | assert expected == generated 102 | end 103 | 104 | test "creates a field Node struct with an alias" do 105 | expected = %Node{ 106 | node_type: :field, 107 | name: "price", 108 | alias: "thePrice" 109 | } 110 | 111 | generated = QueryBuilder.field({"price", "thePrice"}) 112 | 113 | assert expected == generated 114 | end 115 | 116 | test "creates a Node struct with arguments" do 117 | expected = %Node{node_type: :field, name: "price", arguments: %{currency: "USD"}} 118 | 119 | generated = QueryBuilder.field("price", %{currency: "USD"}) 120 | 121 | assert expected == generated 122 | end 123 | 124 | test "creates a Node struct with subfields" do 125 | expected = %Node{ 126 | node_type: :field, 127 | name: "price", 128 | nodes: [ 129 | %Node{node_type: :field, name: "cents"} 130 | ] 131 | } 132 | 133 | generated = 134 | QueryBuilder.field("price", %{}, [ 135 | QueryBuilder.field("cents") 136 | ]) 137 | 138 | assert expected == generated 139 | end 140 | 141 | test "creates a field Node struct with subfields, fragments and arguments" do 142 | expected = %Node{ 143 | node_type: :field, 144 | name: "price", 145 | arguments: %{currency: "USD"}, 146 | nodes: [ 147 | %Node{node_type: :field, name: "cents"}, 148 | %Node{node_type: :fragment_ref, name: "moneyFields"} 149 | ] 150 | } 151 | 152 | generated = 153 | QueryBuilder.field("price", %{currency: "USD"}, [ 154 | QueryBuilder.field("cents"), 155 | QueryBuilder.fragment("moneyFields") 156 | ]) 157 | 158 | assert expected == generated 159 | end 160 | end 161 | 162 | describe "fragment/1" do 163 | test "creates a fragment_ref Node struct" do 164 | expected = %Node{node_type: :fragment_ref, name: "someFields"} 165 | 166 | fragment = QueryBuilder.fragment("someFields") 167 | 168 | assert expected == fragment 169 | end 170 | end 171 | 172 | describe "fragment/3" do 173 | test "creates a fragment Node struct" do 174 | expected = %Node{ 175 | node_type: :fragment, 176 | name: "someFields", 177 | type: "TargetObject", 178 | nodes: [ 179 | %Node{node_type: :field, name: "a_field"} 180 | ] 181 | } 182 | 183 | fragment = QueryBuilder.fragment("someFields", "TargetObject", [Node.field("a_field")]) 184 | 185 | assert expected == fragment 186 | end 187 | end 188 | 189 | describe "inline_fragment/2" do 190 | test "creates an inline fragment Node struct" do 191 | expected = %Node{ 192 | node_type: :inline_fragment, 193 | type: "TargetObject", 194 | nodes: [ 195 | %Node{node_type: :field, name: "a_field"} 196 | ] 197 | } 198 | 199 | fragment = QueryBuilder.inline_fragment("TargetObject", [Node.field("a_field")]) 200 | 201 | assert expected == fragment 202 | end 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /lib/graphql/query_builder.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.QueryBuilder do 2 | @moduledoc """ 3 | Functions to simplify the creation of GraphQL queries. 4 | 5 | The easiest way to use these functions is to `import` this module directly, 6 | this way you'll have all you need to build a query. 7 | 8 | ## Helper functions 9 | 10 | - `query/4` - creates a new "query" operation 11 | - `mutation/4` - creates a new "mutation" operation 12 | - `field/3` - creates a new field (optionals: variables and subfields) 13 | - `fragment/1` - creates a reference to a fragment 14 | - `fragment/3`- creates a fragment 15 | - `inline_fragment/2` - creates an inline fragment 16 | 17 | ## Writing queries and mutations 18 | 19 | As an example, consider the following GraphQL request: 20 | 21 | ``` 22 | query UserQuery($id: Integer = 1) { 23 | user (id: $id) { 24 | id 25 | email 26 | ...personFields 27 | } 28 | } 29 | 30 | fragment personField on Person { 31 | firstName 32 | lastName 33 | } 34 | ``` 35 | 36 | Using the functions in this module, you can create a representation of this 37 | query in this way: 38 | 39 | ``` 40 | q = query("UserQuery", %{id: {"Integer", 1}}, [ 41 | field(:user, %{}, [ 42 | field(:id) 43 | field(:email), 44 | fragment("personFields") 45 | ]) 46 | ], [ 47 | fragment("personFields", "Person", [ 48 | field("firstName"), 49 | field("lastName") 50 | ]) 51 | ]) 52 | ``` 53 | """ 54 | 55 | alias GraphQL.{Node, Query, Variable} 56 | 57 | @doc """ 58 | Creates a new `GraphQL.Query` struct, for a `:query` operation. 59 | """ 60 | @spec query(String.t(), map(), list(), list()) :: Query.t() 61 | def query(name, variables, fields, fragments \\ []) do 62 | build(:query, name, variables, fields, fragments) 63 | end 64 | 65 | @doc """ 66 | Creates a new `GraphQL.Query` struct, for a `:mutation` operation 67 | """ 68 | @spec mutation(String.t(), map(), list(), list()) :: Query.t() 69 | def mutation(name, variables, fields, fragments \\ []) do 70 | build(:mutation, name, variables, fields, fragments) 71 | end 72 | 73 | defp build(operation, name, variables, fields, fragments) do 74 | %Query{ 75 | operation: operation, 76 | name: name, 77 | fields: fields, 78 | fragments: fragments, 79 | variables: parse_variables(variables) 80 | } 81 | end 82 | 83 | @doc """ 84 | Creates a field. 85 | 86 | When rendered, it will have the following body: 87 | 88 | 1. A simple field, no arguments or sub fields 89 | ``` 90 | fieldName 91 | ``` 92 | 93 | 2. A field with an alias 94 | ``` 95 | fieldAlias: fieldName 96 | ``` 97 | 98 | 3. A field with arguments 99 | ``` 100 | fieldName(arg: value) 101 | ``` 102 | 103 | 4. A field with sub fields 104 | ``` 105 | fieldName { 106 | subField 107 | } 108 | ``` 109 | 110 | 5. A field an alias, arguments and sub fields 111 | ``` 112 | fieldAlias: fieldName (arg: value) { 113 | subField 114 | } 115 | ``` 116 | 117 | ## Examples 118 | 119 | iex> field(:some_field) 120 | %GraphQL.Node{node_type: :field, name: :some_field} 121 | 122 | iex> field({:some_field, "fieldAlias"}) 123 | %GraphQL.Node{node_type: :field, name: :some_field, alias: "fieldAlias"} 124 | 125 | iex> field("anotherField", %{}, [field(:id)]) 126 | %GraphQL.Node{node_type: :field, name: "anotherField", nodes: [%GraphQL.Node{node_type: :field, name: :id}]} 127 | 128 | """ 129 | @spec field(Node.name() | Node.name_and_alias(), map(), Keyword.t(Node.t())) :: Node.t() 130 | def field(name, args \\ nil, fields \\ nil, directives \\ nil) do 131 | args = if(args == %{}, do: nil, else: args) 132 | Node.field(name, args, fields, directives) 133 | end 134 | 135 | @doc """ 136 | Creates a `GraphQL.Variable` struct. 137 | """ 138 | @spec var(any(), any(), any()) :: Variable.t() 139 | def var(name, type, value \\ nil) do 140 | %Variable{name: name, type: type, default_value: value} 141 | end 142 | 143 | @spec enum(String.t()) :: {:enum, String.t()} 144 | def enum(name) do 145 | {:enum, name} 146 | end 147 | 148 | @doc """ 149 | Creates a reference to a fragment. Use it inside a field. 150 | 151 | When rendered, it will generate the following body: 152 | 153 | ``` 154 | ...fragmentName 155 | ``` 156 | 157 | ## Examples 158 | 159 | iex> fragment(:fields) 160 | %GraphQL.Node{node_type: :fragment_ref, name: :fields} 161 | """ 162 | @spec fragment(String.t()) :: Node.t() 163 | def fragment(name) do 164 | Node.fragment(name) 165 | end 166 | 167 | @doc """ 168 | Creates a fragment. Use it on the query level. 169 | 170 | 171 | When rendered, it will generate the following body: 172 | 173 | ``` 174 | ... fragmentName on Type { 175 | field1 176 | field2 177 | } 178 | ``` 179 | 180 | ## Examples 181 | 182 | iex> fragment("personFields", "Person", [field(:name)]) 183 | %GraphQL.Node{node_type: :fragment, name: "personFields", type: "Person", nodes: [%GraphQL.Node{node_type: :field, name: :name}]} 184 | """ 185 | @spec fragment(String.t(), String.t(), list()) :: Node.t() 186 | def fragment(name, type, fields) do 187 | Node.fragment(name, type, fields) 188 | end 189 | 190 | @doc """ 191 | Creates an inline fragment. Use it inside a field. 192 | 193 | When rendered, it will generate the following body: 194 | 195 | ``` 196 | ... on Type { 197 | field1 198 | field2 199 | } 200 | ``` 201 | 202 | ## Examples 203 | 204 | iex> inline_fragment("Person", [field(:name)]) 205 | %GraphQL.Node{node_type: :inline_fragment, type: "Person", nodes: [%GraphQL.Node{node_type: :field, name: :name}]} 206 | """ 207 | @spec inline_fragment(String.t(), list()) :: Node.t() 208 | def inline_fragment(type, fields) do 209 | Node.inline_fragment(type, fields) 210 | end 211 | 212 | # Variables 213 | 214 | defp parse_variables(vars) do 215 | Enum.map(vars, &parse_variable/1) 216 | end 217 | 218 | defp parse_variable({name, {type, default}}) do 219 | %Variable{name: name, type: type, default_value: default} 220 | end 221 | 222 | defp parse_variable({name, type}) do 223 | %Variable{name: name, type: type, default_value: nil} 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /test/graphql/query_registry_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.QueryRegistryTest do 2 | use ExUnit.Case 3 | 4 | import GraphQL.LocalBackend, only: [expect: 1] 5 | 6 | import GraphQL.QueryBuilder 7 | 8 | alias GraphQL.{QueryRegistry, Response} 9 | 10 | doctest QueryRegistry, import: true 11 | 12 | @character_query query("CharacterQuery", %{power: "String"}, [ 13 | field(:character, %{power: :"$power"}, [ 14 | field(:id), 15 | field(:name) 16 | ]) 17 | ]) 18 | 19 | @villain_query query("VilainQuery", %{"power" => "String"}, [ 20 | field(:villain, %{power: :"$power"}, [ 21 | field(:id), 22 | field(:name) 23 | ]) 24 | ]) 25 | 26 | @dog_query query("DogQuery", %{breed: "String"}, [ 27 | field(:dogs, %{}, [ 28 | field(:id), 29 | field(:name), 30 | field(:color) 31 | ]) 32 | ]) 33 | 34 | describe "QueryRegistry integration test" do 35 | test "creates, modify and execute a QueryRegistry" do 36 | import QueryRegistry 37 | 38 | registry = new("SuperQuery") 39 | 40 | registry = 41 | registry 42 | |> add_query(@character_query, %{power: "flight"}) 43 | |> add_query(@dog_query) 44 | |> add_resolver(fn _response, acc -> 45 | Map.put(acc, :resolver_result, true) 46 | end) 47 | 48 | expect( 49 | Response.success(%{ 50 | dogs: [ 51 | %{ 52 | id: 456, 53 | name: "Scooby", 54 | color: "Brown" 55 | } 56 | ], 57 | character: %{ 58 | id: 123, 59 | name: "Super Flying Person" 60 | } 61 | }) 62 | ) 63 | 64 | result = execute(registry, %{result: "success"}) 65 | 66 | assert result == {:ok, %{result: "success", resolver_result: true}} 67 | end 68 | 69 | test "returns an error if the same variable is added twice" do 70 | import QueryRegistry 71 | 72 | registry = QueryRegistry.new("Test") 73 | 74 | registry = 75 | registry 76 | |> add_query(@character_query, %{power: "flight"}) 77 | |> add_query(@villain_query, %{power: "invisibility"}) 78 | |> add_resolver(fn _response, acc -> 79 | Map.put(acc, :resolver_result, true) 80 | end) 81 | 82 | expect(Response.success(nil)) 83 | 84 | result = execute(registry, %{result: "ok"}) 85 | 86 | assert result == {:error, "variables declared twice: \"power\""} 87 | end 88 | end 89 | 90 | describe "execute/3" do 91 | test "executes registered queries as fields in a single query and call resolvers" do 92 | test_pid = self() 93 | 94 | resolvers = [ 95 | fn _response, acc -> 96 | send(test_pid, :first_resolver) 97 | [:first_resolver | acc] 98 | end, 99 | fn _response, acc -> 100 | send(test_pid, :second_resolver) 101 | [:second_resolver | acc] 102 | end 103 | ] 104 | 105 | registry = %QueryRegistry{ 106 | name: "BigQuery", 107 | queries: [@character_query, @dog_query], 108 | variables: [ 109 | %{power: "X-Ray Vision"}, 110 | %{breed: "chow chow"} 111 | ], 112 | resolvers: resolvers 113 | } 114 | 115 | expect(fn query, variables, _options -> 116 | expected_query = 117 | query("BigQuery", %{breed: "String", power: "String"}, [ 118 | field(:dogs, %{}, [ 119 | field(:id), 120 | field(:name), 121 | field(:color) 122 | ]), 123 | field(:character, %{power: :"$power"}, [ 124 | field(:id), 125 | field(:name) 126 | ]) 127 | ]) 128 | 129 | assert expected_query.fields == query.fields 130 | assert expected_query.variables == query.variables 131 | assert expected_query.fragments == query.fragments 132 | 133 | assert variables == %{ 134 | power: "X-Ray Vision", 135 | breed: "chow chow" 136 | } 137 | 138 | Response.success(%{ 139 | character: %{ 140 | id: 1, 141 | name: "Saitama" 142 | }, 143 | dog: %{ 144 | id: 123, 145 | name: "Snoopy", 146 | color: "#FFF" 147 | } 148 | }) 149 | end) 150 | 151 | result = QueryRegistry.execute(registry, []) 152 | 153 | assert result == {:ok, [:second_resolver, :first_resolver]} 154 | assert_received :first_resolver 155 | assert_received :second_resolver 156 | end 157 | 158 | test "returns an error tuple when registry is empty" do 159 | registry = QueryRegistry.new("Test") 160 | acc = %{a: 1} 161 | 162 | result = QueryRegistry.execute(registry, acc) 163 | 164 | assert result == {:error, "no queries available"} 165 | end 166 | end 167 | 168 | describe "new/1" do 169 | test "creates a new and empty QueryRegistry" do 170 | assert %QueryRegistry{ 171 | name: "TheQuery", 172 | queries: [], 173 | resolvers: [], 174 | variables: [] 175 | } = QueryRegistry.new("TheQuery") 176 | end 177 | end 178 | 179 | describe "add_query/3" do 180 | test "add a query and variables to an existing QueryRegistry struct" do 181 | registry = %QueryRegistry{ 182 | name: "WebsiteQuery", 183 | queries: [], 184 | resolvers: [], 185 | variables: [] 186 | } 187 | 188 | q1 = 189 | query("userQuery", %{"id" => "Integer"}, [ 190 | field(:user, %{id: :"$id"}, [ 191 | field(:id) 192 | ]) 193 | ]) 194 | 195 | q2 = 196 | query("productQuery", %{}, [ 197 | field(:product, %{}, [ 198 | field(:id) 199 | ]) 200 | ]) 201 | 202 | registry = 203 | registry 204 | |> QueryRegistry.add_query(q1, %{"id" => 123}) 205 | |> QueryRegistry.add_query(q2) 206 | 207 | assert registry.queries == [q2, q1] 208 | assert registry.variables == [%{"id" => 123}] 209 | end 210 | end 211 | 212 | describe "add_resolver/2" do 213 | test "adds a resolver to the internal list of resolvers and keeps the order" do 214 | registry = %QueryRegistry{ 215 | name: "WebsiteQuery", 216 | resolvers: [] 217 | } 218 | 219 | resolver1 = fn _, _ -> 123 end 220 | resolver2 = fn _, _ -> 456 end 221 | 222 | registry = 223 | registry 224 | |> QueryRegistry.add_resolver(resolver1) 225 | |> QueryRegistry.add_resolver(resolver2) 226 | 227 | assert registry.resolvers == [resolver1, resolver2] 228 | end 229 | end 230 | 231 | describe "add_resolvers/2" do 232 | test "adds a resolver list to the internal list of resolvers and keeps the order" do 233 | resolver1 = fn _, _ -> 123 end 234 | resolver2 = fn _, _ -> 456 end 235 | resolver3 = fn _, _ -> 789 end 236 | 237 | registry = %QueryRegistry{ 238 | name: "WebsiteQuery", 239 | resolvers: [resolver1] 240 | } 241 | 242 | registry = QueryRegistry.add_resolvers(registry, [resolver2, resolver3]) 243 | 244 | assert registry.resolvers == [resolver1, resolver2, resolver3] 245 | end 246 | end 247 | end 248 | -------------------------------------------------------------------------------- /lib/graphql/query.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Query do 2 | @moduledoc """ 3 | Functions to create and modify query representations. 4 | """ 5 | alias GraphQL.{Node, Variable} 6 | 7 | @enforce_keys [:operation, :name, :fields] 8 | defstruct [:operation, :name, :fields, :fragments, :variables] 9 | 10 | @typedoc """ 11 | A struct that represents a GraphQL query or mutation. 12 | 13 | The `:operation` field can be `:query`, for a query operation, or `:mutation`, 14 | for a mutation operation. 15 | 16 | The `:name` field is the name of the query or mutation. GraphQL does not 17 | require a name for operations, but this struct will enforce its presence in 18 | order to enrich trace and logging information. 19 | 20 | The `:fields` field is a list of `GraphQL.Node` structs. This the 21 | list of roof fields of a query or mutation. 22 | 23 | The `:fragments` field is also a list of `GraphQL.Node` structs, 24 | but intended to only keep fragment nodes, as they are usually placed after 25 | the root fields in a typical GraphQL query/mutation. 26 | 27 | The `:variables` fields is a list of `GraphQL.Variable` structs, 28 | that represents the expected variables during the request. Note that this list 29 | is the _definition_ of variables, not the _values_ of them. 30 | """ 31 | @type t :: %__MODULE__{ 32 | operation: :query | :mutation, 33 | name: String.t(), 34 | fields: [Node.t()], 35 | fragments: [Node.t()] | nil, 36 | variables: [Variable.t()] | nil 37 | } 38 | 39 | @doc """ 40 | Creates a new query struct for a 'query' operation from a keyword list. 41 | """ 42 | @spec query(Keyword.t()) :: t() 43 | def query(options) do 44 | options = Keyword.put(options, :operation, :query) 45 | struct(__MODULE__, options) 46 | end 47 | 48 | @doc """ 49 | Creates a new query struct for a 'mutation' operation from a keyword list. 50 | """ 51 | @spec mutation(Keyword.t()) :: t() 52 | def mutation(options) do 53 | options = Keyword.put(options, :operation, :mutation) 54 | struct(__MODULE__, options) 55 | end 56 | 57 | @doc """ 58 | Adds a field to a query. 59 | 60 | The `field` argument must be a `GraphQL.Node` struct and its 61 | `:node_type` must be `:field`. 62 | 63 | ## Examples 64 | 65 | iex> f1 = GraphQL.Node.field(:field) 66 | %GraphQL.Node{node_type: :field, name: :field} 67 | iex> f2 = GraphQL.Node.field(:other_field) 68 | %GraphQL.Node{node_type: :field, name: :other_field} 69 | iex> q = %GraphQL.Query{operation: :query, name: "MyQuery", fields: [f1]} 70 | %GraphQL.Query{operation: :query, name: "MyQuery", fields: [f1]} 71 | iex> add_field(q, f2) 72 | %GraphQL.Query{name: "MyQuery", operation: :query, fields: [f2, f1]} 73 | 74 | """ 75 | @spec add_field(t(), Node.t()) :: t() 76 | def add_field(%__MODULE__{fields: fields} = query, %Node{node_type: :field} = field) do 77 | fields = if(fields == nil, do: [], else: fields) 78 | %__MODULE__{query | fields: [field | fields]} 79 | end 80 | 81 | @doc """ 82 | Adds a fragment to a query. 83 | 84 | The `field` argument must be a `GraphQL.Node` struct and its 85 | `:node_type` must be `:field`. 86 | 87 | ## Examples 88 | 89 | iex> f1 = GraphQL.Node.fragment("personFields", "Person", [GraphQL.Node.field(:field)]) 90 | %GraphQL.Node{node_type: :fragment, name: "personFields", type: "Person", nodes: [%GraphQL.Node{node_type: :field, name: :field}]} 91 | iex> f2 = GraphQL.Node.fragment("userFields", "User", [GraphQL.Node.field(:another_field)]) 92 | %GraphQL.Node{node_type: :fragment, name: "userFields", type: "User", nodes: [%GraphQL.Node{node_type: :field, name: :another_field}]} 93 | iex> q = %GraphQL.Query{operation: :query, name: "MyQuery", fields: [], fragments: [f1]} 94 | %GraphQL.Query{operation: :query, name: "MyQuery", fields: [], fragments: [f1]} 95 | iex> add_fragment(q, f2) 96 | %GraphQL.Query{name: "MyQuery", operation: :query, fields: [], fragments: [f2, f1]} 97 | 98 | """ 99 | @spec add_fragment(t(), Node.t()) :: t() 100 | def add_fragment( 101 | %__MODULE__{fragments: fragments} = query, 102 | %Node{node_type: :fragment} = fragment 103 | ) do 104 | fragments = if(fragments == nil, do: [], else: fragments) 105 | %__MODULE__{query | fragments: [fragment | fragments]} 106 | end 107 | 108 | @doc """ 109 | Add a new variable to an existing query 110 | 111 | ## Examples 112 | 113 | iex> v1 = %GraphQL.Variable{name: "id", type: "Integer"} 114 | %GraphQL.Variable{name: "id", type: "Integer"} 115 | iex> v2 = %GraphQL.Variable{name: "slug", type: "String"} 116 | %GraphQL.Variable{name: "slug", type: "String"} 117 | iex> q = %GraphQL.Query{operation: :query, name: "MyQuery", fields: [], variables: [v1]} 118 | %GraphQL.Query{operation: :query, name: "MyQuery", fields: [], variables: [v1]} 119 | iex> add_variable(q, v2) 120 | %GraphQL.Query{operation: :query, name: "MyQuery", fields: [], variables: [v2, v1]} 121 | """ 122 | @spec add_variable(t(), Variable.t()) :: t() 123 | def add_variable(%__MODULE__{variables: variables} = query, %Variable{} = variable) do 124 | variables = if(variables == nil, do: [], else: variables) 125 | %__MODULE__{query | variables: [variable | variables]} 126 | end 127 | 128 | @doc """ 129 | Combine two queries into one query, merging fields, variables and fragments. 130 | 131 | The two queries must have the same operation. 132 | """ 133 | @spec merge(t(), t(), String.t()) :: {:ok, t()} | {:error, any()} 134 | def merge( 135 | %__MODULE__{operation: operation} = query_a, 136 | %__MODULE__{operation: operation} = query_b, 137 | name 138 | ) do 139 | with {:ok, variables} <- merge_variables(query_a.variables || [], query_b.variables || []) do 140 | {:ok, 141 | %__MODULE__{ 142 | name: name, 143 | operation: operation, 144 | fields: (query_a.fields || []) ++ (query_b.fields || []), 145 | fragments: (query_a.fragments || []) ++ (query_b.fragments || []), 146 | variables: variables 147 | }} 148 | else 149 | error -> error 150 | end 151 | end 152 | 153 | defp merge_variables(set_a, set_b) do 154 | repeated_vars = 155 | for v_a <- set_a, v_b <- set_b, reduce: [] do 156 | acc -> 157 | if GraphQL.Variable.same?(v_a, v_b) do 158 | [v_a | acc] 159 | else 160 | acc 161 | end 162 | end 163 | 164 | case repeated_vars do 165 | [] -> 166 | {:ok, set_a ++ set_b} 167 | 168 | _ -> 169 | var_names = 170 | repeated_vars 171 | |> Enum.map(&"\"#{&1.name}\"") 172 | |> Enum.join(", ") 173 | 174 | {:error, "variables declared twice: #{var_names}"} 175 | end 176 | end 177 | 178 | @doc """ 179 | Combines a list of queries into one query, merging fields, variables and fragments. 180 | 181 | All queries must have the same operation. 182 | """ 183 | @spec merge_many([t()], String.t()) :: {:ok, t()} | {:error, any()} 184 | def merge_many(queries, name \\ nil) 185 | 186 | def merge_many([%__MODULE__{} = query], name) do 187 | if name != nil do 188 | {:ok, %__MODULE__{query | name: name}} 189 | else 190 | {:ok, query} 191 | end 192 | end 193 | 194 | def merge_many([first_query | remaining_queries], name) do 195 | result = 196 | Enum.reduce_while(remaining_queries, first_query, fn query, result -> 197 | case merge(query, result, name) do 198 | {:ok, merged_query} -> 199 | {:cont, merged_query} 200 | 201 | {:error, error} -> 202 | {:halt, {:error, error}} 203 | end 204 | end) 205 | 206 | case result do 207 | %__MODULE__{} = query -> {:ok, query} 208 | error -> error 209 | end 210 | end 211 | end 212 | -------------------------------------------------------------------------------- /lib/graphql/node.ex: -------------------------------------------------------------------------------- 1 | defmodule GraphQL.Node do 2 | @moduledoc """ 3 | Functions to create all different types of nodes of a GraphQL operation. 4 | 5 | Usually, this module should not be used directly, since it is easier to use 6 | the function from `GraphQL.QueryBuilder`. 7 | """ 8 | @enforce_keys [:node_type] 9 | defstruct node_type: nil, 10 | name: nil, 11 | alias: nil, 12 | type: nil, 13 | arguments: nil, 14 | nodes: nil, 15 | directives: nil 16 | 17 | @typedoc """ 18 | The GraphQL query element that this node represents. 19 | 20 | The four node types are: 21 | - field: a single field of a GraphQL schema, may have arguments and other nodes 22 | - fragment_ref: a reference to a fragment, used inside fields to import fragment fields 23 | - fragment: a fragment definition, with name, type and fields 24 | - inline_fragment: much like a fragment, but being inline, it does not need a name 25 | """ 26 | @type node_type :: :field | :fragment_ref | :fragment | :inline_fragment 27 | 28 | @typedoc """ 29 | A GraphQL identifier that is not a GraphQL keyword (like mutation, query and fragment) 30 | 31 | Used to identify fields, aliases and fragments. 32 | """ 33 | @type name :: String.t() | atom() 34 | 35 | @typedoc """ 36 | A two-element tuple where the first position is the name of the field and the 37 | second element is the alias of the field. 38 | """ 39 | @type name_and_alias :: {name(), name()} 40 | 41 | @typedoc """ 42 | A struct representing a GraphQL operation node. 43 | 44 | A %Node{} struct can be represent a field, a fragment, an inline fragment or a 45 | fragment reference, identified by the `:node_type` field. 46 | 47 | The `name` represents how this node is identified within the GraphQL operation. 48 | 49 | The `alias` is only used when the `:node_type` is `:field`, and as the name 50 | suggests, represents the alias of the field's name. 51 | 52 | The `arguments` is a map with all the arguments used by a node, and it's only 53 | valid when thew `:node_type` is `:field`. 54 | 55 | The `type` is only used when `:node_type` is `:fragment` or `:inline_fragment`, 56 | and represents the GraphQL type of the fragment. 57 | 58 | The `nodes` is a list of child nodes, that can used to query for complex 59 | objects. 60 | 61 | The `directives` field is an enum with all the graphQL directives to be 62 | applied on a node node. 63 | """ 64 | @type t :: %__MODULE__{ 65 | node_type: node_type(), 66 | name: name(), 67 | alias: name(), 68 | type: String.t(), 69 | arguments: map() | Keyword.t(), 70 | nodes: [t()], 71 | directives: map() | Keyword.t() 72 | } 73 | 74 | @doc """ 75 | Creates a simple field, with no arguments or sub nodes. 76 | 77 | The `name` parameter can be an atom or string, or a two-element tuple with 78 | atoms or strings, where the first element is the actual name of the field and 79 | the second element is the alias of the field. 80 | 81 | ## GraphQL example 82 | 83 | A query with a simple field inside another field: 84 | ``` 85 | query { 86 | user { 87 | id <---- Simple field 88 | } 89 | } 90 | ``` 91 | 92 | A query with a simple field with an alias: 93 | ``` 94 | query { 95 | user { 96 | theId: id <---- Simple field with alias 97 | } 98 | } 99 | ``` 100 | 101 | ## Examples 102 | 103 | iex> field(:my_field) 104 | %GraphQL.Node{node_type: :field, name: :my_field} 105 | 106 | iex> field({:my_field, "field_alias"}) 107 | %GraphQL.Node{node_type: :field, name: :my_field, alias: "field_alias"} 108 | """ 109 | @spec field(name() | name_and_alias()) :: t() 110 | def field(name_spec) 111 | 112 | def field({name, an_alias}) do 113 | %__MODULE__{ 114 | node_type: :field, 115 | name: name, 116 | alias: an_alias 117 | } 118 | end 119 | 120 | def field(name) do 121 | %__MODULE__{ 122 | node_type: :field, 123 | name: name 124 | } 125 | end 126 | 127 | @doc """ 128 | Creates a field with arguments and sub nodes. 129 | 130 | The `name` parameter can be an atom or string, or a two-element tuple with 131 | atoms or strings, where the first element is the actual name of the field and 132 | the second element is the alias of the field. 133 | 134 | The `arguments` parameter is a map. 135 | 136 | The `nodes` argument is a list of `%GraphQL.Node{}` structs. 137 | 138 | ## GraphQL Example 139 | 140 | A query with a field that has arguments, an alias and subfields 141 | 142 | ``` 143 | query { 144 | someObject: object(slug: "the-object") { <----- Field with an alias and arguments 145 | field <----- Sub field 146 | anotherField <----- Sub field 147 | } 148 | } 149 | ``` 150 | 151 | ## Examples 152 | 153 | iex> field(:my_field, %{id: "id"}, [ field(:subfield) ] ) 154 | %GraphQL.Node{node_type: :field, name: :my_field, arguments: %{id: "id"}, nodes: [%GraphQL.Node{node_type: :field, name: :subfield}]} 155 | 156 | iex> field({:my_field, "field_alias"}, %{id: "id"}, [ field(:subfield) ] ) 157 | %GraphQL.Node{node_type: :field, name: :my_field, alias: "field_alias", arguments: %{id: "id"}, nodes: [%GraphQL.Node{node_type: :field, name: :subfield}]} 158 | """ 159 | @spec field(name() | name_and_alias(), map(), [t()], [any()]) :: t() 160 | def field(name_spec, arguments, nodes, directives \\ nil) 161 | 162 | def field({name, an_alias}, arguments, nodes, directives) do 163 | %__MODULE__{ 164 | node_type: :field, 165 | name: name, 166 | alias: an_alias, 167 | arguments: arguments, 168 | nodes: nodes, 169 | directives: directives 170 | } 171 | end 172 | 173 | def field(name, arguments, nodes, directives) do 174 | %__MODULE__{ 175 | node_type: :field, 176 | name: name, 177 | arguments: arguments, 178 | nodes: nodes, 179 | directives: directives 180 | } 181 | end 182 | 183 | @doc """ 184 | Creates a reference to a fragment. 185 | 186 | A fragment reference is used inside a field to import the fields of a fragment. 187 | 188 | ## GraphQL Example 189 | 190 | ``` 191 | query { 192 | object { 193 | ...fieldsFromFragment <----- Fragment Reference 194 | } 195 | } 196 | ``` 197 | 198 | ## Examples 199 | 200 | iex> fragment("myFields") 201 | %GraphQL.Node{node_type: :fragment_ref, name: "myFields"} 202 | 203 | """ 204 | @spec fragment(name()) :: t() 205 | def fragment(name) do 206 | %__MODULE__{ 207 | node_type: :fragment_ref, 208 | name: name 209 | } 210 | end 211 | 212 | @doc """ 213 | Creates a fragment. 214 | 215 | A fragment is used to share fields between other fields 216 | 217 | ## GraphQL Example 218 | 219 | ``` 220 | query { 221 | object { 222 | ...fieldsFromFragment 223 | } 224 | } 225 | 226 | fragment fieldsFromFragment on Type { <------ Fragment 227 | field1 228 | field2 229 | } 230 | ``` 231 | 232 | ## Examples 233 | 234 | iex> fragment("myFields", "SomeType", [field(:field)]) 235 | %GraphQL.Node{node_type: :fragment, name: "myFields", type: "SomeType", nodes: [%GraphQL.Node{node_type: :field, name: :field}]} 236 | 237 | """ 238 | @spec fragment(name(), name(), [t()]) :: t() 239 | def fragment(name, type, fields) do 240 | %__MODULE__{ 241 | node_type: :fragment, 242 | name: name, 243 | type: type, 244 | nodes: fields 245 | } 246 | end 247 | 248 | @doc """ 249 | Creates an inline fragment. 250 | 251 | An inline fragment is used to conditionally add fields on another field depending 252 | on its type 253 | 254 | 255 | ## GraphQL Example 256 | 257 | ``` 258 | query { 259 | object { 260 | ... on Type { <------ Inline Fragment 261 | field1 262 | field2 263 | } 264 | } 265 | } 266 | 267 | ``` 268 | 269 | ## Examples 270 | 271 | iex> inline_fragment("SomeType", [field(:field)]) 272 | %GraphQL.Node{node_type: :inline_fragment, type: "SomeType", nodes: [%GraphQL.Node{node_type: :field, name: :field}]} 273 | 274 | """ 275 | @spec inline_fragment(name(), [t()]) :: t() 276 | def inline_fragment(type, fields) do 277 | %__MODULE__{ 278 | node_type: :inline_fragment, 279 | type: type, 280 | nodes: fields 281 | } 282 | end 283 | end 284 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright (c) 2021, TheRealReal. 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | --------------------------------------------------------------------------------