Add absinthe-socket-link and send all GraphQL via a Phoenix channel.

This commit is contained in:
James Harton 2018-04-08 15:00:11 +12:00
parent be3f4203d2
commit 8a4ed2c727
12 changed files with 173 additions and 42 deletions

5
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"files.associations": {
"*.js": "javascriptreact"
}
}

View file

@ -1,23 +1,15 @@
# Client-side GraphQL # Extra for experts
So, a GraphQL API isn't much use without the ability to use it from the browser, Our little React app is pretty neat. When it loads it queries the server for a
so here's where we implement the faces gallery as a client-side app. Whilst list of people and shows their faces and allows us to run a mutation to add a
it's possible to do progressive enhancement I've removed all functionality from new person to our list.
the faces controller to prove that all the data loading and changing is
happening via GraphQL.
I've used [React](https://reactjs.org/) and [Apollo There's only one problem. What happens if someone else comes along and adds a
client](https://www.apollographql.com/client) via face to our gallery? Our client won't know that the data has changed and will
[react-apollo](https://www.apollographql.com/docs/react/). just keep showing us stale data. This is where [GraphQL
Subscriptions](http://graphql.org/blog/subscriptions-in-graphql-and-relay/) come
in.
Things to look at: Absinthe has built-in support for subscriptions and since we're using Phoenix we
have a well-tested channel implementation to run it over (Absinthe can use it's
* `assets/js/app.js` own socket protocol too, if you're not running in Phoenix).
* `assets/js/components/Gallery.js`
* `assets/js/components/AddFace.js`
* `assets/js/queries/list_people.js`
* `assets/js/queries/import_person.js`
[Demo](http://localhost:4000)
Next, let's flip over to `step-5` to see some magic.

View file

@ -0,0 +1,7 @@
import * as AbsintheSocket from "@absinthe/socket";
import { createAbsintheSocketLink } from "@absinthe/socket-apollo-link";
import { Socket as PhoenixSocket } from "phoenix";
export default createAbsintheSocketLink(AbsintheSocket.create(
new PhoenixSocket("ws://localhost:4000/socket")
));

View file

@ -1,11 +1,9 @@
import { ApolloClient } from 'apollo-client'; import { ApolloClient } from "apollo-client";
import { HttpLink } from 'apollo-link-http'; import { InMemoryCache } from "apollo-cache-inmemory";
import { InMemoryCache } from 'apollo-cache-inmemory'; import absintheSocketLink from "./absinthe-socket-link";
const client = new ApolloClient({ const client = new ApolloClient({
link: new HttpLink({ link: absintheSocketLink,
uri: "/api"
}),
cache: new InMemoryCache(), cache: new InMemoryCache(),
}); });

View file

@ -6,6 +6,7 @@
"watch": "brunch watch --stdin" "watch": "brunch watch --stdin"
}, },
"dependencies": { "dependencies": {
"@absinthe/socket-apollo-link": "^0.1.11",
"apollo-client-preset": "^1.0.8", "apollo-client-preset": "^1.0.8",
"babel-preset-react": "^6.24.1", "babel-preset-react": "^6.24.1",
"graphql": "^0.13.2", "graphql": "^0.13.2",

View file

@ -2,6 +2,53 @@
# yarn lockfile v1 # yarn lockfile v1
"@absinthe/socket-apollo-link@^0.1.11":
version "0.1.11"
resolved "https://registry.yarnpkg.com/@absinthe/socket-apollo-link/-/socket-apollo-link-0.1.11.tgz#8dde213db3cf6e73419693c409ba00c96214a0e6"
dependencies:
"@absinthe/socket" "0.1.10"
apollo-link "1.0.0"
babel-runtime "6.26.0"
flow-static-land "0.2.8"
graphql "0.11.7"
zen-observable "0.6.0"
"@absinthe/socket@0.1.10":
version "0.1.10"
resolved "https://registry.yarnpkg.com/@absinthe/socket/-/socket-0.1.10.tgz#657f7b32cd6161bdc20b28c1377dc3596ea25432"
dependencies:
"@jumpn/utils-array" "0.3.4"
"@jumpn/utils-composite" "0.5.0"
"@jumpn/utils-graphql" "0.5.3"
babel-runtime "6.26.0"
phoenix "1.3.0"
zen-observable "0.6.0"
"@jumpn/utils-array@0.3.4":
version "0.3.4"
resolved "https://registry.yarnpkg.com/@jumpn/utils-array/-/utils-array-0.3.4.tgz#fb4310120108f659dab54075ef93abc56137de5e"
dependencies:
babel-polyfill "6.26.0"
babel-runtime "6.26.0"
flow-static-land "0.2.7"
"@jumpn/utils-composite@0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@jumpn/utils-composite/-/utils-composite-0.5.0.tgz#83580d470d0042f756889d2d057c2675fa1ebac6"
dependencies:
"@jumpn/utils-array" "0.3.4"
babel-polyfill "6.26.0"
babel-runtime "6.26.0"
fast-deep-equal "1.0.0"
flow-static-land "0.2.8"
"@jumpn/utils-graphql@0.5.3":
version "0.5.3"
resolved "https://registry.yarnpkg.com/@jumpn/utils-graphql/-/utils-graphql-0.5.3.tgz#a7b41b0bec34d8dc1873981be0a718190673f430"
dependencies:
babel-runtime "6.26.0"
graphql "0.11.7"
"@types/async@2.0.47": "@types/async@2.0.47":
version "2.0.47" version "2.0.47"
resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.47.tgz#f49ba1dd1f189486beb6e1d070a850f6ab4bd521" resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.47.tgz#f49ba1dd1f189486beb6e1d070a850f6ab4bd521"
@ -163,6 +210,13 @@ apollo-link-http@^1.3.1:
apollo-link "^1.2.1" apollo-link "^1.2.1"
apollo-link-http-common "^0.2.3" apollo-link-http-common "^0.2.3"
apollo-link@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.0.0.tgz#3d334285789c217f95712ebce434d56ce7f3e991"
dependencies:
apollo-utilities "^0.2.0-beta.0"
zen-observable "^0.6.0"
apollo-link@^1.0.0, apollo-link@^1.0.6, apollo-link@^1.2.1: apollo-link@^1.0.0, apollo-link@^1.0.6, apollo-link@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.1.tgz#c120b16059f9bd93401b9f72b94d2f80f3f305d2" resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.1.tgz#c120b16059f9bd93401b9f72b94d2f80f3f305d2"
@ -171,6 +225,10 @@ apollo-link@^1.0.0, apollo-link@^1.0.6, apollo-link@^1.2.1:
apollo-utilities "^1.0.0" apollo-utilities "^1.0.0"
zen-observable-ts "^0.8.6" zen-observable-ts "^0.8.6"
apollo-utilities@^0.2.0-beta.0:
version "0.2.0-rc.3"
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-0.2.0-rc.3.tgz#7bd93be0f587f20c5b46e21880272e305759fdc2"
apollo-utilities@^1.0.0, apollo-utilities@^1.0.11: apollo-utilities@^1.0.0, apollo-utilities@^1.0.11:
version "1.0.11" version "1.0.11"
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.11.tgz#cd36bfa6e5c04eea2caf0c204a0f38a0ad550802" resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.11.tgz#cd36bfa6e5c04eea2caf0c204a0f38a0ad550802"
@ -708,6 +766,14 @@ babel-plugin-transform-strict-mode@^6.24.1:
babel-runtime "^6.22.0" babel-runtime "^6.22.0"
babel-types "^6.24.1" babel-types "^6.24.1"
babel-polyfill@6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153"
dependencies:
babel-runtime "^6.26.0"
core-js "^2.5.0"
regenerator-runtime "^0.10.5"
babel-preset-es2015@^6.24.1: babel-preset-es2015@^6.24.1:
version "6.24.1" version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939" resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939"
@ -787,7 +853,7 @@ babel-register@^6.26.0:
mkdirp "^0.5.1" mkdirp "^0.5.1"
source-map-support "^0.4.15" source-map-support "^0.4.15"
babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: babel-runtime@6.26.0, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0:
version "6.26.0" version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
dependencies: dependencies:
@ -1754,6 +1820,10 @@ extsprintf@^1.2.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
fast-deep-equal@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
fast-deep-equal@^1.0.0: fast-deep-equal@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
@ -1856,6 +1926,14 @@ flat-cache@^1.2.1:
graceful-fs "^4.1.2" graceful-fs "^4.1.2"
write "^0.2.1" write "^0.2.1"
flow-static-land@0.2.7:
version "0.2.7"
resolved "https://registry.yarnpkg.com/flow-static-land/-/flow-static-land-0.2.7.tgz#937f9dcb2780889a609155e5d1a55a993bc2ffb3"
flow-static-land@0.2.8:
version "0.2.8"
resolved "https://registry.yarnpkg.com/flow-static-land/-/flow-static-land-0.2.8.tgz#49617e531396928bae6eb5d8ba32e7071637e5b9"
for-in@^1.0.1: for-in@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@ -2036,6 +2114,12 @@ graphql-tag@^2.4.2, graphql-tag@^2.8.0:
version "2.8.0" version "2.8.0"
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.8.0.tgz#52cdea07a842154ec11a2e840c11b977f9b835ce" resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.8.0.tgz#52cdea07a842154ec11a2e840c11b977f9b835ce"
graphql@0.11.7:
version "0.11.7"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.11.7.tgz#e5abaa9cb7b7cccb84e9f0836bf4370d268750c6"
dependencies:
iterall "1.1.3"
graphql@^0.13.2: graphql@^0.13.2:
version "0.13.2" version "0.13.2"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.13.2.tgz#4c740ae3c222823e7004096f832e7b93b2108270" resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.13.2.tgz#4c740ae3c222823e7004096f832e7b93b2108270"
@ -2422,6 +2506,10 @@ isstream@~0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
iterall@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.1.3.tgz#1cbbff96204056dde6656e2ed2e2226d0e6d72c9"
iterall@^1.2.1: iterall@^1.2.1:
version "1.2.2" version "1.2.2"
resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7"
@ -2972,6 +3060,10 @@ performance-now@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
phoenix@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/phoenix/-/phoenix-1.3.0.tgz#1df2c27f986ee295e37c9983ec28ebac1d7f4a3e"
"phoenix@file:../deps/phoenix": "phoenix@file:../deps/phoenix":
version "1.3.2" version "1.3.2"
@ -3226,6 +3318,10 @@ regenerate@^1.2.1:
version "1.3.3" version "1.3.3"
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f"
regenerator-runtime@^0.10.5:
version "0.10.5"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658"
regenerator-runtime@^0.11.0: regenerator-runtime@^0.11.0:
version "0.11.1" version "0.11.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
@ -3905,6 +4001,14 @@ zen-observable-ts@^0.8.6:
dependencies: dependencies:
zen-observable "^0.7.0" zen-observable "^0.7.0"
zen-observable@0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.6.0.tgz#8a6157ed15348d185d948cfc4a59d90a2c0f70ee"
zen-observable@^0.6.0:
version "0.6.1"
resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.6.1.tgz#01dbed3bc8d02cbe9ee1112c83e04c807f647244"
zen-observable@^0.7.0: zen-observable@^0.7.0:
version "0.7.1" version "0.7.1"
resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.7.1.tgz#f84075c0ee085594d3566e1d6454207f126411b3" resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.7.1.tgz#f84075c0ee085594d3566e1d6454207f126411b3"

View file

@ -14,6 +14,7 @@ defmodule Faces.Application do
supervisor(FacesWeb.Endpoint, []), supervisor(FacesWeb.Endpoint, []),
# Start your own worker by calling: Faces.Worker.start_link(arg1, arg2, arg3) # Start your own worker by calling: Faces.Worker.start_link(arg1, arg2, arg3)
# worker(Faces.Worker, [arg1, arg2, arg3]), # worker(Faces.Worker, [arg1, arg2, arg3]),
supervisor(Absinthe.Subscription, [FacesWeb.Endpoint])
] ]
# See https://hexdocs.pm/elixir/Supervisor.html # See https://hexdocs.pm/elixir/Supervisor.html

View file

@ -1,11 +1,12 @@
defmodule FacesWeb.UserSocket do defmodule FacesWeb.UserSocket do
use Phoenix.Socket use Phoenix.Socket
use Absinthe.Phoenix.Socket, schema: FacesWeb.Schema
## Channels ## Channels
# channel "room:*", FacesWeb.RoomChannel # channel "room:*", FacesWeb.RoomChannel
## Transports ## Transports
transport :websocket, Phoenix.Transports.WebSocket transport(:websocket, Phoenix.Transports.WebSocket)
# transport :longpoll, Phoenix.Transports.LongPoll # transport :longpoll, Phoenix.Transports.LongPoll
# Socket params are passed from the client and can # Socket params are passed from the client and can

View file

@ -1,43 +1,52 @@
defmodule FacesWeb.Endpoint do defmodule FacesWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :faces use Phoenix.Endpoint, otp_app: :faces
use Absinthe.Phoenix.Endpoint
socket "/socket", FacesWeb.UserSocket socket("/socket", FacesWeb.UserSocket)
# Serve at "/" the static files from "priv/static" directory. # Serve at "/" the static files from "priv/static" directory.
# #
# You should set gzip to true if you are running phoenix.digest # You should set gzip to true if you are running phoenix.digest
# when deploying your static files in production. # when deploying your static files in production.
plug Plug.Static, plug(
at: "/", from: :faces, gzip: false, Plug.Static,
at: "/",
from: :faces,
gzip: false,
only: ~w(css fonts images js favicon.ico robots.txt) only: ~w(css fonts images js favicon.ico robots.txt)
)
# Code reloading can be explicitly enabled under the # Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint. # :code_reloader configuration of your endpoint.
if code_reloading? do if code_reloading? do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket)
plug Phoenix.LiveReloader plug(Phoenix.LiveReloader)
plug Phoenix.CodeReloader plug(Phoenix.CodeReloader)
end end
plug Plug.Logger plug(Plug.Logger)
plug Plug.Parsers, plug(
Plug.Parsers,
parsers: [:urlencoded, :multipart, :json], parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"], pass: ["*/*"],
json_decoder: Poison json_decoder: Poison
)
plug Plug.MethodOverride plug(Plug.MethodOverride)
plug Plug.Head plug(Plug.Head)
# The session will be stored in the cookie and signed, # The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with. # this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it. # Set :encryption_salt if you would also like to encrypt it.
plug Plug.Session, plug(
Plug.Session,
store: :cookie, store: :cookie,
key: "_faces_key", key: "_faces_key",
signing_salt: "SBTuT23J" signing_salt: "SBTuT23J"
)
plug FacesWeb.Router plug(FacesWeb.Router)
@doc """ @doc """
Callback invoked for dynamically configuring the endpoint. Callback invoked for dynamically configuring the endpoint.

View file

@ -19,4 +19,15 @@ defmodule FacesWeb.Schema do
resolve(&Resolvers.People.create_person/3) resolve(&Resolvers.People.create_person/3)
end end
end end
subscription do
@desc "New person added"
field :person_added, :person do
config(fn _, _ ->
{:ok, "person_added"}
end)
trigger(:import_person, topic: fn _ -> "person_added" end)
end
end
end end

View file

@ -37,13 +37,14 @@ defmodule Faces.Mixfile do
{:phoenix_pubsub, "~> 1.0"}, {:phoenix_pubsub, "~> 1.0"},
{:phoenix_ecto, "~> 3.2"}, {:phoenix_ecto, "~> 3.2"},
{:postgrex, ">= 0.0.0"}, {:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.10"}, {:phoenix_html, "~> 2.10", override: true},
{:phoenix_live_reload, "~> 1.0", only: :dev}, {:phoenix_live_reload, "~> 1.0", only: :dev},
{:gettext, "~> 0.11"}, {:gettext, "~> 0.11"},
{:cowboy, "~> 1.0"}, {:cowboy, "~> 1.0"},
{:tentacat, "~> 0.9.0"}, {:tentacat, "~> 0.9.0"},
{:absinthe_plug, "~> 1.4"}, {:absinthe_plug, "~> 1.4"},
{:poison, "~> 2.1.0", override: true} {:poison, "~> 2.1.0", override: true},
{:absinthe_phoenix, "~> 1.4.0"}
] ]
end end

View file

@ -1,5 +1,6 @@
%{ %{
"absinthe": {:hex, :absinthe, "1.4.10", "9f8d0c34dfcfd0030d3a3f123c7501e99ab59651731387289dad5885047ebb2a", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, "absinthe": {:hex, :absinthe, "1.4.10", "9f8d0c34dfcfd0030d3a3f123c7501e99ab59651731387289dad5885047ebb2a", [:mix], [{:dataloader, "~> 1.0.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"absinthe_phoenix": {:hex, :absinthe_phoenix, "1.4.2", "cb84c81b94103fdfbbdd8b83b7b4b70a850ab7d3be6a1e56b96de2bb854b09b6", [:mix], [{:absinthe, "~> 1.4.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.4.0", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.2", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.10.5", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:poison, "~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"absinthe_plug": {:hex, :absinthe_plug, "1.4.2", "01bf16f0a637869bcc0a1919935f08ff853501004e7549ddaa3a7788deb48965", [:mix], [{:absinthe, "~> 1.4", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "absinthe_plug": {:hex, :absinthe_plug, "1.4.2", "01bf16f0a637869bcc0a1919935f08ff853501004e7549ddaa3a7788deb48965", [:mix], [{:absinthe, "~> 1.4", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.2 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},