absinthe_phoenix icon indicating copy to clipboard operation
absinthe_phoenix copied to clipboard

Full integration example

Open rafbgarcia opened this issue 6 years ago • 10 comments

Hey there, really excited to use the new subscription feature!

However after few hours still couldn't make it work, I was wondering if there's an example fully integrating absinthe_phoenix with absinthe-socket.

FWIW I've posted my issue here.

Thanks!

rafbgarcia avatar Nov 08 '17 01:11 rafbgarcia

Hey! This is a perfectly reasonable request :) We're within a day or two of doing the final release, and it's the need for that release that has blocked the development of a full example.

The package you link to literally just went up, you're just slightly ahead of the curve. We'll have examples up soon.

benwilson512 avatar Nov 08 '17 01:11 benwilson512

A very quick glance at your gist shows that you are trying to connect the socket on the http endpoint ws://localhost:4001/api/graphql instead of using the socket endpoint ws://localhost:4001/socket.

tlvenn avatar Nov 08 '17 02:11 tlvenn

@tlvenn, thanks a lot man, you're right! :D

Thanks @benwilson512, yea I've followed the commits this afternoon, I was probably the first one to download the new @absinthe/socket lol.

Should I close this issue or leave it as a reminder for the full example? Feel free to close it if it's not necessary.

rafbgarcia avatar Nov 08 '17 02:11 rafbgarcia

We'll leave it up until there are full examples just in case anyone else is also wondering.

benwilson512 avatar Nov 08 '17 02:11 benwilson512

@benwilson512 Tried a simple thing with users and posts. Query and mutation works. Subscription doesn't work What should be the WS Url to be specified in Graphiql . screen shot 2017-12-13 at 6 39 59 pm

Also please share an example of router.ex Thanks in advance

brbharath17 avatar Dec 13 '17 13:12 brbharath17

@brbharath17 Hey there. It looks like it's working fine to me, that's what shows up until something publishes to the subscription.

benwilson512 avatar Dec 13 '17 13:12 benwilson512

As an aside, I think it will be best to handle generic help requests via slack or the forums. This issue is left open for reference until there is a full end to end example up somewhere, but I don't think it's the place to field questions about how to use Absinthe.

benwilson512 avatar Dec 13 '17 13:12 benwilson512

Hey @brbharath17. Please take this to the elixir-lang slack or the forums. Github issues are not the place we want to support one on one questions or learning.

benwilson512 avatar Dec 13 '17 13:12 benwilson512

@benwilson512 . Sure i ll put it on the slack. Sorry

brbharath17 avatar Dec 13 '17 13:12 brbharath17

I'm not sure that my setup would help somebody but I'm happy to share it although it originates from Absinthe guides. I tried to omit unnecessary details for readability sake.

Versions of dependencies:

Frontend:

Deps:

{
  "@absinthe/socket": "^0.2.1",
  "@absinthe/socket-apollo-link": "^0.2.1",
  "@apollo/client": "^3.1.3",
  "graphql": "^15.3.0",
  "phoenix": "^1.5.4",
  "typescript": "^3.9.3
}

TypeDefs Deps:

{
  "@types/absinthe__socket": "^0.2.0",
  "@types/absinthe__socket-apollo-link": "^0.2.0",
  "@types/phoenix": "^1.5.0",
  "@types/react": "^16.9.35"
}

Umbrella Phoenix Web App Deps:

defp deps do
  [
    {:phoenix, "~> 1.5"},
    {:phoenix_pubsub, "~> 2.0"},
    {:phoenix_ecto, "~> 4.0"},
    {:gettext, "~> 0.11"},
    {:my_app, in_umbrella: true}, # My ecto app
    {:plug_cowboy, "~> 2.3"},
    {:corsica, "~> 1.0"},
    {:absinthe, "~> 1.5"},
    {:absinthe_plug, "~> 1.5"},
    {:absinthe_phoenix, "~> 2.0"},
    {:poison, "~> 2.1"},
    {:dataloader, "~> 1.0"},
    {:pow, "~> 1.0.20"},
    {:phoenix_swoosh, "~> 0.3.0"},
    {:gen_smtp, "~> 0.13"}
  ]
end

Frontend modules (Typescript)

src/utils/apiClient/websocketLink.ts

import * as AbsintheSocket from '@absinthe/socket'
import { createAbsintheSocketLink } from '@absinthe/socket-apollo-link'
import { Socket as PhoenixSocket } from 'phoenix'

const phoenixSocket = new PhoenixSocket(
  process.env.REACT_APP_SOCKET_URL as string,
  {
    params: () => {
      const accessToken = localStorage.getItem('accessToken')
      return accessToken ? { access_token: accessToken } : {}
    },
  },
)

const absintheSocket = AbsintheSocket.create(phoenixSocket)

export default createAbsintheSocketLink(absintheSocket)

src/utils/apiClient/authLink.ts

import { ApolloLink } from '@apollo/client'

type Headers = {
  authorization?: string
}

const authLink = new ApolloLink((operation, forward) => {
  const accessToken = localStorage.getItem('accessToken')

  operation.setContext(({ headers }: { headers: Headers }) => ({
    headers: {
      ...headers,
      authorization: accessToken,
    },
  }))

  return forward(operation)
})

export default authLink

src/utils/apiClient/index.ts

import { ApolloClient, from, HttpLink, split, ApolloLink } from '@apollo/client'
import { getMainDefinition } from '@apollo/client/utilities'

import websocketLink from './websocketLink'
import errorLink from './errorLink' // In short here I renew token on expired token error
import authLink from './authLink'
import cache from './cache'

const httpLink = new HttpLink({
  uri: process.env.REACT_APP_API_URL,
})

const transportLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query)
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    )
  },
  (websocketLink as unknown) as ApolloLink, // types doesn't work proper with apollo-client v3 at this moment
  from([errorLink, authLink, httpLink]),
)

const apiClient = new ApolloClient({
  link: transportLink,
  cache,
  credentials: 'include',
})

export default apiClient

Elixir modules (Umbrella Phoenix)

config/config.exs

...
config :my_app_web, MyAppWeb.Endpoint,
  url: [host: "localhost"],
  secret_key_base: "hello there",
  render_errors: [view: MyAppWeb.ErrorView, accepts: ~w(json)],
  pubsub_server: MyAppWeb.PubSub
...

apps/my_app_web/lib/my_app_web/application.ex

defmodule MyAppWeb.Application do
  use Application

  ...
  def start(_type, _args) do
    connect_nodes()

    children = [
      MyAppWeb.Endpoint,
      {Phoenix.PubSub, name: MyAppWeb.PubSub},
      {Absinthe.Subscription, MyAppWeb.Endpoint},
    ]

    opts = [strategy: :one_for_one, name: MyAppWeb.Supervisor]
    Supervisor.start_link(children, opts)
  end
  ...
end

apps/my_app_web/lib/my_app_web/endpoint.ex

defmodule MyAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app_web
  use Absinthe.Phoenix.Endpoint

  socket "/socket", MyAppWeb.UserSocket,
    websocket: true,
    longpoll: false
 ...
end

apps/my_app_web/lib/my_app_web/channels/user_socket.ex

defmodule MyAppWeb.UserSocket do
  use Phoenix.Socket
  use Absinthe.Phoenix.Socket, schema: MyAppWeb.Schema

  def connect(params, socket, _connect_info) do
    current_user = current_user(params)

    socket =
      Absinthe.Phoenix.Socket.put_options(socket,
        context: %{
          current_user: current_user
        }
      )

    {:ok, socket}
  end

  defp current_user(%{"access_token" => access_token}) do
    # Here you should get your user token
  end

  defp current_user(_params), do: nil

  def id(_socket), do: nil
end

I'm open to share more details if it would help someone but this guide looks quite complete

vadimshvetsov avatar Aug 18 '20 04:08 vadimshvetsov