update
This commit is contained in:
4
phoenix/deps/phoenix_ecto/.formatter.exs
Normal file
4
phoenix/deps/phoenix_ecto/.formatter.exs
Normal file
@@ -0,0 +1,4 @@
|
||||
[
|
||||
import_deps: [:ecto],
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
||||
BIN
phoenix/deps/phoenix_ecto/.hex
Normal file
BIN
phoenix/deps/phoenix_ecto/.hex
Normal file
Binary file not shown.
264
phoenix/deps/phoenix_ecto/CHANGELOG.md
Normal file
264
phoenix/deps/phoenix_ecto/CHANGELOG.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# Changelog
|
||||
|
||||
## v4.7.0
|
||||
|
||||
* Bug fixes
|
||||
* Disable migration lock when checking for pending migrations to avoid slow downs
|
||||
|
||||
## v4.6.6
|
||||
|
||||
* Bug fixes
|
||||
* Keep backwards compatibility on sandbox API
|
||||
|
||||
## v4.6.5
|
||||
|
||||
* Bug fixes
|
||||
* Unallow existing allowances when attempting to allow a Plug to access a connection
|
||||
|
||||
## v4.6.4
|
||||
|
||||
* Enhancements
|
||||
* Wrap raised Ecto exceptions so context is not lost
|
||||
* Do not override changeset actions
|
||||
|
||||
## v4.6.3
|
||||
|
||||
* Enhancements
|
||||
* Add prefix option to check repo status plug
|
||||
|
||||
* Bug fix
|
||||
* Fix map.field notation warning on Elixir 1.17
|
||||
|
||||
## v4.6.2
|
||||
|
||||
* Bug fix
|
||||
* Attach directories to Pending Migrations exception
|
||||
|
||||
## v4.6.1
|
||||
|
||||
* Bug fix
|
||||
* Ensure "Create database" action is shown when database is not available
|
||||
|
||||
## v4.6.0
|
||||
|
||||
* Enhancements
|
||||
* Return 400 for character encoding errors in Postgrex
|
||||
* Bump Elixir requirement to v1.11+
|
||||
|
||||
## v4.5.1
|
||||
|
||||
* Bug fix
|
||||
* Fix a regression on nested `inputs_for`
|
||||
|
||||
## v4.5.0
|
||||
|
||||
* Enhancements
|
||||
* Support Phoenix.HTML ~> 4.1
|
||||
* Use `to_form`'s `:action` as changeset action when passed
|
||||
|
||||
## v4.4.3
|
||||
|
||||
* Enhancements
|
||||
* Support Phoenix.HTML ~> 4.0
|
||||
|
||||
## v4.4.2
|
||||
|
||||
* Enhancements
|
||||
* Fix warning on undefined migration function when `ecto_sql` is missing
|
||||
* Support changesets with 3-arity cast function
|
||||
|
||||
## v4.4.1
|
||||
|
||||
* Enhancements
|
||||
* Allow migration_lock to be specified in check_repo_status
|
||||
* Support multiple repos on sandbox plug API
|
||||
* Support configuring multiple custom migration paths
|
||||
|
||||
## v4.4.0
|
||||
|
||||
This release bumps the requirement for Ecto and Phoenix.
|
||||
|
||||
* Enhancements
|
||||
* Trap exits when activating the test sandbox
|
||||
|
||||
## v4.3.0
|
||||
|
||||
* Enhancements
|
||||
* Support `:phoenix_html` v3.0
|
||||
|
||||
## v4.2.1
|
||||
|
||||
* Bug fixes
|
||||
* Only check for storage if we cannot check for migrations. This reduces the amount of operations for successful cases (which are the most common) and avoid issues for when we can't check the storage in the first place
|
||||
|
||||
## v4.2.0
|
||||
|
||||
* Enhancements
|
||||
* Support cast_assoc `with` MFA option on inputs_for
|
||||
|
||||
* Bug fixes
|
||||
* Do not treat `InvalidChangesetError` as 422 as those are not logged
|
||||
* Fix status code in check status exceptions to 503
|
||||
* Use text for floats and decimals as the `input_type` - numerics have many usability issues that led them to not be widely used
|
||||
|
||||
## v4.1.0
|
||||
|
||||
* Enhancements
|
||||
* Add `Phoenix.Ecto.CheckRepoStatus` plug
|
||||
|
||||
## v4.0.0
|
||||
|
||||
* Enhancements
|
||||
* Implement `Plug.Status` for `Ecto.StaleEntryError`
|
||||
* Support Ecto 3.0
|
||||
|
||||
## v3.4.0
|
||||
|
||||
* Enhancements
|
||||
* Use `:normal` formatting when converting `Decimal` to HTML safe
|
||||
* Ignore errors in case `changeset.action` is `:ignore`
|
||||
* Allow `:timeout` option on external sandbox
|
||||
* Extract and translate internal exception from `Ecto.SubQueryError`
|
||||
|
||||
## v3.3.0
|
||||
|
||||
* Enhancements
|
||||
* Support concurrent and transactional end-to-end tests for external HTTP clients using the new `:at` and `:repo` options to the `Phoenix.Ecto.SQL.Sandbox` plug
|
||||
|
||||
## v3.2.3
|
||||
|
||||
* Bug fixes
|
||||
* Make `phoenix_html` dependency optional once again
|
||||
|
||||
## v3.2.2
|
||||
|
||||
* Enhancements
|
||||
* Give `Ecto.InvalidChangesetError` plug_status 422
|
||||
|
||||
* Bug fixes
|
||||
* Do not raise for schemaless structs
|
||||
|
||||
## v3.2.1
|
||||
|
||||
* Bug fixes
|
||||
* Implement proper input_value/4 callback
|
||||
|
||||
## v3.2.0
|
||||
|
||||
* Enhancements
|
||||
* Depend on Phoenix.HTML ~> 2.9
|
||||
|
||||
## v3.1.0
|
||||
|
||||
* Enhancements
|
||||
* Depend on Ecto ~> 2.1 and support new `:naive_datetime` and `:utc_datetime` types
|
||||
|
||||
## v3.0.1
|
||||
|
||||
* Enhancements
|
||||
* Support non-struct data in changeset
|
||||
|
||||
## v3.0.0
|
||||
|
||||
* Enhancements
|
||||
* Add `Phoenix.Ecto.SQL.Sandbox` for concurrent acceptance tests with Phoenix and Ecto based on user-agent
|
||||
* Use the new sandbox based on user-agent
|
||||
* Depend on Phoenix.HTML ~> 2.6
|
||||
* Depend on Ecto ~> 2.0
|
||||
|
||||
* Bug fixes
|
||||
* Do not list errors if changeset has no action
|
||||
|
||||
## v2.0.0
|
||||
|
||||
* Enhancements
|
||||
* Depend on Ecto ~> 1.1
|
||||
|
||||
* Backwards incompatible changes
|
||||
* `f.errors` now returns a raw list of `changeset.errors` for the form's changeset which can be further translated with Phoenix' new Gettext support
|
||||
* No longer implement Poison protocol for `Ecto.Changeset`
|
||||
|
||||
## v1.2.0
|
||||
|
||||
* Enhancements
|
||||
* Depend on Ecto ~> 1.0
|
||||
* Depend on Phoenix.HTML ~> 2.2
|
||||
* Use the new `:as` option for naming inputs fields instead of `:name`
|
||||
|
||||
## v1.1.0
|
||||
|
||||
* Enhancements
|
||||
* Depend on Ecto ~> 0.15
|
||||
* Support `skip_deleted` in inputs_for
|
||||
* Support default values from data rather from `:default` option
|
||||
|
||||
## v1.0.0
|
||||
|
||||
* Enhancements
|
||||
* Depend on Phoenix.HTML ~> 2.1
|
||||
* Depend on Ecto ~> 0.15
|
||||
* Support associations on changesets
|
||||
|
||||
## v0.9.0
|
||||
|
||||
* Enhancements
|
||||
* Depend on Phoenix.HTML ~> 2.0
|
||||
|
||||
## v0.8.1
|
||||
|
||||
* Bug fix
|
||||
* Ensure we can encode decimals and floats from errors messages
|
||||
|
||||
## v0.8.0
|
||||
|
||||
* Enhancements
|
||||
* Depend on Phoenix.HTML ~> 1.4 (includes `input_type` and `input_validation` support)
|
||||
* Include embeds errors during JSON generation
|
||||
|
||||
## v0.7.0
|
||||
|
||||
* Enhancements
|
||||
* Depend on Phoenix.HTML ~> 1.3 (includes `inputs_for` support)
|
||||
|
||||
## v0.6.0
|
||||
|
||||
* Enhancements
|
||||
* Depend on Ecto ~> 0.14
|
||||
|
||||
## v0.5.0
|
||||
|
||||
* Enhancements
|
||||
* Depend on Ecto ~> 0.12
|
||||
|
||||
## v0.4.0
|
||||
|
||||
* Enhancements
|
||||
* Depend on phoenix_html as optional dependency instead of Phoenix
|
||||
* Depend on poison as optional dependency instead of Phoenix
|
||||
|
||||
## v0.3.2
|
||||
|
||||
* Bug fix
|
||||
* Ensure we interpolate `%{count}` in JSON encoding
|
||||
|
||||
## v0.3.1
|
||||
|
||||
* Enhancements
|
||||
* Implement Plug.Exception for Ecto exceptions
|
||||
|
||||
## v0.3.0
|
||||
|
||||
* Enhancements
|
||||
* Support Phoenix v0.11.0 errors entry in form data
|
||||
|
||||
## v0.2.0
|
||||
|
||||
* Enhancements
|
||||
* Implement `Phoenix.HTML.Safe` for `Ecto.Date`, `Ecto.Time` and `Ecto.DateTime`
|
||||
* Implement `Poison.Encoder` for `Ecto.Changeset`, `Decimal`, `Ecto.Date`, `Ecto.Time` and `Ecto.DateTime`
|
||||
|
||||
## v0.1.0
|
||||
|
||||
* Enhancements
|
||||
* Implement `Phoenix.HTML.FormData` for `Ecto.Changeset`
|
||||
* Implement `Phoenix.HTML.Safe` for `Decimal`
|
||||
20
phoenix/deps/phoenix_ecto/LICENSE
Normal file
20
phoenix/deps/phoenix_ecto/LICENSE
Normal file
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2015 Chris McCord
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
130
phoenix/deps/phoenix_ecto/README.md
Normal file
130
phoenix/deps/phoenix_ecto/README.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Phoenix/Ecto
|
||||
|
||||
[](https://github.com/phoenixframework/phoenix_ecto/actions?query=workflow%3ACI)
|
||||
|
||||
A project that integrates [Phoenix](http://github.com/phoenixframework/phoenix) with [Ecto](http://github.com/elixir-lang/ecto), implementing all relevant protocols.
|
||||
|
||||
## Installation
|
||||
|
||||
You can install `phoenix_ecto` by adding it to your list of dependencies in `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[{:phoenix_ecto, "~> 4.0"}]
|
||||
end
|
||||
```
|
||||
|
||||
## The Phoenix <-> Ecto integration
|
||||
|
||||
Thanks to Elixir protocols, the integration between Phoenix and Ecto is simply a matter of implementing a handful of protocols. We provide the following implementations:
|
||||
|
||||
* `Phoenix.HTML.FormData` protocol for `Ecto.Changeset`
|
||||
* `Phoenix.HTML.Safe` protocol for `Decimal`
|
||||
* `Plug.Exception` protocol for the relevant Ecto exceptions
|
||||
|
||||
## Concurrent browser tests
|
||||
|
||||
This library also provides a plug called `Phoenix.Ecto.SQL.Sandbox` that allows developers to run acceptance tests powered by headless browsers such as ChromeDriver and Selenium concurrently. If you are not familiar with Ecto's SQL sandbox, we recommend you to first get acquainted with it by [reading `Ecto.Adapters.SQL.Sandbox` documentation](https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.Sandbox.html).
|
||||
|
||||
To enable concurrent acceptance tests, make sure you are using PostgreSQL and follow the instructions below:
|
||||
|
||||
1. Set a flag to enable the sandbox in `config/test.exs`:
|
||||
|
||||
```elixir
|
||||
config :your_app, sql_sandbox: true
|
||||
```
|
||||
|
||||
2. And use the flag to conditionally add the plug to `lib/your_app/endpoint.ex`:
|
||||
|
||||
```elixir
|
||||
if Application.get_env(:your_app, :sql_sandbox) do
|
||||
plug Phoenix.Ecto.SQL.Sandbox
|
||||
end
|
||||
```
|
||||
|
||||
Make sure that this is placed **before** the line `plug YourApp.Router` (or any other plug that may access the database).
|
||||
|
||||
You can now checkout a sandboxed connection and pass the connection information to an acceptance testing tool like [Hound](https://github.com/hashnuke/hound) or [Wallaby](https://github.com/elixir-wallaby/wallaby).
|
||||
|
||||
### Hound
|
||||
|
||||
To write concurrent acceptance tests with Hound, first add it as a dependency to your `mix.exs`:
|
||||
|
||||
```elixir
|
||||
{:hound, "~> 1.0"}
|
||||
```
|
||||
|
||||
Make sure to start it at the top of your `test/test_helper.exs`:
|
||||
|
||||
```elixir
|
||||
{:ok, _} = Application.ensure_all_started(:hound)
|
||||
```
|
||||
|
||||
Then add the following to your test case (or case template):
|
||||
|
||||
```elixir
|
||||
use Hound.Helpers
|
||||
|
||||
setup tags do
|
||||
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(YourApp.Repo, shared: not tags[:async])
|
||||
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
|
||||
metadata = Phoenix.Ecto.SQL.Sandbox.metadata_for(YourApp.Repo, pid)
|
||||
Hound.start_session(metadata: metadata)
|
||||
:ok
|
||||
end
|
||||
```
|
||||
|
||||
Hound supports multiple drivers like Chrome, Firefox, etc but it does not support concurrent tests under PhantomJS (the default).
|
||||
|
||||
### Wallaby
|
||||
|
||||
To write concurrent acceptance tests with Wallaby, first add it as a dependency to your `mix.exs`:
|
||||
|
||||
```elixir
|
||||
{:wallaby, "~> 0.25", only: :test}
|
||||
```
|
||||
|
||||
Wallaby can take care of setting up the Ecto Sandbox for you if you use `use Wallaby.Feature` in your test module.
|
||||
|
||||
```elixir
|
||||
defmodule MyAppWeb.PageFeature do
|
||||
use ExUnit.Case, async: true
|
||||
use Wallaby.Feature
|
||||
|
||||
feature "shows some text", %{session: session} do
|
||||
session
|
||||
|> visit("/home")
|
||||
|> assert_text("Hello world!")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
If you don't `use Wallaby.Feature`, you can add the following to your test case (or case template):
|
||||
|
||||
```elixir
|
||||
use Wallaby.DSL
|
||||
|
||||
setup tags do
|
||||
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(YourApp.Repo, shared: not tags[:async])
|
||||
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
|
||||
metadata = Phoenix.Ecto.SQL.Sandbox.metadata_for(YourApp.Repo, pid)
|
||||
{:ok, session} = Wallaby.start_session(metadata: metadata)
|
||||
end
|
||||
```
|
||||
|
||||
Wallaby currently supports ChromeDriver and Selenium, allowing testing in almost any browser.
|
||||
|
||||
## Configuration
|
||||
|
||||
The `Plug.Exception` implementations for Ecto exceptions may be disabled by including the error in the mix configuration.
|
||||
|
||||
```elixir
|
||||
config :phoenix_ecto,
|
||||
exclude_ecto_exceptions_from_plug: [Ecto.NoResultsError]
|
||||
```
|
||||
|
||||
## Copyright and License
|
||||
|
||||
Copyright (c) 2015, Chris McCord.
|
||||
|
||||
Phoenix/Ecto source code is licensed under the [MIT License](https://github.com/phoenixframework/phoenix_ecto/blob/master/LICENSE).
|
||||
38
phoenix/deps/phoenix_ecto/hex_metadata.config
Normal file
38
phoenix/deps/phoenix_ecto/hex_metadata.config
Normal file
@@ -0,0 +1,38 @@
|
||||
{<<"links">>,
|
||||
[{<<"GitHub">>,<<"https://github.com/phoenixframework/phoenix_ecto">>}]}.
|
||||
{<<"name">>,<<"phoenix_ecto">>}.
|
||||
{<<"version">>,<<"4.7.0">>}.
|
||||
{<<"description">>,<<"Integration between Phoenix & Ecto">>}.
|
||||
{<<"elixir">>,<<"~> 1.11">>}.
|
||||
{<<"files">>,
|
||||
[<<"lib">>,<<"lib/phoenix_ecto">>,<<"lib/phoenix_ecto/html.ex">>,
|
||||
<<"lib/phoenix_ecto/check_repo_status.ex">>,<<"lib/phoenix_ecto/plug.ex">>,
|
||||
<<"lib/phoenix_ecto/exceptions.ex">>,<<"lib/phoenix_ecto/sql">>,
|
||||
<<"lib/phoenix_ecto/sql/sandbox_session.ex">>,
|
||||
<<"lib/phoenix_ecto/sql/sandbox.ex">>,<<"lib/phoenix_ecto.ex">>,
|
||||
<<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE">>,
|
||||
<<"CHANGELOG.md">>]}.
|
||||
{<<"app">>,<<"phoenix_ecto">>}.
|
||||
{<<"licenses">>,[<<"MIT">>]}.
|
||||
{<<"requirements">>,
|
||||
[[{<<"name">>,<<"phoenix_html">>},
|
||||
{<<"app">>,<<"phoenix_html">>},
|
||||
{<<"optional">>,true},
|
||||
{<<"requirement">>,<<"~> 2.14.2 or ~> 3.0 or ~> 4.1">>},
|
||||
{<<"repository">>,<<"hexpm">>}],
|
||||
[{<<"name">>,<<"ecto">>},
|
||||
{<<"app">>,<<"ecto">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 3.5">>},
|
||||
{<<"repository">>,<<"hexpm">>}],
|
||||
[{<<"name">>,<<"plug">>},
|
||||
{<<"app">>,<<"plug">>},
|
||||
{<<"optional">>,false},
|
||||
{<<"requirement">>,<<"~> 1.9">>},
|
||||
{<<"repository">>,<<"hexpm">>}],
|
||||
[{<<"name">>,<<"postgrex">>},
|
||||
{<<"app">>,<<"postgrex">>},
|
||||
{<<"optional">>,true},
|
||||
{<<"requirement">>,<<"~> 0.16 or ~> 1.0">>},
|
||||
{<<"repository">>,<<"hexpm">>}]]}.
|
||||
{<<"build_tools">>,[<<"mix">>]}.
|
||||
17
phoenix/deps/phoenix_ecto/lib/phoenix_ecto.ex
Normal file
17
phoenix/deps/phoenix_ecto/lib/phoenix_ecto.ex
Normal file
@@ -0,0 +1,17 @@
|
||||
defmodule Phoenix.Ecto do
|
||||
@moduledoc """
|
||||
Integrates Phoenix with Ecto.
|
||||
|
||||
It implements many protocols that make it easier to use
|
||||
Ecto with Phoenix either when working with HTML or JSON.
|
||||
"""
|
||||
use Application
|
||||
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
{DynamicSupervisor, name: Phoenix.Ecto.SQL.SandboxSupervisor, strategy: :one_for_one}
|
||||
]
|
||||
|
||||
Supervisor.start_link(children, strategy: :one_for_one, name: Phoenix.Ecto.Supervisor)
|
||||
end
|
||||
end
|
||||
107
phoenix/deps/phoenix_ecto/lib/phoenix_ecto/check_repo_status.ex
Normal file
107
phoenix/deps/phoenix_ecto/lib/phoenix_ecto/check_repo_status.ex
Normal file
@@ -0,0 +1,107 @@
|
||||
defmodule Phoenix.Ecto.CheckRepoStatus do
|
||||
@moduledoc """
|
||||
A plug that does some checks on your application repos.
|
||||
|
||||
Checks if the storage is up (database is created) or if there are any pending migrations.
|
||||
Both checks can raise an error if the conditions are not met.
|
||||
|
||||
## Plug options
|
||||
|
||||
* `:otp_app` - name of the application which the repos are fetched from
|
||||
* `:migration_paths` - a function that accepts a repo and returns a migration directory, or a list of migration directories, that is used to check for pending migrations
|
||||
* `:migration_lock` - the locking strategy used by the Ecto Adapter when checking for pending migrations. Defaults to `false`. Set to `true` to enable migration locks.
|
||||
* `:prefix` - the prefix used to check for pending migrations.
|
||||
* `:skip_table_creation` - Ecto will not try to create the `schema_migrations` table automatically. This is useful if you are connecting as a DB user without create permissions
|
||||
"""
|
||||
|
||||
@behaviour Plug
|
||||
|
||||
alias Plug.Conn
|
||||
|
||||
@migration_opts [:migration_lock, :prefix, :skip_table_creation]
|
||||
@compile {:no_warn_undefined, Ecto.Migrator}
|
||||
|
||||
def init(opts) do
|
||||
Keyword.fetch!(opts, :otp_app)
|
||||
opts
|
||||
end
|
||||
|
||||
def call(%Conn{} = conn, opts) do
|
||||
try do
|
||||
repos = Application.get_env(opts[:otp_app], :ecto_repos, [])
|
||||
|
||||
for repo <- repos, Process.whereis(repo) do
|
||||
check_pending_migrations!(repo, opts) || check_storage_up!(repo)
|
||||
end
|
||||
|
||||
conn
|
||||
rescue
|
||||
err in [Phoenix.Ecto.StorageNotCreatedError, Phoenix.Ecto.PendingMigrationError] ->
|
||||
Plug.Conn.WrapperError.reraise(conn, :error, err, __STACKTRACE__)
|
||||
end
|
||||
end
|
||||
|
||||
defp check_storage_up!(repo) do
|
||||
try do
|
||||
adapter = repo.__adapter__()
|
||||
|
||||
if Code.ensure_loaded?(adapter) && function_exported?(adapter, :storage_status, 1) do
|
||||
adapter.storage_status(repo.config())
|
||||
end
|
||||
rescue
|
||||
_ -> true
|
||||
else
|
||||
:down -> raise Phoenix.Ecto.StorageNotCreatedError, repo: repo
|
||||
_ -> true
|
||||
end
|
||||
end
|
||||
|
||||
defp check_pending_migrations!(repo, opts) do
|
||||
dirs = migration_directories(repo, opts)
|
||||
|
||||
migrations_fun =
|
||||
Keyword.get_lazy(opts, :mock_migrations_fn, fn ->
|
||||
if Code.ensure_loaded?(Ecto.Migrator),
|
||||
do: &Ecto.Migrator.migrations/3,
|
||||
else: fn _repo, _paths, _opts -> raise "to be rescued" end
|
||||
end)
|
||||
|
||||
true = is_function(migrations_fun, 3)
|
||||
|
||||
migration_opts =
|
||||
opts
|
||||
|> Keyword.take(@migration_opts)
|
||||
|> Keyword.put_new(:migration_lock, false)
|
||||
|
||||
try do
|
||||
repo
|
||||
|> migrations_fun.(dirs, migration_opts)
|
||||
|> Enum.any?(fn {status, _version, _migration} -> status == :down end)
|
||||
rescue
|
||||
_ -> false
|
||||
else
|
||||
true ->
|
||||
raise Phoenix.Ecto.PendingMigrationError,
|
||||
repo: repo,
|
||||
directories: dirs,
|
||||
migration_opts: migration_opts
|
||||
|
||||
false ->
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
defp migration_directories(repo, opts) do
|
||||
case Keyword.fetch(opts, :migration_paths) do
|
||||
{:ok, migration_directories_fn} ->
|
||||
List.wrap(migration_directories_fn.(repo))
|
||||
|
||||
:error ->
|
||||
try do
|
||||
[Ecto.Migrator.migrations_path(repo)]
|
||||
rescue
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
18
phoenix/deps/phoenix_ecto/lib/phoenix_ecto/exceptions.ex
Normal file
18
phoenix/deps/phoenix_ecto/lib/phoenix_ecto/exceptions.ex
Normal file
@@ -0,0 +1,18 @@
|
||||
defmodule Phoenix.Ecto.StorageNotCreatedError do
|
||||
defexception [:repo]
|
||||
|
||||
def message(%__MODULE__{repo: repo}) do
|
||||
"the storage is not created for repo: #{inspect(repo)}. " <>
|
||||
"Try running `mix ecto.create` in the command line to create it"
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Phoenix.Ecto.PendingMigrationError do
|
||||
@enforce_keys [:repo, :directories, :migration_opts]
|
||||
defexception [:repo, :directories, :migration_opts]
|
||||
|
||||
def message(%__MODULE__{repo: repo}) do
|
||||
"there are pending migrations for repo: #{inspect(repo)}. " <>
|
||||
"Try running `mix ecto.migrate` in the command line to migrate it"
|
||||
end
|
||||
end
|
||||
345
phoenix/deps/phoenix_ecto/lib/phoenix_ecto/html.ex
Normal file
345
phoenix/deps/phoenix_ecto/lib/phoenix_ecto/html.ex
Normal file
@@ -0,0 +1,345 @@
|
||||
if Code.ensure_loaded?(Phoenix.HTML) do
|
||||
defimpl Phoenix.HTML.FormData, for: Ecto.Changeset do
|
||||
def to_form(changeset, opts) do
|
||||
%{params: params, data: data, action: action} = changeset
|
||||
{action, opts} = Keyword.pop(opts, :action, action)
|
||||
{name, opts} = Keyword.pop(opts, :as)
|
||||
|
||||
name = to_string(name || form_for_name(data))
|
||||
id = Keyword.get(opts, :id) || name
|
||||
|
||||
%Phoenix.HTML.Form{
|
||||
source: changeset,
|
||||
impl: __MODULE__,
|
||||
id: id,
|
||||
action: action,
|
||||
name: name,
|
||||
errors: form_for_errors(changeset, action),
|
||||
data: data,
|
||||
params: params || %{},
|
||||
hidden: form_for_hidden(data),
|
||||
options: Keyword.put_new(opts, :method, form_for_method(data))
|
||||
}
|
||||
end
|
||||
|
||||
def to_form(source, %{action: parent_action} = form, field, opts) do
|
||||
if Keyword.has_key?(opts, :default) do
|
||||
raise ArgumentError,
|
||||
":default is not supported on inputs_for with changesets. " <>
|
||||
"The default value must be set in the changeset data"
|
||||
end
|
||||
|
||||
{prepend, opts} = Keyword.pop(opts, :prepend, [])
|
||||
{append, opts} = Keyword.pop(opts, :append, [])
|
||||
{name, opts} = Keyword.pop(opts, :as)
|
||||
{id, opts} = Keyword.pop(opts, :id)
|
||||
|
||||
id = to_string(id || form.id <> "_#{field}")
|
||||
name = to_string(name || form.name <> "[#{field}]")
|
||||
|
||||
case find_inputs_for_type!(source, field) do
|
||||
{:one, cast, module} ->
|
||||
changesets =
|
||||
case Map.fetch(source.changes, field) do
|
||||
{:ok, nil} ->
|
||||
[]
|
||||
|
||||
{:ok, map} ->
|
||||
[validate_map!(map, field)]
|
||||
|
||||
_ ->
|
||||
[validate_map!(assoc_from_data(source.data, field), field) || module.__struct__()]
|
||||
end
|
||||
|
||||
for changeset <- skip_replaced(changesets) do
|
||||
%{data: data, params: params} =
|
||||
changeset = to_changeset(changeset, parent_action, module, cast, nil)
|
||||
|
||||
%Phoenix.HTML.Form{
|
||||
source: changeset,
|
||||
action: parent_action,
|
||||
impl: __MODULE__,
|
||||
id: id,
|
||||
name: name,
|
||||
errors: form_for_errors(changeset, parent_action),
|
||||
data: data,
|
||||
params: params || %{},
|
||||
hidden: form_for_hidden(data),
|
||||
options: opts
|
||||
}
|
||||
end
|
||||
|
||||
{:many, cast, module} ->
|
||||
changesets =
|
||||
validate_list!(Map.get(source.changes, field), field) ||
|
||||
validate_list!(assoc_from_data(source.data, field), field) || []
|
||||
|
||||
changesets =
|
||||
if form.params[Atom.to_string(field)] do
|
||||
changesets
|
||||
else
|
||||
prepend ++ changesets ++ append
|
||||
end
|
||||
|
||||
changesets = skip_replaced(changesets)
|
||||
|
||||
for {changeset, index} <- Enum.with_index(changesets) do
|
||||
%{data: data, params: params} =
|
||||
changeset = to_changeset(changeset, parent_action, module, cast, index)
|
||||
|
||||
index_string = Integer.to_string(index)
|
||||
|
||||
%Phoenix.HTML.Form{
|
||||
source: changeset,
|
||||
impl: __MODULE__,
|
||||
action: parent_action,
|
||||
id: id <> "_" <> index_string,
|
||||
name: name <> "[" <> index_string <> "]",
|
||||
index: index,
|
||||
errors: form_for_errors(changeset, parent_action),
|
||||
data: data,
|
||||
params: params || %{},
|
||||
hidden: form_for_hidden(data),
|
||||
options: opts
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def input_value(%{changes: changes, data: data}, %{params: params}, field)
|
||||
when is_atom(field) do
|
||||
case changes do
|
||||
%{^field => value} ->
|
||||
value
|
||||
|
||||
%{} ->
|
||||
string = Atom.to_string(field)
|
||||
|
||||
case params do
|
||||
%{^string => value} -> value
|
||||
%{} -> Map.get(data, field)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def input_value(_data, _form, field) do
|
||||
raise ArgumentError, "expected field to be an atom, got: #{inspect(field)}"
|
||||
end
|
||||
|
||||
def input_type(%{types: types}, _, field) do
|
||||
type = Map.get(types, field, :string)
|
||||
type = if Ecto.Type.primitive?(type), do: type, else: type.type()
|
||||
|
||||
case type do
|
||||
:integer -> :number_input
|
||||
:boolean -> :checkbox
|
||||
:date -> :date_select
|
||||
:time -> :time_select
|
||||
:utc_datetime -> :datetime_select
|
||||
:naive_datetime -> :datetime_select
|
||||
_ -> :text_input
|
||||
end
|
||||
end
|
||||
|
||||
def input_validations(%{required: required, validations: validations} = changeset, _, field) do
|
||||
[required: field in required] ++
|
||||
for {key, validation} <- validations,
|
||||
key == field,
|
||||
attr <- validation_to_attrs(validation, field, changeset),
|
||||
do: attr
|
||||
end
|
||||
|
||||
defp assoc_from_data(data, field) do
|
||||
assoc_from_data(data, Map.fetch!(data, field), field)
|
||||
end
|
||||
|
||||
defp assoc_from_data(%{__meta__: %{state: :built}}, %Ecto.Association.NotLoaded{}, _field) do
|
||||
nil
|
||||
end
|
||||
|
||||
defp assoc_from_data(%{__struct__: struct}, %Ecto.Association.NotLoaded{}, field) do
|
||||
raise ArgumentError,
|
||||
"using inputs_for for association `#{field}` " <>
|
||||
"from `#{inspect(struct)}` but it was not loaded. Please preload your " <>
|
||||
"associations before using them in inputs_for"
|
||||
end
|
||||
|
||||
defp assoc_from_data(_data, value, _field) do
|
||||
value
|
||||
end
|
||||
|
||||
defp skip_replaced(changesets) do
|
||||
Enum.reject(changesets, fn
|
||||
%Ecto.Changeset{action: :replace} -> true
|
||||
_ -> false
|
||||
end)
|
||||
end
|
||||
|
||||
defp validation_to_attrs({:length, opts}, _field, _changeset) do
|
||||
max =
|
||||
if val = Keyword.get(opts, :max) do
|
||||
[maxlength: val]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
min =
|
||||
if val = Keyword.get(opts, :min) do
|
||||
[minlength: val]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
max ++ min
|
||||
end
|
||||
|
||||
defp validation_to_attrs({:number, opts}, field, changeset) do
|
||||
type = Map.get(changeset.types, field, :integer)
|
||||
step_for(type) ++ min_for(type, opts) ++ max_for(type, opts)
|
||||
end
|
||||
|
||||
defp validation_to_attrs(_validation, _field, _changeset) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp step_for(:integer), do: [step: 1]
|
||||
defp step_for(_other), do: [step: "any"]
|
||||
|
||||
defp max_for(type, opts) do
|
||||
cond do
|
||||
max = type == :integer && Keyword.get(opts, :less_than) -> [max: max - 1]
|
||||
max = Keyword.get(opts, :less_than_or_equal_to) -> [max: max]
|
||||
true -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp min_for(type, opts) do
|
||||
cond do
|
||||
min = type == :integer && Keyword.get(opts, :greater_than) -> [min: min + 1]
|
||||
min = Keyword.get(opts, :greater_than_or_equal_to) -> [min: min]
|
||||
true -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp find_inputs_for_type!(changeset, field) do
|
||||
case Map.fetch(changeset.types, field) do
|
||||
{:ok, {tag, %{cardinality: cardinality, on_cast: cast, related: module}}}
|
||||
when tag in [:embed, :assoc] ->
|
||||
{cardinality, cast, module}
|
||||
|
||||
_ ->
|
||||
struct = changeset.data.__struct__
|
||||
|
||||
raise ArgumentError,
|
||||
"could not generate inputs for #{inspect(field)} from #{inspect(struct)}. " <>
|
||||
"Check the field exists and it is one of embeds_one, embeds_many, has_one, " <>
|
||||
"has_many, belongs_to or many_to_many"
|
||||
end
|
||||
end
|
||||
|
||||
defp to_changeset(%Ecto.Changeset{} = changeset, parent_action, _module, _cast, _index),
|
||||
do: apply_action(changeset, parent_action)
|
||||
|
||||
defp to_changeset(%{} = data, parent_action, _module, cast, _index) when is_function(cast, 2),
|
||||
do: apply_action(cast!(cast, data), parent_action)
|
||||
|
||||
defp to_changeset(%{} = data, parent_action, _module, cast, index) when is_function(cast, 3),
|
||||
do: apply_action(cast!(cast, data, index), parent_action)
|
||||
|
||||
defp to_changeset(%{} = data, parent_action, _module, {module, func, arguments} = mfa, _index)
|
||||
when is_atom(module) and is_atom(func) and is_list(arguments),
|
||||
do: apply_action(apply!(mfa, data), parent_action)
|
||||
|
||||
defp to_changeset(%{} = data, parent_action, _module, nil, _index),
|
||||
do: apply_action(Ecto.Changeset.change(data), parent_action)
|
||||
|
||||
defp cast!(cast, data) do
|
||||
case cast.(data, %{}) do
|
||||
%Ecto.Changeset{} = changeset ->
|
||||
changeset
|
||||
|
||||
other ->
|
||||
raise "expected on_cast/2 callback #{inspect(cast)} to return an Ecto.Changeset, " <>
|
||||
"got: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp cast!(cast, data, index) do
|
||||
case cast.(data, %{}, index) do
|
||||
%Ecto.Changeset{} = changeset ->
|
||||
changeset
|
||||
|
||||
other ->
|
||||
raise "expected on_cast/3 callback #{inspect(cast)} to return an Ecto.Changeset, " <>
|
||||
"got: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp apply!({module, func, arguments}, data) do
|
||||
case apply(module, func, [data, %{} | arguments]) do
|
||||
%Ecto.Changeset{} = changeset ->
|
||||
changeset
|
||||
|
||||
other ->
|
||||
raise "expected #{module}.#{func} to return an Ecto.Changeset, " <>
|
||||
"got: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
|
||||
# If the parent changeset had no action, we need to remove the action
|
||||
# from children changeset so we ignore all errors accordingly.
|
||||
defp apply_action(changeset, nil),
|
||||
do: %{changeset | action: nil}
|
||||
|
||||
defp apply_action(changeset, _action),
|
||||
do: changeset
|
||||
|
||||
defp validate_list!(value, _what) when is_list(value) or is_nil(value), do: value
|
||||
|
||||
defp validate_list!(value, what) do
|
||||
raise ArgumentError, "expected #{what} to be a list, got: #{inspect(value)}"
|
||||
end
|
||||
|
||||
defp validate_map!(value, _what) when is_map(value) or is_nil(value), do: value
|
||||
|
||||
defp validate_map!(value, what) do
|
||||
raise ArgumentError, "expected #{what} to be a map/struct, got: #{inspect(value)}"
|
||||
end
|
||||
|
||||
defp form_for_errors(_changeset, nil = _action), do: []
|
||||
defp form_for_errors(_changeset, :ignore = _action), do: []
|
||||
defp form_for_errors(%Ecto.Changeset{errors: errors}, _action), do: errors
|
||||
|
||||
defp form_for_hidden(%{__struct__: module} = data) do
|
||||
module.__schema__(:primary_key)
|
||||
rescue
|
||||
_ -> []
|
||||
else
|
||||
keys -> for k <- keys, v = Map.fetch!(data, k), do: {k, v}
|
||||
end
|
||||
|
||||
defp form_for_hidden(_), do: []
|
||||
|
||||
defp form_for_name(%{__struct__: module}) do
|
||||
module
|
||||
|> Module.split()
|
||||
|> List.last()
|
||||
|> Macro.underscore()
|
||||
end
|
||||
|
||||
defp form_for_name(_) do
|
||||
raise ArgumentError,
|
||||
"cannot generate name for changeset where the data is not backed by a struct. " <>
|
||||
"You must either pass the :as option to form/form_for or use a struct-based changeset"
|
||||
end
|
||||
|
||||
defp form_for_method(%{__meta__: %{state: :loaded}}), do: "put"
|
||||
defp form_for_method(_), do: "post"
|
||||
end
|
||||
|
||||
defimpl Phoenix.HTML.Safe, for: Decimal do
|
||||
def to_iodata(t) do
|
||||
@for.to_string(t, :normal)
|
||||
end
|
||||
end
|
||||
end
|
||||
72
phoenix/deps/phoenix_ecto/lib/phoenix_ecto/plug.ex
Normal file
72
phoenix/deps/phoenix_ecto/lib/phoenix_ecto/plug.ex
Normal file
@@ -0,0 +1,72 @@
|
||||
errors = [
|
||||
{Ecto.CastError, 400},
|
||||
{Ecto.Query.CastError, 400},
|
||||
{Ecto.NoResultsError, 404},
|
||||
{Ecto.StaleEntryError, 409}
|
||||
]
|
||||
|
||||
excluded_exceptions = Application.get_env(:phoenix_ecto, :exclude_ecto_exceptions_from_plug, [])
|
||||
|
||||
for {exception, status_code} <- errors do
|
||||
unless exception in excluded_exceptions do
|
||||
defimpl Plug.Exception, for: exception do
|
||||
def status(_), do: unquote(status_code)
|
||||
def actions(_), do: []
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
unless Ecto.SubQueryError in excluded_exceptions do
|
||||
defimpl Plug.Exception, for: Ecto.SubQueryError do
|
||||
def status(sub_query_error) do
|
||||
Plug.Exception.status(sub_query_error.exception)
|
||||
end
|
||||
|
||||
def actions(_), do: []
|
||||
end
|
||||
end
|
||||
|
||||
unless Phoenix.Ecto.PendingMigrationError in excluded_exceptions do
|
||||
defimpl Plug.Exception, for: Phoenix.Ecto.PendingMigrationError do
|
||||
def status(_error), do: 503
|
||||
|
||||
def actions(%{repo: repo, directories: directories, migration_opts: migration_opts}),
|
||||
do: [
|
||||
%{
|
||||
label: "Run migrations for repo",
|
||||
handler: {__MODULE__, :migrate, [repo, directories, migration_opts]}
|
||||
}
|
||||
]
|
||||
|
||||
def migrate(repo, directories, migration_opts) do
|
||||
Ecto.Migrator.run(repo, directories, :up, Keyword.merge(migration_opts || [], all: true))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
unless Phoenix.Ecto.StorageNotCreatedError in excluded_exceptions do
|
||||
defimpl Plug.Exception, for: Phoenix.Ecto.StorageNotCreatedError do
|
||||
def status(_error), do: 503
|
||||
|
||||
def actions(%{repo: repo}),
|
||||
do: [
|
||||
%{
|
||||
label: "Create database for repo",
|
||||
handler: {__MODULE__, :storage_up, [repo]}
|
||||
}
|
||||
]
|
||||
|
||||
def storage_up(repo), do: repo.__adapter__().storage_up(repo.config())
|
||||
end
|
||||
end
|
||||
|
||||
if Code.ensure_loaded?(Postgrex.Error) do
|
||||
unless Postgrex.Error in excluded_exceptions do
|
||||
defimpl Plug.Exception, for: Postgrex.Error do
|
||||
def status(%{postgres: %{code: :character_not_in_repertoire}}), do: 400
|
||||
def status(_), do: 500
|
||||
|
||||
def actions(_), do: []
|
||||
end
|
||||
end
|
||||
end
|
||||
353
phoenix/deps/phoenix_ecto/lib/phoenix_ecto/sql/sandbox.ex
Normal file
353
phoenix/deps/phoenix_ecto/lib/phoenix_ecto/sql/sandbox.ex
Normal file
@@ -0,0 +1,353 @@
|
||||
defmodule Phoenix.Ecto.SQL.Sandbox do
|
||||
@moduledoc """
|
||||
A plug to allow concurrent, transactional acceptance tests with
|
||||
[`Ecto.Adapters.SQL.Sandbox`](https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.Sandbox.html).
|
||||
|
||||
## Example
|
||||
|
||||
This plug should only be used during tests. First, set a flag to
|
||||
enable it in `config/test.exs`:
|
||||
|
||||
config :your_app, sql_sandbox: true
|
||||
|
||||
And use the flag to conditionally add the plug to `lib/your_app/endpoint.ex`:
|
||||
|
||||
if Application.compile_env(:your_app, :sql_sandbox) do
|
||||
plug Phoenix.Ecto.SQL.Sandbox
|
||||
end
|
||||
|
||||
It's important that this is at the top of `endpoint.ex`, before any other plugs.
|
||||
|
||||
Then, within an acceptance test, checkout a sandboxed connection as before.
|
||||
Use `metadata_for/2` helper to get the session metadata to that will allow access
|
||||
to the test's connection. In general lines, you would write this:
|
||||
|
||||
setup tags do
|
||||
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(YourApp.Repo, shared: not tags[:async])
|
||||
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
|
||||
metadata_header = Phoenix.Ecto.SQL.Sandbox.metadata_for(YourApp.Repo, pid)
|
||||
# pass the metadata to the acceptance test library
|
||||
:ok
|
||||
end
|
||||
|
||||
You can follow the instructions for Wallaby [here](https://hexdocs.pm/wallaby/readme.html#phoenix).
|
||||
|
||||
## Acceptance tests with channels
|
||||
|
||||
To support channels, in addition the above, you need to make it so each channel
|
||||
is allowed within the sandbox. The first step is to access the relevant header
|
||||
metadata.
|
||||
|
||||
To do so, you must declare that you want to pass connection information to your
|
||||
socket. This is typically the user agent header, but it can be a custom x-header
|
||||
too:
|
||||
|
||||
socket "/path", Socket,
|
||||
websocket: [connect_info: [:user_agent, …]]
|
||||
|
||||
socket "/path", Socket,
|
||||
websocket: [connect_info: [:x_headers, …]]
|
||||
|
||||
Now use the `c:Phoenix.Socket.connect/3` callback to access the header and
|
||||
store it in the socket:
|
||||
|
||||
# user_socket.ex
|
||||
def connect(_params, socket, connect_info) do
|
||||
{:ok, assign(socket, :phoenix_ecto_sandbox, connect_info.user_agent)}
|
||||
end
|
||||
|
||||
Or if you are using a custom header:
|
||||
|
||||
Enum.find_value(connect_info.x_headers, fn
|
||||
{"x-my-custom-header", val} -> val
|
||||
_ -> nil
|
||||
end)
|
||||
|
||||
This stores the value on the socket, so it can be available to all of your
|
||||
channels for allowing the sandbox.
|
||||
|
||||
# room_channel.ex
|
||||
def join("room:lobby", _payload, socket) do
|
||||
allow_ecto_sandbox(socket)
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
# This is a great function to extract to a helper module
|
||||
defp allow_ecto_sandbox(socket) do
|
||||
Phoenix.Ecto.SQL.Sandbox.allow(
|
||||
socket.assigns.phoenix_ecto_sandbox,
|
||||
Ecto.Adapters.SQL.Sandbox
|
||||
)
|
||||
end
|
||||
|
||||
`allow/2` needs to be manually called once for each channel, at best directly
|
||||
at the start of `c:Phoenix.Channel.join/3`.
|
||||
|
||||
## Acceptance tests with LiveViews
|
||||
|
||||
LiveViews can be supported in a similar fashion to channels. First declare the
|
||||
`:user_agent` (or a custom header) in your live socket configuration in `endpoint.ex`:
|
||||
|
||||
socket "/live", Phoenix.LiveView.Socket,
|
||||
websocket: [connect_info: [:user_agent, session: @session_options]]
|
||||
|
||||
Now you can use the `on_mount/4` callback to check the header and assign the sandbox:
|
||||
|
||||
defmodule MyApp.LiveAcceptance do
|
||||
import Phoenix.LiveView
|
||||
import Phoenix.Component
|
||||
|
||||
def on_mount(:default, _params, _session, socket) do
|
||||
socket =
|
||||
assign_new(socket, :phoenix_ecto_sandbox, fn ->
|
||||
if connected?(socket), do: get_connect_info(socket, :user_agent)
|
||||
end)
|
||||
|
||||
metadata = socket.assigns.phoenix_ecto_sandbox
|
||||
Phoenix.Ecto.SQL.Sandbox.allow(metadata, Ecto.Adapters.SQL.Sandbox)
|
||||
{:cont, socket}
|
||||
end
|
||||
end
|
||||
|
||||
Now, in your `my_app_web.ex` file, you can invoke this callback for all of your
|
||||
LiveViews if the sandbox configuration, defined at the beginning of the
|
||||
documentation, is enabled:
|
||||
|
||||
def live_view do
|
||||
quote do
|
||||
use Phoenix.LiveView
|
||||
# ...
|
||||
|
||||
if Application.compile_env(:your_app, :sql_sandbox) do
|
||||
on_mount MyApp.LiveAcceptance
|
||||
end
|
||||
|
||||
# ...
|
||||
end
|
||||
end
|
||||
|
||||
If you have `on_mount` hooks in `live_session` defined in your `router.ex`
|
||||
(for example, routes requiring authentication after running `mix phx.gen.auth`
|
||||
to generate your authentication system), make sure the `MyApp.LiveAcceptance`
|
||||
hook runs before, so following hooks have access to the Ecto Sandbox:
|
||||
|
||||
live_session :require_authenticated_user,
|
||||
on_mount:
|
||||
if(Application.compile_env(:your_app, :sql_sandbox),
|
||||
do: [MyAppWeb.AcceptanceHook],
|
||||
else: []
|
||||
) ++ [{MyAppWeb.UserAuth, :ensure_authenticated}] do
|
||||
live "/users/settings", UserSettingsLive, :edit
|
||||
live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
|
||||
end
|
||||
|
||||
## Concurrent end-to-end tests with external clients
|
||||
|
||||
Concurrent and transactional tests for external HTTP clients is supported,
|
||||
allowing for complete end-to-end tests. This is useful for cases such as
|
||||
JavaScript test suites for single page applications that exercise the
|
||||
Phoenix endpoint for end-to-end test setup and teardown. To enable this,
|
||||
you can expose a sandbox route on the `Phoenix.Ecto.SQL.Sandbox` plug by
|
||||
providing the `:at`, and `:repo` options. For example:
|
||||
|
||||
plug Phoenix.Ecto.SQL.Sandbox,
|
||||
at: "/sandbox",
|
||||
repo: MyApp.Repo,
|
||||
timeout: 15_000 # the default
|
||||
|
||||
This would expose a route at `"/sandbox"` for the given repo where
|
||||
external clients send POST requests to spawn a new sandbox session,
|
||||
and DELETE requests to stop an active sandbox session. By default,
|
||||
the external client is expected to pass up the `"user-agent"` header
|
||||
containing serialized sandbox metadata returned from the POST request,
|
||||
but this value may customized with the `:header` option.
|
||||
|
||||
Finally, make sure your repository mode is set either to `:manual`
|
||||
or `{:shared, self()}` before the external client starts. This is
|
||||
typically done by default in your `test/test_helper.exs`, but you
|
||||
may need to do it explicitly depending on your setup:
|
||||
|
||||
Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, :manual)
|
||||
|
||||
"""
|
||||
|
||||
import Plug.Conn
|
||||
alias Plug.Conn
|
||||
alias Phoenix.Ecto.SQL.{SandboxSession, SandboxSupervisor}
|
||||
|
||||
@doc """
|
||||
Spawns a sandbox session to checkout a connection for a remote client.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, _owner_pid, metadata} = start_child(MyApp.Repo)
|
||||
"""
|
||||
def start_child(repos, opts \\ []) do
|
||||
child_spec = {SandboxSession, {repos, self(), opts}}
|
||||
|
||||
case DynamicSupervisor.start_child(SandboxSupervisor, child_spec) do
|
||||
{:ok, owner} ->
|
||||
metadata = metadata_for(repos, owner)
|
||||
{:ok, owner, metadata}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stops a sandbox session holding a connection for a remote client.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:ok, owner_pid, metadata} = start_child(MyApp.Repo)
|
||||
iex> :ok = stop(owner_pid)
|
||||
"""
|
||||
def stop(owner) when is_pid(owner) do
|
||||
GenServer.call(owner, :checkin)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def init(opts \\ []) do
|
||||
session_opts = Keyword.take(opts, [:sandbox, :timeout])
|
||||
|
||||
%{
|
||||
header: Keyword.get(opts, :header, "user-agent"),
|
||||
path: get_path_info(opts[:at]),
|
||||
repos: List.wrap(opts[:repo]),
|
||||
sandbox:
|
||||
session_opts[:sandbox] || {Ecto.Adapters.SQL.Sandbox, :allow, [[unallow_existing: true]]},
|
||||
session_opts: session_opts
|
||||
}
|
||||
end
|
||||
|
||||
defp get_path_info(nil), do: nil
|
||||
defp get_path_info(path), do: Plug.Router.Utils.split(path)
|
||||
|
||||
@doc false
|
||||
def call(%Conn{method: "POST", path_info: path} = conn, %{path: path} = opts) do
|
||||
%{repos: repos, session_opts: session_opts} = opts
|
||||
{:ok, _owner, metadata} = start_child(repos, session_opts)
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("text/plain")
|
||||
|> send_resp(200, encode_metadata(metadata))
|
||||
|> halt()
|
||||
end
|
||||
|
||||
def call(%Conn{method: "DELETE", path_info: path} = conn, %{path: path} = opts) do
|
||||
case decode_metadata(extract_header(conn, opts.header)) do
|
||||
%{owner: owner} ->
|
||||
:ok = stop(owner)
|
||||
|
||||
conn
|
||||
|> put_resp_content_type("text/plain")
|
||||
|> send_resp(200, "")
|
||||
|> halt()
|
||||
|
||||
%{} ->
|
||||
conn
|
||||
|> send_resp(410, "")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
def call(conn, %{header: header, sandbox: sandbox}) do
|
||||
header = extract_header(conn, header)
|
||||
allow(header, sandbox)
|
||||
assign(conn, :phoenix_ecto_sandbox, header)
|
||||
end
|
||||
|
||||
defp extract_header(%Conn{} = conn, header) do
|
||||
conn |> get_req_header(header) |> List.first()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns metadata to establish a sandbox for.
|
||||
|
||||
The metadata is then passed via user-agent/headers to browsers.
|
||||
Upon request, the `Phoenix.Ecto.SQL.Sandbox` plug will decode
|
||||
the header and allow the request process under the sandbox.
|
||||
|
||||
## Options
|
||||
|
||||
* `:trap_exit` - if the browser being used for integration
|
||||
testing navigates away from a page or aborts a AJAX request
|
||||
while the request process is talking to the database, it
|
||||
will corrupt the database connection and make the test fail.
|
||||
Therefore, to avoid intermittent tests, we recommend trapping
|
||||
exits in the request process, so all database connections shut
|
||||
down cleanly. You can disable this behaviour by setting the
|
||||
option to false.
|
||||
|
||||
"""
|
||||
@spec metadata_for(Ecto.Repo.t() | [Ecto.Repo.t()], pid, keyword) :: map
|
||||
def metadata_for(repo_or_repos, pid, opts \\ []) when is_pid(pid) do
|
||||
%{repo: repo_or_repos, owner: pid, trap_exit: Keyword.get(opts, :trap_exit, true)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Encodes metadata generated by `metadata_for/2` for client response.
|
||||
"""
|
||||
def encode_metadata(metadata) do
|
||||
encoded =
|
||||
{:v1, metadata}
|
||||
|> :erlang.term_to_binary()
|
||||
|> Base.url_encode64()
|
||||
|
||||
"BeamMetadata (#{encoded})"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes encoded metadata back into map generated from `metadata_for/2`.
|
||||
"""
|
||||
def decode_metadata(encoded_meta) when is_binary(encoded_meta) do
|
||||
case encoded_meta |> String.split("/") |> List.last() do
|
||||
"BeamMetadata (" <> metadata ->
|
||||
metadata
|
||||
|> binary_part(0, byte_size(metadata) - 1)
|
||||
|> parse_metadata()
|
||||
|
||||
_ ->
|
||||
%{}
|
||||
end
|
||||
end
|
||||
|
||||
def decode_metadata(_), do: %{}
|
||||
|
||||
defp parse_metadata(encoded_metadata) do
|
||||
encoded_metadata
|
||||
|> Base.url_decode64!()
|
||||
|> :erlang.binary_to_term()
|
||||
|> case do
|
||||
{:v1, metadata} -> metadata
|
||||
_ -> %{}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decodes the given metadata and allows the current process
|
||||
under the given sandbox.
|
||||
"""
|
||||
def allow(encoded_metadata, sandbox) when is_binary(encoded_metadata) do
|
||||
metadata = decode_metadata(encoded_metadata)
|
||||
|
||||
with %{trap_exit: true} <- metadata do
|
||||
Process.flag(:trap_exit, true)
|
||||
end
|
||||
|
||||
allow(metadata, sandbox)
|
||||
end
|
||||
|
||||
def allow(%{repo: repo, owner: owner}, sandbox),
|
||||
do: Enum.each(List.wrap(repo), &allow_sandbox(sandbox, &1, owner, self()))
|
||||
|
||||
def allow(%{}, _sandbox), do: :ok
|
||||
def allow(nil, _sandbox), do: :ok
|
||||
|
||||
defp allow_sandbox({m, f, args}, repo, owner, pid),
|
||||
do: apply(m, f, [repo, owner, pid | args])
|
||||
|
||||
defp allow_sandbox(sandbox, repo, owner, pid) when is_atom(sandbox),
|
||||
do: sandbox.allow(repo, owner, pid)
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
defmodule Phoenix.Ecto.SQL.SandboxSession do
|
||||
@moduledoc false
|
||||
use GenServer, restart: :temporary
|
||||
|
||||
@timeout 15_000
|
||||
|
||||
def start_link({repos, client, opts}) do
|
||||
GenServer.start_link(__MODULE__, [repos, client, opts])
|
||||
end
|
||||
|
||||
def init([repos, client, opts]) do
|
||||
timeout = opts[:timeout] || @timeout
|
||||
sandbox = opts[:sandbox] || Ecto.Adapters.SQL.Sandbox
|
||||
|
||||
:ok = checkout_connection(sandbox, repos, client)
|
||||
Process.send_after(self(), :timeout, timeout)
|
||||
|
||||
{:ok, %{repos: repos, client: client, sandbox: sandbox}}
|
||||
end
|
||||
|
||||
def handle_call(:checkin, _from, state) do
|
||||
:ok = checkin_connection(state.sandbox, state.repos, state.client)
|
||||
{:stop, :shutdown, :ok, state}
|
||||
end
|
||||
|
||||
def handle_info(:timeout, state) do
|
||||
:ok = checkin_connection(state.sandbox, state.repos, state.client)
|
||||
{:stop, :shutdown, state}
|
||||
end
|
||||
|
||||
def handle_info({:allowed, repo}, state) do
|
||||
send(state.client, {:allowed, repo})
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp checkin_connection(sandbox, repos, client) do
|
||||
for repo <- repos do
|
||||
:ok = sandbox.checkin(repo, client: client)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp checkout_connection(sandbox, repos, client) do
|
||||
for repo <- repos do
|
||||
:ok = sandbox.checkout(repo, client: client)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
61
phoenix/deps/phoenix_ecto/mix.exs
Normal file
61
phoenix/deps/phoenix_ecto/mix.exs
Normal file
@@ -0,0 +1,61 @@
|
||||
defmodule PhoenixEcto.Mixfile do
|
||||
use Mix.Project
|
||||
|
||||
@source_url "https://github.com/phoenixframework/phoenix_ecto"
|
||||
@version "4.7.0"
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :phoenix_ecto,
|
||||
version: @version,
|
||||
elixir: "~> 1.11",
|
||||
deps: deps(),
|
||||
|
||||
# Hex
|
||||
description: "Integration between Phoenix & Ecto",
|
||||
package: package(),
|
||||
|
||||
# Docs
|
||||
name: "Phoenix/Ecto",
|
||||
docs: [
|
||||
main: "main",
|
||||
extras: ["README.md": [filename: "main", title: "Phoenix/Ecto"]],
|
||||
source_ref: "v#{@version}",
|
||||
source_url: @source_url
|
||||
],
|
||||
xref: [
|
||||
exclude: [
|
||||
{Ecto.Migrator, :migrations, 3},
|
||||
{Ecto.Migrator, :migrations_path, 1},
|
||||
{Ecto.Migrator, :run, 4}
|
||||
]
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
def application do
|
||||
[
|
||||
mod: {Phoenix.Ecto, []},
|
||||
extra_applications: [:logger],
|
||||
env: [exclude_ecto_exceptions_from_plug: []]
|
||||
]
|
||||
end
|
||||
|
||||
defp package do
|
||||
[
|
||||
maintainers: ["José Valim", "Chris Mccord"],
|
||||
licenses: ["MIT"],
|
||||
links: %{"GitHub" => @source_url}
|
||||
]
|
||||
end
|
||||
|
||||
defp deps do
|
||||
[
|
||||
{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", optional: true},
|
||||
{:ecto, "~> 3.5"},
|
||||
{:plug, "~> 1.9"},
|
||||
{:postgrex, "~> 0.16 or ~> 1.0", optional: true},
|
||||
{:ex_doc, ">= 0.0.0", only: :docs}
|
||||
]
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user