|
| 1 | +defmodule ShopifyAPI.GraphQL.GraphQLQuery do |
| 2 | + @moduledoc """ |
| 3 | + A quiery builder for Shopify GraphQL |
| 4 | +
|
| 5 | + The query is made with a scope that implements `ShopifyAPI.Scope` |
| 6 | +
|
| 7 | + In your query file `use ShopifyAPI.GraphQL.GraphQLQuery` and implement |
| 8 | + - query_string/1 |
| 9 | + - name/1 - matches the root name of the query |
| 10 | + - path/1 - a list of access functions for the returning data. |
| 11 | +
|
| 12 | + ```elixir |
| 13 | + defmodule MyApp.Shopify.Query.ThemeList do |
| 14 | + use ShopifyAPI.GraphQL.GraphQLQuery |
| 15 | +
|
| 16 | + @theme_list ~S[ |
| 17 | + query { |
| 18 | + themes(first: 20) { |
| 19 | + edges { |
| 20 | + node { |
| 21 | + name |
| 22 | + id |
| 23 | + role |
| 24 | + } |
| 25 | + } |
| 26 | + } |
| 27 | + } |
| 28 | + ] |
| 29 | +
|
| 30 | + def query_string, do: @theme_list |
| 31 | + def name, do: "themes" |
| 32 | + def path, do: ["edges", Access.all(), "node"] |
| 33 | + end |
| 34 | + ``` |
| 35 | +
|
| 36 | + ```elixir |
| 37 | + def list_themes(%Model.Scope{} = scope, variables) do |
| 38 | + Query.ThemeList.query() |
| 39 | + |> Query.ThemeList.assigns(variables) |
| 40 | + |> Query.ThemeList.execute(scope) |
| 41 | + |> GraphQLResponse.resolve() |
| 42 | + end |
| 43 | + ``` |
| 44 | + """ |
| 45 | + |
| 46 | + defstruct [:name, :query_string, :variables, :path] |
| 47 | + |
| 48 | + alias ShopifyAPI.GraphQL.GraphQLResponse |
| 49 | + |
| 50 | + @type t :: %__MODULE__{ |
| 51 | + name: String.t(), |
| 52 | + query_string: String.t(), |
| 53 | + variables: map(), |
| 54 | + path: [term()] |
| 55 | + } |
| 56 | + |
| 57 | + @callback query_string() :: String.t() |
| 58 | + @callback name() :: String.t() |
| 59 | + @callback path() :: list() |
| 60 | + |
| 61 | + defmacro __using__(_opts) do |
| 62 | + quote do |
| 63 | + @behaviour unquote(__MODULE__) |
| 64 | + |
| 65 | + @type t :: unquote(__MODULE__).t() |
| 66 | + |
| 67 | + @spec query :: t() |
| 68 | + def query do |
| 69 | + query_string() |
| 70 | + |> unquote(__MODULE__).build(name()) |
| 71 | + |> append_path(path()) |
| 72 | + end |
| 73 | + |
| 74 | + defdelegate assign(query, key, value), to: unquote(__MODULE__) |
| 75 | + defdelegate assigns(query, map), to: unquote(__MODULE__) |
| 76 | + defdelegate append_path(query, access), to: unquote(__MODULE__) |
| 77 | + defdelegate execute(query, scope), to: unquote(__MODULE__) |
| 78 | + end |
| 79 | + end |
| 80 | + |
| 81 | + def build(query_string, name) do |
| 82 | + %__MODULE__{ |
| 83 | + name: name, |
| 84 | + query_string: query_string, |
| 85 | + variables: %{}, |
| 86 | + path: [name] |
| 87 | + } |
| 88 | + end |
| 89 | + |
| 90 | + @spec append_path(t(), any()) :: t() |
| 91 | + def append_path(%__MODULE__{} = query, access), |
| 92 | + do: %{query | path: query.path ++ List.wrap(access)} |
| 93 | + |
| 94 | + @spec assign(t(), any(), any()) :: t() |
| 95 | + def assign(%__MODULE__{} = query, key, value), do: assigns(query, %{key => value}) |
| 96 | + |
| 97 | + @spec assigns(t(), map()) :: t() |
| 98 | + def assigns(%__MODULE__{} = query, map) when is_map(map), |
| 99 | + do: %{query | variables: Map.merge(query.variables, map)} |
| 100 | + |
| 101 | + @spec execute(t(), ShopifyAPI.Scope.t()) :: |
| 102 | + {:ok, GraphQLResponse.success_t()} |
| 103 | + | {:ok, GraphQLResponse.failure_t()} |
| 104 | + | {:error, Exception.t()} |
| 105 | + def execute(query, scope), do: ShopifyAPI.GraphQL.execute(query, scope) |
| 106 | + |
| 107 | + @doc """ |
| 108 | + Returns a function that accesses the key/value paths as a map. |
| 109 | +
|
| 110 | + ## Examples |
| 111 | + iex> get_in( |
| 112 | + ...> %{"nodes" => [ |
| 113 | + ...> %{"filename" => "file1", "body" => %{"content" => "file1 content"}}, |
| 114 | + ...> %{"filename" => "file2", "body" => %{"content" => "file2 content"}} |
| 115 | + ...> ]}, |
| 116 | + ...> ["nodes", GraphQLQuery.access_map(["filename"], ["body", "content"])] |
| 117 | + ...> ) |
| 118 | + %{"file1" => "file1 content", "file2" => "file2 content"} |
| 119 | + """ |
| 120 | + @spec access_map(term, term) :: Access.access_fun(data :: map, current_value :: term) |
| 121 | + def access_map(key, value) do |
| 122 | + fn |
| 123 | + :get, data, next when is_list(data) -> |
| 124 | + next.(Map.new(data, &{get_in(&1, key), get_in(&1, value)})) |
| 125 | + |
| 126 | + :get, data, next -> |
| 127 | + next.(%{get_in(data, key) => get_in(data, value)}) |
| 128 | + |
| 129 | + :get_and_update, _data, _next -> |
| 130 | + raise "access_map not implemented for get_and_update" |
| 131 | + end |
| 132 | + end |
| 133 | +end |
0 commit comments