Commit 897a0792 authored by Alex Castaño's avatar Alex Castaño

Create an architecture doc and some comments in schemas

parent 6b6a63e7
Pipeline #32022337 failed with stages
in 3 minutes and 49 seconds
......@@ -30,3 +30,5 @@ config/secret.exs
# Editor config
/.vscode
cover
priv/repo/structure.sql
# Alex' Thoughts 04/10/18
## Applications and Libraries
What is a Elixir/Erlang application, from elixir docs https://hexdocs.pm/elixir/Application.html:
> Applications are the idiomatic way to package software in Erlang/OTP. To get the idea, they are similar to the “library” concept common in other programming languages, but with some additional characteristics.
An application is a component implementing some specific functionality, with a standardized directory structure, configuration, and lifecycle. Applications are loaded, started, and stopped.
We currently have only a single application, Pleroma.
I don't think we need more in the short or medium term.
However, we should split `lib/pleroma` directory in at least two directories:
* `lib/pleroma` where will live the core of our application
* `lib/pleroma_web` where will live the phoenix stuff
This is the new convention in Phoenix.
It tries to split the application logic and the web interface.
The web is an interface, but any application could have more than one.
We could add a command line interface or GraphQL API.
If we keep the logic in the web, reusing code will be much harder.
In later steps we could add more directories, and therefore, more decoupling.
So `lib/pleroma` will be a real application.
It will define an `Application` module.
However, `lib/pleroma_web` does not define an `Application` module,
it won't load, start and stop.
This will be done by `Pleroma`,
so `PleromaWeb` should not be called `application`.
In absence of a better terminology, we'll name it "library".
So, `PleromaWeb` will receive HTTP request from clients and other servers and
it will make calls to `Pleroma` application.
This means `Pleroma` should know nothing about HTTP
and `PleromaWeb` should know nothing about Repo or databases.
The main task `PleromaWeb` will be "translate" Pleroma terminology and send it using HTTP protocol.
# Contexts
It is interesting that currently we are not using contexts, at least, as explained in Phoenix.
The current convention of Phoenix would be:
```
lib
├── pleroma_web
│ ├── channels
│ │ └── user_socket.ex
│ ├── controllers
│ │ ├── page_controller.ex
│ │ └── user_controller.ex
│ ├── endpoint.ex
│ ├── gettext.ex
│ ├── router.ex
│ ├── templates
│ │ ├── layout
│ │ │ └── app.html.eex
│ │ ├── page
│ │ │ └── index.html.eex
│ │ └── user
│ │ ├── edit.html.eex
│ │ ├── form.html.eex
│ │ ├── index.html.eex
│ │ ├── new.html.eex
│ │ └── show.html.eex
│ └── views
│ ├── error_helpers.ex
│ ├── error_view.ex
│ ├── layout_view.ex
│ ├── page_view.ex
│ └── user_view.ex
└── pleroma_web.ex
```
I think current convention in Phoenix is bad and it is an inheritance of Ruby on Rails.
I prefer to create contexts,
very similar to what we are doing the now in:
```
lib/pleroma/web
├── mastodon_api
│ ├── mastodon_api_controller.ex
│ ├── mastodon_api.ex
│ ├── mastodon_socket.ex
│ └── views
│ ├── account_view.ex
│ ├── filter_view.ex
│ ├── list_view.ex
│ ├── mastodon_view.ex
│ └── status_view.ex
├── oauth
│   ├── app.ex
│   ├── authorization.ex
│   ├── fallback_controller.ex
│   ├── oauth_controller.ex
│   ├── oauth_view.ex
│   └── token.ex
├── ostatus
│   ├── activity_representer.ex
│   ├── feed_representer.ex
│   ├── handlers
│   │   ├── delete_handler.ex
│   │   ├── follow_handler.ex
│   │   ├── note_handler.ex
│   │   └── unfollow_handler.ex
│   ├── ostatus_controller.ex
│   ├── ostatus.ex
│   └── user_representer.ex
├── router.ex
```
Keeping this in mind, it is very interesting to read the (Phoenix documentation about this)[https://hexdocs.pm/phoenix/contexts.html#content]
The main point is not creating too many applications or libraries just to separates concerns or application logic.
A context has a public API which should be used by other modules or applications, ie:
* `Pleroma.Actors` is a context main module and it is where the public API.
* `Pleroma.Actors.create(params)`
* `Pleroma.Actors.find_by(params)`
* `Pleroma.Actors.send_notification(actor, notification)`
This context could have internal modules, ie:
* `Pleroma.Actors.Actor # Schema`
* `Pleroma.Actors.Finders # Queries`
* `Pleroma.Actors.CreateCommand # Logic to create an Actor`
Those internal modules should not be used directly by an external contexts and they should not have documentation.
Contexts can talk each other.
In fact different libraries could share context name, ie:
* When `PleromaWeb.Actors.Controller` receives a new registration request and it will call `Pleroma.Actors.create_actor(params)`
However, sometimes we need to use different context in the same time.
Imagine we add new context: `Pleroma.Statistics`
So anytime a user is created we should call: `Pleroma.Statistics.new_registration(actor)`
If we make this call from `PleromaWeb.Actors.Controller` we can forget to add it in a future interface.
In this case, the appropriate way is to create a new function in the application level:
```
defmodule Pleroma do
def create_actor(params)
with {:ok, actor} <- Pleroma.Actors.create(params),
:ok <- `Pleroma.Statistics.new_registration(actor) do
{:ok, actor}
end
end
end
```
The `Pleroma` application is in charge of making the call between different contexts.
This could turn in a mess very quickly.
Too many big functions in the `Pleroma` module, which is the Public API for the application.
Well, we can just create helper modules like: `Pleroma.CreateActor`.
We can move this function and private helpers functions to this module.
So the only responsibility is to create the actor.
And of course it will be a private module:
```
defmodule Pleroma do
@doc """wherever"""
def create_actors(params), do: Pleroma.CreateActor.run(params)
end
defmodule Pleroma.CreateActor do
@moduledoc false
def run(params) do
...
end
end
```
This means no documentation at all.
The doc should be in the Contexts (if they are publics) or in `Pleroma` module.
### Contexts for Pleroma
* `Pleroma.Invitations` deals with Invitations
* `Pleroma.ActivityStream` has `Activity`, `Actor`, etc. schemas
* Think about if it makes sense to split `ActivityPub` and `ActivityStream`
defmodule Pleroma.PasswordResetToken do
@moduledoc """
Pleroma.Accounts.PasswordResetToken
## FIXME
* It should not use Repo in this module
"""
use Ecto.Schema
import Ecto.Changeset
......
defmodule Pleroma.Activity do
# This should be Pleroma.ActivityStream.Activity
# ActivityStream activity schema
# FIXME
# * It should not use Repo in this module
# * I'd move query functions to a new module Pleroma.ActivityStream.ActivityQueries
# MAYBE
# normalize the relation with "Actor" and "Object"?
use Ecto.Schema
alias Pleroma.{Repo, Activity, Notification}
import Ecto.Query
......@@ -13,6 +20,11 @@ defmodule Pleroma.Activity do
timestamps()
end
# Has unique index :)
# This function goes in `Pleroma.ActivityStream`
@doc """
Returns the `Activity` by ActivityPub ID, which is a string
"""
def get_by_ap_id(ap_id) do
Repo.one(
from(
......@@ -58,6 +70,7 @@ defmodule Pleroma.Activity do
Repo.all(all_by_object_ap_id_q(ap_id))
end
# bad: used in mastodon_api/views/status_view...
def create_activity_by_object_id_query(ap_ids) do
from(
activity in Activity,
......@@ -76,9 +89,13 @@ defmodule Pleroma.Activity do
create_activity_by_object_id_query([ap_id])
|> Repo.one()
end
# just matching everything to return nil seems a shortcut
def get_create_activity_by_object_ap_id(_), do: nil
# IMPORTANT
# So normalize it is just find local copy by id, strange
# I need more info to resolve this
# This is very used
def normalize(obj) when is_map(obj), do: Activity.get_by_ap_id(obj["id"])
def normalize(ap_id) when is_binary(ap_id), do: Activity.get_by_ap_id(ap_id)
def normalize(_), do: nil
......
defmodule Pleroma.Object do
# This should be Pleroma.ActivityStream.Object
#
# FIXME
# * It should not use Repo in this module
# * I'd move query functions to a new module Pleroma.ActivityStream.ObjectQueries
use Ecto.Schema
alias Pleroma.{Repo, Object}
import Ecto.{Query, Changeset}
......@@ -21,17 +26,25 @@ defmodule Pleroma.Object do
|> unique_constraint(:ap_id, name: :objects_unique_apid_index)
end
# This should be a secondary function
def get_by_ap_id(nil), do: nil
# Should we check if ap_id is binary?
def get_by_ap_id(ap_id) do
Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
end
# IMPORTANT
# So normalize it is just find local copy by id, strange
# I need more info to resolve this
# This is very used
def normalize(obj) when is_map(obj), do: Object.get_by_ap_id(obj["id"])
def normalize(ap_id) when is_binary(ap_id), do: Object.get_by_ap_id(ap_id)
def normalize(_), do: nil
# This should be the default function
def get_cached_by_ap_id(ap_id) do
# FIXME Bad practice, we should never check the current environment
if Mix.env() == :test do
get_by_ap_id(ap_id)
else
......@@ -49,6 +62,7 @@ defmodule Pleroma.Object do
end
end
# What?
def context_mapping(context) do
Object.change(%Object{}, %{data: %{"id" => context}})
end
......
defmodule Pleroma.User do
# There are plans to split this table in different ones:
# Pleroma.Accounts.User?
# Pleroma.Accounts.PasswordAuthentication
# Pleroma.ActivityPub.Actor
# Pleroma.ActivityPub.Follow
# # FIXME
# * It should not use Web
use Ecto.Schema
import Ecto.{Changeset, Query}
......
defmodule Pleroma.UserInviteToken do
# This should be Pleroma.Invitations.Token
# FIXME No index in token
# TODO Remove after a long period? 6 months?
use Ecto.Schema
import Ecto.Changeset
......@@ -12,6 +15,11 @@ defmodule Pleroma.UserInviteToken do
timestamps()
end
@doc """
Creates an User invite token
Would be a good idea to split the changeset and the repo call.
"""
def create_token do
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
......@@ -23,12 +31,16 @@ defmodule Pleroma.UserInviteToken do
Repo.insert(token)
end
#Create a `Ecto.Changeset` to set `Pleroma.UserInviteToken` as used.
def used_changeset(struct) do
struct
|> cast(%{}, [])
|> put_change(:used, true)
end
# This can be done in just one query
# The return value is not used
# This goes in Pleroma.Invitations public API
def mark_as_used(token) do
with %{used: false} = token <- Repo.get_by(UserInviteToken, %{token: token}),
{:ok, token} <- Repo.update(used_changeset(token)) do
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment