Current section
Files
Jump to
Current section
Files
lib/lazarus.ex
defmodule Lazarus do
@moduledoc """
Public entrypoint for Lazarus.
Most applications interact with Lazarus in two layers:
* `use Lazarus` in a Repo module to inject `Repo.soft_delete*`,
`Repo.hard_delete*`, read-side filtering, and update behavior that skips
soft-deleted rows
* call the functions in this module when you want the same soft-delete
behavior while passing the Repo module explicitly
This module is the repo-explicit API for Lazarus's soft-delete helpers.
Updates use Ecto Repo functions; `use Lazarus` makes `Repo.update*`, loaded
`Repo.insert_or_update*`, and `Repo.update_all/3` skip soft-deleted rows by
default.
`Lazarus.Repo` documents the functions injected into a Repo by `use
Lazarus`.
## Basic usage
defmodule MyApp.Repo do
use Ecto.Repo, otp_app: :my_app, adapter: Ecto.Adapters.Postgres
use Lazarus
end
Use `Lazarus.Schema` in schemas that should support soft deletion:
defmodule MyApp.Post do
use Ecto.Schema
use Lazarus.Schema
schema "posts" do
soft_deletes()
end
end
Use `Lazarus.Migrations` to add the matching columns:
defmodule MyApp.Repo.Migrations.AddSoftDeletesToPosts do
use Ecto.Migration
use Lazarus.Migrations
def change do
alter table(:posts) do
soft_deletes()
end
end
end
## Where to read next
* `Lazarus.Repo` - injected Repo functions and `use Lazarus` integration
* `Lazarus.Schema` - schema helpers and schema-level cascade metadata
* `Lazarus.Migrations` - migration helpers for adding matching columns
* the HexDocs guides - workflow-oriented explanations of queries,
cascading, and association replacement
"""
@doc group: "Soft delete API"
@doc """
Soft-deletes a single struct or changeset with an explicit Repo module.
This is the repo-explicit equivalent of `Repo.soft_delete/2`.
## Returns
* `{:ok, struct}` on success
* `{:error, :not_found}` when the row was already soft-deleted or no
longer exists
## Options
* `:reason` - string reason persisted to `deletion_reason` when the schema
includes that field, or `nil` (default: `nil`)
* `:cascade` - boolean controlling whether eligible associations are
cascaded (accepted values: `true`, `false`; default: `false`)
* `:reload_after_delete` - boolean controlling whether Lazarus reloads the
soft-deleted struct from the database after deletion (accepted values:
`true`, `false`; default: repo config `:reload_after_delete`, then
`false`). The default in-memory return avoids an additional database call
and is faster, but may be less accurate than a fresh load; associations
on the returned struct are reset to `Ecto.Association.NotLoaded`. Loading
after deletion is more accurate, but costs one additional database call.
* `:skip_associations` - list of association field names to skip during
cascading, such as `[:comments, :ratings]`
* `:cascade_depth` - positive integer recursion cap for cascading
(default: `10`)
## Examples
{:ok, deleted_post} = Lazarus.soft_delete(MyApp.Repo, post)
{:ok, deleted_post} = Lazarus.soft_delete(MyApp.Repo, post, reason: "Deleted by user")
{:ok, deleted_post} = Lazarus.soft_delete(MyApp.Repo, post, reload_after_delete: true)
"""
def soft_delete(repo, struct_or_changeset, opts \\ []) do
Lazarus.Runtime.soft_delete(repo, struct_or_changeset, opts)
end
@doc group: "Soft delete API"
@doc """
Soft-deletes a single struct or changeset and raises on failure.
This is the repo-explicit equivalent of `Repo.soft_delete!/2`.
## Returns
* the deleted struct on success
## Options
Accepts the same options as `soft_delete/3`.
## Raises
* `ArgumentError` when the row was already soft-deleted or no longer
exists
## Examples
deleted_post = Lazarus.soft_delete!(MyApp.Repo, post)
deleted_post = Lazarus.soft_delete!(MyApp.Repo, post, reason: "Moderator action")
deleted_post = Lazarus.soft_delete!(MyApp.Repo, post, reload_after_delete: true)
"""
def soft_delete!(repo, struct_or_changeset, opts \\ []) do
Lazarus.Runtime.soft_delete!(repo, struct_or_changeset, opts)
end
@doc group: "Soft delete API"
@doc """
Soft-deletes all rows matched by a schema-aware queryable.
This is the repo-explicit equivalent of `Repo.soft_delete_all/2`.
## Returns
* `{count, nil}` when the query does not include a `select`
* `{count, returned}` when the query includes a `select`, matching the
usual Ecto bulk-update return shape
## Options
* `:reason` - string reason persisted to `deletion_reason` when the schema
includes that field, or `nil` (default: `nil`)
* `:cascade` - boolean controlling whether eligible associations are
cascaded (accepted values: `true`, `false`; default: `false`)
* `:skip_associations` - list of association field names to skip during
cascading, such as `[:comments, :ratings]`
* `:cascade_depth` - positive integer recursion cap for cascading
(default: `10`)
* `:allow_raw_sql` - boolean controlling whether fragments are allowed in
the bulk soft-delete query (accepted values: `true`, `false`; default:
`false`)
* `:allow_schema_less_sources` - boolean controlling whether schema-less
joins and nested sources are allowed in the bulk soft-delete query
(accepted values: `true`, `false`; default: `false`). The root source
must still be schema-aware.
## Query Requirements
* the query root must be schema-aware, such as `Post` or `{"posts", Post}`
* schema-less roots such as `from(row in "posts")` are not supported
* subquery roots such as `from(row in subquery(...))` are not supported
* schema-less joins and nested schema-less sources require
`allow_schema_less_sources: true`
See the "Query Support" guide for the full set of supported query shapes.
## Examples
Lazarus.soft_delete_all(MyApp.Repo, Post)
Lazarus.soft_delete_all(
MyApp.Repo,
from(post in Post, where: post.author_id == ^author_id),
reason: "Bulk cleanup"
)
"""
def soft_delete_all(repo, queryable, opts \\ []) do
Lazarus.Runtime.soft_delete_all(repo, queryable, opts)
end
@doc group: "Query helpers"
@doc """
Recursively applies Lazarus's read-side soft-delete filtering to an Ecto
query.
This is the same query-rewriting step injected into Repos that `use
Lazarus` for reads. `Repo.update_all/3` uses the same recursive filtering
internally.
## Returns
* a rewritten query when filtering is applied
* the original query unchanged when `with_deleted: true` is passed
and the query shape is safe or explicitly allowed
* non-query values unchanged
## Options
* `:with_deleted` - boolean controlling whether soft-delete filtering is
bypassed for the whole query (accepted values: `true`, `false`; default:
`false`)
* `:allow_raw_sql` - boolean controlling whether raw SQL fragments are
allowed (accepted values: `true`, `false`; default: `false`)
* `:allow_schema_less_sources` - boolean controlling whether schema-less
string table sources are allowed (accepted values: `true`, `false`;
default: `false`)
## Raises
* `ArgumentError` when the query contains raw SQL fragments unless
`allow_raw_sql: true` is passed
* `ArgumentError` when the query contains schema-less root or join sources
unless `allow_schema_less_sources: true` is passed
## Examples
query =
from(post in Post, select: post.id)
Lazarus.deep_filter_soft_deleted(query, [])
Lazarus.deep_filter_soft_deleted(query, with_deleted: true)
Lazarus.deep_filter_soft_deleted(query, allow_raw_sql: true)
Lazarus.deep_filter_soft_deleted(query, allow_schema_less_sources: true)
See the "Query Support" guide for the precise root, join, subquery, and CTE
behaviors.
"""
def deep_filter_soft_deleted(query, opts) do
Lazarus.Query.deep_filter_soft_deleted(query, opts)
end
@doc false
defmacro __using__(opts \\ []) do
quote do
use Lazarus.Repo, unquote(opts)
end
end
end