From d250271cd14f37c5f033c6c408c8d128ad5762a5 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Fri, 4 Oct 2019 04:30:25 -0400 Subject: [PATCH] add relationship route for has one --- .formatter.exs | 3 ++ .../json_api/controllers/get_belongs_to.ex | 33 ++++++++++++++++ lib/ash/json_api/controllers/get_has_one.ex | 33 ++++++++++++++++ lib/ash/json_api/route_builder.ex | 26 +++++++++++++ lib/ash/json_api/serializer.ex | 38 ++++++++++++++++--- lib/ash/resource.ex | 8 ++++ lib/ash/resource/relationships/belongs_to.ex | 6 ++- lib/ash/resource/relationships/has_one.ex | 4 +- .../resource/relationships/relationships.ex | 1 + lib/ash/resource/schema.ex | 4 ++ 10 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 lib/ash/json_api/controllers/get_belongs_to.ex create mode 100644 lib/ash/json_api/controllers/get_has_one.ex diff --git a/.formatter.exs b/.formatter.exs index 7c59e81e..36ac8c18 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -5,6 +5,9 @@ locals_without_parens = [ post: 1, attribute: 2, belongs_to: 2, + belongs_to: 3, + has_one: 2, + has_one: 3, has_many: 2, field: 2 ] diff --git a/lib/ash/json_api/controllers/get_belongs_to.ex b/lib/ash/json_api/controllers/get_belongs_to.ex new file mode 100644 index 00000000..dff7a307 --- /dev/null +++ b/lib/ash/json_api/controllers/get_belongs_to.ex @@ -0,0 +1,33 @@ +defmodule Ash.JsonApi.Controllers.GetBelongsTo do + def init(options) do + # initialize options + options + end + + def call(%{path_params: %{"id" => id}} = conn, options) do + resource = options[:resource] + relationship = options[:relationship] + + request = Ash.Request.from(conn, relationship.destination, :get_belongs_to) + + case Ash.Repo.get(resource, id) do + nil -> + conn + # |> put_resp_content_type("text/plain") + |> Plug.Conn.send_resp(404, "uh oh") + + found -> + related = + found + |> Ecto.assoc(relationship.name) + |> Ash.Repo.one() + + serialized = Ash.JsonApi.Serializer.serialize_one(request, related) + + conn + |> Plug.Conn.put_resp_content_type("application/vnd.api+json") + |> Plug.Conn.send_resp(200, serialized) + end + |> Plug.Conn.halt() + end +end diff --git a/lib/ash/json_api/controllers/get_has_one.ex b/lib/ash/json_api/controllers/get_has_one.ex new file mode 100644 index 00000000..540f4992 --- /dev/null +++ b/lib/ash/json_api/controllers/get_has_one.ex @@ -0,0 +1,33 @@ +defmodule Ash.JsonApi.Controllers.GetHasOne do + def init(options) do + # initialize options + options + end + + def call(%{path_params: %{"id" => id}} = conn, options) do + resource = options[:resource] + relationship = options[:relationship] + + request = Ash.Request.from(conn, relationship.destination, :get_has_one) + + case Ash.Repo.get(resource, id) do + nil -> + conn + # |> put_resp_content_type("text/plain") + |> Plug.Conn.send_resp(404, "uh oh") + + found -> + related = + found + |> Ecto.assoc(relationship.name) + |> Ash.Repo.one() + + serialized = Ash.JsonApi.Serializer.serialize_one(request, related) + + conn + |> Plug.Conn.put_resp_content_type("application/vnd.api+json") + |> Plug.Conn.send_resp(200, serialized) + end + |> Plug.Conn.halt() + end +end diff --git a/lib/ash/json_api/route_builder.ex b/lib/ash/json_api/route_builder.ex index 3b626b3e..a3fdc42b 100644 --- a/lib/ash/json_api/route_builder.ex +++ b/lib/ash/json_api/route_builder.ex @@ -3,6 +3,32 @@ defmodule Ash.JsonApi.RouteBuilder do quote bind_quoted: [resource: resource] do Ash.JsonApi.RouteBuilder.build_get_route(resource) Ash.JsonApi.RouteBuilder.build_index_route(resource) + Ash.JsonApi.RouteBuilder.build_belongs_to_relationship_routes(resource) + Ash.JsonApi.RouteBuilder.build_has_one_relationship_routes(resource) + end + end + + defmacro build_belongs_to_relationship_routes(resource) do + quote bind_quoted: [resource: resource] do + for %{expose?: true, type: :belongs_to, route: route} = relationship <- + Ash.relationships(resource) do + get(route, + to: Ash.JsonApi.Controllers.GetBelongsTo, + init_opts: [resource: resource, relationship: relationship] + ) + end + end + end + + defmacro build_has_one_relationship_routes(resource) do + quote bind_quoted: [resource: resource] do + for %{expose?: true, type: :has_one, route: route} = relationship <- + Ash.relationships(resource) do + get(route, + to: Ash.JsonApi.Controllers.GetHasOne, + init_opts: [resource: resource, relationship: relationship] + ) + end end end diff --git a/lib/ash/json_api/serializer.ex b/lib/ash/json_api/serializer.ex index 4e44b90a..5ef3326d 100644 --- a/lib/ash/json_api/serializer.ex +++ b/lib/ash/json_api/serializer.ex @@ -9,8 +9,16 @@ defmodule Ash.JsonApi.Serializer do Jason.encode!(%{data: data, json_api: json_api, links: links}) end + def serialize_one(request, nil) do + # TODO `included` + json_api = %{version: "1.0"} + links = one_links(request) + + Jason.encode!(%{data: nil, json_api: json_api, links: links}) + end + def serialize_one(request, record) do - # TODO `links` and `included` + # TODO `included` data = serialize_one_record(request, record) json_api = %{version: "1.0"} links = one_links(request) @@ -135,20 +143,23 @@ defmodule Ash.JsonApi.Serializer do id: record.id, type: Ash.type(resource), attributes: serialize_attributes(resource, record), - relationships: serialize_relationships(resource, record), + relationships: serialize_relationships(request, record), links: %{ self: at_host(request, Ash.Routes.get(resource, record.id)) } } end - defp serialize_relationships(resource, _record) do + defp serialize_relationships(request, record) do # TODO: links.self, links.related - resource + request.resource |> Ash.relationships() + |> Enum.filter(& &1.expose?) |> Enum.into(%{}, fn relationship -> value = %{ - links: %{}, + links: %{ + self: at_host(with_path_params(request, %{"id" => record.id}), relationship.route) + }, data: %{}, meta: %{} } @@ -157,11 +168,28 @@ defmodule Ash.JsonApi.Serializer do end) end + defp with_path_params(request, params) do + Map.update!(request, :path_params, &Map.merge(&1, params)) + end + defp at_host(request, route) do request.url |> URI.parse() |> Map.put(:query, nil) |> Map.put(:path, "/" <> Path.join(request.json_api_prefix, route)) + |> Map.update!(:path, fn path -> + path + |> Path.split() + |> Enum.map(fn path_element -> + if String.starts_with?(path_element, ":") do + "replacing #{path_element}" + Map.get(request.path_params, String.slice(path_element, 1..-1)) || "" + else + path_element + end + end) + |> Path.join() + end) |> URI.to_string() end diff --git a/lib/ash/resource.ex b/lib/ash/resource.ex index aafec9f4..6b0ffbba 100644 --- a/lib/ash/resource.ex +++ b/lib/ash/resource.ex @@ -20,6 +20,14 @@ defmodule Ash.Resource do @name name @resource_type resource_type + + unless @name do + raise "Must set name" + end + + unless @resource_type do + raise "Must set resource type" + end end end diff --git a/lib/ash/resource/relationships/belongs_to.ex b/lib/ash/resource/relationships/belongs_to.ex index ed72f592..405797bb 100644 --- a/lib/ash/resource/relationships/belongs_to.ex +++ b/lib/ash/resource/relationships/belongs_to.ex @@ -1,10 +1,12 @@ defmodule Ash.Resource.Relationships.BelongsTo do - defstruct [:name, :type, :destination, :destination_field, :source_field] + defstruct [:name, :expose?, :type, :route, :destination, :destination_field, :source_field] - def new(name, related_resource, opts \\ []) do + def new(resource_name, name, related_resource, opts \\ []) do %__MODULE__{ name: name, type: :belongs_to, + expose?: opts[:expose?] || false, + route: resource_name <> "/:id/" <> to_string(name), destination: related_resource, destination_field: opts[:destination_field] || "id", source_field: opts[:source_field] || "#{name}_id" diff --git a/lib/ash/resource/relationships/has_one.ex b/lib/ash/resource/relationships/has_one.ex index b1937dcd..8388628a 100644 --- a/lib/ash/resource/relationships/has_one.ex +++ b/lib/ash/resource/relationships/has_one.ex @@ -1,10 +1,12 @@ defmodule Ash.Resource.Relationships.HasOne do - defstruct [:name, :type, :destination, :destination_field, :source_field] + defstruct [:name, :type, :expose?, :route, :destination, :destination_field, :source_field] def new(resource_name, name, related_resource, opts \\ []) do %__MODULE__{ name: name, type: :has_one, + expose?: opts[:expose?] || false, + route: resource_name <> "/:id/" <> to_string(name), destination: related_resource, destination_field: opts[:destination_field] || "#{resource_name}_id", source_field: opts[:source_field] || "id" diff --git a/lib/ash/resource/relationships/relationships.ex b/lib/ash/resource/relationships/relationships.ex index 727aa2ab..27eb2484 100644 --- a/lib/ash/resource/relationships/relationships.ex +++ b/lib/ash/resource/relationships/relationships.ex @@ -21,6 +21,7 @@ defmodule Ash.Resource.Relationships do defmacro belongs_to(relationship_name, resource, config \\ []) do quote do @relationships Ash.Resource.Relationships.BelongsTo.new( + @name, unquote(relationship_name), unquote(resource), unquote(config) diff --git a/lib/ash/resource/schema.ex b/lib/ash/resource/schema.ex index 3aa73b13..9ca5e50b 100644 --- a/lib/ash/resource/schema.ex +++ b/lib/ash/resource/schema.ex @@ -15,6 +15,10 @@ defmodule Ash.Resource.Schema do for relationship <- Enum.filter(@relationships, &(&1.type == :belongs_to)) do belongs_to relationship.name, relationship.destination end + + for relationship <- Enum.filter(@relationships, &(&1.type == :has_one)) do + has_one relationship.name, relationship.destination + end end end end