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

API Base Setup #27

Merged
merged 1 commit into from
Mar 1, 2022
Merged
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
2 changes: 1 addition & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[
import_deps: [:ecto, :phoenix],
import_deps: [:ecto, :phoenix, :absinthe],
inputs: [
"{mix,.formatter}.exs",
"*.{ex,exs}",
Expand Down
5 changes: 4 additions & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ config :athena,
ecto_repos: [Athena.Repo],
generators: [context_app: :athena, binary_id: true]

config :athena, Athena.Repo, migration_primary_key: [id: :uuid, type: :binary_id]
config :athena, Athena.Repo,
migration_primary_key: [id: :uuid, type: :binary_id],
migration_foreign_key: [column: :id, type: :binary_id],
migration_timestamps: [type: :utc_datetime_usec]

# Configures the endpoint
config :athena, AthenaWeb.Endpoint,
Expand Down
5 changes: 5 additions & 0 deletions lib/athena.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ defmodule Athena do
if it comes from the database, an external API or others.
"""

@doc false
def model do
quote do
use Ecto.Schema

import Ecto.Changeset

alias Ecto.Changeset

@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
@timestamps_opts type: :utc_datetime_usec

@type association(type) :: Ecto.Association.NotLoaded.t() | type
end
Expand Down
4 changes: 2 additions & 2 deletions lib/athena/inventory/event.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ defmodule Athena.Inventory.Event do
locations: association([Location.t()]),
item_groups: association([ItemGroup.t()]),
items: association([Item.t()]),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}

schema "events" do
Expand Down
4 changes: 2 additions & 2 deletions lib/athena/inventory/item.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ defmodule Athena.Inventory.Item do
inverse: boolean,
item_group: association(ItemGroup.t()),
event: association(Event.t()),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}

schema "items" do
Expand Down
4 changes: 2 additions & 2 deletions lib/athena/inventory/item_group.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ defmodule Athena.Inventory.ItemGroup do
event: association(Event.t()),
location: association(Location.t()),
items: association([Item.t()]),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}

schema "item_groups" do
Expand Down
4 changes: 2 additions & 2 deletions lib/athena/inventory/location.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ defmodule Athena.Inventory.Location do
items: association([Item.t()]),
movements_in: association([Movement.t()]),
movements_out: association([Movement.t()]),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}

schema "locations" do
Expand Down
4 changes: 2 additions & 2 deletions lib/athena/inventory/movement.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ defmodule Athena.Inventory.Movement do
item_group: association(ItemGroup.t()),
source_location: association(Location.t() | nil),
destination_location: association(Location.t() | nil),
inserted_at: NaiveDateTime.t(),
updated_at: NaiveDateTime.t()
inserted_at: DateTime.t(),
updated_at: DateTime.t()
}

schema "movements" do
Expand Down
27 changes: 27 additions & 0 deletions lib/athena_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,33 @@ defmodule AthenaWeb do
end
end

@doc false
@spec subschema :: Macro.t()
def subschema do
quote do
use Absinthe.Schema.Notation
use Absinthe.Relay.Schema.Notation, :modern

import Absinthe.Resolution.Helpers, only: [dataloader: 1]
# import AbsintheErrorPayload.Payload

alias __MODULE__.Resolver

alias AthenaWeb.Schema.Dataloader, as: RepoDataLoader
end
end

@doc false
@spec resolver :: Macro.t()
def resolver do
quote do
import Absinthe.Resolution.Helpers
import Ecto.Query

alias Athena.Repo
end
end

@doc """
When used, dispatch to the appropriate controller/view/etc.
"""
Expand Down
2 changes: 1 addition & 1 deletion lib/athena_web/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ defmodule AthenaWeb.Endpoint do
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]

plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser],
pass: ["*/*"],
json_decoder: Phoenix.json_library()

Expand Down
22 changes: 22 additions & 0 deletions lib/athena_web/exception.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defprotocol AthenaWeb.Exception do
@fallback_to_any true

@spec result(exception :: Exception.t()) :: {:ok, term} | {:error, String.t()} | :unknown
def result(exception)
end

defimpl AthenaWeb.Exception, for: Any do
@spec result(exception :: Exception.t()) :: {:ok, term} | {:error, String.t()} | :unknown
def result(%{result: result} = _exception), do: result
def result(_exception), do: :unknown
end

defimpl AthenaWeb.Exception, for: Ecto.NoResultsError do
@spec result(exception :: Exception.t()) :: {:ok, term} | {:error, String.t()} | :unknown
def result(_exception), do: {:ok, nil}
end

defimpl AthenaWeb.Exception, for: Ecto.Query.CastError do
@spec result(exception :: Exception.t()) :: {:ok, term} | {:error, String.t()} | :unknown
def result(_exception), do: {:ok, nil}
end
85 changes: 85 additions & 0 deletions lib/athena_web/middleware/safe.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
defmodule AthenaWeb.Middleware.Safe do
@moduledoc false

alias AthenaWeb.Endpoint

require Logger

@spec add_error_handling(spec :: Absinthe.Middleware.spec()) :: Absinthe.Middleware.spec()
def add_error_handling(spec), do: &(spec |> to_fun(&1, &2) |> exec_safely(&1))

# Absinthe Node Error
defp to_fun(
{{Absinthe.Relay.Node, :global_id_resolver}, nil},
%{state: :resolved} = resolution,
_config
) do
fn -> resolution end
end

defp to_fun({{module, function}, config}, resolution, _config) do
fn -> apply(module, function, [resolution, config]) end
end

defp to_fun({module, config}, resolution, _config) do
fn -> module.call(resolution, config) end
end

defp to_fun(module, resolution, config) when is_atom(module) do
fn -> module.call(resolution, config) end
end

defp to_fun(function, resolution, config) when is_function(function, 2) do
fn -> function.(resolution, config) end
end

defp exec_safely(function, resolution) when is_function(function, 0) do
function.()
catch
kind, reason ->
full_exception = Exception.format(kind, reason, __STACKTRACE__)
result = AthenaWeb.Exception.result(reason)

result =
case {result, Endpoint.config(:debug_errors, false)} do
{{:ok, _} = result, _} ->
result

{{:error, error}, true} ->
{:error,
"""
#{inspect(error, pretty: true)}

DEBUG:
#{full_exception}
"""}

{{:error, error}, false} ->
{:error, error}

{:unknown, true} ->
Logger.error("""
Unknown exception catched:
#{full_exception}
""")

{:error,
"""
unkown error

DEBUG:
#{full_exception}
"""}

{:unknown, false} ->
Logger.error("""
Unknown exception catched:
#{full_exception}
""")

{:error, "unkown error"}
end

Absinthe.Resolution.put_result(resolution, result)
end
end
16 changes: 16 additions & 0 deletions lib/athena_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ defmodule AthenaWeb.Router do

@subresource_actions [:index, :new, :create]

pipeline :api do
plug :accepts, ["json"]
end

pipeline :browser do
plug :accepts, ["html"]

Expand Down Expand Up @@ -82,6 +86,18 @@ defmodule AthenaWeb.Router do
get "/", Redirector, to: "/admin/events"
end

scope "/api" do
pipe_through [:api]

forward(
"/",
Absinthe.Plug.GraphiQL,
schema: AthenaWeb.Schema,
socket: AthenaWeb.UserSocket,
interface: :playground
)
end

defp auth(conn, _opts),
do: Plug.BasicAuth.basic_auth(conn, Application.fetch_env!(:athena, Plug.BasicAuth))
end
66 changes: 66 additions & 0 deletions lib/athena_web/schema.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
defmodule AthenaWeb.Schema do
@moduledoc """
Root GraphQL Schema
"""

use Absinthe.Schema
use Absinthe.Relay.Schema, :modern

alias AthenaWeb.Middleware.Safe
alias AthenaWeb.Schema.Dataloader, as: RepoDataLoader
alias AthenaWeb.Schema.Resolver

@impl Absinthe.Schema
@spec context(context :: map) :: map
def context(context),
do:
Map.put(
context,
:loader,
Dataloader.add_source(Dataloader.new(), RepoDataLoader, RepoDataLoader.data())
)

@impl Absinthe.Schema
@spec plugins :: [atom]
def plugins, do: [Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]

@impl Absinthe.Schema
@spec middleware(
[Absinthe.Middleware.spec(), ...],
Absinthe.Type.Field.t(),
Absinthe.Type.Object.t()
) :: [Absinthe.Middleware.spec(), ...]
def middleware(middleware, _field, _object),
do: Enum.map(middleware, &Safe.add_error_handling/1)

import_types Absinthe.Plug.Types
# import_types AbsintheErrorPayload.ValidationMessageTypes
import_types AthenaWeb.Schema.Event
import_types AthenaWeb.Schema.Scalar.Date
import_types AthenaWeb.Schema.Scalar.Datetime
import_types AthenaWeb.Schema.Scalar.Map
import_types AthenaWeb.Schema.Scalar.Ok
import_types AthenaWeb.Schema.Scalar.URI

node interface do
end

interface :resource do
field :id, non_null(:id)
field :inserted_at, non_null(:datetime)
field :updated_at, non_null(:datetime)

interface :node
end

query do
node field do
resolve(&Resolver.node/2)
end

import_fields :event_queries
end

# mutation do
# end
end
10 changes: 10 additions & 0 deletions lib/athena_web/schema/dataloader.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule AthenaWeb.Schema.Dataloader do
@moduledoc """
Absinthe Dataloader
"""

alias Athena.Repo

@spec data :: Dataloader.Ecto.t()
def data, do: Dataloader.Ecto.new(Repo)
end
33 changes: 33 additions & 0 deletions lib/athena_web/schema/event.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
defmodule AthenaWeb.Schema.Event do
@moduledoc false

use AthenaWeb, :subschema

alias Athena.Inventory.Event

node object(:event) do
field :name, non_null(:string)

# connection field :locations, node_type: :location
# connection field :item_groups, node_type: :item_group
# connection field :items, node_type: :item

field :inserted_at, non_null(:datetime)
field :updated_at, non_null(:datetime)

is_type_of(&match?(%Event{}, &1))

interface :resource
end

connection(node_type: :event, non_null: true)

object :event_queries do
@desc "Get Event By ID"
field :event, :event do
arg :id, non_null(:id)

resolve(&Resolver.event/3)
end
end
end
9 changes: 9 additions & 0 deletions lib/athena_web/schema/event/resolver.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule AthenaWeb.Schema.Event.Resolver do
@moduledoc false

alias Athena.Inventory

@spec event(parent :: term(), args :: map(), resolution :: Absinthe.Resolution.t()) ::
{:ok, term()} | {:error, term()}
def event(_parent, %{id: id}, _resolution), do: {:ok, Inventory.get_event!(id)}
end
Loading