Collections for Phoenix: records of one kind, with fields your users define, seen as a table, a board, a calendar, a chart or a canvas.
Your app has a module of Cardbox's:
defmodule MyApp.Cardbox do
use Cardbox, repo: MyApp.Repo, pubsub: MyApp.PubSub
endand makes collections, adds fields, and reads and writes records with it:
{:ok, books} =
MyApp.Cardbox.create_collection(%{
name: "Reading list",
owner: {"workspace", 42},
fields: [
%{name: "Title", type: "text"},
%{name: "Status", type: "status"},
%{name: "Rating", type: "number", options: %{format: "integer"}},
%{name: "Finished", type: "date"}
]
})
{:ok, dune} = MyApp.Cardbox.insert(books, %{"Title" => "Dune", "Status" => "In progress"})
{:ok, reading} =
MyApp.Cardbox.list(books, %{
"filter" => %{"field" => "status", "op" => "in_group", "value" => "doing"},
"sort" => [%{"field" => "title"}]
})Options of use Cardbox
:repo- the Ecto repo of the tables (mix cardbox.gen.migration). Without it, the module definesrepo/0, for a repo known only at runtime (a library on Cardbox that takes its host's repo from its config).:pubsub- aPhoenix.PubSub, to broadcast each change to the collection's topic (subscribe/1,Cardbox.Event).:views- types of views of your own, beside Cardbox's (Cardbox.View):views: ["timeline", "map"]. A view of one is saved, listed and queried as any, and yourcheck_view/3checks its options; your app draws it. See the Views guide.
Options of the functions
Every function that writes takes :actor, who writes: the person or the
agent of your app, whose id (actor_id/1) each record keeps as who
made it and changed it last, each event carries, and allow?/3 is
asked about.
Every function that reads takes :me (who "me" is in a filter; the
actor's id, or any id), :time_zone (where the days of date filters
are; UTC unless given) and :now.
Callbacks
Four callbacks have defaults, and your module overrides them:
@impl Cardbox
def actor_id(%MyApp.Accounts.User{id: id}), do: "user:#{id}"
@impl Cardbox
def allow?(%MyApp.Accounts.User{} = user, :change_fields, collection),
do: MyApp.Permissions.allowed?(user, :manage, collection)
def allow?(_actor, _action, _subject), do: true
@impl Cardbox
def check_view("timeline", %{"start" => _, "end" => _} = options, _collection), do: {:ok, options}
def check_view("timeline", _options, _collection), do: {:error, "a timeline names its start and end fields"}
def check_view(_type, options, _collection), do: {:ok, options}
@impl Cardbox
def after_update(collection, before, record, changed),
do: {:ok, MyApp.Bills.next(collection, before, record, changed)}
Summary
Callbacks
The id that records and events keep of an actor: by default, its :id as
text, or the actor itself when it is text, or nil.
After an update that changed a record, in its transaction: the records
to insert with it, each one's values (as insert/3 takes them), or an
error that undoes the update. before is the record as it was,
changed the keys of the values that changed. The records are inserted
by the update's actor, after it, and broadcast as :record_inserted;
allow?/3 is asked for the update only. Inserts nothing by default.
Whether an actor may do an action: :create_collection (the subject is
the collection's attributes), :update_collection, :delete_collection,
:change_fields, :insert, :update, :delete, :move, :save_view
and :delete_view (the subject is the collection). True by default.
Reads are not asked: scope the collections a person reads, as your app
scopes its own records.
Checks the options of a view of a type of your own (:views), when it is
saved: the options as they are kept, or an error that says what to write.
The options come with their keys as text. Takes them as they are by
default.
Functions
A collection as an agent reads it: its name, and each field with its key, type, choices and the operators it filters by. Give it to a model as the description of a tool that reads or writes the collection.
The value of a field of a record, read for Elixir. See Cardbox.Record.get/3.
The JSON Schema of the values a collection's
records take, by field key: what insert/3 and update/4 take, in the
forms they keep. For a model's structured output, an agent's tool, or a
form of your app that checks values before they are sent.
Callbacks
The id that records and events keep of an actor: by default, its :id as
text, or the actor itself when it is text, or nil.
@callback after_update( Cardbox.Collection.t(), before :: Cardbox.Record.t(), Cardbox.Record.t(), changed :: [String.t()] ) :: {:ok, [map()]} | {:error, String.t()}
After an update that changed a record, in its transaction: the records
to insert with it, each one's values (as insert/3 takes them), or an
error that undoes the update. before is the record as it was,
changed the keys of the values that changed. The records are inserted
by the update's actor, after it, and broadcast as :record_inserted;
allow?/3 is asked for the update only. Inserts nothing by default.
A bill paid that comes back next month, a task done that opens the next one: what follows a change is your app's, and happens with it or not at all.
Whether an actor may do an action: :create_collection (the subject is
the collection's attributes), :update_collection, :delete_collection,
:change_fields, :insert, :update, :delete, :move, :save_view
and :delete_view (the subject is the collection). True by default.
Reads are not asked: scope the collections a person reads, as your app
scopes its own records.
@callback check_view( type :: String.t(), options :: map(), collection :: Cardbox.Collection.t() ) :: {:ok, map()} | {:error, String.t()}
Checks the options of a view of a type of your own (:views), when it is
saved: the options as they are kept, or an error that says what to write.
The options come with their keys as text. Takes them as they are by
default.
Functions
@spec describe(Cardbox.Collection.t()) :: map()
A collection as an agent reads it: its name, and each field with its key, type, choices and the operators it filters by. Give it to a model as the description of a tool that reads or writes the collection.
@spec get(Cardbox.Record.t(), Cardbox.Collection.t(), term()) :: term()
The value of a field of a record, read for Elixir. See Cardbox.Record.get/3.
@spec json_schema(Cardbox.Collection.t()) :: map()
The JSON Schema of the values a collection's
records take, by field key: what insert/3 and update/4 take, in the
forms they keep. For a model's structured output, an agent's tool, or a
form of your app that checks values before they are sent.
Cardbox.json_schema(tasks)
#=> %{
# "$schema" => "https://json-schema.org/draft/2020-12/schema",
# "title" => "Tasks",
# "type" => "object",
# "properties" => %{
# "title" => %{"title" => "Title", "anyOf" => [%{"type" => "string", ...}, %{"type" => "null"}]},
# "status" => %{
# "title" => "Status",
# "anyOf" => [
# %{"type" => "string", "oneOf" => [%{"const" => "not_started", "title" => "Not started"}, ...]},
# %{"type" => "null"}
# ]
# },
# ...
# },
# "additionalProperties" => false
# }Each value may be null, which empties the field. A choice is its id,
with its name as its title; a day is "2026-10-25" (format: date), a
moment ISO 8601 (format: date-time); a relation a list of record ids.
The four read-only types are not in it. Cardbox takes more than the
schema says (a choice's name, a number as text), and checks every value
as it writes it, whatever a schema let through.