Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: subscription installer #266

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 124 additions & 22 deletions lib/igniter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,9 @@ if Code.ensure_loaded?(Igniter) do
end

@doc "Sets up the phoenix module for AshGraphql"
def setup_phoenix(igniter, schema_name \\ nil) do
def setup_phoenix(igniter, schema_name \\ nil, socket_name \\ nil) do
schema_name = schema_name || Igniter.Project.Module.module_name(igniter, "GraphqlSchema")
socket_name = socket_name || Igniter.Project.Module.module_name(igniter, "GraphqlSocket")

case Igniter.Libs.Phoenix.select_router(igniter) do
{igniter, nil} ->
Expand All @@ -162,7 +163,7 @@ if Code.ensure_loaded?(Igniter) do

{igniter, router} ->
igniter
|> update_endpoints(router)
|> update_endpoints(router, socket_name)
|> Igniter.Libs.Phoenix.add_pipeline(:graphql, "plug AshGraphql.Plug", router: router)
|> Igniter.Libs.Phoenix.add_scope(
"/gql",
Expand All @@ -183,57 +184,158 @@ if Code.ensure_loaded?(Igniter) do
end
end

def setup_application(igniter) do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created these two functions because it felt like there was a pattern. Looking at it afterwards, this all feels pretty phoenix specific with Pheonix.Socket and Endpoints.

I would refactor this and make these defps that are called as part of setup_phoenix. WDYT?

case Igniter.Libs.Phoenix.select_router(igniter) do
{igniter, nil} ->
igniter
|> Igniter.add_warning("""
Unable to configure Absinthe.Subscription beacuse we don't know the right endpoint
""")

{igniter, router} ->
{igniter, endpoints} =
Igniter.Libs.Phoenix.endpoints_for_router(igniter, router)

igniter =
Enum.reduce(endpoints, {nil, igniter}, fn endpoint, {last_child, igniter} ->
igniter
|> Igniter.Project.Application.add_new_child({Absinthe.Subscription, endpoint},
after: endpoint
)
end)

igniter
|> Igniter.Project.Application.add_new_child(AshGraphql.Subscription.Batcher,
after: Absinthe.Subscription
)
end
end

def setup_socket(igniter, schema_name \\ nil, socket_name \\ nil) do
schema_name = schema_name || Igniter.Project.Module.module_name(igniter, "GraphqlSchema")
socket_name = socket_name || Igniter.Project.Module.module_name(igniter, "GraphqlSocket")
otp_app = Igniter.Project.Application.app_name(igniter)

igniter
|> Igniter.Project.Module.find_and_update_or_create_module(
socket_name,
"""
use Phoenix.Socket

use Absinthe.Phoenix.Socket,
schema: #{inspect(schema_name)}

@otp_app #{inspect(otp_app)}

@impl true
def connect(_params, socket, _connect_info) do
{:ok, socket}
end

@impl true
def id(_socket), do: nil
""",
fn zipper ->
# Should never get here
{:ok, zipper}
end
)
end

@doc "Returns all modules that `use AshGraphql`"
def ash_graphql_schemas(igniter) do
Igniter.Project.Module.find_all_matching_modules(igniter, fn _name, zipper ->
match?({:ok, _}, Igniter.Code.Module.move_to_use(zipper, AshGraphql))
end)
end

defp update_endpoints(igniter, router) do
defp update_endpoints(igniter, router, socket_name) do
{igniter, endpoints_that_need_parser} =
Igniter.Libs.Phoenix.endpoints_for_router(igniter, router)

igniter =
Enum.reduce(endpoints_that_need_parser, igniter, fn endpoint, igniter ->
Igniter.Project.Module.find_and_update_module!(igniter, endpoint, fn zipper ->
case Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:plug,
2,
&Igniter.Code.Function.argument_equals?(&1, 0, Plug.Parsers)
) do
{:ok, zipper} ->
with {:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1),
{:ok, zipper} <- Igniter.Code.Keyword.get_key(zipper, :parsers),
{:ok, zipper} <-
Igniter.Code.List.append_new_to_list(zipper, Absinthe.Plug.Parser) do
{:ok, zipper}
else
_ ->
{:warning,
"Could not add `Absinthe.Plug.Parser` to parsers in endpoint #{endpoint}. Please make this change manually."}
end

:error ->
case parser_location(zipper) do
{:ok, zipper} ->
{:ok,
Igniter.Code.Common.add_code(zipper, """
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser],
pass: ["*/*"],
json_decoder: Jason
""")}

_ ->
{:warning,
"Could not add `Absinthe.Plug.Parser` to parsers in endpoint #{endpoint}. Please make this change manually."}
end
end
end)
end)

Enum.reduce(endpoints_that_need_parser, igniter, fn endpoint, igniter ->
Igniter.Project.Module.find_and_update_module!(igniter, endpoint, fn zipper ->
case Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:plug,
2,
&Igniter.Code.Function.argument_equals?(&1, 0, Plug.Parsers)
:socket,
3,
&Igniter.Code.Function.argument_equals?(&1, 1, socket_name)
) do
{:ok, zipper} ->
with {:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1),
{:ok, zipper} <- Igniter.Code.Keyword.get_key(zipper, :parsers),
{:ok, zipper} <-
Igniter.Code.List.append_new_to_list(zipper, Absinthe.Plug.Parser) do
{:ok, zipper}
else
_ ->
{:warning,
"Could not add `Absinthe.Plug.Parser` to parsers in endpoint #{endpoint}. Please make this change manually."}
end
# Already installed
{:ok, zipper}

:error ->
case parser_location(zipper) do
case socket_location(zipper) do
{:ok, zipper} ->
{:ok,
Igniter.Code.Common.add_code(zipper, """
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser],
pass: ["*/*"],
json_decoder: Jason
socket "/ws/gql", #{inspect(socket_name)},
websocket: true,
longpoll: true
""")}

_ ->
{:warning,
"Could not add `Absinthe.Plug.Parser` to parsers in endpoint #{endpoint}. Please make this change manually."}
"Could not add #{socket_name} in endpoint #{endpoint}. Please make this change manually."}
end
end
end)
end)
end

defp socket_location(zipper) do
with :error <-
Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:socket,
3,
&Igniter.Code.Function.argument_equals?(&1, 1, Phoenix.LiveView.Socket)
) do
Igniter.Code.Module.move_to_use(zipper, Phoenix.Endpoint)
end
end

defp parser_location(zipper) do
with :error <-
Igniter.Code.Function.move_to_function_call_in_current_scope(
Expand Down
4 changes: 3 additions & 1 deletion lib/mix/tasks/ash_graphql.install.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ if Code.ensure_loaded?(Igniter) do
if Enum.empty?(candidate_ash_graphql_schemas) do
igniter
|> AshGraphql.Igniter.setup_absinthe_schema(schema_name)
|> AshGraphql.Igniter.setup_phoenix(schema_name)
|> AshGraphql.Igniter.setup_socket(schema_name, nil)
|> AshGraphql.Igniter.setup_phoenix(schema_name, nil)
|> AshGraphql.Igniter.setup_application()
else
igniter
|> Igniter.add_warning("AshGraphql schema already exists, skipping installation.")
Expand Down
1 change: 0 additions & 1 deletion lib/subscriptions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ defmodule AshGraphql.Subscription do
type_override \\ nil,
nested \\ []
) do

query
|> Ash.Query.new()
|> Ash.Query.set_tenant(Map.get(context, :tenant))
Expand Down
5 changes: 3 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ defmodule AshGraphql.MixProject do
{:absinthe, "~> 1.7"},
{:absinthe_phoenix, "~> 2.0.0", optional: true},
{:jason, "~> 1.2"},
{:igniter, "~> 0.3 and >= 0.3.34", optional: true},
{:igniter, path: "../igniter", optional: true, override: true},
{:spark, "~> 2.2 and >= 2.2.10"},
{:owl, "~> 0.11"},
# dev/test dependencies
Expand All @@ -168,7 +168,8 @@ defmodule AshGraphql.MixProject do
{:mix_test_watch, "~> 1.0", only: :dev, runtime: false},
{:simple_sat, ">= 0.0.0", only: :test},
{:mix_audit, ">= 0.0.0", only: [:dev, :test], runtime: false},
{:benchee, "~> 1.1", only: [:dev, :test]}
{:benchee, "~> 1.1", only: [:dev, :test]},
{:phx_new, "~> 1.5", only: [:test]}
]
end

Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"phx_new": {:hex, :phx_new, "1.7.18", "b3654da56a2db9ea69cb41186838a884df52463e2c5629e379ffa8a00356f16e", [:mix], [], "hexpm", "447ae02ab7c64a09942ca5370fce3c866a674f610517d06b46d65b905e81e6b5"},
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
"reactor": {:hex, :reactor, "0.10.3", "41a8c34251148e36dd7c75aa8433f2c2f283f29c097f9eb84a630ab28dd75651", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2b34380e22b69a35943a7bcceffd5a8b766870f1fc9052162a7ff74ef9cdb3b2"},
Expand Down
47 changes: 47 additions & 0 deletions test/mix/tasks/ash_graphql_install_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
defmodule Mix.Tasks.AshGraphqlInstallTest do
use ExUnit.Case

import Igniter.Test

setup do
igniter =
phx_test_project()
|> Igniter.compose_task("ash_graphql.install", ["--yes"])

[igniter: igniter]
end

test "ash_graphql.install", %{igniter: igniter} do
assert_creates(igniter, "lib/test_web/graphql_schema.ex", ~S'''
defmodule TestWeb.GraphqlSchema do
use Absinthe.Schema

use AshGraphql,
domains: []

import_types(Absinthe.Plug.Types)

query do
# Custom Absinthe queries can be placed here
@desc """
Hello! This is a sample query to verify that AshGraphql has been set up correctly.
Remove me once you have a query of your own!
"""
field :say_hello, :string do
resolve(fn _, _, _ ->
{:ok, "Hello from AshGraphql!"}
end)
end
end

mutation do
# Custom Absinthe mutations can be placed here
end

subscription do
# Custom Absinthe subscriptions can be placed here
end
end
''')
end
end
Loading