Cardbox behaviour (Cardbox v0.1.0)

Copy Markdown View Source

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
end

and 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 defines repo/0, for a repo known only at runtime (a library on Cardbox that takes its host's repo from its config).
  • :pubsub - a Phoenix.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 your check_view/3 checks 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

Three 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}

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.

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

actor_id(actor)

@callback actor_id(actor :: term()) :: String.t() | nil

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.

allow?(actor, action, subject)

@callback allow?(actor :: term(), action :: atom(), subject :: term()) :: boolean()

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.

check_view(type, options, collection)

@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

describe(collection)

@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.

get(record, collection, field)

The value of a field of a record, read for Elixir. See Cardbox.Record.get/3.

json_schema(collection)

@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.