├── .formatter.exs ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .tool-versions ├── .vscode └── tasks.json ├── LICENSE ├── README.md ├── lib ├── ash_table.ex └── ash_table │ ├── table.ex │ ├── table.html.heex │ └── table_helpers.ex ├── mix.exs ├── mix.lock ├── test ├── ash_table_test.exs └── test_helper.exs └── test_bed ├── .formatter.exs ├── .gitignore ├── README.md ├── assets ├── css │ └── app.css ├── js │ └── app.js ├── tailwind.config.js └── vendor │ └── topbar.js ├── config ├── config.exs ├── dev.exs ├── prod.exs ├── runtime.exs └── test.exs ├── lib ├── test_bed.ex ├── test_bed │ ├── application.ex │ ├── blog.ex │ ├── blog │ │ ├── author.ex │ │ └── post.ex │ ├── customers.ex │ ├── customers │ │ └── customer.ex │ ├── factory.ex │ ├── mailer.ex │ └── repo.ex ├── test_bed_web.ex └── test_bed_web │ ├── components │ ├── core_components.ex │ ├── layouts.ex │ └── layouts │ │ ├── app.html.heex │ │ └── root.html.heex │ ├── controllers │ ├── error_html.ex │ ├── error_json.ex │ ├── page_controller.ex │ ├── page_html.ex │ └── page_html │ │ └── home.html.heex │ ├── endpoint.ex │ ├── gettext.ex │ ├── live │ ├── customers_live │ │ └── index.ex │ └── posts_live │ │ └── index.ex │ ├── router.ex │ └── telemetry.ex ├── mix.exs ├── mix.lock ├── priv ├── gettext │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ └── errors.pot ├── repo │ ├── migrations │ │ ├── .formatter.exs │ │ ├── 20240629155735_install_3_extensions.exs │ │ ├── 20240629155736_initial_migration.exs │ │ ├── 20240629173304_add_author.exs │ │ ├── 20240701190158_add_customer.exs │ │ └── 20240701190257_add_customer_tenant.exs │ └── seeds.exs ├── resource_snapshots │ └── repo │ │ ├── authors │ │ └── 20240629173304.json │ │ ├── customers │ │ ├── 20240701190158.json │ │ └── 20240701190257.json │ │ ├── extensions.json │ │ └── posts │ │ ├── 20240629155736.json │ │ └── 20240629173304.json └── static │ ├── favicon.ico │ ├── images │ └── logo.svg │ └── robots.txt └── test ├── support ├── conn_case.ex └── data_case.ex ├── test_bed_web ├── controllers │ ├── error_html_test.exs │ ├── error_json_test.exs │ └── page_controller_test.exs └── live │ ├── customers_live │ └── index_test.exs │ └── posts_live │ └── index_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{heex,ex,exs}", "test_bed/{config,lib,test}/**/*.{heex,ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | name: Build and run tests 8 | runs-on: ubuntu-20.04 9 | 10 | services: 11 | db: 12 | image: postgres 13 | ports: ["5432:5432"] 14 | env: 15 | POSTGRES_USER: postgres 16 | POSTGRES_PASSWORD: postgres 17 | POSTRGES_DB: test_bed_test 18 | options: >- 19 | --health-cmd pg_isready 20 | --health-interval 10s 21 | --health-timeout 5s 22 | --health-retries 5 23 | 24 | steps: 25 | - name: Checkout project 26 | uses: actions/checkout@v3 27 | - name: Set up Elixir 28 | uses: erlef/setup-beam@v1 29 | with: 30 | otp-version: '26.2.1' # Define the OTP version [required] 31 | elixir-version: '1.16.3-otp-26' # Define the elixir version [required] 32 | - name: Restore dependencies cache 33 | uses: actions/cache@v2 34 | with: 35 | path: ash_table/deps 36 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 37 | restore-keys: ${{ runner.os }}-mix- 38 | - uses: actions/cache@v1 39 | name: Cache build 40 | with: 41 | path: ash_table/_build 42 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 43 | - name: Install dependencies 44 | run: mix deps.get 45 | - name: Install testbed dependencies 46 | run: mix deps.get 47 | working-directory: ./test_bed 48 | - name: Run elixir tests 49 | run: mix test 50 | working-directory: ./test_bed 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | ash_table-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.16.3-otp-26 2 | erlang 26.0.2 3 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "mix format", 6 | "type": "shell", 7 | "command": "mix", 8 | "args": [ 9 | "format" 10 | ], 11 | "problemMatcher": "$mixTestFailure" 12 | }, 13 | { 14 | "label": "mix test", 15 | "type": "shell", 16 | "command": "mix", 17 | "args": [ 18 | "test", 19 | "--color", 20 | "--warnings-as-errors" 21 | ], 22 | "options": { 23 | "cwd": "${workspaceRoot}/test_bed", 24 | "requireFiles": [ 25 | "test/**/test_helper.exs", 26 | "test/**/*_test.exs" 27 | ] 28 | }, 29 | "problemMatcher": "$mixTestFailure" 30 | }, 31 | { 32 | "label": "mix test failed", 33 | "type": "shell", 34 | "command": "mix", 35 | "args": [ 36 | "test", 37 | "--color", 38 | "--failed", 39 | "--trace" 40 | ], 41 | "options": { 42 | "cwd": "${workspaceRoot}/test_bed", 43 | "requireFiles": [ 44 | "test/**/test_helper.exs", 45 | "test/**/*_test.exs" 46 | ] 47 | }, 48 | "problemMatcher": "$mixTestFailure" 49 | }, 50 | { 51 | "label": "mix test file", 52 | "type": "shell", 53 | "command": "mix", 54 | "args": [ 55 | "test", 56 | "${relativeFile}", 57 | "--color", 58 | "--trace" 59 | ], 60 | "options": { 61 | "cwd": "${workspaceRoot}/test_bed", 62 | "requireFiles": [ 63 | "test/**/test_helper.exs", 64 | "test/**/*_test.exs" 65 | ] 66 | }, 67 | "problemMatcher": "$mixTestFailure" 68 | }, 69 | { 70 | "label": "mix test focused", 71 | "type": "shell", 72 | "command": "mix", 73 | "args": [ 74 | "test", 75 | "${relativeFile}:${lineNumber}", 76 | "--color", 77 | "--trace" 78 | ], 79 | "options": { 80 | "cwd": "${workspaceRoot}/test_bed", 81 | "requireFiles": [ 82 | "test/**/test_helper.exs", 83 | "test/**/*_test.exs" 84 | ] 85 | }, 86 | "problemMatcher": "$mixTestFailure", 87 | "group": { 88 | "kind": "test", 89 | "isDefault": true 90 | } 91 | }, 92 | { 93 | "label": "Initial Setup", 94 | "type": "process", 95 | "command": "mix", 96 | "args": [ 97 | "setup" 98 | ], 99 | "options": { 100 | "cwd": "${workspaceRoot}" 101 | }, 102 | "problemMatcher": [ 103 | "$mixCompileError" 104 | ] 105 | }, 106 | { 107 | "label": "mix compile test", 108 | "type": "process", 109 | "command": "mix", 110 | "args": [ 111 | "test", 112 | "--exclude", 113 | "test", 114 | "--warnings-as-errors" 115 | ], 116 | "options": { 117 | "cwd": "${workspaceRoot}" 118 | }, 119 | "problemMatcher": [ 120 | "$mixCompileWarning", 121 | "$mixCompileError" 122 | ], 123 | "group": { 124 | "kind": "build" 125 | } 126 | }, 127 | { 128 | "label": "mix compile", 129 | "type": "process", 130 | "command": "mix", 131 | "args": [ 132 | "compile", 133 | "--all-warnings" 134 | ], 135 | "options": { 136 | "cwd": "${workspaceRoot}" 137 | }, 138 | "problemMatcher": [ 139 | "$mixCompileWarning", 140 | "$mixCompileError" 141 | ], 142 | "group": { 143 | "kind": "build", 144 | "isDefault": true 145 | } 146 | } 147 | ] 148 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Christopher Nelson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AshTable 2 | 3 | This is a sortable, paginated table component for Ash resources or queries 4 | 5 | ## Status 6 | 7 | - [X] Sort by attribute 8 | - [X] Sort by relation (using function passed to column) 9 | - [X] Pagination 10 | 11 | ## Usage 12 | 13 | Here's an example taken from the test_bed: 14 | 15 | ```elixir 16 | defmodule TestBedWeb.PostsLive.Index do 17 | use TestBedWeb, :live_view 18 | 19 | def render(assigns) do 20 | ~H""" 21 | <.live_component id="posts_table" limit={10} offset={0} sort={{"id", :asc}} module={AshTable.Table} query={TestBed.Blog.Post}> 22 | <:col :let={post} label="Id" sort_key="id"><%= post.id %> 23 | <:col :let={post} label="Title" sort_key="title"> 24 | <%= post.title %> 25 | 26 | <:col :let={post} label="Author" apply_sort={&sort_by_author/2} sort_key="author.name"> 27 | <%= if post.author, do: post.author.name %> 28 | 29 | 30 | """ 31 | end 32 | 33 | require Ash.Sort 34 | 35 | defp sort_by_author(query, direction) do 36 | Ash.Query.sort(query, {Ash.Sort.expr_sort(author.name), direction}) 37 | end 38 | 39 | end 40 | ``` 41 | 42 | In this case the `TestBed.Blog.Post` resource has a title, content, and belongs to Author which has a name. The table is paginated, and sortable by Title and Author name. 43 | 44 | Note use of the `apply_sort` being passed into the `:col`. This is needed for sorting by related properties due to how Ash works, or til I better understand it and find a simpler way :) The `sort_key` assign is still required so that the correct column is identified when the sort event fires. 45 | 46 | ## Running the test_bed example project 47 | 48 | ``` 49 | cd test_bed 50 | mix deps.get 51 | mix ash.setup 52 | mix phx.server 53 | ``` 54 | 55 | ## Future 56 | 57 | Currently there is very little styling. The goal would be to allow a good default, but great flexiblity. Harcoding a dependency on Tailwind or other css frameworks is not desirable, but allowing the user to decide to use one would be great. 58 | -------------------------------------------------------------------------------- /lib/ash_table.ex: -------------------------------------------------------------------------------- 1 | defmodule AshTable do 2 | @moduledoc """ 3 | Documentation for `AshTable`. 4 | """ 5 | 6 | @doc """ 7 | Hello world. 8 | 9 | ## Examples 10 | 11 | iex> AshTable.hello() 12 | :world 13 | 14 | """ 15 | def hello do 16 | :world 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/ash_table/table.ex: -------------------------------------------------------------------------------- 1 | defmodule AshTable.Table do 2 | use Phoenix.LiveComponent 3 | 4 | alias AshTable.TableHelpers 5 | 6 | @moduledoc """ 7 | Generic sortable table component 8 | 9 | Expects the following parameters as assigns: 10 | 11 | * `id` - necessary, as this is a stateful LiveView component 12 | * `query` - An Ash Query or Resource module 13 | * `sort` (optional) - a `t:sort/0` specifying the initial sort direction 14 | * `limit` - page size 15 | * `offset` - initial offset for pagination 16 | * `col` columns 17 | * attribute - the field this column displays, used to sort 18 | * apply_sort - optional arity 2 function which takes query, direction as args 19 | * `caption` (optional) 20 | * `read_options` - an options keyword list of options that will be passed into `Ash.read` when data is fetched. 21 | This allows for specifying `:tenant`, `:actor`, etc. 22 | 23 | """ 24 | 25 | @assigns [ 26 | :id, 27 | :sort, 28 | :query, 29 | :col, 30 | :offset, 31 | :limit, 32 | :read_options 33 | ] 34 | 35 | @default_assigns %{ 36 | limit: 10, 37 | offset: 0, 38 | read_options: [] 39 | } 40 | 41 | @type sort :: {atom | nil, :asc | :desc} 42 | 43 | @impl true 44 | def mount(socket) do 45 | socket 46 | |> assign(sort: {nil, :asc}) 47 | |> then(&{:ok, &1}) 48 | end 49 | 50 | @impl true 51 | def update(assigns, socket) do 52 | socket 53 | |> assign(apply_defaults(assigns)) 54 | |> assign(:query, assigns.query) 55 | |> fetch_data() 56 | |> then(&{:ok, &1}) 57 | end 58 | 59 | defp apply_defaults(assigns) do 60 | @default_assigns |> Map.merge(Map.take(assigns, @assigns)) 61 | end 62 | 63 | defp fetch_data( 64 | %{ 65 | assigns: %{ 66 | query: query, 67 | sort: sort, 68 | col: columns, 69 | limit: limit, 70 | offset: offset, 71 | read_options: read_options 72 | } 73 | } = socket 74 | ) do 75 | results = 76 | query 77 | |> apply_sort(sort, columns) 78 | |> Ash.read!(Keyword.merge(read_options, page: [limit: limit, offset: offset])) 79 | 80 | assign(socket, :results, results) 81 | end 82 | 83 | defp rows_from(%Ash.Page.Offset{results: results}), do: results 84 | 85 | defp apply_sort(query, {sort_key, direction}, columns) do 86 | col = columns |> Enum.find(&(&1[:sort_key] == sort_key)) 87 | 88 | case col do 89 | %{apply_sort: apply_sort} when is_function(apply_sort) -> apply_sort.(query, direction) 90 | _ -> Ash.Query.sort(query, {String.to_existing_atom(sort_key), direction}) 91 | end 92 | end 93 | 94 | @impl true 95 | def handle_event( 96 | "sort", 97 | %{"column" => column, "direction" => direction} = _params, 98 | socket 99 | ) do 100 | direction = String.to_existing_atom(direction) 101 | sort = {column, direction} 102 | 103 | socket 104 | |> assign(sort: sort) 105 | |> fetch_data() 106 | |> then(&{:noreply, &1}) 107 | end 108 | 109 | def handle_event("set_page", %{"offset" => offset}, socket) do 110 | socket 111 | |> assign(offset: String.to_integer(offset)) 112 | |> fetch_data() 113 | |> then(&{:noreply, &1}) 114 | end 115 | 116 | def sort_class(column_key, {sort_key, direction}) do 117 | if String.to_existing_atom(column_key) == sort_key do 118 | Atom.to_string(direction) 119 | else 120 | "none" 121 | end 122 | end 123 | 124 | def sort_direction(column_key, sort) when is_binary(column_key) do 125 | column_key 126 | |> String.to_existing_atom() 127 | |> sort_direction(sort) 128 | end 129 | 130 | def sort_direction(column_key, {column_key, direction}), do: toggle_direction(direction) 131 | def sort_direction(_, _), do: :asc 132 | 133 | def toggle_direction(:asc), do: :desc 134 | def toggle_direction(:desc), do: :asc 135 | 136 | def sort_normalized_keys(keys) do 137 | fn obj -> 138 | keys |> Enum.map(&(obj[&1] || "")) |> Enum.map(&String.downcase/1) |> List.to_tuple() 139 | end 140 | end 141 | 142 | def noreply(term) do 143 | {:noreply, term} 144 | end 145 | 146 | def ok(term) do 147 | {:ok, term} 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /lib/ash_table/table.html.heex: -------------------------------------------------------------------------------- 1 |
2 | 3 | <%= if assigns[:caption] do %> 4 | 5 | <% end %> 6 | 7 | 8 | <%= for col <- @col, Map.get(col, :when, true) do %> 9 | <%= if Map.has_key?(col, :sort_key) do %> 10 | 11 | 12 | 13 | <% else %> 14 | 15 | <%= col.label %> 16 | 17 | <% end %> 18 | <% end %> 19 | 20 | 21 | 22 | <%= for row <- rows_from(@results) do %> 23 | 24 | <%= for col <- @col, Map.get(col, :when, true) do %> 25 | 28 | <% end %> 29 | 30 | <% end %> 31 | 32 |
<%= @caption %>
26 | <%= render_slot(col, row) %> 27 |
33 | 34 |
35 | 36 | -------------------------------------------------------------------------------- /lib/ash_table/table_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule AshTable.TableHelpers do 2 | @moduledoc false 3 | use Phoenix.Component 4 | 5 | def column_header(assigns) do 6 | ~H""" 7 | 14 | <%= render_slot(@inner_block) %> 15 | 16 | """ 17 | end 18 | 19 | def sort_button(assigns) do 20 | ~H""" 21 | 30 | """ 31 | end 32 | 33 | def paginator(assigns) do 34 | ~H""" 35 | 36 |
Viewing <%= @results.offset + 1 %> to <%= @results.offset + @results.limit %>
37 | 38 | """ 39 | end 40 | 41 | defp sort_direction(column, sort) do 42 | with %{sort_key: column_key} when is_binary(column_key) <- column, 43 | {^column_key, direction} <- sort do 44 | toggle_direction(direction) 45 | else 46 | _ -> :asc 47 | end 48 | end 49 | 50 | defp toggle_direction(:asc), do: :desc 51 | defp toggle_direction(:desc), do: :asc 52 | 53 | defp sort_class(column, sort) do 54 | with %{sort_key: column_key} when is_binary(column_key) <- column, 55 | {^column_key, direction} <- sort do 56 | Atom.to_string(direction) 57 | else 58 | _ -> "none" 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule AshTable.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :ash_table, 7 | version: "0.2.0", 8 | elixir: "~> 1.14", 9 | description: "A sortable, paginated table as a LiveView component, that integrates with Ash", 10 | start_permanent: Mix.env() == :prod, 11 | deps: deps(), 12 | package: [ 13 | licenses: ["MIT"], 14 | links: %{"Github" => "https://github.com/launchscout/ash_table"} 15 | ] 16 | ] 17 | end 18 | 19 | # Run "mix help compile.app" to learn about applications. 20 | def application do 21 | [ 22 | extra_applications: [:logger] 23 | ] 24 | end 25 | 26 | # Run "mix help deps" to learn about dependencies. 27 | defp deps do 28 | [ 29 | {:phoenix_live_view, "~> 0.20.2"}, 30 | {:ash, "~> 3.0"}, 31 | {:ash_postgres, "~> 2.0"}, 32 | {:ash_phoenix, "~> 2.0"}, 33 | {:ex_doc, ">= 0.0.0"} 34 | ] 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "ash": {:hex, :ash, "3.0.16", "8eaebd5a9f3ee404937ac811a240799613b0619026e097436132d60eaf18ed16", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, ">= 0.8.1 and < 1.0.0-0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.1.18 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "36c0d7653f7fb1d13cc03e1cc7ea7f6b9aadd278b9c9375ff5f0636ed0d7a785"}, 3 | "ash_phoenix": {:hex, :ash_phoenix, "2.0.4", "0d18a3371879b875865180aaabc1697a35a6dcb9ebd2f346456208214bd02c9e", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.3 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "f3ea5309b42cdcaafc0ca713757cd4bb4819e02aeacc5195a040a955e861767d"}, 4 | "ash_postgres": {:hex, :ash_postgres, "2.0.12", "0cd30b5eab6ef6fc77d1f29c23bd8b9ad62e676f8aa14bf9194d3cf87e10adf2", [:mix], [{:ash, ">= 3.0.15 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.2.6 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "a6536a641bcb3dc0ff5b351c35b9334e5586170037f88c0035f532dcba872700"}, 5 | "ash_sql": {:hex, :ash_sql, "0.2.7", "56bfddcb4cf3edbbf702e2b665497309e43672fbf449ef049f4805211b9cd1b7", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "14622713cc08ede8fd0d2618b1718d759a6ee28839b8f738e6ee084703bd9437"}, 6 | "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, 7 | "comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"}, 8 | "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, 9 | "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, 10 | "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, 11 | "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, 12 | "ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"}, 13 | "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, 14 | "ex_doc": {:hex, :ex_doc, "0.34.1", "9751a0419bc15bc7580c73fde506b17b07f6402a1e5243be9e0f05a68c723368", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d441f1a86a235f59088978eff870de2e815e290e44a8bd976fe5d64470a4c9d2"}, 15 | "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, 16 | "glob_ex": {:hex, :glob_ex, "0.1.7", "eae6b6377147fb712ac45b360e6dbba00346689a87f996672fe07e97d70597b1", [:mix], [], "hexpm", "decc1c21c0c73df3c9c994412716345c1692477b9470e337f628a7e08da0da6a"}, 17 | "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"}, 18 | "igniter": {:hex, :igniter, "0.2.4", "71fc8a473c07de9bacfaa26862427d695b49d263c2f484a256fdb38fcc3471cc", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, "~> 0.9", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "361b9bc44f6e36161076bde02ed75287280b5aa2c7d0ad9bde83d521cc875115"}, 19 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 20 | "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, 21 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, 22 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 23 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, 24 | "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, 25 | "mint": {:hex, :mint, "1.6.1", "065e8a5bc9bbd46a41099dfea3e0656436c5cbcb6e741c80bd2bad5cd872446f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4fc518dcc191d02f433393a72a7ba3f6f94b101d094cb6bf532ea54c89423780"}, 26 | "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 27 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 28 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 29 | "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, 30 | "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, 31 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.17", "f396bbdaf4ba227b82251eb75ac0afa6b3da5e509bc0d030206374237dfc9450", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61d741ffb78c85fdbca0de084da6a48f8ceb5261a79165b5a0b59e5f65ce98b"}, 32 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, 33 | "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, 34 | "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, 35 | "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, 36 | "postgrex": {:hex, :postgrex, "0.18.0", "f34664101eaca11ff24481ed4c378492fed2ff416cd9b06c399e90f321867d7e", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a042989ba1bc1cca7383ebb9e461398e3f89f868c92ce6671feb7ef132a252d1"}, 37 | "reactor": {:hex, :reactor, "0.8.4", "344d02ba4a0010763851f4e4aa0ff190ebe7e392e3c27c6cd143dde077b986e7", [:mix], [{:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "49c1fd3c786603cec8140ce941c41c7ea72cc4411860ccdee9876c4ca2204f81"}, 38 | "req": {:hex, :req, "0.5.1", "90584216d064389a4ff2d4279fe2c11ff6c812ab00fa01a9fb9d15457f65ba70", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7ea96a1a95388eb0fefa92d89466cdfedba24032794e5c1147d78ec90db7edca"}, 39 | "rewrite": {:hex, :rewrite, "0.10.5", "6afadeae0b9d843b27ac6225e88e165884875e0aed333ef4ad3bf36f9c101bed", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "51cc347a4269ad3a1e7a2c4122dbac9198302b082f5615964358b4635ebf3d4f"}, 40 | "sourceror": {:hex, :sourceror, "1.4.0", "be87319b1579191e25464005d465713079b3fd7124a3938a1e6cf4def39735a9", [:mix], [], "hexpm", "16751ca55e3895f2228938b703ad399b0b27acfe288eff6c0e629ed3e6ec0358"}, 41 | "spark": {:hex, :spark, "2.2.6", "4f160462f45c0be2bccdc4700e7ffc6b2e97b4e38f57eed2349bc9dab4aaa66c", [:mix], [{:igniter, "~> 0.2", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "1e0e012978be808232a502a116d4b99b5059ab3760453438b155ac048f82ce20"}, 42 | "spitfire": {:hex, :spitfire, "0.1.3", "7ea0f544005dfbe48e615ed90250c9a271bfe126914012023fd5e4b6b82b7ec7", [:mix], [], "hexpm", "d53b5107bcff526a05c5bb54c95e77b36834550affd5830c9f58760e8c543657"}, 43 | "splode": {:hex, :splode, "0.2.4", "71046334c39605095ca4bed5d008372e56454060997da14f9868534c17b84b53", [:mix], [], "hexpm", "ca3b95f0d8d4b482b5357954fec857abd0fa3ea509d623334c1328e7382044c2"}, 44 | "stream_data": {:hex, :stream_data, "1.1.1", "fd515ca95619cca83ba08b20f5e814aaf1e5ebff114659dc9731f966c9226246", [:mix], [], "hexpm", "45d0cd46bd06738463fd53f22b70042dbb58c384bb99ef4e7576e7bb7d3b8c8c"}, 45 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 46 | "typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"}, 47 | "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, 48 | "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"}, 49 | } 50 | -------------------------------------------------------------------------------- /test/ash_table_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AshTableTest do 2 | use ExUnit.Case 3 | doctest AshTable 4 | 5 | test "greets the world" do 6 | assert AshTable.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test_bed/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto, :ecto_sql, :phoenix], 3 | subdirectories: ["priv/*/migrations"], 4 | plugins: [Phoenix.LiveView.HTMLFormatter], 5 | inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "priv/*/seeds.exs"] 6 | ] 7 | -------------------------------------------------------------------------------- /test_bed/.gitignore: -------------------------------------------------------------------------------- 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 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | 25 | # Ignore package tarball (built via "mix hex.build"). 26 | test_bed-*.tar 27 | 28 | # Ignore assets that are produced by build tools. 29 | /priv/static/assets/ 30 | 31 | # Ignore digested assets cache. 32 | /priv/static/cache_manifest.json 33 | 34 | # In case you use Node.js/npm, you want to ignore these. 35 | npm-debug.log 36 | /assets/node_modules/ 37 | 38 | -------------------------------------------------------------------------------- /test_bed/README.md: -------------------------------------------------------------------------------- 1 | # TestBed 2 | 3 | To start your Phoenix server: 4 | 5 | * Run `mix setup` to install and setup dependencies 6 | * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` 7 | 8 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 9 | 10 | Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). 11 | 12 | ## Learn more 13 | 14 | * Official website: https://www.phoenixframework.org/ 15 | * Guides: https://hexdocs.pm/phoenix/overview.html 16 | * Docs: https://hexdocs.pm/phoenix 17 | * Forum: https://elixirforum.com/c/phoenix-forum 18 | * Source: https://github.com/phoenixframework/phoenix 19 | -------------------------------------------------------------------------------- /test_bed/assets/css/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; 4 | 5 | th[aria-sort="desc"] button::after { 6 | content: "▼"; 7 | color: currentcolor; 8 | font-size: 100%; 9 | top: 0; 10 | } 11 | 12 | th[aria-sort="asc"] button::after { 13 | content: "▲"; 14 | color: currentcolor; 15 | font-size: 100%; 16 | top: 0; 17 | } 18 | 19 | /* This file is for your main application CSS */ 20 | -------------------------------------------------------------------------------- /test_bed/assets/js/app.js: -------------------------------------------------------------------------------- 1 | // If you want to use Phoenix channels, run `mix help phx.gen.channel` 2 | // to get started and then uncomment the line below. 3 | // import "./user_socket.js" 4 | 5 | // You can include dependencies in two ways. 6 | // 7 | // The simplest option is to put them in assets/vendor and 8 | // import them using relative paths: 9 | // 10 | // import "../vendor/some-package.js" 11 | // 12 | // Alternatively, you can `npm install some-package --prefix assets` and import 13 | // them using a path starting with the package name: 14 | // 15 | // import "some-package" 16 | // 17 | 18 | // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. 19 | import "phoenix_html" 20 | // Establish Phoenix Socket and LiveView configuration. 21 | import {Socket} from "phoenix" 22 | import {LiveSocket} from "phoenix_live_view" 23 | import topbar from "../vendor/topbar" 24 | 25 | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") 26 | let liveSocket = new LiveSocket("/live", Socket, { 27 | longPollFallbackMs: 2500, 28 | params: {_csrf_token: csrfToken} 29 | }) 30 | 31 | // Show progress bar on live navigation and form submits 32 | topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) 33 | window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) 34 | window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) 35 | 36 | // connect if there are any LiveViews on the page 37 | liveSocket.connect() 38 | 39 | // expose liveSocket on window for web console debug logs and latency simulation: 40 | // >> liveSocket.enableDebug() 41 | // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session 42 | // >> liveSocket.disableLatencySim() 43 | window.liveSocket = liveSocket 44 | 45 | -------------------------------------------------------------------------------- /test_bed/assets/tailwind.config.js: -------------------------------------------------------------------------------- 1 | // See the Tailwind configuration guide for advanced usage 2 | // https://tailwindcss.com/docs/configuration 3 | 4 | const plugin = require("tailwindcss/plugin") 5 | const fs = require("fs") 6 | const path = require("path") 7 | 8 | module.exports = { 9 | content: [ 10 | "./js/**/*.js", 11 | "../lib/test_bed_web.ex", 12 | "../lib/test_bed_web/**/*.*ex" 13 | ], 14 | theme: { 15 | extend: { 16 | colors: { 17 | brand: "#FD4F00", 18 | } 19 | }, 20 | }, 21 | plugins: [ 22 | require("@tailwindcss/forms"), 23 | // Allows prefixing tailwind classes with LiveView classes to add rules 24 | // only when LiveView classes are applied, for example: 25 | // 26 | //
27 | // 28 | plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])), 29 | plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])), 30 | plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])), 31 | plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])), 32 | 33 | // Embeds Heroicons (https://heroicons.com) into your app.css bundle 34 | // See your `CoreComponents.icon/1` for more information. 35 | // 36 | plugin(function({matchComponents, theme}) { 37 | let iconsDir = path.join(__dirname, "../deps/heroicons/optimized") 38 | let values = {} 39 | let icons = [ 40 | ["", "/24/outline"], 41 | ["-solid", "/24/solid"], 42 | ["-mini", "/20/solid"], 43 | ["-micro", "/16/solid"] 44 | ] 45 | icons.forEach(([suffix, dir]) => { 46 | fs.readdirSync(path.join(iconsDir, dir)).forEach(file => { 47 | let name = path.basename(file, ".svg") + suffix 48 | values[name] = {name, fullPath: path.join(iconsDir, dir, file)} 49 | }) 50 | }) 51 | matchComponents({ 52 | "hero": ({name, fullPath}) => { 53 | let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "") 54 | let size = theme("spacing.6") 55 | if (name.endsWith("-mini")) { 56 | size = theme("spacing.5") 57 | } else if (name.endsWith("-micro")) { 58 | size = theme("spacing.4") 59 | } 60 | return { 61 | [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`, 62 | "-webkit-mask": `var(--hero-${name})`, 63 | "mask": `var(--hero-${name})`, 64 | "mask-repeat": "no-repeat", 65 | "background-color": "currentColor", 66 | "vertical-align": "middle", 67 | "display": "inline-block", 68 | "width": size, 69 | "height": size 70 | } 71 | } 72 | }, {values}) 73 | }) 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /test_bed/assets/vendor/topbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * topbar 2.0.0, 2023-02-04 4 | * https://buunguyen.github.io/topbar 5 | * Copyright (c) 2021 Buu Nguyen 6 | */ 7 | (function (window, document) { 8 | "use strict"; 9 | 10 | // https://gist.github.com/paulirish/1579671 11 | (function () { 12 | var lastTime = 0; 13 | var vendors = ["ms", "moz", "webkit", "o"]; 14 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 15 | window.requestAnimationFrame = 16 | window[vendors[x] + "RequestAnimationFrame"]; 17 | window.cancelAnimationFrame = 18 | window[vendors[x] + "CancelAnimationFrame"] || 19 | window[vendors[x] + "CancelRequestAnimationFrame"]; 20 | } 21 | if (!window.requestAnimationFrame) 22 | window.requestAnimationFrame = function (callback, element) { 23 | var currTime = new Date().getTime(); 24 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 25 | var id = window.setTimeout(function () { 26 | callback(currTime + timeToCall); 27 | }, timeToCall); 28 | lastTime = currTime + timeToCall; 29 | return id; 30 | }; 31 | if (!window.cancelAnimationFrame) 32 | window.cancelAnimationFrame = function (id) { 33 | clearTimeout(id); 34 | }; 35 | })(); 36 | 37 | var canvas, 38 | currentProgress, 39 | showing, 40 | progressTimerId = null, 41 | fadeTimerId = null, 42 | delayTimerId = null, 43 | addEvent = function (elem, type, handler) { 44 | if (elem.addEventListener) elem.addEventListener(type, handler, false); 45 | else if (elem.attachEvent) elem.attachEvent("on" + type, handler); 46 | else elem["on" + type] = handler; 47 | }, 48 | options = { 49 | autoRun: true, 50 | barThickness: 3, 51 | barColors: { 52 | 0: "rgba(26, 188, 156, .9)", 53 | ".25": "rgba(52, 152, 219, .9)", 54 | ".50": "rgba(241, 196, 15, .9)", 55 | ".75": "rgba(230, 126, 34, .9)", 56 | "1.0": "rgba(211, 84, 0, .9)", 57 | }, 58 | shadowBlur: 10, 59 | shadowColor: "rgba(0, 0, 0, .6)", 60 | className: null, 61 | }, 62 | repaint = function () { 63 | canvas.width = window.innerWidth; 64 | canvas.height = options.barThickness * 5; // need space for shadow 65 | 66 | var ctx = canvas.getContext("2d"); 67 | ctx.shadowBlur = options.shadowBlur; 68 | ctx.shadowColor = options.shadowColor; 69 | 70 | var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0); 71 | for (var stop in options.barColors) 72 | lineGradient.addColorStop(stop, options.barColors[stop]); 73 | ctx.lineWidth = options.barThickness; 74 | ctx.beginPath(); 75 | ctx.moveTo(0, options.barThickness / 2); 76 | ctx.lineTo( 77 | Math.ceil(currentProgress * canvas.width), 78 | options.barThickness / 2 79 | ); 80 | ctx.strokeStyle = lineGradient; 81 | ctx.stroke(); 82 | }, 83 | createCanvas = function () { 84 | canvas = document.createElement("canvas"); 85 | var style = canvas.style; 86 | style.position = "fixed"; 87 | style.top = style.left = style.right = style.margin = style.padding = 0; 88 | style.zIndex = 100001; 89 | style.display = "none"; 90 | if (options.className) canvas.classList.add(options.className); 91 | document.body.appendChild(canvas); 92 | addEvent(window, "resize", repaint); 93 | }, 94 | topbar = { 95 | config: function (opts) { 96 | for (var key in opts) 97 | if (options.hasOwnProperty(key)) options[key] = opts[key]; 98 | }, 99 | show: function (delay) { 100 | if (showing) return; 101 | if (delay) { 102 | if (delayTimerId) return; 103 | delayTimerId = setTimeout(() => topbar.show(), delay); 104 | } else { 105 | showing = true; 106 | if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId); 107 | if (!canvas) createCanvas(); 108 | canvas.style.opacity = 1; 109 | canvas.style.display = "block"; 110 | topbar.progress(0); 111 | if (options.autoRun) { 112 | (function loop() { 113 | progressTimerId = window.requestAnimationFrame(loop); 114 | topbar.progress( 115 | "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2) 116 | ); 117 | })(); 118 | } 119 | } 120 | }, 121 | progress: function (to) { 122 | if (typeof to === "undefined") return currentProgress; 123 | if (typeof to === "string") { 124 | to = 125 | (to.indexOf("+") >= 0 || to.indexOf("-") >= 0 126 | ? currentProgress 127 | : 0) + parseFloat(to); 128 | } 129 | currentProgress = to > 1 ? 1 : to; 130 | repaint(); 131 | return currentProgress; 132 | }, 133 | hide: function () { 134 | clearTimeout(delayTimerId); 135 | delayTimerId = null; 136 | if (!showing) return; 137 | showing = false; 138 | if (progressTimerId != null) { 139 | window.cancelAnimationFrame(progressTimerId); 140 | progressTimerId = null; 141 | } 142 | (function loop() { 143 | if (topbar.progress("+.1") >= 1) { 144 | canvas.style.opacity -= 0.05; 145 | if (canvas.style.opacity <= 0.05) { 146 | canvas.style.display = "none"; 147 | fadeTimerId = null; 148 | return; 149 | } 150 | } 151 | fadeTimerId = window.requestAnimationFrame(loop); 152 | })(); 153 | }, 154 | }; 155 | 156 | if (typeof module === "object" && typeof module.exports === "object") { 157 | module.exports = topbar; 158 | } else if (typeof define === "function" && define.amd) { 159 | define(function () { 160 | return topbar; 161 | }); 162 | } else { 163 | this.topbar = topbar; 164 | } 165 | }.call(this, window, document)); 166 | -------------------------------------------------------------------------------- /test_bed/config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | 7 | # General application configuration 8 | import Config 9 | 10 | config :test_bed, 11 | ecto_repos: [TestBed.Repo], 12 | generators: [timestamp_type: :utc_datetime] 13 | 14 | # Configures the endpoint 15 | config :test_bed, TestBedWeb.Endpoint, 16 | url: [host: "localhost"], 17 | adapter: Bandit.PhoenixAdapter, 18 | render_errors: [ 19 | formats: [html: TestBedWeb.ErrorHTML, json: TestBedWeb.ErrorJSON], 20 | layout: false 21 | ], 22 | pubsub_server: TestBed.PubSub, 23 | live_view: [signing_salt: "oMtOzrbs"] 24 | 25 | config :test_bed, 26 | ash_domains: [TestBed.Blog, TestBed.Customers] 27 | 28 | # Configures the mailer 29 | # 30 | # By default it uses the "Local" adapter which stores the emails 31 | # locally. You can see the emails in your browser, at "/dev/mailbox". 32 | # 33 | # For production it's recommended to configure a different adapter 34 | # at the `config/runtime.exs`. 35 | config :test_bed, TestBed.Mailer, adapter: Swoosh.Adapters.Local 36 | 37 | # Configure esbuild (the version is required) 38 | config :esbuild, 39 | version: "0.17.11", 40 | test_bed: [ 41 | args: 42 | ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), 43 | cd: Path.expand("../assets", __DIR__), 44 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} 45 | ] 46 | 47 | # Configure tailwind (the version is required) 48 | config :tailwind, 49 | version: "3.4.0", 50 | test_bed: [ 51 | args: ~w( 52 | --config=tailwind.config.js 53 | --input=css/app.css 54 | --output=../priv/static/assets/app.css 55 | ), 56 | cd: Path.expand("../assets", __DIR__) 57 | ] 58 | 59 | # Configures Elixir's Logger 60 | config :logger, :console, 61 | format: "$time $metadata[$level] $message\n", 62 | metadata: [:request_id] 63 | 64 | # Use Jason for JSON parsing in Phoenix 65 | config :phoenix, :json_library, Jason 66 | 67 | # Import environment specific config. This must remain at the bottom 68 | # of this file so it overrides the configuration defined above. 69 | import_config "#{config_env()}.exs" 70 | -------------------------------------------------------------------------------- /test_bed/config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Configure your database 4 | config :test_bed, TestBed.Repo, 5 | username: "postgres", 6 | password: "postgres", 7 | hostname: "localhost", 8 | database: "test_bed_dev", 9 | stacktrace: true, 10 | show_sensitive_data_on_connection_error: true, 11 | pool_size: 10 12 | 13 | # For development, we disable any cache and enable 14 | # debugging and code reloading. 15 | # 16 | # The watchers configuration can be used to run external 17 | # watchers to your application. For example, we can use it 18 | # to bundle .js and .css sources. 19 | config :test_bed, TestBedWeb.Endpoint, 20 | # Binding to loopback ipv4 address prevents access from other machines. 21 | # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. 22 | http: [ip: {127, 0, 0, 1}, port: 4000], 23 | check_origin: false, 24 | code_reloader: true, 25 | debug_errors: true, 26 | secret_key_base: "VcxZjOXWllz952MBjItAnOZQxDTaVZQRtlZSpY+eZhuW3rsRrEaRah4FE1ihgTEh", 27 | watchers: [ 28 | esbuild: {Esbuild, :install_and_run, [:test_bed, ~w(--sourcemap=inline --watch)]}, 29 | tailwind: {Tailwind, :install_and_run, [:test_bed, ~w(--watch)]} 30 | ] 31 | 32 | # ## SSL Support 33 | # 34 | # In order to use HTTPS in development, a self-signed 35 | # certificate can be generated by running the following 36 | # Mix task: 37 | # 38 | # mix phx.gen.cert 39 | # 40 | # Run `mix help phx.gen.cert` for more information. 41 | # 42 | # The `http:` config above can be replaced with: 43 | # 44 | # https: [ 45 | # port: 4001, 46 | # cipher_suite: :strong, 47 | # keyfile: "priv/cert/selfsigned_key.pem", 48 | # certfile: "priv/cert/selfsigned.pem" 49 | # ], 50 | # 51 | # If desired, both `http:` and `https:` keys can be 52 | # configured to run both http and https servers on 53 | # different ports. 54 | 55 | # Watch static and templates for browser reloading. 56 | config :test_bed, TestBedWeb.Endpoint, 57 | live_reload: [ 58 | patterns: [ 59 | ~r"priv/static/(?!uploads/).*(js|css|png|jpeg|jpg|gif|svg)$", 60 | ~r"priv/gettext/.*(po)$", 61 | ~r"lib/test_bed_web/(controllers|live|components)/.*(ex|heex)$" 62 | ] 63 | ] 64 | 65 | # Enable dev routes for dashboard and mailbox 66 | config :test_bed, dev_routes: true 67 | 68 | # Do not include metadata nor timestamps in development logs 69 | config :logger, :console, format: "[$level] $message\n" 70 | 71 | # Set a higher stacktrace during development. Avoid configuring such 72 | # in production as building large stacktraces may be expensive. 73 | config :phoenix, :stacktrace_depth, 20 74 | 75 | # Initialize plugs at runtime for faster development compilation 76 | config :phoenix, :plug_init_mode, :runtime 77 | 78 | config :phoenix_live_view, 79 | # Include HEEx debug annotations as HTML comments in rendered markup 80 | debug_heex_annotations: true, 81 | # Enable helpful, but potentially expensive runtime checks 82 | enable_expensive_runtime_checks: true 83 | 84 | # Disable swoosh api client as it is only required for production adapters. 85 | config :swoosh, :api_client, false 86 | -------------------------------------------------------------------------------- /test_bed/config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Note we also include the path to a cache manifest 4 | # containing the digested version of static files. This 5 | # manifest is generated by the `mix assets.deploy` task, 6 | # which you should run after static files are built and 7 | # before starting your production server. 8 | config :test_bed, TestBedWeb.Endpoint, cache_static_manifest: "priv/static/cache_manifest.json" 9 | 10 | # Configures Swoosh API Client 11 | config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: TestBed.Finch 12 | 13 | # Disable Swoosh Local Memory Storage 14 | config :swoosh, local: false 15 | 16 | # Do not print debug messages in production 17 | config :logger, level: :info 18 | 19 | # Runtime production configuration, including reading 20 | # of environment variables, is done on config/runtime.exs. 21 | -------------------------------------------------------------------------------- /test_bed/config/runtime.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # config/runtime.exs is executed for all environments, including 4 | # during releases. It is executed after compilation and before the 5 | # system starts, so it is typically used to load production configuration 6 | # and secrets from environment variables or elsewhere. Do not define 7 | # any compile-time configuration in here, as it won't be applied. 8 | # The block below contains prod specific runtime configuration. 9 | 10 | # ## Using releases 11 | # 12 | # If you use `mix release`, you need to explicitly enable the server 13 | # by passing the PHX_SERVER=true when you start it: 14 | # 15 | # PHX_SERVER=true bin/test_bed start 16 | # 17 | # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` 18 | # script that automatically sets the env var above. 19 | if System.get_env("PHX_SERVER") do 20 | config :test_bed, TestBedWeb.Endpoint, server: true 21 | end 22 | 23 | if config_env() == :prod do 24 | database_url = 25 | System.get_env("DATABASE_URL") || 26 | raise """ 27 | environment variable DATABASE_URL is missing. 28 | For example: ecto://USER:PASS@HOST/DATABASE 29 | """ 30 | 31 | maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: [] 32 | 33 | config :test_bed, TestBed.Repo, 34 | # ssl: true, 35 | url: database_url, 36 | pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), 37 | socket_options: maybe_ipv6 38 | 39 | # The secret key base is used to sign/encrypt cookies and other secrets. 40 | # A default value is used in config/dev.exs and config/test.exs but you 41 | # want to use a different value for prod and you most likely don't want 42 | # to check this value into version control, so we use an environment 43 | # variable instead. 44 | secret_key_base = 45 | System.get_env("SECRET_KEY_BASE") || 46 | raise """ 47 | environment variable SECRET_KEY_BASE is missing. 48 | You can generate one by calling: mix phx.gen.secret 49 | """ 50 | 51 | host = System.get_env("PHX_HOST") || "example.com" 52 | port = String.to_integer(System.get_env("PORT") || "4000") 53 | 54 | config :test_bed, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") 55 | 56 | config :test_bed, TestBedWeb.Endpoint, 57 | url: [host: host, port: 443, scheme: "https"], 58 | http: [ 59 | # Enable IPv6 and bind on all interfaces. 60 | # Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. 61 | # See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0 62 | # for details about using IPv6 vs IPv4 and loopback vs public addresses. 63 | ip: {0, 0, 0, 0, 0, 0, 0, 0}, 64 | port: port 65 | ], 66 | secret_key_base: secret_key_base 67 | 68 | # ## SSL Support 69 | # 70 | # To get SSL working, you will need to add the `https` key 71 | # to your endpoint configuration: 72 | # 73 | # config :test_bed, TestBedWeb.Endpoint, 74 | # https: [ 75 | # ..., 76 | # port: 443, 77 | # cipher_suite: :strong, 78 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 79 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 80 | # ] 81 | # 82 | # The `cipher_suite` is set to `:strong` to support only the 83 | # latest and more secure SSL ciphers. This means old browsers 84 | # and clients may not be supported. You can set it to 85 | # `:compatible` for wider support. 86 | # 87 | # `:keyfile` and `:certfile` expect an absolute path to the key 88 | # and cert in disk or a relative path inside priv, for example 89 | # "priv/ssl/server.key". For all supported SSL configuration 90 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 91 | # 92 | # We also recommend setting `force_ssl` in your config/prod.exs, 93 | # ensuring no data is ever sent via http, always redirecting to https: 94 | # 95 | # config :test_bed, TestBedWeb.Endpoint, 96 | # force_ssl: [hsts: true] 97 | # 98 | # Check `Plug.SSL` for all available options in `force_ssl`. 99 | 100 | # ## Configuring the mailer 101 | # 102 | # In production you need to configure the mailer to use a different adapter. 103 | # Also, you may need to configure the Swoosh API client of your choice if you 104 | # are not using SMTP. Here is an example of the configuration: 105 | # 106 | # config :test_bed, TestBed.Mailer, 107 | # adapter: Swoosh.Adapters.Mailgun, 108 | # api_key: System.get_env("MAILGUN_API_KEY"), 109 | # domain: System.get_env("MAILGUN_DOMAIN") 110 | # 111 | # For this example you need include a HTTP client required by Swoosh API client. 112 | # Swoosh supports Hackney and Finch out of the box: 113 | # 114 | # config :swoosh, :api_client, Swoosh.ApiClient.Hackney 115 | # 116 | # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details. 117 | end 118 | -------------------------------------------------------------------------------- /test_bed/config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | # Configure your database 4 | # 5 | # The MIX_TEST_PARTITION environment variable can be used 6 | # to provide built-in test partitioning in CI environment. 7 | # Run `mix help test` for more information. 8 | config :test_bed, TestBed.Repo, 9 | username: "postgres", 10 | password: "postgres", 11 | hostname: "localhost", 12 | database: "test_bed_test#{System.get_env("MIX_TEST_PARTITION")}", 13 | pool: Ecto.Adapters.SQL.Sandbox, 14 | pool_size: System.schedulers_online() * 2 15 | 16 | # We don't run a server during test. If one is required, 17 | # you can enable the server option below. 18 | config :test_bed, TestBedWeb.Endpoint, 19 | http: [ip: {127, 0, 0, 1}, port: 4002], 20 | secret_key_base: "ASMDSrPwYglSQK65JnBTHFehzKSH90M5j4Av2MTY1fFam9X2bGvhs7q3b+1BbSti", 21 | server: false 22 | 23 | # In test we don't send emails. 24 | config :test_bed, TestBed.Mailer, adapter: Swoosh.Adapters.Test 25 | 26 | # Disable swoosh api client as it is only required for production adapters. 27 | config :swoosh, :api_client, false 28 | 29 | # Print only warnings and errors during test 30 | config :logger, level: :warning 31 | 32 | # Initialize plugs at runtime for faster test compilation 33 | config :phoenix, :plug_init_mode, :runtime 34 | 35 | config :phoenix_live_view, 36 | # Enable helpful, but potentially expensive runtime checks 37 | enable_expensive_runtime_checks: true 38 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBed do 2 | @moduledoc """ 3 | TestBed keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed/application.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBed.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @impl true 9 | def start(_type, _args) do 10 | children = [ 11 | TestBedWeb.Telemetry, 12 | TestBed.Repo, 13 | {DNSCluster, query: Application.get_env(:test_bed, :dns_cluster_query) || :ignore}, 14 | {Phoenix.PubSub, name: TestBed.PubSub}, 15 | # Start the Finch HTTP client for sending emails 16 | {Finch, name: TestBed.Finch}, 17 | # Start a worker by calling: TestBed.Worker.start_link(arg) 18 | # {TestBed.Worker, arg}, 19 | # Start to serve requests, typically the last entry 20 | TestBedWeb.Endpoint 21 | ] 22 | 23 | # See https://hexdocs.pm/elixir/Supervisor.html 24 | # for other strategies and supported options 25 | opts = [strategy: :one_for_one, name: TestBed.Supervisor] 26 | Supervisor.start_link(children, opts) 27 | end 28 | 29 | # Tell Phoenix to update the endpoint configuration 30 | # whenever the application is updated. 31 | @impl true 32 | def config_change(changed, _new, removed) do 33 | TestBedWeb.Endpoint.config_change(changed, removed) 34 | :ok 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed/blog.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBed.Blog do 2 | use Ash.Domain 3 | 4 | resources do 5 | resource TestBed.Blog.Post do 6 | # Define an interface for calling resource actions. 7 | define :create_post, action: :create 8 | define :list_posts, action: :read 9 | define :update_post, action: :update 10 | define :destroy_post, action: :destroy 11 | define :get_post, args: [:id], action: :by_id 12 | end 13 | 14 | resource TestBed.Blog.Author do 15 | # Define an interface for calling resource actions. 16 | define :create_author, action: :create 17 | define :list_authors, action: :read 18 | define :update_author, action: :update 19 | define :destroy_author, action: :destroy 20 | define :get_author, args: [:id], action: :by_id 21 | end 22 | 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed/blog/author.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBed.Blog.Author do 2 | # Using Ash.Resource turns this module into an Ash resource. 3 | use Ash.Resource, 4 | # Tells Ash where the generated code interface belongs 5 | domain: TestBed.Blog, 6 | # Tells Ash you want this resource to store its data in Postgres. 7 | data_layer: AshPostgres.DataLayer 8 | 9 | # The Postgres keyword is specific to the AshPostgres module. 10 | postgres do 11 | # Tells Postgres what to call the table 12 | table("authors") 13 | # Tells Ash how to interface with the Postgres table 14 | repo(TestBed.Repo) 15 | end 16 | 17 | actions do 18 | # Exposes default built in actions to manage the resource 19 | defaults([:read, :destroy]) 20 | 21 | create :create do 22 | primary? true 23 | # accept title as input 24 | accept([:name]) 25 | end 26 | 27 | update :update do 28 | # accept content as input 29 | accept([:name]) 30 | end 31 | 32 | # Defines custom read action which fetches post by id. 33 | read :by_id do 34 | # This action has one argument :id of type :uuid 35 | argument(:id, :uuid, allow_nil?: false) 36 | # Tells us we expect this action to return a single result 37 | get?(true) 38 | # Filters the `:id` given in the argument 39 | # against the `id` of each element in the resource 40 | filter(expr(id == ^arg(:id))) 41 | end 42 | end 43 | 44 | # Attributes are simple pieces of data that exist in your resource 45 | attributes do 46 | # Add an autogenerated UUID primary key called `:id`. 47 | uuid_primary_key(:id) 48 | # Add a string type attribute called `:title` 49 | attribute :name, :string do 50 | # We don't want the title to ever be `nil` 51 | allow_nil?(false) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed/blog/post.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBed.Blog.Post do 2 | 3 | alias TestBed.Blog.Author 4 | # Using Ash.Resource turns this module into an Ash resource. 5 | use Ash.Resource, 6 | # Tells Ash where the generated code interface belongs 7 | domain: TestBed.Blog, 8 | # Tells Ash you want this resource to store its data in Postgres. 9 | data_layer: AshPostgres.DataLayer 10 | 11 | # The Postgres keyword is specific to the AshPostgres module. 12 | postgres do 13 | # Tells Postgres what to call the table 14 | table("posts") 15 | # Tells Ash how to interface with the Postgres table 16 | repo(TestBed.Repo) 17 | end 18 | 19 | actions do 20 | # Exposes default built in actions to manage the resource 21 | defaults([:read, :destroy]) 22 | 23 | create :create do 24 | 25 | # accept title as input 26 | accept([:title, :author_id]) 27 | 28 | argument :author, :map 29 | 30 | change manage_relationship(:author, type: :create) 31 | end 32 | 33 | update :update do 34 | # accept content as input 35 | accept([:content]) 36 | end 37 | 38 | # Defines custom read action which fetches post by id. 39 | read :by_id do 40 | # This action has one argument :id of type :uuid 41 | argument(:id, :uuid, allow_nil?: false) 42 | # Tells us we expect this action to return a single result 43 | get?(true) 44 | # Filters the `:id` given in the argument 45 | # against the `id` of each element in the resource 46 | filter(expr(id == ^arg(:id))) 47 | end 48 | end 49 | 50 | # Attributes are simple pieces of data that exist in your resource 51 | attributes do 52 | # Add an autogenerated UUID primary key called `:id`. 53 | uuid_primary_key(:id) 54 | # Add a string type attribute called `:title` 55 | attribute :title, :string do 56 | # We don't want the title to ever be `nil` 57 | allow_nil?(false) 58 | end 59 | 60 | # Add a string type attribute called `:content` 61 | # If allow_nil? is not specified, then content can be nil 62 | attribute(:content, :string) 63 | end 64 | 65 | preparations do 66 | prepare build(load: [:author]) 67 | end 68 | 69 | relationships do 70 | belongs_to :author, Author 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed/customers.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBed.Customers do 2 | use Ash.Domain 3 | 4 | resources do 5 | resource TestBed.Customers.Customer do 6 | # Define an interface for calling resource actions. 7 | define :create_customer, action: :create 8 | define :list_customers, action: :read 9 | define :update_customer, action: :update 10 | define :destroy_customer, action: :destroy 11 | define :get_customer, args: [:id], action: :by_id 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed/customers/customer.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBed.Customers.Customer do 2 | # Using Ash.Resource turns this module into an Ash resource. 3 | use Ash.Resource, 4 | # Tells Ash where the generated code interface belongs 5 | domain: TestBed.Customers, 6 | # Tells Ash you want this resource to store its data in Postgres. 7 | data_layer: AshPostgres.DataLayer 8 | 9 | # The Postgres keyword is specific to the AshPostgres module. 10 | postgres do 11 | # Tells Postgres what to call the table 12 | table("customers") 13 | # Tells Ash how to interface with the Postgres table 14 | repo(TestBed.Repo) 15 | end 16 | 17 | multitenancy do 18 | strategy :attribute 19 | attribute :tenant 20 | global? true 21 | end 22 | 23 | actions do 24 | # Exposes default built in actions to manage the resource 25 | defaults([:read, :destroy]) 26 | 27 | create :create do 28 | # accept title as input 29 | accept([:name, :tenant]) 30 | end 31 | 32 | update :update do 33 | # accept content as input 34 | accept([:name]) 35 | end 36 | 37 | # Defines custom read action which fetches post by id. 38 | read :by_id do 39 | # This action has one argument :id of type :uuid 40 | argument(:id, :uuid, allow_nil?: false) 41 | # Tells us we expect this action to return a single result 42 | get?(true) 43 | # Filters the `:id` given in the argument 44 | # against the `id` of each element in the resource 45 | filter(expr(id == ^arg(:id))) 46 | end 47 | end 48 | 49 | # Attributes are simple pieces of data that exist in your resource 50 | attributes do 51 | # Add an autogenerated UUID primary key called `:id`. 52 | uuid_primary_key(:id) 53 | # Add a string type attribute called `:title` 54 | attribute :name, :string do 55 | # We don't want the title to ever be `nil` 56 | allow_nil?(false) 57 | end 58 | 59 | attribute :tenant, :string 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed/factory.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBed.Factory do 2 | use Smokestack 3 | 4 | alias TestBed.Blog.Post 5 | 6 | factory Post do 7 | attribute :title, &Faker.Lorem.sentence/1 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed/mailer.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBed.Mailer do 2 | use Swoosh.Mailer, otp_app: :test_bed 3 | end 4 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBed.Repo do 2 | use AshPostgres.Repo, otp_app: :test_bed 3 | 4 | # Installs extensions that ash commonly uses 5 | def installed_extensions do 6 | ["ash-functions", "uuid-ossp", "citext"] 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed_web.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBedWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, components, channels, and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use TestBedWeb, :controller 9 | use TestBedWeb, :html 10 | 11 | The definitions below will be executed for every controller, 12 | component, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define additional modules and import 17 | those modules here. 18 | """ 19 | 20 | def static_paths, do: ~w(assets fonts images favicon.ico robots.txt) 21 | 22 | def router do 23 | quote do 24 | use Phoenix.Router, helpers: false 25 | 26 | # Import common connection and controller functions to use in pipelines 27 | import Plug.Conn 28 | import Phoenix.Controller 29 | import Phoenix.LiveView.Router 30 | end 31 | end 32 | 33 | def channel do 34 | quote do 35 | use Phoenix.Channel 36 | end 37 | end 38 | 39 | def controller do 40 | quote do 41 | use Phoenix.Controller, 42 | formats: [:html, :json], 43 | layouts: [html: TestBedWeb.Layouts] 44 | 45 | import Plug.Conn 46 | import TestBedWeb.Gettext 47 | 48 | unquote(verified_routes()) 49 | end 50 | end 51 | 52 | def live_view do 53 | quote do 54 | use Phoenix.LiveView, 55 | layout: {TestBedWeb.Layouts, :app} 56 | 57 | unquote(html_helpers()) 58 | end 59 | end 60 | 61 | def live_component do 62 | quote do 63 | use Phoenix.LiveComponent 64 | 65 | unquote(html_helpers()) 66 | end 67 | end 68 | 69 | def html do 70 | quote do 71 | use Phoenix.Component 72 | 73 | # Import convenience functions from controllers 74 | import Phoenix.Controller, 75 | only: [get_csrf_token: 0, view_module: 1, view_template: 1] 76 | 77 | # Include general helpers for rendering HTML 78 | unquote(html_helpers()) 79 | end 80 | end 81 | 82 | defp html_helpers do 83 | quote do 84 | # HTML escaping functionality 85 | import Phoenix.HTML 86 | # Core UI components and translation 87 | import TestBedWeb.CoreComponents 88 | import TestBedWeb.Gettext 89 | 90 | # Shortcut for generating JS commands 91 | alias Phoenix.LiveView.JS 92 | 93 | # Routes generation with the ~p sigil 94 | unquote(verified_routes()) 95 | end 96 | end 97 | 98 | def verified_routes do 99 | quote do 100 | use Phoenix.VerifiedRoutes, 101 | endpoint: TestBedWeb.Endpoint, 102 | router: TestBedWeb.Router, 103 | statics: TestBedWeb.static_paths() 104 | end 105 | end 106 | 107 | @doc """ 108 | When used, dispatch to the appropriate controller/live_view/etc. 109 | """ 110 | defmacro __using__(which) when is_atom(which) do 111 | apply(__MODULE__, which, []) 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed_web/components/core_components.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBedWeb.CoreComponents do 2 | @moduledoc """ 3 | Provides core UI components. 4 | 5 | At first glance, this module may seem daunting, but its goal is to provide 6 | core building blocks for your application, such as modals, tables, and 7 | forms. The components consist mostly of markup and are well-documented 8 | with doc strings and declarative assigns. You may customize and style 9 | them in any way you want, based on your application growth and needs. 10 | 11 | The default components use Tailwind CSS, a utility-first CSS framework. 12 | See the [Tailwind CSS documentation](https://tailwindcss.com) to learn 13 | how to customize them or feel free to swap in another framework altogether. 14 | 15 | Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage. 16 | """ 17 | use Phoenix.Component 18 | 19 | alias Phoenix.LiveView.JS 20 | import TestBedWeb.Gettext 21 | 22 | @doc """ 23 | Renders a modal. 24 | 25 | ## Examples 26 | 27 | <.modal id="confirm-modal"> 28 | This is a modal. 29 | 30 | 31 | JS commands may be passed to the `:on_cancel` to configure 32 | the closing/cancel event, for example: 33 | 34 | <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}> 35 | This is another modal. 36 | 37 | 38 | """ 39 | attr :id, :string, required: true 40 | attr :show, :boolean, default: false 41 | attr :on_cancel, JS, default: %JS{} 42 | slot :inner_block, required: true 43 | 44 | def modal(assigns) do 45 | ~H""" 46 | 328 | """ 329 | end 330 | 331 | def input(%{type: "select"} = assigns) do 332 | ~H""" 333 |
334 | <.label for={@id}><%= @label %> 335 | 345 | <.error :for={msg <- @errors}><%= msg %> 346 |
347 | """ 348 | end 349 | 350 | def input(%{type: "textarea"} = assigns) do 351 | ~H""" 352 |
353 | <.label for={@id}><%= @label %> 354 | 365 | <.error :for={msg <- @errors}><%= msg %> 366 |
367 | """ 368 | end 369 | 370 | # All other inputs text, datetime-local, url, password, etc. are handled here... 371 | def input(assigns) do 372 | ~H""" 373 |
374 | <.label for={@id}><%= @label %> 375 | 388 | <.error :for={msg <- @errors}><%= msg %> 389 |
390 | """ 391 | end 392 | 393 | @doc """ 394 | Renders a label. 395 | """ 396 | attr :for, :string, default: nil 397 | slot :inner_block, required: true 398 | 399 | def label(assigns) do 400 | ~H""" 401 | 404 | """ 405 | end 406 | 407 | @doc """ 408 | Generates a generic error message. 409 | """ 410 | slot :inner_block, required: true 411 | 412 | def error(assigns) do 413 | ~H""" 414 |

415 | <.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" /> 416 | <%= render_slot(@inner_block) %> 417 |

418 | """ 419 | end 420 | 421 | @doc """ 422 | Renders a header with title. 423 | """ 424 | attr :class, :string, default: nil 425 | 426 | slot :inner_block, required: true 427 | slot :subtitle 428 | slot :actions 429 | 430 | def header(assigns) do 431 | ~H""" 432 |
433 |
434 |

435 | <%= render_slot(@inner_block) %> 436 |

437 |

438 | <%= render_slot(@subtitle) %> 439 |

440 |
441 |
<%= render_slot(@actions) %>
442 |
443 | """ 444 | end 445 | 446 | @doc ~S""" 447 | Renders a table with generic styling. 448 | 449 | ## Examples 450 | 451 | <.table id="users" rows={@users}> 452 | <:col :let={user} label="id"><%= user.id %> 453 | <:col :let={user} label="username"><%= user.username %> 454 | 455 | """ 456 | attr :id, :string, required: true 457 | attr :rows, :list, required: true 458 | attr :row_id, :any, default: nil, doc: "the function for generating the row id" 459 | attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row" 460 | 461 | attr :row_item, :any, 462 | default: &Function.identity/1, 463 | doc: "the function for mapping each row before calling the :col and :action slots" 464 | 465 | slot :col, required: true do 466 | attr :label, :string 467 | end 468 | 469 | slot :action, doc: "the slot for showing user actions in the last table column" 470 | 471 | def table(assigns) do 472 | assigns = 473 | with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do 474 | assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end) 475 | end 476 | 477 | ~H""" 478 |
479 | 480 | 481 | 482 | 483 | 486 | 487 | 488 | 493 | 494 | 506 | 517 | 518 | 519 |
<%= col[:label] %> 484 | <%= gettext("Actions") %> 485 |
499 |
500 | 501 | 502 | <%= render_slot(col, @row_item.(row)) %> 503 | 504 |
505 |
507 |
508 | 509 | 513 | <%= render_slot(action, @row_item.(row)) %> 514 | 515 |
516 |
520 |
521 | """ 522 | end 523 | 524 | @doc """ 525 | Renders a data list. 526 | 527 | ## Examples 528 | 529 | <.list> 530 | <:item title="Title"><%= @post.title %> 531 | <:item title="Views"><%= @post.views %> 532 | 533 | """ 534 | slot :item, required: true do 535 | attr :title, :string, required: true 536 | end 537 | 538 | def list(assigns) do 539 | ~H""" 540 |
541 |
542 |
543 |
<%= item.title %>
544 |
<%= render_slot(item) %>
545 |
546 |
547 |
548 | """ 549 | end 550 | 551 | @doc """ 552 | Renders a back navigation link. 553 | 554 | ## Examples 555 | 556 | <.back navigate={~p"/posts"}>Back to posts 557 | """ 558 | attr :navigate, :any, required: true 559 | slot :inner_block, required: true 560 | 561 | def back(assigns) do 562 | ~H""" 563 |
564 | <.link 565 | navigate={@navigate} 566 | class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700" 567 | > 568 | <.icon name="hero-arrow-left-solid" class="h-3 w-3" /> 569 | <%= render_slot(@inner_block) %> 570 | 571 |
572 | """ 573 | end 574 | 575 | @doc """ 576 | Renders a [Heroicon](https://heroicons.com). 577 | 578 | Heroicons come in three styles – outline, solid, and mini. 579 | By default, the outline style is used, but solid and mini may 580 | be applied by using the `-solid` and `-mini` suffix. 581 | 582 | You can customize the size and colors of the icons by setting 583 | width, height, and background color classes. 584 | 585 | Icons are extracted from the `deps/heroicons` directory and bundled within 586 | your compiled app.css by the plugin in your `assets/tailwind.config.js`. 587 | 588 | ## Examples 589 | 590 | <.icon name="hero-x-mark-solid" /> 591 | <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" /> 592 | """ 593 | attr :name, :string, required: true 594 | attr :class, :string, default: nil 595 | 596 | def icon(%{name: "hero-" <> _} = assigns) do 597 | ~H""" 598 | 599 | """ 600 | end 601 | 602 | ## JS Commands 603 | 604 | def show(js \\ %JS{}, selector) do 605 | JS.show(js, 606 | to: selector, 607 | transition: 608 | {"transition-all transform ease-out duration-300", 609 | "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95", 610 | "opacity-100 translate-y-0 sm:scale-100"} 611 | ) 612 | end 613 | 614 | def hide(js \\ %JS{}, selector) do 615 | JS.hide(js, 616 | to: selector, 617 | time: 200, 618 | transition: 619 | {"transition-all transform ease-in duration-200", 620 | "opacity-100 translate-y-0 sm:scale-100", 621 | "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"} 622 | ) 623 | end 624 | 625 | def show_modal(js \\ %JS{}, id) when is_binary(id) do 626 | js 627 | |> JS.show(to: "##{id}") 628 | |> JS.show( 629 | to: "##{id}-bg", 630 | transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"} 631 | ) 632 | |> show("##{id}-container") 633 | |> JS.add_class("overflow-hidden", to: "body") 634 | |> JS.focus_first(to: "##{id}-content") 635 | end 636 | 637 | def hide_modal(js \\ %JS{}, id) do 638 | js 639 | |> JS.hide( 640 | to: "##{id}-bg", 641 | transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"} 642 | ) 643 | |> hide("##{id}-container") 644 | |> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"}) 645 | |> JS.remove_class("overflow-hidden", to: "body") 646 | |> JS.pop_focus() 647 | end 648 | 649 | @doc """ 650 | Translates an error message using gettext. 651 | """ 652 | def translate_error({msg, opts}) do 653 | # When using gettext, we typically pass the strings we want 654 | # to translate as a static argument: 655 | # 656 | # # Translate the number of files with plural rules 657 | # dngettext("errors", "1 file", "%{count} files", count) 658 | # 659 | # However the error messages in our forms and APIs are generated 660 | # dynamically, so we need to translate them by calling Gettext 661 | # with our gettext backend as first argument. Translations are 662 | # available in the errors.po file (as we use the "errors" domain). 663 | if count = opts[:count] do 664 | Gettext.dngettext(TestBedWeb.Gettext, "errors", msg, msg, count, opts) 665 | else 666 | Gettext.dgettext(TestBedWeb.Gettext, "errors", msg, opts) 667 | end 668 | end 669 | 670 | @doc """ 671 | Translates the errors for a field from a keyword list of errors. 672 | """ 673 | def translate_errors(errors, field) when is_list(errors) do 674 | for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts}) 675 | end 676 | end 677 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed_web/components/layouts.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBedWeb.Layouts do 2 | @moduledoc """ 3 | This module holds different layouts used by your application. 4 | 5 | See the `layouts` directory for all templates available. 6 | The "root" layout is a skeleton rendered as part of the 7 | application router. The "app" layout is set as the default 8 | layout on both `use TestBedWeb, :controller` and 9 | `use TestBedWeb, :live_view`. 10 | """ 11 | use TestBedWeb, :html 12 | 13 | embed_templates "layouts/*" 14 | end 15 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed_web/components/layouts/app.html.heex: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 |

8 | v<%= Application.spec(:phoenix, :vsn) %> 9 |

10 |
11 | 25 |
26 |
27 |
28 |
29 | <.flash_group flash={@flash} /> 30 | <%= @inner_content %> 31 |
32 |
33 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed_web/components/layouts/root.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <.live_title suffix=" · Phoenix Framework"> 8 | <%= assigns[:page_title] || "TestBed" %> 9 | 10 | 11 | 13 | 14 | 15 | <%= @inner_content %> 16 | 17 | 18 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed_web/controllers/error_html.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBedWeb.ErrorHTML do 2 | @moduledoc """ 3 | This module is invoked by your endpoint in case of errors on HTML requests. 4 | 5 | See config/config.exs. 6 | """ 7 | use TestBedWeb, :html 8 | 9 | # If you want to customize your error pages, 10 | # uncomment the embed_templates/1 call below 11 | # and add pages to the error directory: 12 | # 13 | # * lib/test_bed_web/controllers/error_html/404.html.heex 14 | # * lib/test_bed_web/controllers/error_html/500.html.heex 15 | # 16 | # embed_templates "error_html/*" 17 | 18 | # The default is to render a plain text page based on 19 | # the template name. For example, "404.html" becomes 20 | # "Not Found". 21 | def render(template, _assigns) do 22 | Phoenix.Controller.status_message_from_template(template) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed_web/controllers/error_json.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBedWeb.ErrorJSON do 2 | @moduledoc """ 3 | This module is invoked by your endpoint in case of errors on JSON requests. 4 | 5 | See config/config.exs. 6 | """ 7 | 8 | # If you want to customize a particular status code, 9 | # you may add your own clauses, such as: 10 | # 11 | # def render("500.json", _assigns) do 12 | # %{errors: %{detail: "Internal Server Error"}} 13 | # end 14 | 15 | # By default, Phoenix returns the status message from 16 | # the template name. For example, "404.json" becomes 17 | # "Not Found". 18 | def render(template, _assigns) do 19 | %{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBedWeb.PageController do 2 | use TestBedWeb, :controller 3 | 4 | def home(conn, _params) do 5 | # The home page is often custom made, 6 | # so skip the default app layout. 7 | render(conn, :home, layout: false) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed_web/controllers/page_html.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBedWeb.PageHTML do 2 | @moduledoc """ 3 | This module contains pages rendered by PageController. 4 | 5 | See the `page_html` directory for all templates available. 6 | """ 7 | use TestBedWeb, :html 8 | 9 | embed_templates "page_html/*" 10 | end 11 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed_web/controllers/page_html/home.html.heex: -------------------------------------------------------------------------------- 1 | <.flash_group flash={@flash} /> 2 | 41 |
42 |
43 | 49 |

50 | Phoenix Framework 51 | 52 | v<%= Application.spec(:phoenix, :vsn) %> 53 | 54 |

55 |

56 | Peace of mind from prototype to production. 57 |

58 |

59 | Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale. 60 |

61 | 221 |
222 |
223 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBedWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :test_bed 3 | 4 | # The session will be stored in the cookie and signed, 5 | # this means its contents can be read but not tampered with. 6 | # Set :encryption_salt if you would also like to encrypt it. 7 | @session_options [ 8 | store: :cookie, 9 | key: "_test_bed_key", 10 | signing_salt: "FTa1sM+o", 11 | same_site: "Lax" 12 | ] 13 | 14 | socket "/live", Phoenix.LiveView.Socket, 15 | websocket: [connect_info: [session: @session_options]], 16 | longpoll: [connect_info: [session: @session_options]] 17 | 18 | # Serve at "/" the static files from "priv/static" directory. 19 | # 20 | # You should set gzip to true if you are running phx.digest 21 | # when deploying your static files in production. 22 | plug Plug.Static, 23 | at: "/", 24 | from: :test_bed, 25 | gzip: false, 26 | only: TestBedWeb.static_paths() 27 | 28 | # Code reloading can be explicitly enabled under the 29 | # :code_reloader configuration of your endpoint. 30 | if code_reloading? do 31 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 32 | plug Phoenix.LiveReloader 33 | plug Phoenix.CodeReloader 34 | plug Phoenix.Ecto.CheckRepoStatus, otp_app: :test_bed 35 | end 36 | 37 | plug Phoenix.LiveDashboard.RequestLogger, 38 | param_key: "request_logger", 39 | cookie_key: "request_logger" 40 | 41 | plug Plug.RequestId 42 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 43 | 44 | plug Plug.Parsers, 45 | parsers: [:urlencoded, :multipart, :json], 46 | pass: ["*/*"], 47 | json_decoder: Phoenix.json_library() 48 | 49 | plug Plug.MethodOverride 50 | plug Plug.Head 51 | plug Plug.Session, @session_options 52 | plug TestBedWeb.Router 53 | end 54 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBedWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import TestBedWeb.Gettext 9 | 10 | # Simple translation 11 | gettext("Here is the string to translate") 12 | 13 | # Plural translation 14 | ngettext("Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3) 17 | 18 | # Domain-based translation 19 | dgettext("errors", "Here is the error message to translate") 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :test_bed 24 | end 25 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed_web/live/customers_live/index.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBedWeb.CustomersLive.Index do 2 | use TestBedWeb, :live_view 3 | 4 | def render(assigns) do 5 | ~H""" 6 | <.live_component id="customers_table" read_options={[tenant: "bob"]} limit={10} offset={0} sort={{"id", :asc}} module={AshTable.Table} query={TestBed.Customers.Customer}> 7 | <:col :let={customer} label="Id"><%= customer.id %> 8 | <:col :let={customer} label="Name" sort_key="name"> 9 | <%= customer.name %> 10 | 11 | 12 | """ 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed_web/live/posts_live/index.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBedWeb.PostsLive.Index do 2 | use TestBedWeb, :live_view 3 | 4 | def render(assigns) do 5 | ~H""" 6 | <.live_component module={AshTable.Table} 7 | id="posts_table" 8 | limit={10} offset={0} 9 | sort={{"id", :asc}} 10 | query={TestBed.Blog.Post}> 11 | <:col :let={post} label="Id"><%= post.id %> 12 | <:col :let={post} label="Title" sort_key="title"> 13 | <%= post.title %> 14 | 15 | <:col :let={post} label="Author" apply_sort={&sort_by_author/2} sort_key="author.name"> 16 | <%= if post.author, do: post.author.name %> 17 | 18 | 19 | """ 20 | end 21 | 22 | require Ash.Sort 23 | 24 | defp sort_by_author(query, direction) do 25 | Ash.Query.sort(query, {Ash.Sort.expr_sort(author.name), direction}) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBedWeb.Router do 2 | use TestBedWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_live_flash 8 | plug :put_root_layout, html: {TestBedWeb.Layouts, :root} 9 | plug :protect_from_forgery 10 | plug :put_secure_browser_headers 11 | end 12 | 13 | pipeline :api do 14 | plug :accepts, ["json"] 15 | end 16 | 17 | scope "/", TestBedWeb do 18 | pipe_through :browser 19 | 20 | get "/", PageController, :home 21 | 22 | live "/posts", PostsLive.Index 23 | live "/customers", CustomersLive.Index 24 | end 25 | 26 | # Other scopes may use custom stacks. 27 | # scope "/api", TestBedWeb do 28 | # pipe_through :api 29 | # end 30 | 31 | # Enable LiveDashboard and Swoosh mailbox preview in development 32 | if Application.compile_env(:test_bed, :dev_routes) do 33 | # If you want to use the LiveDashboard in production, you should put 34 | # it behind authentication and allow only admins to access it. 35 | # If your application does not have an admins-only section yet, 36 | # you can use Plug.BasicAuth to set up some basic authentication 37 | # as long as you are also using SSL (which you should anyway). 38 | import Phoenix.LiveDashboard.Router 39 | 40 | scope "/dev" do 41 | pipe_through :browser 42 | 43 | live_dashboard "/dashboard", metrics: TestBedWeb.Telemetry 44 | forward "/mailbox", Plug.Swoosh.MailboxPreview 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test_bed/lib/test_bed_web/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBedWeb.Telemetry do 2 | use Supervisor 3 | import Telemetry.Metrics 4 | 5 | def start_link(arg) do 6 | Supervisor.start_link(__MODULE__, arg, name: __MODULE__) 7 | end 8 | 9 | @impl true 10 | def init(_arg) do 11 | children = [ 12 | # Telemetry poller will execute the given period measurements 13 | # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics 14 | {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} 15 | # Add reporters as children of your supervision tree. 16 | # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} 17 | ] 18 | 19 | Supervisor.init(children, strategy: :one_for_one) 20 | end 21 | 22 | def metrics do 23 | [ 24 | # Phoenix Metrics 25 | summary("phoenix.endpoint.start.system_time", 26 | unit: {:native, :millisecond} 27 | ), 28 | summary("phoenix.endpoint.stop.duration", 29 | unit: {:native, :millisecond} 30 | ), 31 | summary("phoenix.router_dispatch.start.system_time", 32 | tags: [:route], 33 | unit: {:native, :millisecond} 34 | ), 35 | summary("phoenix.router_dispatch.exception.duration", 36 | tags: [:route], 37 | unit: {:native, :millisecond} 38 | ), 39 | summary("phoenix.router_dispatch.stop.duration", 40 | tags: [:route], 41 | unit: {:native, :millisecond} 42 | ), 43 | summary("phoenix.socket_connected.duration", 44 | unit: {:native, :millisecond} 45 | ), 46 | summary("phoenix.channel_joined.duration", 47 | unit: {:native, :millisecond} 48 | ), 49 | summary("phoenix.channel_handled_in.duration", 50 | tags: [:event], 51 | unit: {:native, :millisecond} 52 | ), 53 | 54 | # Database Metrics 55 | summary("test_bed.repo.query.total_time", 56 | unit: {:native, :millisecond}, 57 | description: "The sum of the other measurements" 58 | ), 59 | summary("test_bed.repo.query.decode_time", 60 | unit: {:native, :millisecond}, 61 | description: "The time spent decoding the data received from the database" 62 | ), 63 | summary("test_bed.repo.query.query_time", 64 | unit: {:native, :millisecond}, 65 | description: "The time spent executing the query" 66 | ), 67 | summary("test_bed.repo.query.queue_time", 68 | unit: {:native, :millisecond}, 69 | description: "The time spent waiting for a database connection" 70 | ), 71 | summary("test_bed.repo.query.idle_time", 72 | unit: {:native, :millisecond}, 73 | description: 74 | "The time the connection spent waiting before being checked out for the query" 75 | ), 76 | 77 | # VM Metrics 78 | summary("vm.memory.total", unit: {:byte, :kilobyte}), 79 | summary("vm.total_run_queue_lengths.total"), 80 | summary("vm.total_run_queue_lengths.cpu"), 81 | summary("vm.total_run_queue_lengths.io") 82 | ] 83 | end 84 | 85 | defp periodic_measurements do 86 | [ 87 | # A module, function and arguments to be invoked periodically. 88 | # This function must call :telemetry.execute/3 and a metric must be added above. 89 | # {TestBedWeb, :count_users, []} 90 | ] 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test_bed/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule TestBed.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :test_bed, 7 | version: "0.1.0", 8 | elixir: "~> 1.14", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | start_permanent: Mix.env() == :prod, 11 | aliases: aliases(), 12 | deps: deps() 13 | ] 14 | end 15 | 16 | # Configuration for the OTP application. 17 | # 18 | # Type `mix help compile.app` for more information. 19 | def application do 20 | [ 21 | mod: {TestBed.Application, []}, 22 | extra_applications: [:logger, :runtime_tools] 23 | ] 24 | end 25 | 26 | # Specifies which paths to compile per environment. 27 | defp elixirc_paths(:test), do: ["lib", "test/support"] 28 | defp elixirc_paths(_), do: ["lib"] 29 | 30 | # Specifies your project dependencies. 31 | # 32 | # Type `mix help deps` for examples and options. 33 | defp deps do 34 | [ 35 | {:phoenix, "~> 1.7.12"}, 36 | {:phoenix_ecto, "~> 4.4"}, 37 | {:ecto_sql, "~> 3.10"}, 38 | {:postgrex, ">= 0.0.0"}, 39 | {:phoenix_html, "~> 4.0"}, 40 | {:phoenix_live_reload, "~> 1.2", only: :dev}, 41 | {:phoenix_live_view, "~> 0.20.2"}, 42 | {:floki, ">= 0.30.0", only: :test}, 43 | {:phoenix_live_dashboard, "~> 0.8.3"}, 44 | {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, 45 | {:tailwind, "~> 0.2", runtime: Mix.env() == :dev}, 46 | {:heroicons, 47 | github: "tailwindlabs/heroicons", 48 | tag: "v2.1.1", 49 | sparse: "optimized", 50 | app: false, 51 | compile: false, 52 | depth: 1}, 53 | {:swoosh, "~> 1.5"}, 54 | {:finch, "~> 0.13"}, 55 | {:telemetry_metrics, "~> 1.0"}, 56 | {:telemetry_poller, "~> 1.0"}, 57 | {:gettext, "~> 0.20"}, 58 | {:jason, "~> 1.2"}, 59 | {:dns_cluster, "~> 0.1.1"}, 60 | {:bandit, "~> 1.2"}, 61 | {:ash, "~> 3.0"}, 62 | {:picosat_elixir, "~> 0.2.3"}, 63 | {:ash_postgres, "~> 2.0"}, 64 | {:ash_phoenix, "~> 2.0"}, 65 | {:ash_authentication, "~> 4.0"}, 66 | {:ash_authentication_phoenix, "~> 2.0"}, 67 | {:smokestack, "~> 0.6.1-rc.2"}, 68 | {:faker, ">= 0.0.0"}, 69 | {:ash_table, path: ".."} 70 | ] 71 | end 72 | 73 | # Aliases are shortcuts or tasks specific to the current project. 74 | # For example, to install project dependencies and perform other setup tasks, run: 75 | # 76 | # $ mix setup 77 | # 78 | # See the documentation for `Mix` for more info on aliases. 79 | defp aliases do 80 | [ 81 | setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"], 82 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 83 | "ecto.reset": ["ecto.drop", "ecto.setup"], 84 | test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"], 85 | "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"], 86 | "assets.build": ["tailwind test_bed", "esbuild test_bed"], 87 | "assets.deploy": [ 88 | "tailwind test_bed --minify", 89 | "esbuild test_bed --minify", 90 | "phx.digest" 91 | ] 92 | ] 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /test_bed/mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "ash": {:hex, :ash, "3.0.16", "8eaebd5a9f3ee404937ac811a240799613b0619026e097436132d60eaf18ed16", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, ">= 0.8.1 and < 1.0.0-0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.1.18 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "36c0d7653f7fb1d13cc03e1cc7ea7f6b9aadd278b9c9375ff5f0636ed0d7a785"}, 3 | "ash_authentication": {:hex, :ash_authentication, "4.0.1", "27e5fcda1022897a02903441a049ba9e5f655e51a757039d946f5bce1de0447c", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, "~> 2.0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, ">= 0.2.8 and < 1.0.0-0", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.18.0", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "e204585c8eed2d46a12e7031da48a169c513d5074ba43da90be0a92f7e1e0413"}, 4 | "ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.0.0", "1d2dd0abc9b9e008ea4423e902eb24825dbf4b9d1329bd079d7064ecfc45d319", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, "~> 4.0", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, "~> 2.0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}], "hexpm", "6a7c24d57ef6f7a4456d5ba139c8221df6a7ed81f15707a23fc33ad369e43a36"}, 5 | "ash_phoenix": {:hex, :ash_phoenix, "2.0.4", "0d18a3371879b875865180aaabc1697a35a6dcb9ebd2f346456208214bd02c9e", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.3 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "f3ea5309b42cdcaafc0ca713757cd4bb4819e02aeacc5195a040a955e861767d"}, 6 | "ash_postgres": {:hex, :ash_postgres, "2.0.12", "0cd30b5eab6ef6fc77d1f29c23bd8b9ad62e676f8aa14bf9194d3cf87e10adf2", [:mix], [{:ash, ">= 3.0.15 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.2.6 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "a6536a641bcb3dc0ff5b351c35b9334e5586170037f88c0035f532dcba872700"}, 7 | "ash_sql": {:hex, :ash_sql, "0.2.7", "56bfddcb4cf3edbbf702e2b665497309e43672fbf449ef049f4805211b9cd1b7", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "14622713cc08ede8fd0d2618b1718d759a6ee28839b8f738e6ee084703bd9437"}, 8 | "assent": {:hex, :assent, "0.2.10", "27e544c3428996c8ad744d473b3ceae86e4eb7db6bc7432676420e67e9148dd7", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "8483bf9621e994795a70a4ad8fda725abfb6a9675d63a9bfd4217c76d4a2d82a"}, 9 | "bandit": {:hex, :bandit, "1.5.5", "df28f1c41f745401fe9e85a6882033f5f3442ab6d30c8a2948554062a4ab56e0", [:mix], [{:hpax, "~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f21579a29ea4bc08440343b2b5f16f7cddf2fea5725d31b72cf973ec729079e1"}, 10 | "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"}, 11 | "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, 12 | "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, 13 | "comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"}, 14 | "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, 15 | "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, 16 | "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, 17 | "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, 18 | "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, 19 | "ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"}, 20 | "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, 21 | "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"}, 22 | "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, 23 | "ex_doc": {:hex, :ex_doc, "0.34.1", "9751a0419bc15bc7580c73fde506b17b07f6402a1e5243be9e0f05a68c723368", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d441f1a86a235f59088978eff870de2e815e290e44a8bd976fe5d64470a4c9d2"}, 24 | "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"}, 25 | "faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"}, 26 | "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, 27 | "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, 28 | "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"}, 29 | "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, 30 | "glob_ex": {:hex, :glob_ex, "0.1.7", "eae6b6377147fb712ac45b360e6dbba00346689a87f996672fe07e97d70597b1", [:mix], [], "hexpm", "decc1c21c0c73df3c9c994412716345c1692477b9470e337f628a7e08da0da6a"}, 31 | "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized"]}, 32 | "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"}, 33 | "igniter": {:hex, :igniter, "0.2.4", "71fc8a473c07de9bacfaa26862427d695b49d263c2f484a256fdb38fcc3471cc", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, "~> 0.9", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "361b9bc44f6e36161076bde02ed75287280b5aa2c7d0ad9bde83d521cc875115"}, 34 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 35 | "joken": {:hex, :joken, "2.6.1", "2ca3d8d7f83bf7196296a3d9b2ecda421a404634bfc618159981a960020480a1", [:mix], [{:jose, "~> 1.11.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "ab26122c400b3d254ce7d86ed066d6afad27e70416df947cdcb01e13a7382e68"}, 36 | "jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"}, 37 | "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, 38 | "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, 39 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, 40 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, 41 | "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, 42 | "mint": {:hex, :mint, "1.6.1", "065e8a5bc9bbd46a41099dfea3e0656436c5cbcb6e741c80bd2bad5cd872446f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4fc518dcc191d02f433393a72a7ba3f6f94b101d094cb6bf532ea54c89423780"}, 43 | "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 44 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 45 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 46 | "phoenix": {:hex, :phoenix, "1.7.14", "a7d0b3f1bc95987044ddada111e77bd7f75646a08518942c72a8440278ae7825", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c7859bc56cc5dfef19ecfc240775dae358cbaa530231118a9e014df392ace61a"}, 47 | "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.2", "3b83b24ab5a2eb071a20372f740d7118767c272db386831b2e77638c4dcc606d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "3f94d025f59de86be00f5f8c5dd7b5965a3298458d21ab1c328488be3b5fcd59"}, 48 | "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, 49 | "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, 50 | "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.4", "4508e481f791ce62ec6a096e13b061387158cbeefacca68c6c1928e1305e23ed", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "2984aae96994fbc5c61795a73b8fb58153b41ff934019cfb522343d2d3817d59"}, 51 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, 52 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.20.17", "f396bbdaf4ba227b82251eb75ac0afa6b3da5e509bc0d030206374237dfc9450", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61d741ffb78c85fdbca0de084da6a48f8ceb5261a79165b5a0b59e5f65ce98b"}, 53 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, 54 | "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, 55 | "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, 56 | "picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"}, 57 | "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, 58 | "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, 59 | "postgrex": {:hex, :postgrex, "0.18.0", "f34664101eaca11ff24481ed4c378492fed2ff416cd9b06c399e90f321867d7e", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a042989ba1bc1cca7383ebb9e461398e3f89f868c92ce6671feb7ef132a252d1"}, 60 | "reactor": {:hex, :reactor, "0.8.4", "344d02ba4a0010763851f4e4aa0ff190ebe7e392e3c27c6cd143dde077b986e7", [:mix], [{:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "49c1fd3c786603cec8140ce941c41c7ea72cc4411860ccdee9876c4ca2204f81"}, 61 | "recase": {:hex, :recase, "0.8.1", "ab98cd35857a86fa5ca99036f575241d71d77d9c2ab0c39aacf1c9b61f6f7d1d", [:mix], [], "hexpm", "9fd8d63e7e43bd9ea385b12364e305778b2bbd92537e95c4b2e26fc507d5e4c2"}, 62 | "req": {:hex, :req, "0.5.1", "90584216d064389a4ff2d4279fe2c11ff6c812ab00fa01a9fb9d15457f65ba70", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7ea96a1a95388eb0fefa92d89466cdfedba24032794e5c1147d78ec90db7edca"}, 63 | "rewrite": {:hex, :rewrite, "0.10.5", "6afadeae0b9d843b27ac6225e88e165884875e0aed333ef4ad3bf36f9c101bed", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "51cc347a4269ad3a1e7a2c4122dbac9198302b082f5615964358b4635ebf3d4f"}, 64 | "slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"}, 65 | "smokestack": {:hex, :smokestack, "0.6.2", "0bd5bb477e1d6ca3aaae46641912d9f2bbb5663d3975a4b9c9fa6d082941c64e", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:recase, "~> 0.7", [hex: :recase, repo: "hexpm", optional: false]}, {:spark, "~> 2.1", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "1499bcf570dbc120c6a16ca3e32d857aabe2270180c2b7e76de6543f61d05c40"}, 66 | "sourceror": {:hex, :sourceror, "1.4.0", "be87319b1579191e25464005d465713079b3fd7124a3938a1e6cf4def39735a9", [:mix], [], "hexpm", "16751ca55e3895f2228938b703ad399b0b27acfe288eff6c0e629ed3e6ec0358"}, 67 | "spark": {:hex, :spark, "2.2.6", "4f160462f45c0be2bccdc4700e7ffc6b2e97b4e38f57eed2349bc9dab4aaa66c", [:mix], [{:igniter, "~> 0.2", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "1e0e012978be808232a502a116d4b99b5059ab3760453438b155ac048f82ce20"}, 68 | "spitfire": {:hex, :spitfire, "0.1.3", "7ea0f544005dfbe48e615ed90250c9a271bfe126914012023fd5e4b6b82b7ec7", [:mix], [], "hexpm", "d53b5107bcff526a05c5bb54c95e77b36834550affd5830c9f58760e8c543657"}, 69 | "splode": {:hex, :splode, "0.2.4", "71046334c39605095ca4bed5d008372e56454060997da14f9868534c17b84b53", [:mix], [], "hexpm", "ca3b95f0d8d4b482b5357954fec857abd0fa3ea509d623334c1328e7382044c2"}, 70 | "stream_data": {:hex, :stream_data, "1.1.1", "fd515ca95619cca83ba08b20f5e814aaf1e5ebff114659dc9731f966c9226246", [:mix], [], "hexpm", "45d0cd46bd06738463fd53f22b70042dbb58c384bb99ef4e7576e7bb7d3b8c8c"}, 71 | "swoosh": {:hex, :swoosh, "1.16.9", "20c6a32ea49136a4c19f538e27739bb5070558c0fa76b8a95f4d5d5ca7d319a1", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "878b1a7a6c10ebbf725a3349363f48f79c5e3d792eb621643b0d276a38acc0a6"}, 72 | "tailwind": {:hex, :tailwind, "0.2.3", "277f08145d407de49650d0a4685dc062174bdd1ae7731c5f1da86163a24dfcdb", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "8e45e7a34a676a7747d04f7913a96c770c85e6be810a1d7f91e713d3a3655b5d"}, 73 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 74 | "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"}, 75 | "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, 76 | "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, 77 | "typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"}, 78 | "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, 79 | "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"}, 80 | } 81 | -------------------------------------------------------------------------------- /test_bed/priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | 13 | ## From Ecto.Changeset.cast/4 14 | msgid "can't be blank" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.unique_constraint/3 18 | msgid "has already been taken" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.put_change/3 22 | msgid "is invalid" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_acceptance/3 26 | msgid "must be accepted" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_format/3 30 | msgid "has invalid format" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_subset/3 34 | msgid "has an invalid entry" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.validate_exclusion/3 38 | msgid "is reserved" 39 | msgstr "" 40 | 41 | ## From Ecto.Changeset.validate_confirmation/3 42 | msgid "does not match confirmation" 43 | msgstr "" 44 | 45 | ## From Ecto.Changeset.no_assoc_constraint/3 46 | msgid "is still associated with this entry" 47 | msgstr "" 48 | 49 | msgid "are still associated with this entry" 50 | msgstr "" 51 | 52 | ## From Ecto.Changeset.validate_length/3 53 | msgid "should have %{count} item(s)" 54 | msgid_plural "should have %{count} item(s)" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | msgid "should be %{count} character(s)" 59 | msgid_plural "should be %{count} character(s)" 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | msgid "should be %{count} byte(s)" 64 | msgid_plural "should be %{count} byte(s)" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | msgid "should have at least %{count} item(s)" 69 | msgid_plural "should have at least %{count} item(s)" 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | msgid "should be at least %{count} character(s)" 74 | msgid_plural "should be at least %{count} character(s)" 75 | msgstr[0] "" 76 | msgstr[1] "" 77 | 78 | msgid "should be at least %{count} byte(s)" 79 | msgid_plural "should be at least %{count} byte(s)" 80 | msgstr[0] "" 81 | msgstr[1] "" 82 | 83 | msgid "should have at most %{count} item(s)" 84 | msgid_plural "should have at most %{count} item(s)" 85 | msgstr[0] "" 86 | msgstr[1] "" 87 | 88 | msgid "should be at most %{count} character(s)" 89 | msgid_plural "should be at most %{count} character(s)" 90 | msgstr[0] "" 91 | msgstr[1] "" 92 | 93 | msgid "should be at most %{count} byte(s)" 94 | msgid_plural "should be at most %{count} byte(s)" 95 | msgstr[0] "" 96 | msgstr[1] "" 97 | 98 | ## From Ecto.Changeset.validate_number/3 99 | msgid "must be less than %{number}" 100 | msgstr "" 101 | 102 | msgid "must be greater than %{number}" 103 | msgstr "" 104 | 105 | msgid "must be less than or equal to %{number}" 106 | msgstr "" 107 | 108 | msgid "must be greater than or equal to %{number}" 109 | msgstr "" 110 | 111 | msgid "must be equal to %{number}" 112 | msgstr "" 113 | -------------------------------------------------------------------------------- /test_bed/priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here has no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | ## From Ecto.Changeset.cast/4 11 | msgid "can't be blank" 12 | msgstr "" 13 | 14 | ## From Ecto.Changeset.unique_constraint/3 15 | msgid "has already been taken" 16 | msgstr "" 17 | 18 | ## From Ecto.Changeset.put_change/3 19 | msgid "is invalid" 20 | msgstr "" 21 | 22 | ## From Ecto.Changeset.validate_acceptance/3 23 | msgid "must be accepted" 24 | msgstr "" 25 | 26 | ## From Ecto.Changeset.validate_format/3 27 | msgid "has invalid format" 28 | msgstr "" 29 | 30 | ## From Ecto.Changeset.validate_subset/3 31 | msgid "has an invalid entry" 32 | msgstr "" 33 | 34 | ## From Ecto.Changeset.validate_exclusion/3 35 | msgid "is reserved" 36 | msgstr "" 37 | 38 | ## From Ecto.Changeset.validate_confirmation/3 39 | msgid "does not match confirmation" 40 | msgstr "" 41 | 42 | ## From Ecto.Changeset.no_assoc_constraint/3 43 | msgid "is still associated with this entry" 44 | msgstr "" 45 | 46 | msgid "are still associated with this entry" 47 | msgstr "" 48 | 49 | ## From Ecto.Changeset.validate_length/3 50 | msgid "should have %{count} item(s)" 51 | msgid_plural "should have %{count} item(s)" 52 | msgstr[0] "" 53 | msgstr[1] "" 54 | 55 | msgid "should be %{count} character(s)" 56 | msgid_plural "should be %{count} character(s)" 57 | msgstr[0] "" 58 | msgstr[1] "" 59 | 60 | msgid "should be %{count} byte(s)" 61 | msgid_plural "should be %{count} byte(s)" 62 | msgstr[0] "" 63 | msgstr[1] "" 64 | 65 | msgid "should have at least %{count} item(s)" 66 | msgid_plural "should have at least %{count} item(s)" 67 | msgstr[0] "" 68 | msgstr[1] "" 69 | 70 | msgid "should be at least %{count} character(s)" 71 | msgid_plural "should be at least %{count} character(s)" 72 | msgstr[0] "" 73 | msgstr[1] "" 74 | 75 | msgid "should be at least %{count} byte(s)" 76 | msgid_plural "should be at least %{count} byte(s)" 77 | msgstr[0] "" 78 | msgstr[1] "" 79 | 80 | msgid "should have at most %{count} item(s)" 81 | msgid_plural "should have at most %{count} item(s)" 82 | msgstr[0] "" 83 | msgstr[1] "" 84 | 85 | msgid "should be at most %{count} character(s)" 86 | msgid_plural "should be at most %{count} character(s)" 87 | msgstr[0] "" 88 | msgstr[1] "" 89 | 90 | msgid "should be at most %{count} byte(s)" 91 | msgid_plural "should be at most %{count} byte(s)" 92 | msgstr[0] "" 93 | msgstr[1] "" 94 | 95 | ## From Ecto.Changeset.validate_number/3 96 | msgid "must be less than %{number}" 97 | msgstr "" 98 | 99 | msgid "must be greater than %{number}" 100 | msgstr "" 101 | 102 | msgid "must be less than or equal to %{number}" 103 | msgstr "" 104 | 105 | msgid "must be greater than or equal to %{number}" 106 | msgstr "" 107 | 108 | msgid "must be equal to %{number}" 109 | msgstr "" 110 | -------------------------------------------------------------------------------- /test_bed/priv/repo/migrations/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto_sql], 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /test_bed/priv/repo/migrations/20240629155735_install_3_extensions.exs: -------------------------------------------------------------------------------- 1 | defmodule TestBed.Repo.Migrations.Install3Extensions20240629155733 do 2 | @moduledoc """ 3 | Installs any extensions that are mentioned in the repo's `installed_extensions/0` callback 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | execute(""" 12 | CREATE OR REPLACE FUNCTION ash_elixir_or(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) 13 | AS $$ SELECT COALESCE(NULLIF($1, FALSE), $2) $$ 14 | LANGUAGE SQL 15 | IMMUTABLE; 16 | """) 17 | 18 | execute(""" 19 | CREATE OR REPLACE FUNCTION ash_elixir_or(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) 20 | AS $$ SELECT COALESCE($1, $2) $$ 21 | LANGUAGE SQL 22 | IMMUTABLE; 23 | """) 24 | 25 | execute(""" 26 | CREATE OR REPLACE FUNCTION ash_elixir_and(left BOOLEAN, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) AS $$ 27 | SELECT CASE 28 | WHEN $1 IS TRUE THEN $2 29 | ELSE $1 30 | END $$ 31 | LANGUAGE SQL 32 | IMMUTABLE; 33 | """) 34 | 35 | execute(""" 36 | CREATE OR REPLACE FUNCTION ash_elixir_and(left ANYCOMPATIBLE, in right ANYCOMPATIBLE, out f1 ANYCOMPATIBLE) AS $$ 37 | SELECT CASE 38 | WHEN $1 IS NOT NULL THEN $2 39 | ELSE $1 40 | END $$ 41 | LANGUAGE SQL 42 | IMMUTABLE; 43 | """) 44 | 45 | execute(""" 46 | CREATE OR REPLACE FUNCTION ash_trim_whitespace(arr text[]) 47 | RETURNS text[] AS $$ 48 | DECLARE 49 | start_index INT = 1; 50 | end_index INT = array_length(arr, 1); 51 | BEGIN 52 | WHILE start_index <= end_index AND arr[start_index] = '' LOOP 53 | start_index := start_index + 1; 54 | END LOOP; 55 | 56 | WHILE end_index >= start_index AND arr[end_index] = '' LOOP 57 | end_index := end_index - 1; 58 | END LOOP; 59 | 60 | IF start_index > end_index THEN 61 | RETURN ARRAY[]::text[]; 62 | ELSE 63 | RETURN arr[start_index : end_index]; 64 | END IF; 65 | END; $$ 66 | LANGUAGE plpgsql 67 | IMMUTABLE; 68 | """) 69 | 70 | execute(""" 71 | CREATE OR REPLACE FUNCTION ash_raise_error(json_data jsonb) 72 | RETURNS BOOLEAN AS $$ 73 | BEGIN 74 | -- Raise an error with the provided JSON data. 75 | -- The JSON object is converted to text for inclusion in the error message. 76 | RAISE EXCEPTION 'ash_error: %', json_data::text; 77 | RETURN NULL; 78 | END; 79 | $$ LANGUAGE plpgsql; 80 | """) 81 | 82 | execute(""" 83 | CREATE OR REPLACE FUNCTION ash_raise_error(json_data jsonb, type_signal ANYCOMPATIBLE) 84 | RETURNS ANYCOMPATIBLE AS $$ 85 | BEGIN 86 | -- Raise an error with the provided JSON data. 87 | -- The JSON object is converted to text for inclusion in the error message. 88 | RAISE EXCEPTION 'ash_error: %', json_data::text; 89 | RETURN NULL; 90 | END; 91 | $$ LANGUAGE plpgsql; 92 | """) 93 | 94 | execute("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"") 95 | execute("CREATE EXTENSION IF NOT EXISTS \"citext\"") 96 | end 97 | 98 | def down do 99 | # Uncomment this if you actually want to uninstall the extensions 100 | # when this migration is rolled back: 101 | execute( 102 | "DROP FUNCTION IF EXISTS ash_raise_error(jsonb), ash_raise_error(jsonb, ANYCOMPATIBLE), ash_elixir_and(BOOLEAN, ANYCOMPATIBLE), ash_elixir_and(ANYCOMPATIBLE, ANYCOMPATIBLE), ash_elixir_or(ANYCOMPATIBLE, ANYCOMPATIBLE), ash_elixir_or(BOOLEAN, ANYCOMPATIBLE), ash_trim_whitespace(text[])" 103 | ) 104 | 105 | # execute("DROP EXTENSION IF EXISTS \"uuid-ossp\"") 106 | # execute("DROP EXTENSION IF EXISTS \"citext\"") 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /test_bed/priv/repo/migrations/20240629155736_initial_migration.exs: -------------------------------------------------------------------------------- 1 | defmodule TestBed.Repo.Migrations.InitialMigration do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | create table(:posts, primary_key: false) do 12 | add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true 13 | add :title, :text, null: false 14 | add :content, :text 15 | end 16 | end 17 | 18 | def down do 19 | drop table(:posts) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test_bed/priv/repo/migrations/20240629173304_add_author.exs: -------------------------------------------------------------------------------- 1 | defmodule TestBed.Repo.Migrations.AddAuthor do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | alter table(:posts) do 12 | add :author_id, :uuid 13 | end 14 | 15 | create table(:authors, primary_key: false) do 16 | add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true 17 | end 18 | 19 | alter table(:posts) do 20 | modify :author_id, 21 | references(:authors, 22 | column: :id, 23 | name: "posts_author_id_fkey", 24 | type: :uuid, 25 | prefix: "public" 26 | ) 27 | end 28 | 29 | alter table(:authors) do 30 | add :name, :text, null: false 31 | end 32 | end 33 | 34 | def down do 35 | alter table(:authors) do 36 | remove :name 37 | end 38 | 39 | drop constraint(:posts, "posts_author_id_fkey") 40 | 41 | alter table(:posts) do 42 | modify :author_id, :uuid 43 | end 44 | 45 | drop table(:authors) 46 | 47 | alter table(:posts) do 48 | remove :author_id 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test_bed/priv/repo/migrations/20240701190158_add_customer.exs: -------------------------------------------------------------------------------- 1 | defmodule TestBed.Repo.Migrations.AddCustomer do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | create table(:customers, primary_key: false) do 12 | add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true 13 | add :name, :text, null: false 14 | end 15 | end 16 | 17 | def down do 18 | drop table(:customers) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test_bed/priv/repo/migrations/20240701190257_add_customer_tenant.exs: -------------------------------------------------------------------------------- 1 | defmodule TestBed.Repo.Migrations.AddCustomerTenant do 2 | @moduledoc """ 3 | Updates resources based on their most recent snapshots. 4 | 5 | This file was autogenerated with `mix ash_postgres.generate_migrations` 6 | """ 7 | 8 | use Ecto.Migration 9 | 10 | def up do 11 | alter table(:customers) do 12 | add :tenant, :text 13 | end 14 | end 15 | 16 | def down do 17 | alter table(:customers) do 18 | remove :tenant 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test_bed/priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # TestBed.Repo.insert!(%TestBed.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | 13 | alias TestBed.Blog 14 | for i <- 0..30 do 15 | Blog.create_post(%{title: Faker.Lorem.sentence(3), author: %{name: Faker.Person.name()}}) 16 | end 17 | -------------------------------------------------------------------------------- /test_bed/priv/resource_snapshots/repo/authors/20240629173304.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": [ 3 | { 4 | "default": "fragment(\"gen_random_uuid()\")", 5 | "size": null, 6 | "type": "uuid", 7 | "source": "id", 8 | "references": null, 9 | "allow_nil?": false, 10 | "generated?": false, 11 | "primary_key?": true 12 | }, 13 | { 14 | "default": "nil", 15 | "size": null, 16 | "type": "text", 17 | "source": "name", 18 | "references": null, 19 | "allow_nil?": false, 20 | "generated?": false, 21 | "primary_key?": false 22 | } 23 | ], 24 | "table": "authors", 25 | "hash": "78AA4A269BDB8412221028DAA269E20456525E6AC03FB40DF3BB6F751DABAFBB", 26 | "repo": "Elixir.TestBed.Repo", 27 | "identities": [], 28 | "schema": null, 29 | "multitenancy": { 30 | "global": null, 31 | "attribute": null, 32 | "strategy": null 33 | }, 34 | "base_filter": null, 35 | "check_constraints": [], 36 | "custom_indexes": [], 37 | "custom_statements": [], 38 | "has_create_action": true 39 | } -------------------------------------------------------------------------------- /test_bed/priv/resource_snapshots/repo/customers/20240701190158.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": [ 3 | { 4 | "default": "fragment(\"gen_random_uuid()\")", 5 | "size": null, 6 | "type": "uuid", 7 | "source": "id", 8 | "references": null, 9 | "allow_nil?": false, 10 | "primary_key?": true, 11 | "generated?": false 12 | }, 13 | { 14 | "default": "nil", 15 | "size": null, 16 | "type": "text", 17 | "source": "name", 18 | "references": null, 19 | "allow_nil?": false, 20 | "primary_key?": false, 21 | "generated?": false 22 | } 23 | ], 24 | "table": "customers", 25 | "hash": "BADEB24D65A2EBB827E19ED799EF5EEE49D330D9AFAB676EC839C9F8AA3F7554", 26 | "repo": "Elixir.TestBed.Repo", 27 | "schema": null, 28 | "check_constraints": [], 29 | "identities": [], 30 | "custom_indexes": [], 31 | "multitenancy": { 32 | "global": null, 33 | "attribute": null, 34 | "strategy": null 35 | }, 36 | "base_filter": null, 37 | "custom_statements": [], 38 | "has_create_action": true 39 | } -------------------------------------------------------------------------------- /test_bed/priv/resource_snapshots/repo/customers/20240701190257.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": [ 3 | { 4 | "default": "fragment(\"gen_random_uuid()\")", 5 | "size": null, 6 | "type": "uuid", 7 | "source": "id", 8 | "references": null, 9 | "allow_nil?": false, 10 | "primary_key?": true, 11 | "generated?": false 12 | }, 13 | { 14 | "default": "nil", 15 | "size": null, 16 | "type": "text", 17 | "source": "name", 18 | "references": null, 19 | "allow_nil?": false, 20 | "primary_key?": false, 21 | "generated?": false 22 | }, 23 | { 24 | "default": "nil", 25 | "size": null, 26 | "type": "text", 27 | "source": "tenant", 28 | "references": null, 29 | "allow_nil?": true, 30 | "primary_key?": false, 31 | "generated?": false 32 | } 33 | ], 34 | "table": "customers", 35 | "hash": "B640F6BA59C5052A5E3DC92C688EF6EB098A25BB3331685885B5571FAD7201A6", 36 | "repo": "Elixir.TestBed.Repo", 37 | "multitenancy": { 38 | "global": false, 39 | "attribute": "tenant", 40 | "strategy": "attribute" 41 | }, 42 | "schema": null, 43 | "check_constraints": [], 44 | "identities": [], 45 | "custom_indexes": [], 46 | "base_filter": null, 47 | "custom_statements": [], 48 | "has_create_action": true 49 | } -------------------------------------------------------------------------------- /test_bed/priv/resource_snapshots/repo/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "installed": [ 3 | "ash-functions", 4 | "uuid-ossp", 5 | "citext" 6 | ], 7 | "ash_functions_version": 3 8 | } -------------------------------------------------------------------------------- /test_bed/priv/resource_snapshots/repo/posts/20240629155736.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": [ 3 | { 4 | "default": "fragment(\"gen_random_uuid()\")", 5 | "size": null, 6 | "type": "uuid", 7 | "source": "id", 8 | "references": null, 9 | "allow_nil?": false, 10 | "generated?": false, 11 | "primary_key?": true 12 | }, 13 | { 14 | "default": "nil", 15 | "size": null, 16 | "type": "text", 17 | "source": "title", 18 | "references": null, 19 | "allow_nil?": false, 20 | "generated?": false, 21 | "primary_key?": false 22 | }, 23 | { 24 | "default": "nil", 25 | "size": null, 26 | "type": "text", 27 | "source": "content", 28 | "references": null, 29 | "allow_nil?": true, 30 | "generated?": false, 31 | "primary_key?": false 32 | } 33 | ], 34 | "table": "posts", 35 | "hash": "E9BDBE9739282EFD16C4EE8BF9DACD483F0FE5BE26F780D328139ABDC167FBD6", 36 | "repo": "Elixir.TestBed.Repo", 37 | "schema": null, 38 | "check_constraints": [], 39 | "custom_indexes": [], 40 | "custom_statements": [], 41 | "identities": [], 42 | "multitenancy": { 43 | "global": null, 44 | "attribute": null, 45 | "strategy": null 46 | }, 47 | "base_filter": null, 48 | "has_create_action": true 49 | } -------------------------------------------------------------------------------- /test_bed/priv/resource_snapshots/repo/posts/20240629173304.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": [ 3 | { 4 | "default": "fragment(\"gen_random_uuid()\")", 5 | "size": null, 6 | "type": "uuid", 7 | "source": "id", 8 | "references": null, 9 | "allow_nil?": false, 10 | "generated?": false, 11 | "primary_key?": true 12 | }, 13 | { 14 | "default": "nil", 15 | "size": null, 16 | "type": "text", 17 | "source": "title", 18 | "references": null, 19 | "allow_nil?": false, 20 | "generated?": false, 21 | "primary_key?": false 22 | }, 23 | { 24 | "default": "nil", 25 | "size": null, 26 | "type": "text", 27 | "source": "content", 28 | "references": null, 29 | "allow_nil?": true, 30 | "generated?": false, 31 | "primary_key?": false 32 | }, 33 | { 34 | "default": "nil", 35 | "size": null, 36 | "type": "uuid", 37 | "source": "author_id", 38 | "references": { 39 | "name": "posts_author_id_fkey", 40 | "table": "authors", 41 | "schema": "public", 42 | "multitenancy": { 43 | "global": null, 44 | "attribute": null, 45 | "strategy": null 46 | }, 47 | "primary_key?": true, 48 | "destination_attribute": "id", 49 | "deferrable": false, 50 | "index?": false, 51 | "match_type": null, 52 | "match_with": null, 53 | "on_delete": null, 54 | "on_update": null, 55 | "destination_attribute_default": null, 56 | "destination_attribute_generated": null 57 | }, 58 | "allow_nil?": true, 59 | "generated?": false, 60 | "primary_key?": false 61 | } 62 | ], 63 | "table": "posts", 64 | "hash": "2E8F5D65E571C947B8D750B8BEA252C71517E8769982B1C4AB722013B3D8E829", 65 | "repo": "Elixir.TestBed.Repo", 66 | "identities": [], 67 | "schema": null, 68 | "multitenancy": { 69 | "global": null, 70 | "attribute": null, 71 | "strategy": null 72 | }, 73 | "base_filter": null, 74 | "check_constraints": [], 75 | "custom_indexes": [], 76 | "custom_statements": [], 77 | "has_create_action": true 78 | } -------------------------------------------------------------------------------- /test_bed/priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/launchscout/ash_table/760dbab9941c8e7e68362177aa9a3b091fbe680e/test_bed/priv/static/favicon.ico -------------------------------------------------------------------------------- /test_bed/priv/static/images/logo.svg: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /test_bed/priv/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /test_bed/test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBedWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use TestBedWeb.ConnCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # The default endpoint for testing 23 | @endpoint TestBedWeb.Endpoint 24 | 25 | use TestBedWeb, :verified_routes 26 | 27 | # Import conveniences for testing with connections 28 | import Plug.Conn 29 | import Phoenix.ConnTest 30 | import TestBedWeb.ConnCase 31 | end 32 | end 33 | 34 | setup tags do 35 | TestBed.DataCase.setup_sandbox(tags) 36 | {:ok, conn: Phoenix.ConnTest.build_conn()} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test_bed/test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule TestBed.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 8 | 9 | Finally, if the test case interacts with the database, 10 | we enable the SQL sandbox, so changes done to the database 11 | are reverted at the end of every test. If you are using 12 | PostgreSQL, you can even run database tests asynchronously 13 | by setting `use TestBed.DataCase, async: true`, although 14 | this option is not recommended for other databases. 15 | """ 16 | 17 | use ExUnit.CaseTemplate 18 | 19 | using do 20 | quote do 21 | alias TestBed.Repo 22 | 23 | import Ecto 24 | import Ecto.Changeset 25 | import Ecto.Query 26 | import TestBed.DataCase 27 | end 28 | end 29 | 30 | setup tags do 31 | TestBed.DataCase.setup_sandbox(tags) 32 | :ok 33 | end 34 | 35 | @doc """ 36 | Sets up the sandbox based on the test tags. 37 | """ 38 | def setup_sandbox(tags) do 39 | pid = Ecto.Adapters.SQL.Sandbox.start_owner!(TestBed.Repo, shared: not tags[:async]) 40 | on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) 41 | end 42 | 43 | @doc """ 44 | A helper that transforms changeset errors into a map of messages. 45 | 46 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 47 | assert "password is too short" in errors_on(changeset).password 48 | assert %{password: ["password is too short"]} = errors_on(changeset) 49 | 50 | """ 51 | def errors_on(changeset) do 52 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 53 | Regex.replace(~r"%{(\w+)}", message, fn _, key -> 54 | opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() 55 | end) 56 | end) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test_bed/test/test_bed_web/controllers/error_html_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TestBedWeb.ErrorHTMLTest do 2 | use TestBedWeb.ConnCase, async: true 3 | 4 | # Bring render_to_string/4 for testing custom views 5 | import Phoenix.Template 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(TestBedWeb.ErrorHTML, "404", "html", []) == "Not Found" 9 | end 10 | 11 | test "renders 500.html" do 12 | assert render_to_string(TestBedWeb.ErrorHTML, "500", "html", []) == "Internal Server Error" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test_bed/test/test_bed_web/controllers/error_json_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TestBedWeb.ErrorJSONTest do 2 | use TestBedWeb.ConnCase, async: true 3 | 4 | test "renders 404" do 5 | assert TestBedWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}} 6 | end 7 | 8 | test "renders 500" do 9 | assert TestBedWeb.ErrorJSON.render("500.json", %{}) == 10 | %{errors: %{detail: "Internal Server Error"}} 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test_bed/test/test_bed_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TestBedWeb.PageControllerTest do 2 | use TestBedWeb.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get(conn, ~p"/") 6 | assert html_response(conn, 200) =~ "Peace of mind from prototype to production" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test_bed/test/test_bed_web/live/customers_live/index_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TestBed.CustomersLive.IndexTest do 2 | use TestBedWeb.ConnCase 3 | 4 | import Phoenix.LiveViewTest 5 | 6 | alias TestBed.Customers 7 | 8 | test "read options specifying tenant", %{conn: conn} do 9 | Customers.create_customer!(%{name: "Bobs Tires", tenant: "bob"}) 10 | Customers.create_customer!(%{name: "Freds Tires", tenant: "fred"}) 11 | 12 | {:ok, _view, html} = live(conn, ~p"/customers") 13 | 14 | assert html =~ "Bobs Tires" 15 | refute html =~ "Freds Tires" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test_bed/test/test_bed_web/live/posts_live/index_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TestBed.BlogLive.IndexTest do 2 | use TestBedWeb.ConnCase 3 | 4 | import Phoenix.LiveViewTest 5 | 6 | alias TestBed.Blog 7 | 8 | test "sort by title", %{conn: conn} do 9 | Blog.create_post!(%{title: "Posty"}) 10 | Blog.create_post!(%{title: "Zazzle"}) 11 | Blog.create_post!(%{title: "Bubble"}) 12 | 13 | {:ok, view, _html} = live(conn, ~p"/posts") 14 | 15 | view 16 | |> element(~s/th button[phx-value-column="title"]/) 17 | |> render_click() 18 | 19 | assert has_element?(view, ~s/table tr:first-child/, "Bubble") 20 | end 21 | 22 | test "sort by author name", %{conn: conn} do 23 | author1 = Blog.create_author!(%{name: "Poo"}) 24 | author2 = Blog.create_author!(%{name: "Yap"}) 25 | author3 = Blog.create_author!(%{name: "Bob"}) 26 | 27 | Blog.create_post!(%{author_id: author1.id, title: "Wut"}) 28 | Blog.create_post!(%{author_id: author2.id, title: "Wutter"}) 29 | Blog.create_post!(%{author_id: author3.id, title: "Wuttest"}) 30 | 31 | {:ok, view, _html} = live(conn, ~p"/posts") 32 | 33 | view 34 | |> element(~s/th button[phx-value-column="author.name"]/) 35 | |> render_click() 36 | 37 | assert has_element?(view, ~s/table tr:first-child/, "Bob") 38 | end 39 | 40 | test "pagination", %{conn: conn} do 41 | for i <- 0..20 do 42 | Blog.create_post!(%{title: "Post #{i}"}) 43 | end 44 | 45 | {:ok, view, html} = live(conn, ~p"/posts") 46 | 47 | assert html =~ "1 to 10" 48 | 49 | view 50 | |> element(~s/[phx-click="set_page"]/) 51 | |> render_click() =~ "11 to 20" 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test_bed/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Ecto.Adapters.SQL.Sandbox.mode(TestBed.Repo, :manual) 3 | --------------------------------------------------------------------------------