Skip to content

Commit 3eb4444

Browse files
committed
feat: add MongoDB database option to Phoenix installer
1 parent e504295 commit 3eb4444

11 files changed

Lines changed: 144 additions & 2 deletions

File tree

installer/lib/mix/tasks/phx.new.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ defmodule Mix.Tasks.Phx.New do
2525
2626
* `postgres` - via https://github.com/elixir-ecto/postgrex
2727
* `mysql` - via https://github.com/elixir-ecto/myxql
28+
* `mongo` - via https://github.com/elixir-mongo/mongodb_ecto
2829
* `mssql` - via https://github.com/livehelpnow/tds
2930
* `sqlite3` - via https://github.com/elixir-sqlite/ecto_sqlite3
3031

installer/lib/phx_new/generator.ex

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,10 @@ defmodule Phx.New.Generator do
406406
{:ecto_sqlite3, Ecto.Adapters.SQLite3, fs_db_config(app, module)}
407407
end
408408

409+
defp get_ecto_adapter("mongo", app, module) do
410+
{:mongodb_ecto, Mongo.Ecto, mongo_db_config(app, module)}
411+
end
412+
409413
defp get_ecto_adapter(db, _app, _mod) do
410414
Mix.raise("Unknown database #{inspect(db)}")
411415
end
@@ -455,6 +459,38 @@ defmodule Phx.New.Generator do
455459
]
456460
end
457461

462+
defp mongo_db_config(app, module) do
463+
[
464+
dev: [
465+
mongo_url: "mongodb://localhost:27017/#{app}_dev",
466+
pool_size: 10,
467+
stacktrace: true,
468+
show_sensitive_data_on_connection_error: true
469+
],
470+
test: [
471+
mongo_url:
472+
{:literal,
473+
~s|"mongodb://localhost:27017/#{app}_test\#{System.get_env("MIX_TEST_PARTITION")}"|},
474+
pool_size: 5
475+
],
476+
test_setup_all: "",
477+
test_setup: "Mongo.Ecto.truncate(#{inspect(module)}.Repo)",
478+
prod_variables: """
479+
database_url =
480+
System.get_env("DATABASE_URL") ||
481+
raise \"""
482+
environment variable DATABASE_URL is missing.
483+
For example: mongodb://user:pass@host/database
484+
\"""
485+
""",
486+
prod_config: """
487+
mongo_url: database_url,
488+
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
489+
""",
490+
binary_id: true
491+
]
492+
end
493+
458494
defp socket_db_config(app, module, user, pass) do
459495
[
460496
dev: [

installer/lib/phx_new/interactive.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ defmodule Phx.New.Interactive do
44
@databases [
55
{"postgres", "PostgreSQL (postgrex)"},
66
{"mysql", "MySQL (myxql)"},
7+
{"mongo", "MongoDB (mongodb_ecto)"},
78
{"mssql", "MSSQL (tds)"},
89
{"sqlite3", "SQLite3 (ecto_sqlite3)"},
910
{"none", "None"}

installer/lib/phx_new/project.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ defmodule Phx.New.Project do
6666
Keyword.fetch!(binding, :mailer)
6767
end
6868

69+
def mongo?(%Project{opts: opts}) do
70+
Keyword.get(opts, :database) == "mongo"
71+
end
72+
6973
def verbose?(%Project{opts: opts}) do
7074
Keyword.get(opts, :verbose, false)
7175
end

installer/lib/phx_new/single.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ defmodule Phx.New.Single do
105105
{:eex, :app, "phx_mailer/lib/app_name/mailer.ex.eex": "lib/:app/mailer.ex"}
106106
])
107107

108+
template(:docker_compose, [
109+
{:eex, :project, "phx_single/docker-compose.yml.eex": "docker-compose.yml"}
110+
])
111+
108112
def prepare_project(%Project{app: app, base_path: base_path} = project) when not is_nil(app) do
109113
if in_umbrella?(base_path) do
110114
%{project | in_umbrella?: true, project_path: Path.dirname(Path.dirname(base_path))}
@@ -147,6 +151,7 @@ defmodule Phx.New.Single do
147151
if Project.html?(project), do: gen_html(project)
148152
if Project.mailer?(project), do: gen_mailer(project)
149153
if Project.gettext?(project), do: gen_gettext(project)
154+
if Project.mongo?(project), do: gen_docker_compose(project)
150155

151156
gen_assets(project)
152157
project
@@ -186,4 +191,8 @@ defmodule Phx.New.Single do
186191
def gen_mailer(%Project{} = project) do
187192
copy_from(project, __MODULE__, :mailer)
188193
end
194+
195+
def gen_docker_compose(%Project{} = project) do
196+
copy_from(project, __MODULE__, :docker_compose)
197+
end
189198
end

installer/templates/phx_ecto/data_case.ex.eex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@ defmodule <%= @app_module %>.DataCase do
77
your tests.
88

99
Finally, if the test case interacts with the database,
10+
<% if String.trim(@adapter_config[:test_setup_all]) == "" do %>
11+
we truncate all collections before each test, so changes done
12+
to the database are reverted.
13+
<% else %>
1014
we enable the SQL sandbox, so changes done to the database
1115
are reverted at the end of every test. If you are using
1216
PostgreSQL, you can even run database tests asynchronously
1317
by setting `use <%= @app_module %>.DataCase, async: true`, although
1418
this option is not recommended for other databases.
19+
<% end %>
1520
"""
1621

1722
use ExUnit.CaseTemplate
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
services:
2+
mongodb:
3+
image: mongo:latest
4+
ports:
5+
- "27017:27017"
6+
environment:
7+
MONGO_INITDB_DATABASE: <%= @app_name %>_dev
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## MongoDB
2+
3+
When the project uses `--database mongo` (adapter: `Mongo.Ecto`):
4+
5+
- **Primary keys must be `:binary_id`** — configured automatically via `config :app, :generators, binary_id: true`. All `mix phx.gen.*` commands honour this automatically; no `--binary-id` flag needed.
6+
- **No SQL joins** — use embedded schemas (`embeds_one`, `embeds_many`) for nested data, or separate queries for associations.
7+
- **Migrations create collections and indexes**, not tables. `create table(:name)` → MongoDB collection. `add :col, :type` → no-op (schema-less).
8+
- **Test isolation** uses `Mongo.Ecto.truncate(Repo)` before each test, not `Ecto.Adapters.SQL.Sandbox`.
9+
- **Connection string** uses `mongo_url:` config key. In production, set `DATABASE_URL` env var to a MongoDB URI (`mongodb://...` or `mongodb+srv://...`).
10+
- **Atlas** — swap `DATABASE_URL` to your Atlas connection string; no code changes needed.
11+
- `:decimal` type is not supported by `mongodb_ecto`.

installer/test/phx_new_test.exs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,35 @@ defmodule Mix.Tasks.Phx.NewTest do
748748
end)
749749
end
750750

751+
test "new with mongo adapter" do
752+
in_tmp("new with mongo adapter", fn ->
753+
project_path = Path.join(File.cwd!(), "custom_path")
754+
Mix.Tasks.Phx.New.run([project_path, "--database", "mongo"])
755+
756+
assert_file("custom_path/mix.exs", ":mongodb_ecto")
757+
758+
assert_file("custom_path/config/dev.exs", [
759+
~r/mongo_url:/,
760+
~r/mongodb:\/\/localhost:27017/
761+
])
762+
763+
assert_file("custom_path/config/test.exs", [~r/mongo_url:/])
764+
765+
assert_file("custom_path/config/runtime.exs", [~r/DATABASE_URL/])
766+
767+
assert_file("custom_path/config/config.exs", ~r/generators: \[.*binary_id: true.*\]/)
768+
769+
assert_file("custom_path/lib/custom_path/repo.ex", "Mongo.Ecto")
770+
771+
assert_file(
772+
"custom_path/test/support/data_case.ex",
773+
"Mongo.Ecto.truncate"
774+
)
775+
776+
assert_file("custom_path/docker-compose.yml", "mongo:7")
777+
end)
778+
end
779+
751780
test "new with invalid database adapter" do
752781
in_tmp("new with invalid database adapter", fn ->
753782
project_path = Path.join(File.cwd!(), "custom_path")
@@ -863,8 +892,8 @@ defmodule Mix.Tasks.Phx.NewTest do
863892
in_tmp("new interactive custom", fn ->
864893
# path
865894
send(self(), {:mix_shell_input, :prompt, "custom_app"})
866-
# database: sqlite3 (option 4)
867-
send(self(), {:mix_shell_input, :prompt, "4"})
895+
# database: sqlite3 (option 5)
896+
send(self(), {:mix_shell_input, :prompt, "5"})
868897
# binary_id: yes
869898
send(self(), {:mix_shell_input, :prompt, "y"})
870899
# web: API-only (option 3, skips assets prompt)

installer/test/phx_new_umbrella_test.exs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,33 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
680680
end)
681681
end
682682

683+
test "new with mongo adapter" do
684+
in_tmp("new with mongo adapter", fn ->
685+
app = "custom_path"
686+
project_path = Path.join(File.cwd!(), app)
687+
Mix.Tasks.Phx.New.run([project_path, "--umbrella", "--database", "mongo"])
688+
689+
assert_file(app_path(app, "mix.exs"), ":mongodb_ecto")
690+
assert_file(app_path(app, "lib/custom_path/repo.ex"), "Mongo.Ecto")
691+
692+
assert_file(root_path(app, "config/dev.exs"), [
693+
~r/mongo_url:/,
694+
~r/mongodb:\/\/localhost:27017/
695+
])
696+
697+
assert_file(root_path(app, "config/test.exs"), [~r/mongo_url:/])
698+
699+
assert_file(root_path(app, "config/runtime.exs"), [~r/DATABASE_URL/])
700+
701+
assert_file(root_path(app, "config/config.exs"), ~r/generators: \[.*binary_id: true.*\]/)
702+
703+
assert_file(
704+
app_path(app, "test/support/data_case.ex"),
705+
"Mongo.Ecto.truncate"
706+
)
707+
end)
708+
end
709+
683710
test "new with invalid database adapter" do
684711
in_tmp("new with invalid database adapter", fn ->
685712
project_path = Path.join(File.cwd!(), "custom_path")

0 commit comments

Comments
 (0)