Absinthe and storing JSONB

I’m building a React app that uses Draft.js extensively for it’s editable content.

I’m already using Absinthe with Phoenix and Apollo on the client side. It works wonderfully with the exception of the draft.js content bits. I want to store the content as JSONB fields in postgres. Converting the editable content to some other format would lose quite a lot of information so storing the whole object makes sense in this case.

Doing this isn’t possible with Absinthe right now. There’s isn’t a way of passing an object like the content object below through.

%{"content" => %{"blocks" => [%{"data" => %{}, "depth" => 0, "entityRanges" => [], "inlineStyleRanges" => [], "key" => "bec7t", "text" => "Hello", "type" => "unstyled"}], "entityMap" => %{}}}

I’ve looked around and this has been brought up before:

https://github.com/absinthe-graphql/absinthe/issues/206
https://github.com/absinthe-graphql/absinthe/issues/267

In both cases it has been denied but I was hoping someone around here might have figured out a way around this or just some tips about how I could approach it.

I know the graphql spec doesn’t allow for this but specs are meant for breaking ;). Besides Apollo has already solved this http://dev.apollodata.com/tools/graphql-tools/scalars.html and I think it’s quite useful.

Hey there!

I think you’re misunderstanding the answer in each of those issues. The second issue is largely unrelated, as it’s essentially just a matter of how the syntax of custom scalars works broadly. In the first issue all we’re saying is that we aren’t going to include the custom scalar type by default, not that it’s impossible to have one.

In fact this first issue supplies a code snippet that you can just add to your schema to get the result you want:

scalar :json do
  parse fn input ->
    case Poison.decode(input.value) do
      {:ok, result} -> result
      _ -> :error
    end
  end

  serialize &Poison.encode!/1
end

The only caveat here is that Absinthe expects that you’ll send this JSON blob as an actual string since the idea is that it’s treated as an opaque blob as far as Absinthe is concerned. It’ll get decoded inside the parse function above and then it’s just a regular map w/ string keys that you can do with internally as you wish.

2 Likes

Hi Ben!

Thanks for the swift response. First of all apologies for my poorly worded query. I was having a braindead Monday and my social/communications skills weren’t functioning well.

I realise now it’s Tuesday that saying something was denied reads a bit negatively. I didn’t mean it to, it’s just that my social skills were glitching. I’m actually an Absinthe fan and you can’t add every feature asked about in GitHub issues, your lib would become monstrous.

I had actually used exactly that snippet as you described to create a custom json scalar but I was getting errors that were to do with Absinthe.Phase.Document.Arguments.Parse not liking the shape of what the custom json scalar was returning. I thought it was because it didn’t like the value being a map but I’ll dig in again and figure it out. Thanks for pointing me in the right direction.

I’ll post a solution when I find it, in case someone else is Googling for a similar problem.

Ah, if you provide a stack trace I’d be more than happy to help you get it sorted out!

Hey Ben,

Sorry for the very slow response and thanks for offering to have a look. I think we’re in quite different timezones and I got completely wrapped up in Javascriptland yesterday. The more I can do things with GraphQL the happier I will be.

Here’s what I was trying to do with this mutation

mutation($content: Json!) {
    createLetter(content: $content) {
    	content
    }
  }

Query variables:

{
  "content": "{\"entityMap\":{},\"blocks\":[{\"type\":\"unstyled\",\"text\":\"Hello\",\"key\":\"bec7t\",\"inlineStyleRanges\":[],\"entityRanges\":[],\"depth\":0,\"data\":{}}]}"
}

This throws

[info] Sent 500 in 69ms
[error] #PID<0.812.0> running MyApp.Web.Endpoint terminated
Server: localhost:4000 (http)
Request: POST /graphiql
** (exit) an exception was raised:
    ** (CaseClauseError) no case clause matching: %{"blocks" => [%{"data" => %{}, "depth" => 0, "entityRanges" => [], "inlineStyleRanges" => [], "key" => "bec7t", "text" => "Hello", "type" => "unstyled"}], "entityMap" => %{}}
        (absinthe) lib/absinthe/phase/document/arguments/parse.ex:35: Absinthe.Phase.Document.Arguments.Parse.build_value/3
        (absinthe) lib/absinthe/phase/document/arguments/parse.ex:23: Absinthe.Phase.Document.Arguments.Parse.handle_node/2
        (absinthe) lib/absinthe/blueprint/transform.ex:14: anonymous fn/3 in Absinthe.Blueprint.Transform.prewalk/2
        (absinthe) lib/absinthe/blueprint/transform.ex:109: Absinthe.Blueprint.Transform.node_with_children/5
        (absinthe) lib/absinthe/blueprint/transform.ex:123: anonymous fn/4 in Absinthe.Blueprint.Transform.walk_children/5
        (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3
        (absinthe) lib/absinthe/blueprint/transform.ex:113: Absinthe.Blueprint.Transform.node_with_children/5
        (elixir) lib/enum.ex:1325: Enum."-map_reduce/3-lists^mapfoldl/2-0-"/3
        (absinthe) lib/absinthe/blueprint/transform.ex:123: anonymous fn/4 in Absinthe.Blueprint.Transform.walk_children/5
        (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3
        (absinthe) lib/absinthe/blueprint/transform.ex:113: Absinthe.Blueprint.Transform.node_with_children/5
        (elixir) lib/enum.ex:1325: Enum."-map_reduce/3-lists^mapfoldl/2-0-"/3
        (absinthe) lib/absinthe/blueprint/transform.ex:123: anonymous fn/4 in Absinthe.Blueprint.Transform.walk_children/5
        (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3
        (absinthe) lib/absinthe/blueprint/transform.ex:113: Absinthe.Blueprint.Transform.node_with_children/5
        (elixir) lib/enum.ex:1325: Enum."-map_reduce/3-lists^mapfoldl/2-0-"/3
        (absinthe) lib/absinthe/blueprint/transform.ex:123: anonymous fn/4 in Absinthe.Blueprint.Transform.walk_children/5
        (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3
        (absinthe) lib/absinthe/blueprint/transform.ex:113: Absinthe.Blueprint.Transform.node_with_children/5
        (absinthe) lib/absinthe/blueprint/transform.ex:13: Absinthe.Blueprint.Transform.prewalk/2

I’d added the custom scalar type just like the snippet you mentioned and I thought it was returning an :ok tupple that Parse.build_value would be expecting with but perhaps not

  scalar :json, description: "JSON field type in postgres" do
    parse fn input ->
      case Poison.decode(input.value) do
        {:ok, result} -> result
        _ -> :error
      end
    end

    serialize &Poison.encode!/1
  end
1 Like

Ah, parse functions are supposed to return either {:ok, result} | :error. I’ll make a note to create a more specific exception.

1 Like

Ah! Thank you Ben! That was really easy in the end. I should have picked up on it but staring at it wasn’t helping. So this just works now.

  scalar :json, description: "JSON field type in postgres" do
    parse fn input ->
      case Poison.decode(input.value) do
        {:ok, result} -> {:ok, result}
        _ -> :error
      end
    end

    serialize &Poison.encode!/1
  end

Thank you. That’s less api endpoints and redux reducers for me.

2 Likes