hacking docs

This commit is contained in:
jjl 2022-03-24 17:10:48 +01:00
parent 2b977717a7
commit 78bcce16f9
5 changed files with 716 additions and 51 deletions

View file

@ -8,7 +8,6 @@ It originally started with requests by Moodle users to be able to share and coll
Hacking on it is actually pretty fun. The codebase has a unique feeling to work with and we've relentlessly refactored to manage the ever-growing complexity that a distributed social networking toolkit implies.
That said, it is not easy to understand without context, which is what this document is here to provide.
## Design Decisions
Feature goals:
@ -23,55 +22,38 @@ Operational goals:
- Light on resources for small deployments.
- Scalable for large deployments.
## Stack
Our main implementation language is [Elixir](https://www.elixir-lang.org/), which is designed for building reliable systems.
Our main implementation language is [Elixir](https://www.elixir-lang.org/), which is designed for
building reliable systems. We have almost [our own dialect](./BONFIRE-FLAVOURED-ELIXIR.md).
We use the [Phoenix](https://www.phoenixframework.org/) web framework with [LiveView](https://hexdocs.pm/phoenix_live_view/) and [Surface](https://surface-ui.org/documentation) for UI components and views.
We use the [Phoenix](https://www.phoenixframework.org/) web framework with
[LiveView](https://hexdocs.pm/phoenix_live_view/) and [Surface](https://surface-ui.org/documentation)
for UI components and views.
Surface is a different syntax for LiveView that is designed to be more convenient and understandable to frontend developers, with extra compile time checks. Surface views and components are compiled into LiveView code (so once you hit runtime, Surface in effect doesn't exist any more).
Surface is a different syntax for LiveView that is designed to be more convenient and understandable
to frontend developers, with extra compile time checks. Surface views and components are compiled
into LiveView code (so once you hit runtime, Surface in effect doesn't exist any more).
Some extensions use the [Absinthe](https://absinthe-graphql.org/) GraphQL library to expose an API.
## The Bonfire Environment
## Core Concepts
We like to think of bonfire as a comfortable way of developing software - there are a lot of
conveniences built in once you know how they all work. The gotcha is that while you don't know them,
it can be a bit overwhelming. Don't worry, we've got your back.
There are a few things we take for granted in the bonfire ecosystem. Knowing a little about them will make understanding
the code a lot simpler:
* The pointers system.
* Our access control system.
## The pointers system
Social networks are essentially graphs. The most important part of them is not any particular piece of content, but how
that content links to everything else. In reality, social networks are messy heavy interrelated graphs where more or
less anything can link to more or less anything else. Our database is structured to support the messy realities of the
social network.
In particular, we need to be able to permit "anything" to link to "anything". We impose a 'universal primary key'
concept on the database as follows:
* We need a uuid-like universal identifier (we choose the [ulid](https://github.com/ulid/spec) format).
* We choose which tables will participate in the scheme ("pointables") and record them in the "tables" table.
* When a record is inserted in a participating table, a "pointer" record is also automatically inserted (by trigger)
linking the ID ("pointer id") to the ID of the triggering table ("table id").
* Any foreign key columns can reference the "pointers" table if they don't know which table it is in.
While this indeed solved the stated problem, it introduced a new one: how do we effectively work with objects if we
don't know what type they are? Our solution for this comes in two parts:
* Mixin tables.
* Helper libraries.
Where a pointable table represents an object, a mixin table represents data about an object. It is a collection of
related fields that may be common to multiple pointables. In essence, they allow you to work with data regardless of
which pointable you are working with. Mixins have a pointer ID as a primary key, thus each object may only 'use' a mixin once.
A good example of this is our access control mixin, `controlled`, which contains a foreign key to an `acl`. Thus
`controlled` represents "mixing in" access control to a pointable.
<!-- * [Elixir from 10,000 ft.](./ELIXIR.md) - some notes to consider when learning elixir. -->
* [Bonfire-flavoured Elixir](./BONFIRE-FLAVOURED-ELIXIR.md) - an introduction to the way write elixir.
* [Bonfire's Database: an Introduction](./DATABASE.md) - an overview of how our database is designed.
* [Boundaries](./BOUNDARIES.md) - an introduction to our access control system.
<!-- * [Epics](./EPICS.md) -->
<!-- * []() -->
<!-- * []() -->
<!-- * []() -->
<!-- * []() -->
Note: these are still at the early draft stage, we expect to gradually improve documentation over time.
## Code Structure

View file

@ -0,0 +1,118 @@
# Bonfire-flavoured Elixir
Bonfire has a few libraries that are widely used internally and make writing elixir feel a little
bit different. To help you get less confused by this, I've put together this handy guide on what I'm
calling "bonfire-flavoured elixir"!
## Arrows
The elixir [|> ("pipe") operator](https://hexdocs.pm/elixir/Kernel.html#%7C%3E/2) is one of the
things that seems to get people excited about elixir. I suspect it's because they're lazy about
coming up with names, which I can appreciate. Unfortunately it's kind of limiting. The moment you
need to pipe a parameter into a position that isn't the first one, it breaks down and you have to
drop out of the pipeline format or write a secondary function to handle it.
Not any more! By simply inserting `...` where you would like the value to be inserted, it will
override where it is placed! This allows you to keep on piping while accommodating that function
with the annoying argument order.
I stole the idea from [an existing library](https://hexdocs.pm/magritte/Magritte.html) and removed a
few arbitrary limitations in the process. Here is part of the test suite:
```elixir
defmodule ArrowsTest do
use ExUnit.Case
use Arrows
def double(x), do: x * 2
def double_fst(x, _), do: x * 2
def double_snd(_, x), do: x * 2
def add_snd_thd(_, x, y), do: x + y
test "|>" do
assert 4 == (2 |> double)
assert 4 == (2 |> double())
assert 4 == (2 |> double(...))
assert 8 == (2 |> double(double(...)))
assert 4 == (2 |> double_fst(1))
assert 4 == (2 |> double_fst(..., 1))
assert 8 == (2 |> double_fst(double(...), 1))
assert 4 == (2 |> double_snd(1, ...))
assert 8 == (2 |> double_snd(1, double(...)))
assert 3 == (2 |> add_snd_thd(1, ..., 1))
assert 4 == (2 |> add_snd_thd(1, ..., ...))
assert 6 == (2 |> add_snd_thd(1, ..., double(...)))
for x <- [:yes, 2, nil, false] do
assert {:ok, x} == (x |> {:ok, ...})
end
end
end
```
A few little extra features you might notice here:
* You can move the parameter into a subexpression, as in `2 |> double_fst(double(...), 1)` where
double will be called before the parameter is passed to `double_fst`.
* You can use `...` multiple times, substituting it in multiple places.
* The right hand side need not even be a function call, you can use any expression with `...`.
### Ok-pipe
We also have an `ok-pipe` operator, `~>`, which only pipes into the next function if the result from
the last one was considered a success. It's inspired by [OK](https://hexdocs.pm/ok/readme.html), but
we have chosen to do things slightly differently so it better fits with our regular pipe.
input | result |
:------------------------- | :-------------- |
`{:ok, x}` | `fun.(x)` |
`{:error, e}` | `{:error, e}` |
`nil` | `nil` |
`x when not is_nil(x)` | `fun.(x)` |
In the case of a function returning an ok/error tuple being on the left hand side, this is
straightforward to determine. In the event of `{:ok, x}`, x will be passed into the right hand side
to call. In the event of `{:error, x}`, the result will be `{:error, x}`.
We also deal with a lot of functions that indicate failure by returning nil. `~>` tries to 'do what
i mean' for both of these so you can have one pipe operator to rule them all. If `nil` is a valid
result, you must thus be sure to wrap it in an `ok` tuple when it occurs on the left hand side of `~>`.
`|>` and `~>` compose in the way you'd expect; i.e. a `~>` receiving an error tuple or nil will stop
executing the rest of the chain of (mixed) pipes.
## Where
`Where` provides replacements for the macros in `Logger` and the `IO.inspect` function with versions
that output code location information. The first argument will be `inspect`ed and the second (where
provided) will be used as a label:
```
iex(1)> import Where
Where
iex(2)> debug(:no, "the answer is") # log at debug
11:19:09.915 [debug] [iex:2] the answer is: :no
:no
iex(3)> Where.dump(%{a: :map}, "it") # inspect something on stdout
[iex:3] it: %{a: :map}
%{a: :map}
```
When used in a code file, the location information becomes slightly more useful, e.g.:
```
[lib/test_where.ex:15@Test.Where.example/2] Here's an empty list: []
```
You may also notice from the iex output that it returns its first argument. This makes it ideal for
inserting into a pipeline for debugging purposes:
```elixir
do_something()
|> debug("output of do_something/0")
```
When you no longer need to debug this, the location of the debug statement is already in the output
so you know where to remove it or comment it out! Bliss!
You will find the codebase uses this a lot and the debugs are frequently commented out. Just
uncomment the ones that would help you with a particular debugging task and you're away.

85
docs/BOUNDARIES.md Normal file
View file

@ -0,0 +1,85 @@
# Bonfire Boundaries
Boundaries is Bonfire's flexible framework for full
per-user/per-object/per-action access control. It makes it easy to
ensure that users may only see or do what they are supposed to.
## Users and Circles
Ignoring any future bot support, boundaries ultimately apply to users.
Circles are a way of categorising users. Each user has their own set
of circles that they can add to and categorise other users in as they
please.
Circles allow a user to categorise work colleagues differently from
friends. They can choose to allow different interactions from users in
the two circles or limit which content each sees on a per-item basis.
## Verbs
Verbs represent actions that the user could perform, such as reading a
post or replying to a message.
Each verb has a unique ID, like the table IDs from `pointers` which
must be known to the system through configuration.
## Permissions
Permissions can take one of three values:
* `true`
* `false`
* `nil` (or `null` to postgresql).
`true` and `false` are easy enough to understand as yes and no, but what is `nil`?
`nil` represents `no answer`. in isolation, it is the same as `false`.
Because a user could be in more than one circle and each circle may
have a different permission, we need a way of combining permissions to
produce a final result permission. `nil` is treated differently here:
left | right | result
:------ | :------ | :-----
`nil` | `nil` | `nil`
`nil` | `true` | `true`
`nil` | `false` | `false`
`true` | `nil` | `true`
`true` | `true` | `true`
`true` | `false` | `false`
`false` | `nil` | `false`
`false` | `true` | `false`
`false` | `false` | `false`
To be considered granted, the result of combining the permissions must
be `true` - `nil` is as good as `false` again here.
`nil` can thus be seen as a sort of `weak false`, being easily
overridden by a true, but also not by itself granting anything.
At first glance, this may seem a little odd, but it gives us a little
additional flexibility which is useful for implementing features such
as blocks (where `false` is really useful!). With a little practice,
it feels quite natural to use.
## ACLs and Grants
An ACL is "just" a collection of Grants.
Grants combine the ID of the ACL they exist in with a verb id, a user
or circle id and a permission, thus providing a decision about whether
a particular action is permitted for a particular user (or all users
in a particular circle).
Conceptually, an ACL contains a grant for every user-or-circle/verb
combination, but most of the permissions are `nil`. We do not record
grants with `nil` permissions in the database, saving substantially
on storage space and compute requirements.
## `Controlled` - Applying boundaries to an object
An object is linked to one or more `ACL`s by the `Controlled`
multimixin, which pairs an object ID with an ACL ID. Because it is a
multimixin, a given object can have multiple ACLs applied. In the case
of overlap, permissions are combined in the manner described earlier.

479
docs/DATABASE.md Normal file
View file

@ -0,0 +1,479 @@
# Bonfire's Database - An Introduction
Bonfire uses the excellent PostgreSQL database for most data storage. PostgreSQL allows us to make a
wide range of queries and to make them relatively fast while upholding data integrity guarantees.
Postgres is a schema-led database - it expects you to define what fields go in each table and to
reference the table you are referring to when you refer to a record with a foreign key.
A social network, by contrast, is actually a graph of objects. Objects can refer to other objects by
their ID without knowing their type. We would like the flexibility to have a foreign key that
references `any referenceable object`. We call our such system `pointers`.
This guide is a brief introduction to pointers. It assumes a little knowledge:
* Basic understanding of how PostgreSQL works, in particular:
* Tables being made up of fields.
* What a primary key is and why it's useful.
* Foreign keys and relationships between tables (1:1, 1:Many, Many:1, Many:Many).
* Views as virtual tables backed by a SQL query.
* Basic understanding of elixir (enough to follow the examples).
* Basic working knowledge of the ecto database library (schema and migration definitions)
## Identifying objects - the ULID type
All referenceable objects in the system have a unique ID whose type is the
[`ULID`](https://github.com/ulid/spec) It'sa lot like a `UUID` in that you can generate unique ones
independently of the database. It's also a little different, being made up of two parts:
* The current timestamp, to millisecond precision.
* Strong random padding for uniqueness.
This means that it naturally sorts by time to the millisecond (close enough for us), giving us a
performance advantage on creation time-ordered queries! By contrast, UUIDv4 is randomly
distributed - a worst case scenario for ordering!
If you've only worked with integer primary keys before, you are probably used to letting the
database dispense an ID for you. With `ULID` (or `UUID`), IDs can be known *before* they are stored,
greatly easing the process of storing a graph of data and allowing us to do more of the preparation
work outside of a transaction for increased performance.
In PostgreSQL, we actually store `ULID`s as `UUID` columns, owing to the lack of a `ULID` column
type shipping with postgresql and them both being the same size. You mostly will not notice this
because it's handled for you, but there are a few places it can come up:
* Ecto debug and error output may show either binary values or UUID-formatted values.
* Hand-written sql in migrations may need to convert table IDs to the `UUID` format before use.
## It's just a table, dave
The `pointers` system is mostly based around a single table represented by the `Pointers.Pointer`
schema with the following fields:
* `id` (ULID) - the database-wide unique id for the object, primary key.
* `table_id` (ULID) - identifies the type type of the object, references `Pointers.Table`.
* `deleted_at` (timestamp, default: null) - when the object was deleted.
Every object that is stored in the system will have a record in this table. It may also have records
in other tables (handy for storing more than 3 fields about the object!).
Don't worry about `Pointers.Table` for now, just know that every object type will have a
record there so `Pointers.Pointer.table_id` can reference it.
## Mixins - storing data about objects
Mixins are tables which contain extra information on behalf of objects. Each object can choose to
record or not record information for each mixin. Sample mixins include:
* user profile (containing a name, location and summary)
* post content (containing the escaped html body of a post or message)
* created (containing the id of the object creator)
In this way, they are reusable across different object types. One mixin may (not) be used by any
number of objects. This is mostly driven by the type of the object we are storing, but can also be
driven by user input.
Mixins are just tables too! The only requirement is they have a `ULID` primary key which references
`Pointers.Pointer`. The developer of the mixin is free to put whatever other fields they want in the
table, so long as they have that primary key.
Here is a sample mixin definition for a user profile:
```elixir
defmodule Bonfire.Data.Social.Profile do
use Pointers.Mixin,
otp_app: :bonfire_data_social,
source: "bonfire_data_social_profile"
mixin_schema do
field :name, :string
field :summary, :string
field :website, :string
field :location, :string
end
end
```
Aside from `use`ing `Pointers.Mixin` instead of `Ecto.Schema` and calling `mixin_schema` instead of
`schema`, pretty similar, right? The `ULID` primary key referencing `Pointers.Pointer` will be
automatically added for you by `mixin_schema`.
The arguments to `use Pointers.Mixin` are:
* `otp_app`: the otp app name to use when loading dynamic configuration, e.g. the current app (required)
* `source`: the underlying table name to use in the database
We will cover dynamic configuration later. For now, you can use the otp app that includes the module.
## Multimixins
Multimixins are like mixins, except that where an object may have 0 or 1 of a particular mixins, an
object may have any number of a particular multimixin.
For this to work, a multimixin must have a *compound primary key* which must contain an `id` column
referencing `Pointers.Pointer` and at least one other field which will collectively be unique.
An example multimixin is used for publishing an item to feeds:
```elixir
defmodule Bonfire.Data.Social.FeedPublish do
use Pointers.Mixin,
otp_app: :bonfire_data_social,
source: "bonfire_data_social_feed_publish"
alias Pointers.Pointer
mixin_schema do
belongs_to :feed, Pointer, primary_key: true
end
end
```
Notice that this looks very similar to defining a mixin. Indeed, the only difference is the
`primary_key: true` in this line, which adds a field to the compound primary key:
```elixir
belongs_to :feed, Pointer, primary_key: true
```
This results in ecto recording a compound primary key of `(id, feed_id)` for the schema (the id is
added for you as with regular mixins).
## Declaring Object Types
### Picking a table id
The first step to declaring a table is picking a unique table ID in ULID format. You could just
generate one at the terminal, but since these IDs are special, we tend to assign a synthetic ULID
that are readable as words so they stand out in debug output.
For example, the ID for the `Feed` table is: `1TFEEDS0NTHES0V1S0FM0RTA1S`, which can be read as "It
feeds on the souls of mortals". Feel free to have a little fun coming up with them, it makes debug
output a little more cheery! The rules are:
* The alphabet is [Crockford's Base32](https://en.wikipedia.org/wiki/Base32#Crockford's_Base32).
* They must be 26 characters in length.
* The first character must be a digit in the range 0-7.
To help you with this, the `Pointers.ULID.synthesise!/1` method takes an alphanumeric
binary and tries to return you it transliterated into a valid ULID. Example usage:
```
iex(1)> Pointers.ULID.synthesise!("itfeedsonthesouls")
11:20:28.299 [error] Too short, need 9 chars.
:ok
iex(2)> Pointers.ULID.synthesise!("itfeedsonthesoulsofmortalsandothers")
11:20:31.819 [warn] Too long, chopping off last 9 chars
"1TFEEDS0NTHES0V1S0FM0RTA1S"
iex(3)> Pointers.ULID.synthesise!("itfeedsonthesoulsofmortals")
"1TFEEDS0NTHES0V1S0FM0RTA1S"
iex(4)> Pointers.ULID.synthesise!("gtfeedsonthesoulsofmortals")
11:21:03.268 [warn] First character must be a digit in the range 0-7, replacing with 7
"7TFEEDS0NTHES0V1S0FM0RTA1S"
```
### Virtuals
Virtuals are the simplest and most common type of object. Here's a definition of block:
```elixir
defmodule Bonfire.Data.Social.Block do
use Pointers.Virtual,
otp_app: :bonfire_data_social,
table_id: "310CK1NGSTVFFAV01DSSEE1NG1",
source: "bonfire_data_social_block"
alias Bonfire.Data.Edges.Edge
virtual_schema do
has_one :edge, Edge, foreign_key: :id
end
end
```
It should look quite similar to a mixin definition, except that we `use Pointers.Virtual` this time
(passing an additional `table_id` argument) and we call `virtual_schema`.
The primary limitation of a virtual is that you cannot put extra fields into one. This also means
that `belongs_to` is not generally permitted because it results in adding a field. `has_one` and
`has_many` work just fine as they do not cause the creation of fields in the schema.
This is not usually a problem, as extra fields can be put into mixins or multimixins as appropriate.
Under the hood, a virtual has a view (in the example, called `bonfire_data_social_block`). It looks
like a table with just an id, but it's populated with all the ids of blocks that are not
deleted. When the view is inserted into, a record is created in the `pointers` table for you transparently. When
you delete from the view, the corresponding `pointers` entry is marked deleted for you.
### Pointables
The other, lesser used, type of object is called the pointable. The major difference is that unlike
the simple case of virtuals, pointers are not backed by views, but by tables.
When a record is inserted into a pointable table, a copy is made in the `pointers` table for you
transparently. When you delete from the table, the the corresponding `pointers` entry is marked
deleted for you. In these ways, they behave very much like virtuals. By having a table, however, we
are free to add new fields.
Pointables pay for this flexibility by being slightly more expensive than virtuals:
* Records must be inserted into/deleted from two tables (the pointable table and the `pointers` table).
* The pointable table needs its own primary key index.
Here is a definition of a pointable type (indicating an activitypub document whose type we don't
recognise, stored as a json blob):
```elixir
defmodule Bonfire.Data.Social.APActivity do
use Pointers.Pointable,
otp_app: :bonfire_data_social,
table_id: "30NF1REAPACTTAB1ENVMBER0NE",
source: "bonfire_data_social_apactivity"
pointable_schema do
field :json, :map
end
end
```
The choice of using a pointable instead of a virtual and a mixin is ultimately up to you.
## Writing Migrations
Migrations are typically included in schema libraries as public APIs you can call within your
project's migrations.
### Virtuals
Most virtuals are incredibly simple to migrate for:
```elixir
defmodule Bonfire.Data.Social.Post.Migration do
import Pointers.Migration
alias Bonfire.Data.Social.Post
def migrate_post(), do: migrate_virtual(Post)
end
```
If you need to do more work, it can be a little trickier. Here's an example for `block`, which
also creates a unique index on another table:
```elixir
defmodule Bonfire.Data.Social.Block.Migration do
import Ecto.Migration
import Pointers.Migration
import Bonfire.Data.Edges.Edge.Migration
alias Bonfire.Data.Social.Block
def migrate_block_view(), do: migrate_virtual(Block)
def migrate_block_unique_index(), do: migrate_type_unique_index(Block)
def migrate_block(dir \\ direction())
def migrate_block(:up) do
migrate_block_view()
migrate_block_unique_index()
end
def migrate_block(:down) do
migrate_block_unique_index()
migrate_block_view()
end
end
```
Notice how we have to write our `up` and `down` versions separately to get the correct
ordering of operations. Handling down migrations can be a bit awkward in ecto.
### Pointables
As of now, pointables are a little trickier to define flexibly than virtuals because we want to
preserve the ability for the user to add new fields. There are some questions about how useful this
is in practice, so we might go for a simpler option in future.
Example:
```elixir
defmodule Bonfire.Data.Social.APActivity.Migration do
use Ecto.Migration
import Pointers.Migration
alias Bonfire.Data.Social.APActivity
defp make_apactivity_table(exprs) do
quote do
require Pointers.Migration
Pointers.Migration.create_pointable_table(Bonfire.Data.Social.APActivity) do
Ecto.Migration.add :json, :jsonb
unquote_splicing(exprs)
end
end
end
defmacro create_apactivity_table, do: make_apactivity_table([])
defmacro create_apactivity_table([do: body]), do: make_apactivity_table(body)
def drop_apactivity_table(), do: drop_pointable_table(APActivity)
defp maa(:up), do: make_apactivity_table([])
defp maa(:down) do
quote do: Bonfire.Data.Social.APActivity.Migration.drop_apactivity_table()
end
defmacro migrate_apactivity() do
quote do
if Ecto.Migration.direction() == :up,
do: unquote(maa(:up)),
else: unquote(maa(:down))
end
end
end
```
## Mixins
Mixins look much like pointables:
```elixir
defmodule Bonfire.Data.Social.Profile.Migration do
import Pointers.Migration
alias Bonfire.Data.Social.Profile
# create_profile_table/{0,1}
defp make_profile_table(exprs) do
quote do
require Pointers.Migration
Pointers.Migration.create_mixin_table(Bonfire.Data.Social.Profile) do
Ecto.Migration.add :name, :text
Ecto.Migration.add :summary, :text
Ecto.Migration.add :website, :text
Ecto.Migration.add :location, :text
Ecto.Migration.add :icon_id, strong_pointer(Bonfire.Files.Media)
Ecto.Migration.add :image_id, strong_pointer(Bonfire.Files.Media)
unquote_splicing(exprs)
end
end
end
defmacro create_profile_table(), do: make_profile_table([])
defmacro create_profile_table([do: {_, _, body}]), do: make_profile_table(body)
# drop_profile_table/0
def drop_profile_table(), do: drop_mixin_table(Profile)
# migrate_profile/{0,1}
defp mp(:up), do: make_profile_table([])
defp mp(:down) do
quote do
Bonfire.Data.Social.Profile.Migration.drop_profile_table()
end
end
defmacro migrate_profile() do
quote do
if Ecto.Migration.direction() == :up,
do: unquote(mp(:up)),
else: unquote(mp(:down))
end
end
end
```
## Multimixins
Similar to mixins:
```elixir
defmodule Bonfire.Data.Social.FeedPublish.Migration do
import Ecto.Migration
import Pointers.Migration
alias Bonfire.Data.Social.FeedPublish
@feed_publish_table FeedPublish.__schema__(:source)
# create_feed_publish_table/{0,1}
defp make_feed_publish_table(exprs) do
quote do
require Pointers.Migration
Pointers.Migration.create_mixin_table(Bonfire.Data.Social.FeedPublish) do
Ecto.Migration.add :feed_id,
Pointers.Migration.strong_pointer(), primary_key: true
unquote_splicing(exprs)
end
end
end
defmacro create_feed_publish_table(), do: make_feed_publish_table([])
defmacro create_feed_publish_table([do: {_, _, body}]), do: make_feed_publish_table(body)
def drop_feed_publish_table(), do: drop_pointable_table(FeedPublish)
def migrate_feed_publish_feed_index(dir \\ direction(), opts \\ [])
def migrate_feed_publish_feed_index(:up, opts),
do: create_if_not_exists(index(@feed_publish_table, [:feed_id], opts))
def migrate_feed_publish_feed_index(:down, opts),
do: drop_if_exists(index(@feed_publish_table, [:feed_id], opts))
defp mf(:up) do
quote do
Bonfire.Data.Social.FeedPublish.Migration.create_feed_publish_table()
Bonfire.Data.Social.FeedPublish.Migration.migrate_feed_publish_feed_index()
end
end
defp mf(:down) do
quote do
Bonfire.Data.Social.FeedPublish.Migration.migrate_feed_publish_feed_index()
Bonfire.Data.Social.FeedPublish.Migration.drop_feed_publish_table()
end
end
defmacro migrate_feed_publish() do
quote do
if Ecto.Migration.direction() == :up,
do: unquote(mf(:up)),
else: unquote(mf(:down))
end
end
defmacro migrate_feed_publish(dir), do: mf(dir)
end
```
### More examples
Take a look at a few of the migrations in our data libraries. Between them, they cover most
scenarios by now:
* [bonfire_data_social](https://github.com/bonfire-networks/bonfire_data_social/)
* [bonfire_data_access_control](https://github.com/bonfire-networks/bonfire_data_access_control/)
* [bonfire_data_identity](https://github.com/bonfire-networks/bonfire_data_identity/)
* [bonfire_data_edges](https://github.com/bonfire-networks/bonfire_data_edges/) (feat. bonus triggers)
If you want to know exactly what's happening, I can only suggest reading the code for
[Pointers.Migration](https://github.com/bonfire-networks/pointers/blob/main/lib/migration.ex), it's
surprisingly readable.

View file

@ -158,23 +158,23 @@ that too:
make mix~"bonfire.user.admin.promote root" # for username `root`
```
<!-- ## The Bonfire Environment -->
## The Bonfire Environment
<!-- We like to think of bonfire as a comfortable way of developing software - there are a lot of -->
<!-- conveniences built in once you know how they all work. The gotcha is that while you don't know them, -->
<!-- it can be a bit overwhelming. Don't worry, we've got your back. -->
We like to think of bonfire as a comfortable way of developing software - there are a lot of
conveniences built in once you know how they all work. The gotcha is that while you don't know them,
it can be a bit overwhelming. Don't worry, we've got your back.
<!-- * [Elixir from 10,000 ft.](./ELIXIR.md) - some notes to consider when learning elixir. -->
<!-- * [Bonfire-flavoured Elixir](./BONFIRE-FLAVOURED-ELIXIR.md) - an introduction to the way we code. -->
<!-- * [Bonfire's Database](./DATABASE.md) - an introduction to how we model, store and query data. -->
<!-- * [Boundaries](./BOUNDARIES.md) - an introduction to our access control system -->
* [Bonfire-flavoured Elixir](./BONFIRE-FLAVOURED-ELIXIR.md) - an introduction to the way write elixir.
* [Bonfire's Database: an Introduction](./DATABASE.md) - an overview of how our database is designed.
* [Boundaries](./BOUNDARIES.md) - an introduction to our access control system.
<!-- * [Epics](./EPICS.md) -->
<!-- * []() -->
<!-- * []() -->
<!-- * []() -->
<!-- * []() -->
<!-- Note: these are still at the early draft stage, we expect to gradually improve documentation over time. -->
Note: these are still at the early draft stage, we expect to gradually improve documentation over time.
## Documentation
@ -188,10 +188,11 @@ The code is somewhat documented inline. You can generate HTML docs (using `Exdoc
- You can migrate the DB when the app is running (useful in a release): `EctoSparkles.ReleaseTasks.migrate`
### Usage under Windows (MSYS or CYGWIN)
### Usage under Windows (WSL, MSYS or CYGWIN)
If you plan on using the `Makefile` (its rather handy), you must have symlinks enabled.
You must enable developer mode, and set `core.symlink = true`, [see link.](https://stackoverflow.com/a/59761201)
By default, the `Makefile` requires symlinks, which can be enabled with the help of [this link](https://stackoverflow.com/a/59761201).
See the [pull request adding WSL support](https://github.com/bonfire-networks/bonfire-app/pull/111) for details about usage without symlinks.
## Make commands