improvement: support base_filter? true option

docs: docs overhaul
This commit is contained in:
Zach Daniel 2024-05-02 15:46:13 -04:00
parent f6ef8a91d5
commit 0032daca42
14 changed files with 128 additions and 70 deletions

View file

@ -1,6 +1,7 @@
spark_locals_without_parens = [
archive_related: 1,
attribute: 1,
base_filter?: 1,
exclude_destroy_actions: 1,
exclude_read_actions: 1
]

View file

@ -1,24 +1,24 @@
# Ash Archival
![Logo](https://github.com/ash-project/ash/blob/main/logos/cropped-for-header-black-text.png?raw=true#gh-light-mode-only)
![Logo](https://github.com/ash-project/ash/blob/main/logos/cropped-for-header-white-text.png?raw=true#gh-dark-mode-only)
A small but useful resource extension for [Ash Framework](https://github.com/ash-project/ash), which configures resources to be archived instead of destroyed.
![Elixir CI](https://github.com/ash-project/ash_archival/workflows/CI/badge.svg)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Hex version badge](https://img.shields.io/hexpm/v/ash_archival.svg)](https://hex.pm/packages/ash_archival)
[![Hexdocs badge](https://img.shields.io/badge/docs-hexdocs-purple)](https://hexdocs.pm/ash_archival)
## Installation
# AshArchival
The package can be installed by adding `ash_archival` to your list of dependencies in `mix.exs`:
AshArchival is an [Ash](https://hexdocs.pm/ash) extension that provides a push-button solution for soft deleting records, instead of destroying them.
```elixir
def deps do
[
{:ash_archival, "~> 0.1"}
]
end
```
## Tutorials
## Using the archive extension
- [Get Started with AshArchival](documentation/tutorials/get-started-with-ash-archival.md)
On your ash resource add `AshArchival.Resource` to your extensions. For more details see the docs at https://ash-hq.org.
## Topics
```elixir
use Ash.Resource,
extensions: [AshArchival.Resource]
```
- [How does AshArchival work?](documentation/topics/how-does-ash-archival-work.md)
- [Unarchiving](documentation/topics/unarchiving.md)
## Reference
- [AshArchival DSL](documentation/dsls/DSL:-AshArchival.Resource.md)

View file

@ -3,6 +3,7 @@ import Config
if Mix.env() == :test do
config :ash, :validate_domain_resource_inclusion?, false
config :ash, :validate_domain_config_inclusion?, false
config :logger, level: :warning
end
if Mix.env() == :dev do
@ -15,6 +16,6 @@ if Mix.env() == :dev do
manage_mix_version?: true,
# Instructs the tool to manage the version in your README.md
# Pass in `true` to use `"README.md"` or a string to customize
manage_readme_version: "README.md",
manage_readme_version: ["README.md", "documentation/topics/get-started-with-ash-archival.md"],
version_tag_prefix: "v"
end

View file

@ -5,7 +5,7 @@ This file was generated by Spark. Do not edit it by hand.
Configures a resource to be archived instead of destroyed for all destroy actions.
For more information, see [Archival](/documentation/topics/archival.md)
For more information, see [the getting started guide](/documentation/tutorials/get-started-with-ash-archival.md)
## archive
@ -21,6 +21,7 @@ A section for configuring how archival is configured for a resource.
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`attribute`](#archive-attribute){: #archive-attribute } | `atom` | `:archived_at` | The attribute in which to store the archival flag (the current datetime). |
| [`base_filter?`](#archive-base_filter?){: #archive-base_filter? } | `atom` | `false` | Whether or not a base filter exists that applies the `is_nil(archived_at)` rule. |
| [`exclude_read_actions`](#archive-exclude_read_actions){: #archive-exclude_read_actions } | `atom \| list(atom)` | `[]` | A read action or actions that should show archived items. They will not get the automatic `is_nil(archived_at)` filter. |
| [`exclude_destroy_actions`](#archive-exclude_destroy_actions){: #archive-exclude_destroy_actions } | `atom \| list(atom)` | `[]` | A destroy action or actions that should *not* archive, but instead be left alone. This allows for having a destroy *or* archive pattern. |
| [`archive_related`](#archive-archive_related){: #archive-archive_related } | `list(atom)` | `[]` | A list of relationships that should have all related items archived when this is archived. Notifications are not sent for this operation. |

View file

@ -1,45 +0,0 @@
# Archival
## Extension
This extension modifies a resource in the following ways.
1. Adds a private `archived_at` `utc_datetime_usec` attribute.
2. Adds a preparation that filters each action for `is_nil(archived_at)` (except for excluded actions)
3. Marks all destroy actions as `soft?`, turning them into updates (except for excluded actions)
4. Adds a change to all destroy actions that sets `archived_at` to the current timestamp
5. Adds a change that will iteratively load and destroy anything configured in `d:AshArchival.Resource.archive|archive_related`
## Upgrading from < 1.0
Before 1.0 of this library, a `base_filter` was added to the resource to hide archived items. To retain the old behavior (which includes database structure),
add the `base_filter` and `base_filter_sql` yourself.
## Base Filter
Using a `base_filter` for your `archived_at` field has a lot of benefits if you are using `ash_postgres`, but comes with one major drawback, which is that it is not possible to exclude certain read actions from archival. If you wish to use a base filter, you will need to create a separate resource to read from the archived items. We may introduce a way to bypass the base filter at some point in the future.
To add a `base_filter` and `base_filter_sql` to your resource:
```elixir
resource do
base_filter expr(is_nil(archived_at))
end
postgres do
...
base_filter_sql "(archived_at IS NULL)"
end
```
### Benefits of base_filter
1. unique indexes will exclude archived items
2. custom indexes will exclude archived items
3. check constraints will not be applied to archived items
If you want these benefits, add the appropriate `base_filter`.
## More
See the [Unarchiving guide](/documentation/topics/unarchiving.md) For more.

View file

@ -0,0 +1,11 @@
# How does Archival Work?
We make modifications to the resource to enable soft deletes. Here's a breakdown of what the extension does:
## Resource Modifications
1. Adds a private `archived_at` `utc_datetime_usec` attribute.
2. Adds a preparation that filters each action for `is_nil(archived_at)` (except for excluded actions, or if you have `base_filter?` set to `true`).
3. Marks all destroy actions as `soft?`, turning them into updates (except for excluded actions)
4. Adds a change to all destroy actions that sets `archived_at` to the current timestamp
5. Adds a change that will iteratively load and destroy anything configured in `d:AshArchival.Resource.archive|archive_related`

View file

@ -1,5 +1,7 @@
# Un-archiving
If you want to unarchive a resource that uses a base filter, you will need to define a separate resource that uses the same storage and has no base filter. The rest of this guide applies for folks who _aren't_ using a `base_filter`.
Un-archiving can be accomplished by creating a read action that is skipped, using `exclude_read_actions`. Then, you can create an update action that sets that attribute to `nil`. For example:
```elixir

View file

@ -0,0 +1,8 @@
## Upgrading to 1.0
## Implementation changed from base_filter to preparations
Before 1.0 of this library, a `base_filter` was added to the resource to hide archived items. To retain the old behavior (which includes database structure),
add the `base_filter` and `base_filter_sql` yourself.
See the [getting started guide](documentation/tutorials/get-started-with-ash-archival.md) for more on base filters.

View file

@ -0,0 +1,59 @@
# Get Started with AshArchival
## Installation
First, add the dependency to your `mix.exs` file
```elixir
{:ash_archival, "~> 1.0.0-rc.1"}
```
and add `:ash_archival` to your `.formatter.exs`
```elixir
import_deps: [..., :ash_archival]
```
## Adding to a resource
To add archival to a resource, add the extension to the resource:
```elixir
use Ash.Resource,
extensions: [..., AshArchival.Resource]
```
And thats it! Now, when you destroy a record, it will be archived instead, using an `archived_at` attribute.
See [How Does Ash Archival Work?](/documentation/topics/get-started-with-ash-archival.md) for what modifications are made to a resource, and read on for info on the tradeoffs of leveraging `d:Ash.Resource.Dsl.resource.base_filter`.
## Base Filter
Using a `d:Ash.Resource.Dsl.resource.base_filter` for your `archived_at` field has a lot of benefits if you are using `ash_postgres`, but comes with one major drawback, which is that it is not possible to exclude certain read actions from archival. If you wish to use a base filter, you will need to create a separate resource to read from the archived items. We may introduce a way to bypass the base filter at some point in the future.
To add a `base_filter` and `base_filter_sql` to your resource:
```elixir
resource do
base_filter expr(is_nil(archived_at))
end
postgres do
...
base_filter_sql "(archived_at IS NULL)"
end
```
Add `base_filter? true` to the `archive` configuration of your resource to tell it that it doesn't need to add the filter itself.
### Benefits of base_filter
1. unique indexes will exclude archived items
2. custom indexes will exclude archived items
3. check constraints will not be applied to archived items
If you want these benefits, add the appropriate `base_filter`.
## More
See the [Unarchiving guide](/documentation/topics/unarchiving.md) For more.

View file

@ -3,7 +3,8 @@ defmodule AshArchival.Resource.Changes.FilterArchivedForUpserts do
use Ash.Resource.Change
def change(changeset, _, _) do
if changeset.context.private[:upsert?] do
if changeset.context.private[:upsert?] &&
!AshArchival.Resource.Info.archive_base_filter?(changeset.resource) do
attribute = AshArchival.Resource.Info.archive_attribute!(changeset.resource)
Ash.Changeset.filter(changeset, expr(is_nil(^ref(attribute))))
else

View file

@ -6,7 +6,8 @@ defmodule AshArchival.Resource.Preparations.FilterArchived do
excluded_actions =
AshArchival.Resource.Info.archive_exclude_read_actions!(query.resource)
if query.action.name in excluded_actions do
if query.action.name in excluded_actions ||
AshArchival.Resource.Info.archive_base_filter?(query.resource) do
query
else
attribute = AshArchival.Resource.Info.archive_attribute!(query.resource)

View file

@ -8,6 +8,11 @@ defmodule AshArchival.Resource do
default: :archived_at,
doc: "The attribute in which to store the archival flag (the current datetime)."
],
base_filter?: [
type: :atom,
default: false,
doc: "Whether or not a base filter exists that applies the `is_nil(archived_at)` rule."
],
exclude_read_actions: [
type: {:wrap_list, :atom},
default: [],
@ -35,7 +40,7 @@ defmodule AshArchival.Resource do
@moduledoc """
Configures a resource to be archived instead of destroyed for all destroy actions.
For more information, see [Archival](/documentation/topics/archival.md)
For more information, see [the getting started guide](/documentation/tutorials/get-started-with-ash-archival.md)
"""
use Spark.Dsl.Extension,

View file

@ -3,7 +3,7 @@ defmodule AshArchival.MixProject do
@version "1.0.0-rc.1"
@description """
A small resource extension that sets a resource up to archive instead of destroy.
An Ash extension to implement archival (soft deletion) for resources.
"""
def project do
@ -37,11 +37,13 @@ defmodule AshArchival.MixProject do
defp docs do
[
main: "archival",
main: "readme",
source_ref: "v#{@version}",
extras: [
"documentation/topics/archival.md",
{"README.md", title: "Home"},
"documentation/tutorials/get-started-with-ash-archival.md",
"documentation/topics/unarchiving.md",
"documentation/topics/how-does-ash-archival-work.md",
"documentation/dsls/DSL:-AshArchival.Resource.md"
],
groups_for_extras: [

View file

@ -193,6 +193,17 @@ defmodule ArchivalTest do
assert archived.archived_at
end
test "archived records are hidden" do
post =
Post
|> Ash.Changeset.for_create(:create)
|> Ash.create!()
assert :ok = post |> Ash.destroy!()
assert [] = Ash.read!(Post)
end
test "upserts don't consider archived records" do
post =
Post