Commit 99c82478 authored by Alex Castaño's avatar Alex Castaño

Add GraphQL connections

parent 0e8b046e
......@@ -44,5 +44,7 @@ defmodule ActivityPub.ObjectAspect do
field(:followed, :boolean, virtual: true)
field(:liked, :boolean, virtual: true)
field(:cursor, :integer, virtual: true)
end
end
defmodule ActivityPub.SQL.Paginate do
import Ecto.Query
def call(query, params) do
def by_local_id(query, params) do
params = normalize_params(params)
query
|> select_cursor()
|> where(^dynamic_where(params))
|> limit(^params[:limit])
|> order_by([entity: entity], [{^params[:order], entity.local_id}])
end
def by_collection_insert(query, params) do
params = normalize_params(params)
query
|> collection_select_cursor()
|> where(^collection_dynamic_where(params))
|> limit(^params[:limit])
|> order_by([..., c], [{^params[:order], c.id}])
end
defp normalize_params(query_params) do
query_params = Enum.into(query_params, %{})
%{
limit: calc_limit(query_params),
order: calc_order(query_params),
starting_after: query_params[:starting_after] || query_params["starting_after"],
ending_before: query_params[:ending_before] || query_params["ending_before"]
after: query_params[:after] || query_params["after"],
before: query_params[:before] || query_params["before"]
}
|> calc_order()
end
defp calc_limit(query_params) do
Enum.min([query_params[:limit] || query_params["limit"] || 20, 100])
end
def calc_order(query_params) do
(query_params[:order] || query_params["order"])
|> case do
value when value in ["asc", "desc"] -> String.to_atom(value)
value when value in [:asc, :desc] -> value
_ -> :desc
end
defp calc_order(%{after: nil, before: cursor} = params) when not is_nil(cursor),
do: Map.put(params, :order, :asc)
defp calc_order(params),
do: Map.put(params, :order, :desc)
defp select_cursor(query) do
from([entity: entity] in query, select_merge: %{cursor: entity.local_id})
end
defp dynamic_where(query_params) do
true
|> starting_after_filter(query_params)
|> ending_before_filter(query_params)
|> after_filter(query_params)
|> before_filter(query_params)
end
defp starting_after_filter(dynamic, %{starting_after: nil}), do: dynamic
defp after_filter(dynamic, %{after: nil}), do: dynamic
defp starting_after_filter(dynamic, %{starting_after: id}) do
defp after_filter(dynamic, %{after: id}) when not is_nil(id) do
dynamic([entity: entity], entity.local_id < ^id and ^dynamic)
end
defp ending_before_filter(dynamic, %{ending_before: nil}), do: dynamic
defp before_filter(dynamic, %{before: nil}), do: dynamic
defp ending_before_filter(dynamic, %{ending_before: id}) do
defp before_filter(dynamic, %{before: id}) do
dynamic([entity: entity], entity.local_id > ^id and ^dynamic)
end
def meta(query_params, values) do
calc_prev_page(query_params, values)
|> Map.merge(calc_next_page(query_params, values))
defp collection_select_cursor(query) do
from([..., col] in query, select_merge: %{cursor: col.id})
end
defp collection_dynamic_where(query_params) do
true
|> collection_after_filter(query_params)
|> collection_before_filter(query_params)
end
defp collection_after_filter(dynamic, %{after: nil}), do: dynamic
defp collection_after_filter(dynamic, %{after: id}) when not is_nil(id) do
dynamic([..., c], c.id < ^id and ^dynamic)
end
defp collection_before_filter(dynamic, %{before: nil}), do: dynamic
defp collection_before_filter(dynamic, %{before: id}) do
dynamic([..., c], c.id > ^id and ^dynamic)
end
def meta(values, params) do
params = normalize_params(params)
%{
newer: calc_newer_page(params, values),
older: calc_older_page(params, values)
}
end
def with_meta(values, query_params) do
{values, meta(query_params, values)}
end
defp calc_prev_page(_, []), do: %{}
defp calc_prev_page(_, [%{local_id: id} | _]), do: %{previous_page: %{ending_before: id}}
defp calc_newer_page(%{order: :asc, limit: limit}, values) when length(values) < limit,
do: nil
defp calc_newer_page(%{order: :asc, limit: limit}, values) when length(values) >= limit,
do: List.last(values).cursor
defp calc_newer_page(%{order: :desc, after: nil}, _), do: nil
defp calc_newer_page(%{order: :desc, after: id}, []), do: id - 1
defp calc_newer_page(%{order: :desc}, [entity | _]),
do: entity.cursor
defp calc_next_page(%{limit: limit}, values) when length(values) >= limit,
do: %{next_page: %{starting_after: List.last(values).local_id}}
defp calc_next_page(_, _),
do: %{}
defp calc_older_page(%{order: :desc, limit: limit}, values) when length(values) < limit,
do: nil
defp calc_older_page(%{order: :desc, limit: limit}, values) when length(values) >= limit,
do: List.last(values).cursor
defp calc_older_page(%{order: :asc, before: nil}, _), do: nil
defp calc_older_page(%{order: :asc, before: id}, []), do: id + 1
defp calc_older_page(%{order: :asc}, [entity | _]), do: entity.cursor
end
......@@ -18,6 +18,11 @@ defmodule ActivityPub.SQL.Query do
|> to_entity()
end
def count(%Ecto.Query{} = query, opts \\ []) do
query
|> Repo.aggregate(:count, :local_id, opts)
end
# FIXME this should not be here?
def delete_all(%Ecto.Query{} = query) do
query
......@@ -44,13 +49,22 @@ defmodule ActivityPub.SQL.Query do
|> one()
end
def get_by_local_id(id, opts \\ []) when is_integer(id) do
def get_by_local_id(id, opts \\ [])
def get_by_local_id(id, opts) when is_integer(id) do
new()
|> where(local_id: id)
|> preload_aspect(Keyword.get(opts, :aspect, []))
|> one()
end
def get_by_local_id(ids, opts) when is_list(ids) do
from(e in new(),
where: e.local_id in ^ids
)
|> preload_aspect(Keyword.get(opts, :aspect, []))
|> all()
end
def get_by_id(id, opts \\ []) when is_binary(id) do
case UrlBuilder.get_local_id(id) do
{:ok, local_id} ->
......@@ -72,7 +86,11 @@ defmodule ActivityPub.SQL.Query do
end
def paginate(%Ecto.Query{} = query, opts \\ %{}) do
Paginate.call(query, opts)
Paginate.by_local_id(query, opts)
end
def paginate_collection(%Ecto.Query{} = query, opts \\ %{}) do
Paginate.by_collection_insert(query, opts)
end
def with_type(%Ecto.Query{} = query, type) when is_binary(type) do
......
......@@ -3,100 +3,273 @@ defmodule MoodleNet do
alias ActivityPub.SQL.Query
alias MoodleNet.Policy
require ActivityPub.Guards, as: APG
def list_communities(opts \\ %{}) do
# User connections
defp joined_communities_query(actor) do
Query.new()
|> Query.with_type("MoodleNet:Community")
|> Query.paginate(opts)
|> Query.belongs_to(:following, actor)
end
def joined_communities_list(actor, opts \\ %{}) do
joined_communities_query(actor)
|> Query.paginate_collection(opts)
|> Query.all()
end
def list_following_communities(actor, opts \\ %{}) do
def joined_communities_count(actor) do
joined_communities_query(actor)
|> Query.count()
end
defp following_collection_query(actor) do
Query.new()
|> Query.with_type("MoodleNet:Community")
|> Query.with_type("MoodleNet:Collection")
|> Query.belongs_to(:following, actor)
end
def following_collection_list(actor, opts \\ %{}) do
following_collection_query(actor)
|> Query.paginate_collection(opts)
|> Query.all()
end
def following_collection_count(actor) do
following_collection_query(actor)
|> Query.count()
end
defp community_thread_query(community) do
Query.new()
|> Query.with_type("Note")
|> Query.has(:context, community)
|> has_no_replies()
end
def community_thread_list(community, opts \\ %{}) do
community_thread_query(community)
|> Query.paginate(opts)
|> Query.all()
end
def list_communities_with_collection(collection, opts \\ %{}) do
def community_thread_count(community) do
community_thread_query(community)
|> Query.count()
end
# Community connections
def list_communities(opts \\ %{}) do
Query.new()
|> Query.with_type("MoodleNet:Community")
|> Query.has(:attributed_to, collection[:local_id])
|> Query.paginate(opts)
|> Query.all()
end
def list_collections(entity, opts \\ %{})
def list_collections(entity_id, opts) when is_integer(entity_id) do
defp community_collection_query(community) do
Query.new()
|> Query.with_type("MoodleNet:Collection")
|> Query.has(:attributed_to, entity_id)
|> Query.has(:attributed_to, community)
end
def community_collection_list(community, opts \\ %{}) do
community_collection_query(community)
|> Query.paginate(opts)
|> Query.all()
end
def list_collections(entity, opts) do
list_collections(entity[:local_id], opts)
def community_collection_count(community) do
community_collection_query(community)
|> Query.count()
end
def list_following_collections(actor, opts \\ %{}) do
# Collection connections
defp collection_follower_query(collection) do
Query.new()
|> Query.with_type("MoodleNet:Collection")
|> Query.belongs_to(:following, actor)
|> Query.paginate(opts)
|> Query.with_type("Person")
|> Query.belongs_to(:followers, collection)
end
def collection_follower_list(collection, opts \\ %{}) do
collection_follower_query(collection)
|> Query.paginate_collection(opts)
|> Query.all()
end
def list_resources(entity_id, opts \\ %{})
def collection_follower_count(collection) do
collection_follower_query(collection)
|> Query.count()
end
def list_resources(entity_id, opts) when is_integer(entity_id) do
defp collection_resource_query(collection) do
Query.new()
|> Query.with_type("MoodleNet:EducationalResource")
|> Query.has(:attributed_to, entity_id)
|> Query.paginate(opts)
|> Query.has(:attributed_to, collection)
end
def collection_resource_list(collection, opts \\ %{}) do
collection_resource_query(collection)
|> Query.paginate_collection(opts)
|> Query.all()
end
def list_resources(entity, opts) do
list_resources(ActivityPub.Entity.local_id(entity), opts)
def collection_resource_count(collection) do
collection_resource_query(collection)
|> Query.count()
end
def list_threads(context_id, opts \\ %{}) do
defp collection_thread_query(collection) do
Query.new()
|> Query.with_type("Note")
|> Query.has(:context, context_id)
|> Query.has(:context, collection)
|> has_no_replies()
end
def collection_thread_list(collection, opts \\ %{}) do
collection_thread_query(collection)
|> Query.paginate(opts)
|> Query.all()
end
def collection_thread_count(collection) do
collection_thread_query(collection)
|> Query.count()
end
defp collection_liker_query(collection) do
Query.new()
|> Query.with_type("Person")
|> Query.belongs_to(:likers, collection)
end
def collection_liker_list(collection, opts \\ %{}) do
collection_liker_query(collection)
|> Query.paginate_collection(opts)
|> Query.all()
end
def collection_liker_count(collection) do
collection_liker_query(collection)
|> Query.count()
end
# Resource connections
defp resource_liker_query(resource) do
Query.new()
|> Query.with_type("Person")
|> Query.belongs_to(:likers, resource)
end
def resource_liker_list(resource, opts \\ %{}) do
resource_liker_query(resource)
|> Query.paginate_collection(opts)
|> Query.all()
end
def resource_liker_count(resource) do
resource_liker_query(resource)
|> Query.count()
end
# Comment connections
defp comment_liker_query(comment) do
Query.new()
|> Query.with_type("Person")
|> Query.belongs_to(:likers, comment)
end
def comment_liker_list(comment, opts \\ %{}) do
comment_liker_query(comment)
|> Query.paginate_collection(opts)
|> Query.all()
end
def comment_liker_count(comment) do
comment_liker_query(comment)
|> Query.count()
end
defp comment_reply_query(comment) do
Query.new()
|> Query.with_type("Note")
|> Query.has(:in_reply_to, comment)
end
def comment_reply_list(comment, opts \\ %{}) do
comment_reply_query(comment)
|> Query.paginate_collection(opts)
|> Query.all()
end
def comment_reply_count(comment) do
comment_reply_query(comment)
|> Query.count()
end
def list_resources(entity_id, opts \\ %{})
def list_resources(entity_id, opts) when is_integer(entity_id) do
Query.new()
|> Query.with_type("MoodleNet:EducationalResource")
|> Query.has(:attributed_to, entity_id)
|> Query.paginate(opts)
|> Query.all()
end
def page_info(results, opts) do
ActivityPub.SQL.Paginate.meta(results, opts)
end
defp has_no_replies(query) do
import Ecto.Query, only: [from: 2]
from([entity: entity] in query,
left_join: rel in fragment("activity_pub_object_in_reply_tos"),
on: entity.local_id == rel.subject_id,
where: is_nil(rel.target_id)
left_join: rel in fragment("activity_pub_object_in_reply_tos"),
on: entity.local_id == rel.subject_id,
where: is_nil(rel.target_id)
)
end
def list_comments(context_id, opts \\ %{}) do
defp user_comment_query(actor) do
Query.new()
|> Query.with_type("Note")
|> Query.has(:context, context_id)
|> Query.has(:attributed_to, actor)
end
def user_comment_list(actor, opts \\ %{}) do
user_comment_query(actor)
|> Query.paginate(opts)
|> Query.all()
end
def list_replies(in_reply_to_id, opts \\ %{}) do
def user_comment_count(actor) do
user_comment_query(actor)
|> Query.count()
end
defp community_member_query(community) do
Query.new()
|> Query.with_type("Note")
|> Query.has(:in_reply_to, in_reply_to_id)
|> Query.paginate(opts)
|> Query.with_type("Person")
|> Query.has(:following, community)
end
def community_member_list(community, opts \\ %{})
when APG.has_type(community, "MoodleNet:Community") do
community_member_query(community)
|> Query.paginate_collection(opts)
|> Query.all()
end
def community_member_count(community)
when APG.has_type(community, "MoodleNet:Community") do
community_member_query(community)
|> Query.count()
end
def create_community(actor, attrs) do
attrs = Map.put(attrs, "type", "MoodleNet:Community")
......@@ -339,6 +512,18 @@ defmodule MoodleNet do
end
end
def like_collection(actor, collection)
when has_type(actor, "Person") and has_type(collection, "MoodleNet:Collection") do
collection = preload_community(collection)
attrs = %{type: "Like", actor: actor, object: collection}
with :ok <- Policy.like_collection?(actor, collection, attrs),
{:ok, activity} = ActivityPub.new(attrs),
{:ok, _activity} <- ActivityPub.apply(activity) do
{:ok, true}
end
end
def like_resource(actor, resource)
when has_type(actor, "Person") and has_type(resource, "MoodleNet:EducationalResource") do
resource = preload_community(resource)
......
......@@ -36,6 +36,12 @@ defmodule MoodleNet.Policy do
actor_follows!(actor, community)
end
def like_collection?(actor, collection, _attrs)
when has_type(collection, "MoodleNet:Collection") and has_type(actor, "Person") do
community = get_community(collection)
actor_follows!(actor, community)
end
defp actor_follows!(actor, object) do
if Query.has?(actor, :following, object), do: :ok, else: {:error, :forbidden}
end
......
......@@ -8,7 +8,10 @@ defmodule MoodleNetWeb.GraphQL.Schema do
query do
@desc "Get list of communities"
field :communities, non_null(list_of(non_null(:community))) do
field :communities, non_null(:community_page) do
arg(:limit, :integer)
arg(:before, :integer)
arg(:after, :integer)
resolve(&MoodleNetSchema.list_communities/2)
end
......@@ -18,58 +21,18 @@ defmodule MoodleNetWeb.GraphQL.Schema do
resolve(MoodleNetSchema.resolve_by_id_and_type("MoodleNet:Community"))
end
@desc "Get list of following communities"
field :following_communities, non_null(list_of(non_null(:community))) do
resolve(&MoodleNetSchema.list_following_communities/2)
end
@desc "Get list of collections"
field :collections, non_null(list_of(non_null(:collection))) do
arg(:community_local_id, non_null(:integer))
resolve(&MoodleNetSchema.list_collections/2)
end
@desc "Get a collection"
field :collection, :collection do
arg(:local_id, non_null(:integer))
resolve(MoodleNetSchema.resolve_by_id_and_type("MoodleNet:Collection"))
end
@desc "Get list of following collections"
field :following_collections, non_null(list_of(non_null(:collection))) do
resolve(&MoodleNetSchema.list_following_collections/2)
end
@desc "Get list of resources"
field :resources, non_null(list_of(non_null(:resource))) do
arg(:collection_local_id, non_null(:integer))
resolve(&MoodleNetSchema.list_resources/2)
end
@desc "Get a resource"
field :resource, :resource do
arg(:local_id, non_null(:integer))
resolve(MoodleNetSchema.resolve_by_id_and_type("MoodleNet:EducationalResource"))
end
@desc "Get list of threads"
field :threads, non_null(list_of(non_null(:comment))) do
arg(:context_local_id, non_null(:integer))
resolve(&MoodleNetSchema.list_threads/2)
end
@desc "Get list of comments"
field :comments, non_null(list_of(non_null(:comment))) do
arg(:context_local_id, non_null(:integer))
resolve(&MoodleNetSchema.list_comments/2)
end
@desc "Get list of replies"
field :replies, non_null(list_of(non_null(:comment))) do
arg(:in_reply_to_local_id, non_null(:integer))
resolve(&MoodleNetSchema.list_replies/2)
end
@desc "Get a comment"
field :comment, :comment do
arg(:local_id, non_null(:integer))
......@@ -80,6 +43,12 @@ defmodule MoodleNetWeb.GraphQL.Schema do
field :me, type: :me do
resolve(&MoodleNetSchema.me/2)
end
@desc "Get an user"
field :user, type: :user do
arg(:local_id, non_null(:integer))
resolve(&MoodleNetSchema.user/2)
end
end
mutation do
......@@ -222,6 +191,12 @@ defmodule MoodleNetWeb.GraphQL.Schema do
resolve(&MoodleNetSchema.like_resource/2)
end
@desc "Like a collection"
field :like_collection, type: :boolean do
arg(:local_id, non_null(:integer))
resolve(&MoodleNetSchema.like_collection/2)
end
@desc "Undo a previous like to a comment"
field :undo_like_comment, type: :boolean do
arg(:local_id, non_null(:integer))
......@@ -234,6 +209,12 @@ defmodule MoodleNetWeb.GraphQL.Schema do
resolve(&MoodleNetSchema.undo_like_resource/2)
end
@desc "Undo a previous like to a collection"
field :undo_like_collection, type: :boolean do
arg(:local_id, non_null(:integer))
resolve(&MoodleNetSchema.undo_like_collection/2)
end
@desc "Login"
field :create_session, type: :auth_payload do
arg(:email, non_null(:string))
......
defmodule MoodleNetWeb.GraphQL.CollectionSchema do
use Absinthe.Schema.Notation
alias MoodleNetWeb.GraphQL.MoodleNetSchema, as: Resolver
object :collection do
field(:id, :string)
field(:local_id, :integer)
field(:local, :boolean)
field(:type, list_of(:string))
field(:name, :string)
field(:content, :string)
field(:summary, :string)
field(:preferred_username, :string)
field(:icon, :string)
field(:primary_language, :string)
field(:community, non_null(:community), do: resolve(Resolver.with_assoc(:attributed_to, single: true)))
field :followers, non_null(:collection_followers_connection) do
arg(:limit, :integer)
arg(:before, :integer)
arg(:after, :integer)
resolve(Resolver.with_connection(:collection_follower))
end
field :resources, non_null(:collection_resources_connection) do
arg(:limit, :integer)
arg(:before, :integer)
arg(:after, :integer)
resolve(Resolver.with_connection(:collection_resource))
end
field :threads, non_null(:collection_threads_connection) do
arg(:limit, :integer)
arg(:before, :integer)
arg(:after, :integer)
resolve(Resolver.with_connection(:collection_thread))
end
field :likers, non_null(:collection_likers_connection) do
arg(:limit, :integer)
arg(:before, :integer)
arg(:after, :integer)
resolve(Resolver.with_connection(:collection_liker))
end
field(:published, :string)
field(:updated, :string)
field(:followed, non_null(:boolean), do: resolve(Resolver.with_bool_join(:follow)))
end