Skip to content

Commit 404d8d6

Browse files
committed
Extracted
0 parents  commit 404d8d6

File tree

13 files changed

+383
-0
lines changed

13 files changed

+383
-0
lines changed

.formatter.exs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
spark_locals_without_parens = [except: 1, include_primary_read?: 1, sort: 1]
2+
3+
[
4+
import_deps: [:spark, :reactor, :ash],
5+
inputs: [
6+
"{mix,.formatter}.exs",
7+
"{config,lib,test}/**/*.{ex,exs}"
8+
],
9+
plugins: [Spark.Formatter],
10+
locals_without_parens: spark_locals_without_parens,
11+
export: [
12+
locals_without_parens: spark_locals_without_parens
13+
]
14+
]

.github/workflows/elixir.yaml

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
name: Elixir CI
2+
3+
# Define workflow that runs when changes are pushed to the
4+
# `main` branch or pushed to a PR branch that targets the `main`
5+
# branch. Change the branch name if your project uses a
6+
# different name for the main branch like "master" or "production".
7+
on:
8+
push:
9+
branches: ["main"] # adapt branch for project
10+
pull_request:
11+
branches: ["main"] # adapt branch for project
12+
13+
# Sets the ENV `MIX_ENV` to `test` for running tests
14+
env:
15+
MIX_ENV: test
16+
17+
permissions:
18+
contents: read
19+
20+
jobs:
21+
test:
22+
# Set up a Postgres DB service. By default, Phoenix applications
23+
# use Postgres. This creates a database for running tests.
24+
# Additional services can be defined here if required.
25+
services:
26+
db:
27+
image: postgres:12
28+
ports: ["5432:5432"]
29+
env:
30+
POSTGRES_PASSWORD: postgres
31+
options: >-
32+
--health-cmd pg_isready
33+
--health-interval 10s
34+
--health-timeout 5s
35+
--health-retries 5
36+
37+
runs-on: ubuntu-latest
38+
name: Test on OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}}
39+
strategy:
40+
# Specify the OTP and Elixir versions to use when building
41+
# and running the workflow steps.
42+
matrix:
43+
otp: ["27.0.0.0"] # Define the OTP version [required]
44+
elixir: ["1.18.0"] # Define the elixir version [required]
45+
steps:
46+
# Step: Identity Elixir + Erlang image as the base.
47+
- name: Set up Elixir
48+
uses: erlef/setup-beam@v1
49+
with:
50+
otp-version: ${{matrix.otp}}
51+
elixir-version: ${{matrix.elixir}}
52+
53+
# Step: Check out the code.
54+
- name: Checkout code
55+
uses: actions/checkout@v3
56+
57+
# Step: Define how to cache deps. Restores existing cache if present.
58+
- name: Cache deps
59+
id: cache-deps
60+
uses: actions/cache@v3
61+
env:
62+
cache-name: cache-elixir-deps
63+
with:
64+
path: deps
65+
key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }}
66+
restore-keys: |
67+
${{ runner.os }}-mix-${{ env.cache-name }}-
68+
69+
# Step: Define how to cache the `_build` directory. After the first run,
70+
# this speeds up tests runs a lot. This includes not re-compiling our
71+
# project's downloaded deps every run.
72+
- name: Cache compiled build
73+
id: cache-build
74+
uses: actions/cache@v3
75+
env:
76+
cache-name: cache-compiled-build
77+
with:
78+
path: _build
79+
key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }}
80+
restore-keys: |
81+
${{ runner.os }}-mix-${{ env.cache-name }}-
82+
${{ runner.os }}-mix-
83+
84+
# Step: Conditionally bust the cache when job is re-run.
85+
# Sometimes, we may have issues with incremental builds that are fixed by
86+
# doing a full recompile. In order to not waste dev time on such trivial
87+
# issues (while also reaping the time savings of incremental builds for
88+
# *most* day-to-day development), force a full recompile only on builds
89+
# that are retried.
90+
- name: Clean to rule out incremental build as a source of flakiness
91+
if: github.run_attempt != '1'
92+
run: |
93+
mix deps.clean --all
94+
mix clean
95+
shell: sh
96+
97+
# Step: Download project dependencies. If unchanged, uses
98+
# the cached version.
99+
- name: Install dependencies
100+
run: mix deps.get
101+
102+
# Step: Compile the project treating any warnings as errors.
103+
# Customize this step if a different behavior is desired.
104+
- name: Compiles without warnings
105+
run: mix compile --warnings-as-errors
106+
107+
# Step: Check that the checked in code has already been formatted.
108+
# This step fails if something was found unformatted.
109+
# Customize this step as desired.
110+
- name: Check Formatting
111+
run: mix format --check-formatted
112+
113+
# Step: Execute the tests.
114+
- name: Run tests
115+
run: mix test

.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# If the VM crashes, it generates a dump, let's ignore it too.
14+
erl_crash.dump
15+
16+
# Also ignore archive artifacts (built via "mix archive.build").
17+
*.ez
18+
19+
# Ignore package tarball (built via "mix hex.build").
20+
ash_always_select-*.tar
21+
22+
# Temporary files, for example, from tests.
23+
/tmp/
24+
*.code-workspace

.tool-versions

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
erlang 27.3
2+
elixir 1.18.3-otp-27

README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# AshDefaultSort
2+
3+
Configure a default sort to apply when a read action has no sort.
4+
5+
## Installation
6+
7+
Add `ash_default_sort` to your list of dependencies in `mix.exs`:
8+
9+
```elixir
10+
def deps do
11+
[
12+
{:ash_default_sort, "~> 0.1.0"}
13+
]
14+
end
15+
```
16+
17+
## Usage
18+
19+
```elixir
20+
defmodule Post do
21+
use Ash.Resource,
22+
data_layer: Ash.DataLayer.Postgres,
23+
extensions: [AshDefaultSort]
24+
25+
actions do
26+
read :read do
27+
primary? true
28+
end
29+
30+
read :read_sorted do
31+
prepare build(sort: [id: :desc])
32+
end
33+
34+
read :read_all do
35+
end
36+
37+
read :read_every do
38+
end
39+
40+
default_sort do
41+
sort [like_count: :desc, created_at: :desc]
42+
include_primary_read? false
43+
except [:read_all]
44+
end
45+
end
46+
```
47+
48+
In the example above, [like_count: :desc, created_at: :desc] is applied to `read_every`.
49+
50+
This is because `read` is excluded by `include_primary_read? false`, `read_sorted` is not affected because it already includes a sort, and `read_all` is excluded by the `except` option.
51+
52+
## License
53+
54+
MIT

config/config.exs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import Config
2+
3+
config :ash, :validate_domain_config_inclusion?, false

lib/ash_default_sort.ex

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
defmodule AshDefaultSort do
2+
@default_sort %Spark.Dsl.Section{
3+
name: :default_sort,
4+
describe: """
5+
Configure a default sort to apply when a read action has no sort.
6+
""",
7+
examples: [
8+
"""
9+
default_sort do
10+
sort [user_id: :asc, created_at: :desc]
11+
include_primary_read? true
12+
except [:read_without_sort]
13+
end
14+
"""
15+
],
16+
schema: [
17+
sort: [
18+
type: :keyword_list,
19+
required: false,
20+
default: [],
21+
doc: "The sort to apply when Ash.Query has no sort"
22+
],
23+
include_primary_read?: [
24+
type: :boolean,
25+
required: false,
26+
default: false,
27+
doc: "Whether to apply to the primary read action as well"
28+
],
29+
except: [
30+
type: {:wrap_list, :atom},
31+
required: false,
32+
default: [],
33+
doc: "List of read actions to exclude"
34+
]
35+
],
36+
entities: []
37+
}
38+
39+
use Spark.Dsl.Extension,
40+
sections: [@default_sort],
41+
transformers: [AshDefaultSort.Transformer]
42+
end

lib/info.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
defmodule AshDefaultSort.Info do
2+
use Spark.InfoGenerator, extension: AshDefaultSort, sections: [:default_sort]
3+
end

lib/preparation.ex

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
defmodule AshDefaultSort.Preparation do
2+
use Ash.Resource.Preparation
3+
4+
def prepare(query, [sort: sort], _context) do
5+
query
6+
|> Ash.Query.before_action(fn
7+
%Ash.Query{sort: [_ | _]} = sorted_query ->
8+
sorted_query
9+
10+
%Ash.Query{sort: []} = unsorted_query ->
11+
unsorted_query |> Ash.Query.sort(sort)
12+
end)
13+
end
14+
end

lib/transformer.ex

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
defmodule AshDefaultSort.Transformer do
2+
use Spark.Dsl.Transformer
3+
4+
alias Spark.Dsl.Transformer
5+
alias Ash.Resource.Builder
6+
7+
def after?(_), do: true
8+
9+
def transform(dsl_state) do
10+
sort = Transformer.get_option(dsl_state, [:default_sort], :sort)
11+
12+
include_primary_read? =
13+
Transformer.get_option(dsl_state, [:default_sort], :include_primary_read?)
14+
15+
except = Transformer.get_option(dsl_state, [:default_sort], :except)
16+
17+
dsl_state
18+
|> Transformer.get_entities([:actions])
19+
|> Enum.filter(&(&1.type == :read))
20+
|> Enum.reject(&(&1.name in except))
21+
|> Enum.reject(&(&1.primary? && !include_primary_read?))
22+
|> Enum.map(fn %Ash.Resource.Actions.Read{preparations: preparations} = read ->
23+
{:ok, preparation} = Builder.build_preparation({AshDefaultSort.Preparation, sort: sort})
24+
%Ash.Resource.Actions.Read{read | preparations: preparations ++ [preparation]}
25+
end)
26+
|> Enum.reduce(dsl_state, fn action, dsl_state ->
27+
Transformer.replace_entity(
28+
dsl_state,
29+
[:actions],
30+
action,
31+
&(&1.name == action.name)
32+
)
33+
end)
34+
|> then(fn dsl_state -> {:ok, dsl_state} end)
35+
end
36+
end

0 commit comments

Comments
 (0)