mirror of
https://github.com/ash-project/ash_archival.git
synced 2024-09-17 03:52:54 +12:00
improvement: support base_filter? true
option
docs: docs overhaul
This commit is contained in:
parent
f6ef8a91d5
commit
0032daca42
14 changed files with 128 additions and 70 deletions
|
@ -1,6 +1,7 @@
|
|||
spark_locals_without_parens = [
|
||||
archive_related: 1,
|
||||
attribute: 1,
|
||||
base_filter?: 1,
|
||||
exclude_destroy_actions: 1,
|
||||
exclude_read_actions: 1
|
||||
]
|
||||
|
|
34
README.md
34
README.md
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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.
|
11
documentation/topics/how-does-ash-archival-work.md
Normal file
11
documentation/topics/how-does-ash-archival-work.md
Normal 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`
|
|
@ -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
|
||||
|
|
8
documentation/topics/upgrading-to-1.0.md
Normal file
8
documentation/topics/upgrading-to-1.0.md
Normal 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.
|
59
documentation/tutorials/get-started-with-ash-archival.md
Normal file
59
documentation/tutorials/get-started-with-ash-archival.md
Normal 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.
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
8
mix.exs
8
mix.exs
|
@ -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: [
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue