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