Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions lib/live_admin/resource.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ defmodule LiveAdmin.Resource do
end
end

@doc """
Fetches every record of `resource` whose primary key is contained in `keys`.

Records are loaded from `repo` within the given schema `prefix` (the Ecto
query prefix used for multi-tenancy). Returns a list of schema structs.
"""
def all(keys, resource, prefix, repo) do
key = LiveAdmin.primary_key!(resource)

Expand All @@ -44,13 +50,24 @@ defmodule LiveAdmin.Resource do
|> repo.all(prefix: prefix)
end

@doc """
Same as `find/5`, but raises `Ecto.NoResultsError` when no record matches.
"""
def find!(key, resource, prefix, repo, config) do
find(key, resource, prefix, repo, config) ||
raise(Ecto.NoResultsError,
queryable: Keyword.fetch!(resource.__live_admin_config__(), :schema)
)
end

@doc """
Fetches the single record of `resource` identified by primary key `key`.

The lookup runs against the query built by `query/3` (so any `query_with`
override is respected) within the given schema `prefix`. Returns the record,
or `nil` when it is not found or when `key` cannot be cast to the primary key
type.
"""
def find(key, resource, prefix, repo, config) do
resource
|> query(nil, config)
Expand All @@ -59,6 +76,12 @@ defmodule LiveAdmin.Resource do
Ecto.Query.CastError -> nil
end

@doc """
Deletes `record`.

When the resource configures `delete_with`, that function is invoked with
`record` and the current `session` instead of the default `repo.delete/1`.
"""
def delete(record, resource, session, repo, config) do
resource
|> LiveAdmin.fetch_config(:delete_with, config)
Expand All @@ -74,6 +97,21 @@ defmodule LiveAdmin.Resource do
end
end

@doc """
Lists records of `resource` for the index view.

`opts` is an enumerable that tunes the query; all keys are optional:

* `:page` - one-based page number (default `1`)
* `:per` - page size (defaults to the session's `index_page_size`)
* `:sort_attr` - field to order by (defaults to the primary key)
* `:sort_dir` - `:asc` or `:desc` (default `:asc`)
* `:search` - search string passed to `query/3`
* `:prefix` - schema prefix to query within

Returns a `{records, total_count}` tuple, where `total_count` ignores
pagination so it reflects the full result set.
"""
def list(resource, opts, session, repo, config) do
opts =
opts
Expand All @@ -100,6 +138,14 @@ defmodule LiveAdmin.Resource do
}
end

@doc """
Builds an `Ecto.Changeset` for `resource`.

Pass an existing `record` to build an update changeset, or omit it (or pass
`nil`) to build a changeset over a new struct for creation. `params` are cast
according to the resource's editable fields, skipping `immutable_fields` and
handling embeds. This drives the create and edit forms.
"""
def change(resource, record \\ nil, params \\ %{}, config)

def change(resource, record, params, config) when is_struct(record) do
Expand All @@ -113,6 +159,13 @@ defmodule LiveAdmin.Resource do
|> build_changeset(resource, params, config)
end

@doc """
Creates a new record of `resource` from `params`.

When the resource configures `create_with`, that function is invoked with
`params` and the current `session`. Otherwise a changeset built by `change/4`
is inserted via `repo`, within the session's prefix.
"""
def create(resource, params, session, repo, config) do
resource
|> LiveAdmin.fetch_config(:create_with, config)
Expand All @@ -130,6 +183,13 @@ defmodule LiveAdmin.Resource do
end
end

@doc """
Updates `record` with `params`.

When the resource configures `update_with`, that function is invoked with
`record`, `params`, and the current `session`. Otherwise a changeset built by
`change/4` is persisted via the resource's configured `ecto_repo`.
"""
def update(record, resource, params, session, config) do
resource
|> LiveAdmin.fetch_config(:update_with, config)
Expand All @@ -149,6 +209,14 @@ defmodule LiveAdmin.Resource do
end
end

@doc """
Runs validation for `changeset` and marks it with the `:validate` action.

When the resource configures `validate_with`, that function receives the
`changeset` and current `session` and returns the changeset to use; otherwise
the changeset is returned unchanged. Setting the `:validate` action lets the
form surface errors without attempting to persist.
"""
def validate(changeset, resource, session, config) do
resource
|> LiveAdmin.fetch_config(:validate_with, config)
Expand All @@ -160,6 +228,14 @@ defmodule LiveAdmin.Resource do
|> Map.put(:action, :validate)
end

@doc """
Returns the displayable fields of `resource`.

Each entry is a `{field_name, native_type, opts}` tuple, where `native_type`
is the underlying Ecto type (custom types are resolved to the type they cast
to) and `opts` carries `immutable: boolean`. Fields listed in the resource's
`hidden_fields` are excluded, and those in `immutable_fields` are flagged.
"""
def fields(resource, config) do
schema = Keyword.fetch!(resource.__live_admin_config__(), :schema)
hidden_fields = LiveAdmin.fetch_config(resource, :hidden_fields, config)
Expand Down Expand Up @@ -291,6 +367,15 @@ defmodule LiveAdmin.Resource do

defp parse_map_param(param), do: param

@doc """
Builds the base `Ecto.Query` used to list and look up records of `resource`.

When the resource configures `query_with`, that function is invoked with the
`resource` and `search` term and its result is used as-is. Otherwise the
resource's schema is queried directly; a non-empty `search` string is applied
across the resource's fields, supporting both `term` (all fields) and
`field:term` (single field) syntax.
"""
def query(resource, search, config) do
resource
|> fetch_config(:query_with, config)
Expand All @@ -316,6 +401,13 @@ defmodule LiveAdmin.Resource do
end
end

@doc """
Renders the value of `field` on `record` for display.

When the resource configures `render_with`, that function receives the
`record`, `field`, and current `session` and returns the content to display.
Otherwise the raw value is formatted by `render/2` based on its `type`.
"""
def render(record, field, type, resource, config, session) do
resource
|> LiveAdmin.fetch_config(:render_with, config)
Expand All @@ -326,6 +418,13 @@ defmodule LiveAdmin.Resource do
end
end

@doc """
Formats a raw `value` for display according to its Ecto `type`.

Handles the common built-in types (dates and times via `Calendar.strftime/2`,
maps and embeds via `inspect/2`, arrays element-wise) and falls back to an
HTML-safe rendering for everything else. `nil` renders as an empty string.
"""
def render(nil, _), do: ""
def render(val, {_, {Ecto.Embedded, _}}), do: inspect(val, pretty: true)
def render(val, :map), do: inspect(val, pretty: true)
Expand Down
12 changes: 12 additions & 0 deletions lib/live_admin/session/agent.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
defmodule LiveAdmin.Session.Agent do
@moduledoc """
Default `LiveAdmin.Session.Store` implementation backed by an in-memory `Agent`.

Sessions are held in process state keyed by id, so they are fast but ephemeral:
restarting the server clears the store. When a socket reconnects to a session
that no longer exists, `load!/1` raises `InvalidSessionId`; the session is then
regenerated automatically on reload, so the error is safe to ignore.

To persist sessions across restarts, implement your own
`LiveAdmin.Session.Store` and set it via the `:session_store` config.
"""

defmodule InvalidSessionId do
defexception [:message]

Expand Down
36 changes: 36 additions & 0 deletions lib/live_admin/session/store.ex
Original file line number Diff line number Diff line change
@@ -1,10 +1,46 @@
defmodule LiveAdmin.Session.Store do
@moduledoc """
Behaviour for persisting LiveAdmin sessions.

A session holds per-user UI state (page size, sorting, prefix, etc.) that must
outlive individual LiveView mounts. LiveAdmin looks the store up from the
`:session_store` application config and defaults to `LiveAdmin.Session.Agent`,
an in-memory implementation.

Provide your own module to persist sessions elsewhere (for example a database
or cache so state survives server restarts) by implementing these callbacks
and setting:

config :live_admin, session_store: MyApp.LiveAdmin.SessionStore
"""

@type session :: LiveAdmin.Session.t()
@type conn :: Plug.Conn.t()
@type id :: String.t()
@type live_session :: map()

@doc """
Establishes the session for `conn` and returns its id.

Called during the plug pipeline. Implementations should derive a stable id
(for example from the authenticated user) and ensure a session exists for it,
creating one if necessary.
"""
@callback init!(conn) :: id

@doc """
Loads and returns the session identified by `id`.

Called on LiveView mount. Implementations should raise if no session exists
for `id` (it will be regenerated on the next request).
"""
@callback load!(id) :: session

@doc """
Persists `session`, keyed by its id, and returns `:ok`.

Called whenever session state changes so subsequent `load!/1` calls observe
the update.
"""
@callback persist!(session) :: :ok
end
Loading