Mapping database primary keys to opaque IDs for graphql in Absinthe

I’m building a graphql api using Absinthe and Ecto.

Most of my models use integer primary keys. In graphql there is the built in ID scalar type that’s typically used as an identifier for types.

For the most part many of my graphql types map closely to Ecto models. Currently I’m just using the database PK for each type’s id field and specified it as the ID type.

I’ve read that it’s a good idea to use “opaque IDs” for graphql types. A common approach is base64 encode the name of the type along with some ID. E.g. Base.encode64("User:25").

Say I wanted to do something like this what’s the best approach with Absinthe/Ecto?

The naive solution seems to be just base64 encode/decode in query and mutation resolvers. But this can get pretty repetetive when applied to loads of resolvers.

Another approach I’ve considered is defining a custom scalar type like OpaqueID and defining serialize and parse functions for it. E.g.

defmodule MyApp.Graphql.Types.OpaqueID do
  use Absinthe.Schema.Notation

  scalar :opaque_id, name: "OpaqueID" do
    serialize(&encode_opaque_id/1)
    parse(&decode_opaque_id/1)
  end

  defp encode_opaque_id(v) do
    "#{v}"
    |> Base.encode64(padding: false)
  end

  defp decode_opaque_id(%Absinthe.Blueprint.Input.String{value: value}) do
    Base.decode64(value, padding: false)
  end

  defp decode_opaque_id(%Absinthe.Blueprint.Input.Null{}) do
    {:ok, nil}
  end

  defp decode_opaque_id(_) do
    :error
  end
end

I can then just state that the id field of my graphql entity is :opaque_id.

This eliminates the repetition problem but involves using a custom scalar type for something that looks like the ID type can handle just fine.

I also only have access to the numeric ID returned from the resolver here so I can’t include the entity name (e.g. User) in the identifier, making it not very opaque.

Are there any other approaches?

1 Like

Hello and welcome to thee forum,

Did You consider adding Absinthe.Relay.Node?

It adds an opaque global ID.

2 Likes

Thanks for the welcome.

I’m not using Relay on the client and I’m also not using the node pattern so I don’t think this is the most appropriate approach. Also seems slightly overkill to use this library just for the purpose of changing ID format. Thanks for the suggestion though

We aren’t using Relay either but the node convention and node IDs are really quite useful. A top level node field saves you from having 50 different top level fields to power various “show” pages. The Node.ParseIDs middleware will handle turning the base64 ids back to regular ids, and provide error messages to the front end if the types mismatch. Overall, it’s quite worth it in my opinion.

4 Likes

If you don’t mind can you share the article where opaque IDs are a good way to go to create an absinthe api?

I am really interested to see what the author of that article had to say.

Thanks

I agree it does look nice but it will involve a pretty significant redesign of my API. I don’t really have that many top-level fields as anyway.

I may have been getting slightly ahead of myself. I going to change my question a bit by asking how I might obscure the DB primary key from the ID in the graphql fields - mostly to help prevent issues that might arise from using incrementing numbers as IDs.

One option (that I use for a few models) is to use a UUID as the primary key and expose it via a custom scalar type.

An alternative might be to use something like hashids to convert the numeric IDs to a non-sequential string. Would a customer scalar type be best for this or is there some middleware that might be better?

I think in general, use of a custom scalar for IDs vs the built in ID type should be primarily used when you want the client to know certain properties of the ID. That is to say, when you don’t want them to be particularly opaque. If the client should see them as opaque, then you should use the ID type, and then use middleware to do any transformations of the value into and out of your system.

Just to make sure we’re on the same page, you can use the node id values without any changes to the rest of your API. Even use of the node interface field causes literally zero other changes to the API.

1 Like