Commit 68242434 authored by Alex Castaño's avatar Alex Castaño

Merge branch 'release/0.0.8'

parents 846ce0ca 2eedea1d
...@@ -29,9 +29,19 @@ build_with_cache: ## Build the Docker image ...@@ -29,9 +29,19 @@ build_with_cache: ## Build the Docker image
-t moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD) . -t moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD) .
@echo moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD) @echo moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD)
tag_latest: tag:
@echo docker tag moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD) moodlenet/moodlenet:latest @echo docker tag moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD) moodlenet/moodlenet:latest
docker tag moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD) moodlenet/moodlenet:latest @docker tag moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD) moodlenet/moodlenet:latest
@echo docker tag moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD) moodlenet/moodlenet:$(APP_VSN)
@docker tag moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD) moodlenet/moodlenet:$(APP_VSN)
push:
@echo docker push moodlenet/moodlenet:latest
@docker push moodlenet/moodlenet:latest
@echo docker push moodlenet/moodlenet:$(APP_VSN)
@docker push moodlenet/moodlenet:$(APP_VSN)
@echo docker push moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD)
@docker push moodlenet/moodlenet:$(APP_VSN)-$(APP_BUILD)
run: ## Run the app in Docker run: ## Run the app in Docker
docker run\ docker run\
......
...@@ -41,5 +41,8 @@ defmodule ActivityPub.ObjectAspect do ...@@ -41,5 +41,8 @@ defmodule ActivityPub.ObjectAspect do
# adding because it is easier # adding because it is easier
assoc(:likers) assoc(:likers)
field(:likers_count, :integer, autogenerated: true) field(:likers_count, :integer, autogenerated: true)
field(:followed, :boolean, virtual: true)
field(:liked, :boolean, virtual: true)
end end
end end
...@@ -5,7 +5,8 @@ defmodule ActivityPub.Field do ...@@ -5,7 +5,8 @@ defmodule ActivityPub.Field do
functional: true, functional: true,
type: nil, type: nil,
default: nil, default: nil,
autogenerated: false autogenerated: false,
virtual: false
def build(opts) do def build(opts) do
opts = add_default_value(opts) opts = add_default_value(opts)
......
...@@ -52,7 +52,9 @@ defmodule ActivityPub.SQL.Query do ...@@ -52,7 +52,9 @@ defmodule ActivityPub.SQL.Query do
def get_by_id(id, opts \\ []) when is_binary(id) do def get_by_id(id, opts \\ []) when is_binary(id) do
case UrlBuilder.get_local_id(id) do case UrlBuilder.get_local_id(id) do
{:ok, local_id} -> get_by_local_id(local_id, opts) {:ok, local_id} ->
get_by_local_id(local_id, opts)
:error -> :error ->
new() new()
|> where(id: id) |> where(id: id)
......
defmodule ActivityPub.SQLAspect do defmodule ActivityPub.SQLAspect do
alias ActivityPub.{SQLObjectAspect, SQLActorAspect, SQLActivityAspect, SQLCollectionAspect, SQLResourceAspect} alias ActivityPub.{
SQLObjectAspect,
SQLActorAspect,
SQLActivityAspect,
SQLCollectionAspect,
SQLResourceAspect
}
alias ActivityPub.SQL.Associations.{ManyToMany, BelongsTo, Collection} alias ActivityPub.SQL.Associations.{ManyToMany, BelongsTo, Collection}
def all(), do: [SQLObjectAspect, SQLActorAspect, SQLActivityAspect, SQLCollectionAspect, SQLResourceAspect] def all(),
do: [
SQLObjectAspect,
SQLActorAspect,
SQLActivityAspect,
SQLCollectionAspect,
SQLResourceAspect
]
# FIXME make this similar to aspect where the user can redifine # FIXME make this similar to aspect where the user can redifine
# assocs and fields to be persisted in another way than the default! # assocs and fields to be persisted in another way than the default!
...@@ -106,7 +119,12 @@ defmodule ActivityPub.SQLAspect do ...@@ -106,7 +119,12 @@ defmodule ActivityPub.SQLAspect do
type = if field_def.functional, do: field_def.type, else: {:array, field_def.type} type = if field_def.functional, do: field_def.type, else: {:array, field_def.type}
field(name, type) opts =
field_def
|> Map.take([:virtual, :default])
|> Keyword.new()
field(name, type, opts)
end end
end end
end end
......
...@@ -2,6 +2,8 @@ defmodule MoodleNet do ...@@ -2,6 +2,8 @@ defmodule MoodleNet do
import ActivityPub.Guards import ActivityPub.Guards
alias ActivityPub.SQL.Query alias ActivityPub.SQL.Query
alias MoodleNet.Policy
def list_communities(opts \\ %{}) do def list_communities(opts \\ %{}) do
Query.new() Query.new()
|> Query.with_type("MoodleNet:Community") |> Query.with_type("MoodleNet:Community")
...@@ -45,6 +47,24 @@ defmodule MoodleNet do ...@@ -45,6 +47,24 @@ defmodule MoodleNet do
list_resources(ActivityPub.Entity.local_id(entity), opts) list_resources(ActivityPub.Entity.local_id(entity), opts)
end end
def list_threads(context_id, opts \\ %{}) do
Query.new()
|> Query.with_type("Note")
|> Query.has(:context, context_id)
|> has_no_replies()
|> Query.paginate(opts)
|> Query.all()
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)
)
end
def list_comments(context_id, opts \\ %{}) do def list_comments(context_id, opts \\ %{}) do
Query.new() Query.new()
|> Query.with_type("Note") |> Query.with_type("Note")
...@@ -61,11 +81,13 @@ defmodule MoodleNet do ...@@ -61,11 +81,13 @@ defmodule MoodleNet do
|> Query.all() |> Query.all()
end end
def create_community(attrs) do def create_community(actor, attrs) do
attrs = Map.put(attrs, "type", "MoodleNet:Community") attrs = Map.put(attrs, "type", "MoodleNet:Community")
with {:ok, entity} <- ActivityPub.new(attrs) do with {:ok, entity} <- ActivityPub.new(attrs),
ActivityPub.insert(entity) {:ok, entity} <- ActivityPub.insert(entity),
{:ok, true} <- MoodleNet.join_community(actor, entity) do
{:ok, entity}
end end
end end
...@@ -121,14 +143,18 @@ defmodule MoodleNet do ...@@ -121,14 +143,18 @@ defmodule MoodleNet do
:ok :ok
end end
def create_collection(community, attrs) when has_type(community, "MoodleNet:Community") do def create_collection(actor, community, attrs)
when has_type(community, "MoodleNet:Community") do
attrs = attrs =
attrs attrs
|> Map.put(:type, "MoodleNet:Collection") |> Map.put(:type, "MoodleNet:Collection")
|> Map.put(:attributed_to, [community]) |> Map.put(:attributed_to, [community])
with {:ok, entity} <- ActivityPub.new(attrs) do with :ok <- Policy.create_collection?(actor, community, attrs),
ActivityPub.insert(entity) {:ok, entity} <- ActivityPub.new(attrs),
{:ok, entity} <- ActivityPub.insert(entity),
{:ok, true} <- follow_collection(actor, entity) do
{:ok, entity}
end end
end end
...@@ -158,14 +184,17 @@ defmodule MoodleNet do ...@@ -158,14 +184,17 @@ defmodule MoodleNet do
:ok :ok
end end
def create_resource(_actor, collection, attrs) def create_resource(actor, collection, attrs)
when has_type(collection, "MoodleNet:Collection") do when has_type(collection, "MoodleNet:Collection") do
attrs = attrs =
attrs attrs
|> Map.put(:type, "MoodleNet:EducationalResource") |> Map.put(:type, "MoodleNet:EducationalResource")
|> Map.put(:attributed_to, [collection]) |> Map.put(:attributed_to, [collection])
with {:ok, entity} <- ActivityPub.new(attrs) do collection = Query.preload_assoc(collection, :attributed_to)
with :ok <- Policy.create_resource?(actor, collection, attrs),
{:ok, entity} <- ActivityPub.new(attrs) do
ActivityPub.insert(entity) ActivityPub.insert(entity)
end end
end end
...@@ -186,9 +215,10 @@ defmodule MoodleNet do ...@@ -186,9 +215,10 @@ defmodule MoodleNet do
end end
def copy_resource(actor, resource, collection) do def copy_resource(actor, resource, collection) do
resource = resource resource =
|> Query.preload_aspect(:resource) resource
|> Query.preload_assoc([:icon]) |> Query.preload_aspect(:resource)
|> Query.preload_assoc([:icon])
attrs = attrs =
Map.take(resource, [ Map.take(resource, [
...@@ -210,6 +240,7 @@ defmodule MoodleNet do ...@@ -210,6 +240,7 @@ defmodule MoodleNet do
:time_required, :time_required,
:typical_age_range :typical_age_range
]) ])
url = get_in(resource, [:icon, Access.at(0), :url]) url = get_in(resource, [:icon, Access.at(0), :url])
attrs = Map.put(attrs, :icon, %{type: "Image", url: url}) attrs = Map.put(attrs, :icon, %{type: "Image", url: url})
create_resource(actor, collection, attrs) create_resource(actor, collection, attrs)
...@@ -218,6 +249,8 @@ defmodule MoodleNet do ...@@ -218,6 +249,8 @@ defmodule MoodleNet do
def create_thread(author, context, attrs) def create_thread(author, context, attrs)
when has_type(author, "Person") and has_type(context, "MoodleNet:Community") when has_type(author, "Person") and has_type(context, "MoodleNet:Community")
when has_type(author, "Person") and has_type(context, "MoodleNet:Collection") do when has_type(author, "Person") and has_type(context, "MoodleNet:Collection") do
context = preload_community(context)
attrs attrs
|> Map.put(:context, [context]) |> Map.put(:context, [context])
|> Map.put(:attributed_to, [author]) |> Map.put(:attributed_to, [author])
...@@ -226,7 +259,11 @@ defmodule MoodleNet do ...@@ -226,7 +259,11 @@ defmodule MoodleNet do
def create_reply(author, in_reply_to, attrs) def create_reply(author, in_reply_to, attrs)
when has_type(author, "Person") and has_type(in_reply_to, "Note") do when has_type(author, "Person") and has_type(in_reply_to, "Note") do
context = Query.new() |> Query.belongs_to(:context, in_reply_to) |> Query.one() context =
Query.new()
|> Query.belongs_to(:context, in_reply_to)
|> Query.one()
|> preload_community()
attrs attrs
|> Map.put(:context, [context]) |> Map.put(:context, [context])
...@@ -237,8 +274,11 @@ defmodule MoodleNet do ...@@ -237,8 +274,11 @@ defmodule MoodleNet do
defp create_comment(attrs) do defp create_comment(attrs) do
attrs = attrs |> Map.put("type", "Note") attrs = attrs |> Map.put("type", "Note")
[context] = attrs[:context]
[actor] = attrs[:attributed_to]
with {:ok, entity} <- ActivityPub.new(attrs) do with :ok <- Policy.create_comment?(actor, context, attrs),
{:ok, entity} <- ActivityPub.new(attrs) do
ActivityPub.insert(entity) ActivityPub.insert(entity)
end end
end end
...@@ -251,8 +291,19 @@ defmodule MoodleNet do ...@@ -251,8 +291,19 @@ defmodule MoodleNet do
end end
end end
def follow(follower, following) do def join_community(actor, community)
params = %{type: "Follow", actor: follower, object: following} when has_type(actor, "Person") and has_type(community, "MoodleNet:Community") do
params = %{type: "Follow", actor: actor, object: community}
with {:ok, activity} = ActivityPub.new(params),
{:ok, _activity} <- ActivityPub.apply(activity) do
{:ok, true}
end
end
def follow_collection(actor, collection)
when has_type(actor, "Person") and has_type(collection, "MoodleNet:Collection") do
params = %{type: "Follow", actor: actor, object: collection}
with {:ok, activity} = ActivityPub.new(params), with {:ok, activity} = ActivityPub.new(params),
{:ok, _activity} <- ActivityPub.apply(activity) do {:ok, _activity} <- ActivityPub.apply(activity) do
...@@ -260,6 +311,30 @@ defmodule MoodleNet do ...@@ -260,6 +311,30 @@ defmodule MoodleNet do
end end
end end
def like_comment(actor, comment)
when has_type(actor, "Person") and has_type(comment, "Note") do
comment = preload_community(comment)
attrs = %{type: "Like", actor: actor, object: comment}
with :ok <- Policy.like_comment?(actor, comment, 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)
attrs = %{type: "Like", actor: actor, object: resource}
with :ok <- Policy.like_resource?(actor, resource, attrs),
{:ok, activity} = ActivityPub.new(attrs),
{:ok, _activity} <- ActivityPub.apply(activity) do
{:ok, true}
end
end
def like(liker, liked) do def like(liker, liked) do
params = %{type: "Like", actor: liker, object: liked} params = %{type: "Like", actor: liker, object: liked}
...@@ -290,9 +365,13 @@ defmodule MoodleNet do ...@@ -290,9 +365,13 @@ defmodule MoodleNet do
end end
defp find_current_relation(subject, relation, object) do defp find_current_relation(subject, relation, object) do
if Query.has?(subject, relation, object), if Query.has?(subject, relation, object) do
do: :ok, :ok
else: {:error, {:not_found, nil, "Activity"}} else
subject_id = ActivityPub.Entity.local_id(subject)
object_id = ActivityPub.Entity.local_id(object)
{:error, {:not_found, [subject_id, object_id], "Activity"}}
end
end end
defp find_activity(type, actor, object) do defp find_activity(type, actor, object) do
...@@ -303,11 +382,30 @@ defmodule MoodleNet do ...@@ -303,11 +382,30 @@ defmodule MoodleNet do
|> Query.last() |> Query.last()
|> case do |> case do
nil -> nil ->
{:error, {:not_found, nil, "Activity"}} actor_id = ActivityPub.Entity.local_id(actor)
object_id = ActivityPub.Entity.local_id(object)
{:error, {:not_found, [actor_id, object_id], "Activity"}}
activity -> activity ->
activity = Query.preload_assoc(activity, actor: {[:actor], []}, object: {[:actor], []}) activity = Query.preload_assoc(activity, actor: {[:actor], []}, object: {[:actor], []})
{:ok, activity} {:ok, activity}
end end
end end
defp preload_community(community) when has_type(community, "MoodleNet:Community"),
do: community
defp preload_community(collection) when has_type(collection, "MoodleNet:Collection"),
do: Query.preload_assoc(collection, :attributed_to)
defp preload_community(resource) when has_type(resource, "MoodleNet:EducationalResource") do
Query.preload_assoc(resource, attributed_to: :attributed_to)
end
defp preload_community(comment) when has_type(comment, "Note") do
comment = Query.preload_assoc(comment, :context)
[context] = comment.context
context = preload_community(context)
%{comment | context: [context]}
end
end end
...@@ -218,6 +218,7 @@ defmodule MoodleNet.Accounts do ...@@ -218,6 +218,7 @@ defmodule MoodleNet.Accounts do
end end
def is_email_in_whitelist?(email) do def is_email_in_whitelist?(email) do
Repo.get(WhitelistEmail, email) != nil String.ends_with?(email, "@moodle.com") ||
Repo.get(WhitelistEmail, email) != nil
end end
end end
...@@ -44,7 +44,7 @@ defmodule MoodleNet.Factory do ...@@ -44,7 +44,7 @@ defmodule MoodleNet.Factory do
"icon" => attributes(:image), "icon" => attributes(:image),
"preferred_username" => Faker.Internet.user_name(), "preferred_username" => Faker.Internet.user_name(),
"summary" => Faker.Lorem.sentence(), "summary" => Faker.Lorem.sentence(),
"primaryLanguage" => "es", "primary_language" => "es",
} }
end end
...@@ -55,7 +55,7 @@ defmodule MoodleNet.Factory do ...@@ -55,7 +55,7 @@ defmodule MoodleNet.Factory do
"url" => Faker.Internet.url(), "url" => Faker.Internet.url(),
"summary" => Faker.Lorem.sentence(), "summary" => Faker.Lorem.sentence(),
"icon" => attributes(:image), "icon" => attributes(:image),
"primaryLanguage" => "es", "primary_language" => "es",
"same_as" => "https://hq.moodle.net/r/98765", "same_as" => "https://hq.moodle.net/r/98765",
"in_language" => ["en-GB"], "in_language" => ["en-GB"],
"public_access" => true, "public_access" => true,
...@@ -131,21 +131,21 @@ defmodule MoodleNet.Factory do ...@@ -131,21 +131,21 @@ defmodule MoodleNet.Factory do
app app
end end
def community(attrs \\ %{}) do def community(actor, attrs \\ %{}) do
attrs = attributes(:community, attrs) attrs = attributes(:community, attrs)
{:ok, c} = MoodleNet.create_community(attrs) {:ok, c} = MoodleNet.create_community(actor, attrs)
c c
end end
def collection(community, attrs \\ %{}) do def collection(actor, community, attrs \\ %{}) do
attrs = attributes(:collection, attrs) attrs = attributes(:collection, attrs)
{:ok, c} = MoodleNet.create_collection(community, attrs) {:ok, c} = MoodleNet.create_collection(actor, community, attrs)
c c
end end
def resource(context, attrs \\ %{}) do def resource(actor, context, attrs \\ %{}) do
attrs = attributes(:resource, attrs) attrs = attributes(:resource, attrs)
{:ok, c} = MoodleNet.create_resource(nil, context, attrs) {:ok, c} = MoodleNet.create_resource(actor, context, attrs)
c c
end end
......
defmodule MoodleNet.Policy do
import ActivityPub.Guards
alias ActivityPub.SQL.Query
def create_collection?(actor, community, _attrs)
when has_type(community, "MoodleNet:Community") and has_type(actor, "Person") do
actor_follows!(actor, community)
end
def create_resource?(actor, collection, _attrs)
when has_type(collection, "MoodleNet:Collection") and has_type(actor, "Person") do
community = get_community(collection)
actor_follows!(actor, community)
end
def create_comment?(actor, community, _attrs)
when has_type(community, "MoodleNet:Community") and has_type(actor, "Person") do
actor_follows!(actor, community)
end
def create_comment?(actor, collection, _attrs)
when has_type(collection, "MoodleNet:Collection") and has_type(actor, "Person") do
community = get_community(collection)
actor_follows!(actor, community)
end
def like_comment?(actor, comment, _attrs)
when has_type(comment, "Note") and has_type(actor, "Person") do
community = get_community(comment)
actor_follows!(actor, community)
end
def like_resource?(actor, resource, _attrs)
when has_type(resource, "MoodleNet:EducationalResource") and has_type(actor, "Person") do
community = get_community(resource)
actor_follows!(actor, community)
end
defp actor_follows!(actor, object) do
if Query.has?(actor, :following, object), do: :ok, else: {:error, :forbidden}
end
defp get_community(comment) when has_type(comment, "Note") do
[context] = comment.context
get_community(context)
end
defp get_community(resource) when has_type(resource, "MoodleNet:EducationalResource") do
[collection] = resource.attributed_to
get_community(collection)
end
defp get_community(collection) when has_type(collection, "MoodleNet:Collection") do
[community] = collection.attributed_to
community
end
defp get_community(community) when has_type(community, "MoodleNet:Community"), do: community
end
...@@ -32,8 +32,6 @@ defmodule MoodleNet.ReleaseTasks do ...@@ -32,8 +32,6 @@ defmodule MoodleNet.ReleaseTasks do
def migrate_db(_) do def migrate_db(_) do
start_apps() start_apps()
Enum.each(@repos, &create_repo/1)
start_repos() start_repos()
Enum.each(@repos, &migrate_repo/1) Enum.each(@repos, &migrate_repo/1)
...@@ -65,7 +63,6 @@ defmodule MoodleNet.ReleaseTasks do ...@@ -65,7 +63,6 @@ defmodule MoodleNet.ReleaseTasks do
def seed_db(_) do def seed_db(_) do
start_apps() start_apps()
Enum.each(@repos, &create_repo/1)
start_repos() start_repos()
......
...@@ -42,6 +42,12 @@ defmodule MoodleNetWeb.GraphQL.Schema do ...@@ -42,6 +42,12 @@ defmodule MoodleNetWeb.GraphQL.Schema do
resolve(MoodleNetSchema.resolve_by_id_and_type("MoodleNet:EducationalResource")) resolve(MoodleNetSchema.resolve_by_id_and_type("MoodleNet:EducationalResource"))
end 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" @desc "Get list of comments"
field :comments, non_null(list_of(non_null(:comment))) do field :comments, non_null(list_of(non_null(:comment))) do
arg(:context_local_id, non_null(:integer)) arg(:context_local_id, non_null(:integer))
...@@ -170,28 +176,52 @@ defmodule MoodleNetWeb.GraphQL.Schema do ...@@ -170,28 +176,52 @@ defmodule MoodleNetWeb.GraphQL.Schema do
resolve(&MoodleNetSchema.delete_user/2) resolve(&MoodleNetSchema.delete_user/2)
end end
@desc "Follow an actor" @desc "Join a community"
field :follow, type: :boolean do field :join_community, type: :boolean do
arg(:actor_local_id, non_null(:integer)) arg(:community_local_id, non_null(:integer))
resolve(&MoodleNetSchema.create_follow/2) resolve(&MoodleNetSchema.join_community/2)
end end
@desc "Unfollow an actor" @desc "Undo join a community"
field :unfollow, type: :boolean do field :undo_join_community, type: :boolean do
arg(:actor_local_id, non_null(:integer)) arg(:community_local_id, non_null(:integer))
resolve(&MoodleNetSchema.destroy_follow/2) resolve(&MoodleNetSchema.undo_join_community/2)
end
@desc "Follow a collection"
field :follow_collection, type: :boolean do
arg(:collection_local_id, non_null(:integer))
resolve(&MoodleNetSchema.follow_collection/2)
end
@desc "Undo follow a collection"
field :undo_follow_collection, type: :boolean do
arg(:collection_local_id, non_null(:integer))
resolve(&MoodleNetSchema.undo_follow_collection/2)
end
@desc "Like a comment"
field :like_comment, type: :boolean do
arg(:local_id, non_null(:integer))
resolve(&MoodleNetSchema.like_comment/2)
end
@desc "Like a resource"
field :like_resource, type: :boolean do
arg(:local_id, non_null(:integer))
resolve(&MoodleNetSchema.like_resource/2)
end end
@desc "Like an object" @desc "Undo a previous like to a comment"
field :like, type: :boolean do field :undo_like_comment, type: :boolean do
arg(:local_id, non_null(:integer)) arg(:local_id, non_null(:integer))
resolve(&MoodleNetSchema.create_like/2) resolve(&MoodleNetSchema.undo_like_comment/2)
end end
@desc "Unlike an object" @desc "Undo a previous like to a resource"
field :unlike, type: :boolean do field :undo_like_resource, type: :boolean do
arg(:local_id, non_null(:integer)) arg(:local_id, non_null(:integer))
resolve(&MoodleNetSchema.destroy_like/2) resolve(&MoodleNetSchema.undo_like_resource/2)
end end
@desc "Login" @desc "Login"
......
...@@ -108,6 +108,8 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do ...@@ -108,6 +108,8 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do
field(:published, :string) field(:published, :string)
field(:updated, :string) field(:updated, :string)
field(:followed, non_null(:boolean), do: resolve(with_bool_join(:follow)))
end end
input_object :community_input do input_object :community_input do
...@@ -158,6 +160,8 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do ...@@ -158,6 +160,8 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do