Better way to manage postgresql JSONB data in absinthe-graphql

hi everyone so far so good with graphql and Absinthe. Its getting bit tricky with some data structs
here is the example of schema

     object :uploads do  
           field :links, list_of(:links_types)
     end

the links_types looks like

     object :links_types do
          field  :url,  :string
          field  :format, :string
      end    

so the data structure that is fetched from postgresql looks this
%{id:1, links: %{data: [ %{"url" => "xyz.com", "format" => "format1" }, %{ "url": "abc.com", format => "format2"]}}

i managed to remove data key from links map , so now links: is list of maps as we described in graphql schema still i get null result

sample query

          id,
           links{
             url
            }
       }

result is

        id:1,
        links: [ {url: null}, {url: null}]
  }

I think i am making some mistake here or there is something better way to define schema. thanks in advance

1 Like

Hey there!

Can you elaborate a bit on your data? Normally if you use a jsonb column the keys are strings not atoms. I’m also confused by the single quotes, are you intentionally storing char lists instead of strings?

You’ll likely just need a small inline resolution function on your links field, but in order to suggest one I need to understand the shape of your data better.

1 Like

@benwilson512 thanks for quick reply. I think the problem is my data structure was map with arrow
x = %{"a" => "xyz"}
I thing in the absinthe library we access map as x.a which gave null i think. idn i maybe wrong.

i was thinking to convert arrow map into atoms is there better way to solve this. like some config to access with the arrow maps as well? x["a"]

1 Like

finally solved with own helper
```

Enum.map(list, fn(x)->
   for {key, val} <- x, into: %{}, do: {String.to_existing_atom(key), val}
end)
```

thanks. still wondering is there better solution :slight_smile:

1 Like

First off, some elixir terminology. All elixir maps are “arrow maps”. %{foo: 1} is exactly the same as %{:foo => 1}. If the key is an atom you can write it in the %{foo: 1} form, but it’s just a visual change.

%{:foo => 1} is a map with the atom key :foo.
%{"foo" => 1} is a map with the string key "foo".

What you’re trying to figure out is how to get values out of a map with string keys. The answer does not need to be converting them to atoms.

You haven’t really supplied enough of your code for me to tell you exactly where to put stuff. For example you just said you created a helper, but you didn’t say where you put that code or what values you gave it (what is list?) so it doesn’t help us. Help us to help you, give us the full context.

The basic way to use string keys is as follows:

query do
  field :thing, :thing_with_string_keys do
    {:ok, %{"name" => "ben", "age" => 25}}
  end
end

object :thing_with_string_keys do
  field :name, :string, resolve: key("name")
  field :age, :integer, resolve: key("name")
end

def key(key_name) do
  fn thing, _, _ ->
    {:ok, Map.get(thing, key_name}
  end
end
5 Likes

As a final note, please don’t try to split the conversation. It makes it harder to help you.

1 Like

thanks got it and sorry :slight_smile: cheers finally able to work this out. Thanks again

1 Like

Since you are looking for a general idea, here’s how I tackled a similar problem. I like Ben’s solution with the helper.

My data structure is different, and Ben’s solution is better, but it may help to see nonetheless.

I have a jsonb column with permissions, e.g.

{"add":[], "roles": ["employee", "manager"], "exclude":[]}

…

  object :permissions do
   field :add, list_of(:string) do
     resolve(fn(%{"add" => add},_,_) ->
       {:ok, add}
     end)
   end
   field :exclude, list_of(:string) do
     resolve(fn(%{"exclude" => exclude },_,_) ->
       {:ok, exclude}
     end)
   end
   field :roles, list_of(:string) do
     resolve(fn(%{"roles" => roles},_,_) ->
       {:ok, roles}
     end)
   end
  end
1 Like

this is just what we needed, thank you for the solution @benwilson512

This isn’t what you’re asking, but it is relevant to the title of the thread, so for the benefit of others visiting it, here’s how I handle JSONB fields in Absinthe where the fields are unknown ahead of time (you’ll notice some custom handling for GeoJSON included):

@desc "A JSON document containing more info beyond specified fields"
field(:extra_info, :json)

which relies on this scalar type:

defmodule MyAPI.GraphQL.JSON do
  @moduledoc """
  The Json scalar type allows arbitrary JSON values to be passed in and out.
  Requires `{ :jason, "~> 1.1" }` package: https://github.com/michalmuskala/jason
  """
  defstruct data: nil, normalized: nil, raw: nil, errors: [], flags: %{}, open_ended: false
  alias Absinthe.Blueprint.Input
  use Absinthe.Schema.Notation
  require Protocol

  scalar :json, name: "Json" do
    description("Arbitrary json stored as a string")
    serialize(&encode/1)
    parse(&decode/1)
  end

  @spec decode(Input.String.t()) :: {:ok, term} | {:error, term}
  @spec decode(Input.Null.t()) :: {:ok, nil}
  defp decode(%Input.String{value: value}) do
    try do
      {:ok, Jason.decode!(value)}
    rescue e ->
      :error
    end
  end
  defp decode(%Input.Null{}), do: {:ok, nil}
  defp decode(_), do: :error

  defp encode(%Geo.Point{} = geo) do
    with {:ok, geo_json} <- Geo.JSON.encode(geo) do
      geo_json
    end
  end

  defp encode(value) when is_struct(value) do
    value
  end

  defp encode(value), do: value
end

Edit: Re-reading it now, I see several ways it can be improved, but it’s worked pretty well for us so far.