# `Cardbox`
[🔗](https://github.com/hattoriai/cardbox/blob/v0.1.0/lib/cardbox.ex#L1)

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 `c:check_view/3` checks its
    options; your app draws it. See the [Views](views.md) guide.

## Options of the functions

Every function that writes takes `:actor`, who writes: the person or the
agent of your app, whose id (`c:actor_id/1`) each record keeps as who
made it and changed it last, each event carries, and `c: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}

# `actor_id`

```elixir
@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?`

```elixir
@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`

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

# `describe`

```elixir
@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`

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

# `json_schema`

```elixir
@spec json_schema(Cardbox.Collection.t()) :: map()
```

The [JSON Schema](https://json-schema.org) 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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
