wojtekmach / acme_bank Goto Github PK
View Code? Open in Web Editor NEWAn example ☂ project
License: MIT License
An example ☂ project
License: MIT License
It would be great to have the link to the slides(https://speakerdeck.com/wojtekmach/building-an-umbrella-project) and eventually to the video in the readme.
I have a question about the Bank
OTP app. Is my understanding correct that it is a higher level app that controls how the other lower level (Auth
etc) interact? Also, would it be possible for there to be multiple higher level apps in order to split up the business logic or is there generally just one?
Hope this makes sense. Thanks so much :)
Thanks for creating this as an example of doing an Umbrella project. It's been very helpful as a resource. :)
The problem I have is around transactions and tests when testing a feature like CustomerRegistration that spans Repos (Bank and Auth).
If you create a new test file called customer_registration_test.exs
and add the following contents you can see the problem I'm having...
defmodule Bank.CustomerRegistrationTest do
use Bank.Case
# doctest Bank
alias Bank.CustomerRegistration
@moduletag isolation: :serializable
describe "create/3" do
test "creates auth and user records" do
# Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()})
{:ok, %{update: customer}} = CustomerRegistration.create("name", "[email protected]", "asdfasdf1")
assert customer.auth_account_id != nil
end
end
end
If you run mix test
at the umbrella root, it fails with this error...
1) test create/3 creates auth and user records (Bank.CustomerRegistrationTest)
test/bank/customer_registration_test.exs:10
** (DBConnection.OwnershipError) cannot find ownership process for #PID<0.805.0>.
When using ownership, you must manage connections in one
of the three ways:
* By explicitly checking out a connection
* By explicitly allowing a spawned process
* By running the pool in shared mode
The first two options require every new process to explicitly
check a connection out or be allowed by calling checkout or
allow respectively.
The third option requires a {:shared, pid} mode to be set.
If using shared mode in tests, make sure your tests are not
async.
If you are reading this error, it means you have not done one
of the steps above or that the owner process has crashed.
See Ecto.Adapters.SQL.Sandbox docs for more information.
stacktrace:
[...]
If I run the tests from the Bank application (cd apps/bank) it will pass the first time and fail the second.
$ mix test
................
Finished in 0.2 seconds
16 tests, 0 failures
Randomized with seed 284417
$ mix test
...............
1) test create/3 creates auth and user records (Bank.CustomerRegistrationTest)
test/bank/customer_registration_test.exs:10
** (MatchError) no match of right hand side value: {:error, :account, #Ecto.Changeset<action: :insert, changes: %{email: "[email protected]", password: "asdfasdf1", password_hash: "$2b$04$SXRoZna6IjYLIQeo1VdsFeXiadeR2hq3Sd1t5g48v2qyRtJpFekIG"}, errors: [email: {"has already been taken", []}], data: #Auth.Account<>, valid?: false>, %{customer: %Bank.Customer{__meta__: #Ecto.Schema.Metadata<:loaded, "bank_customers">, auth_account_id: nil, email: "[email protected]", id: 46, inserted_at: #Ecto.DateTime<2016-10-13 11:44:10>, updated_at: #Ecto.DateTime<2016-10-13 11:44:10>, username: "name", wallet: %Bank.Ledger.Account{__meta__: #Ecto.Schema.Metadata<:loaded, "bank_accounts">, currency: "USD", id: 103, inserted_at: #Ecto.DateTime<2016-10-13 11:44:10>, name: "Wallet: name", type: "liability", updated_at: #Ecto.DateTime<2016-10-13 11:44:10>}, wallet_id: 103}}}
stacktrace:
test/bank/customer_registration_test.exs:12: (test)
The problem here is that the created Auth.Account
isn't cleaned up after the first test run and we have left-over data that creates a conflict. You have to reset the DB like this MIX_ENV=test mix ecto.reset
.
One solution I had was to delete the Auth Repo data in the setup of the test, but then you'd have to run async: false
and it felt messy.
Have you solved any of these problems yet?
Does the introduction of contexts in Phoenix 1.3 change the way this app is structured?
Hello,
I have question related to MasterProxy.
In the code below
cond do
conn.request_path =~ ~r{/backoffice} ->
Backoffice.Endpoint.call(conn, [])
true ->
BankWeb.Endpoint.call(conn, [])
end
It's uses /backoffice regular expression to dispatch to two endpoints.
I think we still missing something in here. In both the endpoints, there are assets is fetched through static_plug in both the endpoints. Do you know how to edit the code to cover those cases also.
Thanks
Dev.
Wondering...
Getting this error when trying to deploy to heroku (using the quick deploy option on the repo):
[33mwarning: [0mvariable "package" does not exist and is being expanded to "package()", please use parentheses to remove the ambiguity or change the variable name
/app/deps/scrivener_ecto/mix.exs:10
�[33mwarning: �[0mvariable "deps" does not exist and is being expanded to "deps()", please use parentheses to remove the ambiguity or change the variable name
/app/deps/scrivener_ecto/mix.exs:12
�[33mwarning: �[0mvariable "aliases" does not exist and is being expanded to "aliases()", please use parentheses to remove the ambiguity or change the variable name
/app/deps/scrivener_ecto/mix.exs:13
�[33mwarning: �[0mvariable "deps" does not exist and is being expanded to "deps()", please use parentheses to remove the ambiguity or change the variable name
/app/deps/ex_admin/mix.exs:16
�[33mwarning: �[0mvariable "package" does not exist and is being expanded to "package()", please use parentheses to remove the ambiguity or change the variable name
/app/deps/ex_admin/mix.exs:17
�[33mwarning: �[0mvariable "package" does not exist and is being expanded to "package()", please use parentheses to remove the ambiguity or change the variable name
/app/deps/ex_queb/mix.exs:13
�[33mwarning: �[0mvariable "deps" does not exist and is being expanded to "deps()", please use parentheses to remove the ambiguity or change the variable name
/app/deps/ex_queb/mix.exs:15
�[31m
15:58:27.684 [error] GenServer #PID<0.255.0> terminating
** (ArgumentError) argument error
(postgrex) lib/postgrex/utils.ex:38: anonymous fn/1 in Postgrex.Utils.parse_version/1
(elixir) lib/enum.ex:1229: Enum."-map/2-lists^map/1-0-"/2
(elixir) lib/enum.ex:1229: Enum."-map/2-lists^map/1-0-"/2
(postgrex) lib/postgrex/utils.ex:38: Postgrex.Utils.parse_version/1
(postgrex) lib/postgrex/protocol.ex:497: Postgrex.Protocol.bootstrap_send/4
(postgrex) lib/postgrex/protocol.ex:353: Postgrex.Protocol.handshake/2
(db_connection) lib/db_connection/connection.ex:134: DBConnection.Connection.connect/2
(connection) lib/connection.ex:622: Connection.enter_connect/5
Last message: nil
State: Postgrex.Protocol
�[0m�[31m
15:58:27.698 [error] GenServer #PID<0.262.0> terminating
** (ArgumentError) argument error
(postgrex) lib/postgrex/utils.ex:38: anonymous fn/1 in Postgrex.Utils.parse_version/1
(elixir) lib/enum.ex:1229: Enum."-map/2-lists^map/1-0-"/2
(elixir) lib/enum.ex:1229: Enum."-map/2-lists^map/1-0-"/2
(postgrex) lib/postgrex/utils.ex:38: Postgrex.Utils.parse_version/1
(postgrex) lib/postgrex/protocol.ex:497: Postgrex.Protocol.bootstrap_send/4
(postgrex) lib/postgrex/protocol.ex:353: Postgrex.Protocol.handshake/2
(db_connection) lib/db_connection/connection.ex:134: DBConnection.Connection.connect/2
(connection) lib/connection.ex:622: Connection.enter_connect/5
Last message: nil
State: Postgrex.Protocol
�[0m** (exit) exited in: :gen_server.call(#PID<0.255.0>, {:checkout, #Reference<0.0.6.1820>, true, :infinity}, 5000)
** (EXIT) an exception was raised:
** (ArgumentError) argument error
(postgrex) lib/postgrex/utils.ex:38: anonymous fn/1 in Postgrex.Utils.parse_version/1
(elixir) lib/enum.ex:1229: Enum."-map/2-lists^map/1-0-"/2
(elixir) lib/enum.ex:1229: Enum."-map/2-lists^map/1-0-"/2
(postgrex) lib/postgrex/utils.ex:38: Postgrex.Utils.parse_version/1
(postgrex) lib/postgrex/protocol.ex:497: Postgrex.Protocol.bootstrap_send/4
(postgrex) lib/postgrex/protocol.ex:353: Postgrex.Protocol.handshake/2
(db_connection) lib/db_connection/connection.ex:134: DBConnection.Connection.connect/2
(connection) lib/connection.ex:622: Connection.enter_connect/5
(db_connection) lib/db_connection/poolboy.ex:112: DBConnection.Poolboy.checkout/3
(db_connection) lib/db_connection.ex:712: DBConnection.checkout/2
(db_connection) lib/db_connection.ex:619: DBConnection.run/3
(db_connection) lib/db_connection.ex:921: DBConnection.run_meter/3
(db_connection) lib/db_connection.ex:463: DBConnection.prepare_execute/4
(ecto) lib/ecto/adapters/postgres/connection.ex:91: Ecto.Adapters.Postgres.Connection.execute/4
(ecto) lib/ecto/adapters/sql.ex:235: Ecto.Adapters.SQL.sql_call/6
(ecto) lib/ecto/adapters/sql.ex:185: Ecto.Adapters.SQL.query!/5
For correct work of this example you also need to install packages for phoenix applications:
$ mix ecto.setup
$ cd apps/bank_web
$ npm install
$ cd ../backoffice/
$ npm install
$ cd ../..
$ mix phoenix.server
Hi, I'm new with Elixir based apps and following your steps, the next error was shown when I tried to run the migrations with ecto.setup:
18:35:48.949 [error] Postgrex.Protocol (#PID<0.415.0>) failed to connect: ** (Postgrex.Error) ssl not available
I've tried upgrading and recompiling all the libraries, incluiding the postgrex, I removed the SSL parameter located in the Auth configuration. It is something that I`m missing?
Thank you.
Looking at the code I see that the user account has a "foreign key" to the authentication:
schema "bank_customers" do
field :auth_account_id, :integer
Shouldn't this relation be inversed: auth linking to the underlying customer ?
schema "auth_accounts" do
field :customer_id, :integer
This would bring more modularity: being able to swap/add authentication methods in the future without changing the customer model.
It's still easy to use: the session bit authenticates the user and we get the customer_id without going through an extra query (if we're only interested in the id).
I thought that was what slide 112 was all about, it enlightened me!
Though the implementation I found brought me down a bit..
Tell me what you think!
[error] Failed to start Ranch listener MasterProxy.Plug.HTTP in :ranch_tcp:listen([port: 3333]) for reason :eaddrinuse (address already in use)
When I run mix run --no-halt
and iex -S mix
inside of /apps/master_proxy
, I get the error above. I thought about trying to have something like this:
if :erlang.whereis(MasterProxy.Plug.HTTP) == :undefined, do: [Plug.Adapters.Cowboy.child_spec(:http, MasterProxy.Plug, [], [port: port])], else: []
Doesn't seem to work though. Any thoughts?
I'm new to OTP and Elixir.
If I'm understanding this correctly, the balance for each account is kept in memory in OTP. Each time the application launches or a supervisor needs to restart a GenServer, the balance would need to be recalculated from the ledger entries in bank_entries
table in the database. When a transaction occurs, the OTP records it to the database and transforms the values in memory.
If a balance is queried through the web interface, it would go to the OTP bank and read it directly from memory instead of having to recalculate it.
Is this correct? Am I understanding this the way it should be?
This call from bank.ex
def balance(%Customer{wallet: wallet}), do: Ledger.balance(wallet)
Is calling ledger.ex
def balance(%Account{id: id, type: type, currency: currency}) do
q = from(t in Entry,
select: fragment("SUM(CASE WHEN b0.type = 'credit' THEN (b0.amount).cents ELSE -(b0.amount).cents END)"),
where: t.account_id == ^id)
balance = Repo.one(q) || 0
balance = do_balance(balance, type)
%Money{cents: balance, currency: currency}
end
and this is where it is kept in memory.
Hi @wojtekmach,
Thanks again for this awesome example and talk, I am studying this and had some questions. I hope you don't mind if I open issues here.
My question is around websockets. I have a similar situation where there is both a "public" and an "admin" client that would be great fit for two different phoenix apps in an umbrella. However at least one of them needs a websocket connection as well as an API.
I believe in this example app websockets would not be supported because plug doesn't support them. Have you thought about this at all?
I am assuming you would have to use Phoenix.Endpoint in the masterproxy app (but maybe not as a Phoenix app?). Anything to consider with that?
Amount shown for the bank_entries amount does not separate decimals from the amount being transferred. Have transferred "5.00" USD, parses well, however, it appears as "500" both in bank_entries table and the transaction log.
SET TRANSACTION ISOLATION LEVEL serializable []
[debug] QUERY OK db=2.9ms
INSERT INTO "bank_entries" ("account_id","amount","description","type","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5,$6) RETURNING "id" [3, {500, "USD"}, "pay back time. !!!", "debit", {{2018, 3, 2}, {4, 46, 1, 0}}, {{2018, 3, 2}, {4, 46, 1, 0}}]
[debug] QUERY OK db=4.3ms
INSERT INTO "bank_entries" ("account_id","amount","description","type","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5,$6) RETURNING "id" [1, {500, "USD"}, "pay back time. !!!", "credit", {{2018, 3, 2}, {4, 46, 1, 0}}, {{2018, 3, 2}, {4, 46, 1, 0}}]
[debug] QUERY OK source="bank_entries" db=3.7ms
Does it something to do with multiplying with 100 in the money.ex?
defp do_parse(sign, dollars, cents, currency) do
sign = if sign == "-", do: -1, else: 1
cents = sign * (String.to_integer(dollars) * 100 + String.to_integer(cents))
{:ok, %Money{cents: cents, currency: currency}}
end
Using the boilerplate "hello" app an the Phoenix guides site and the master proxy in this application I am seeing the following errors on my websocket connections:
[info] GET /phoenix/live_reload/socket/websocket
[debug] ** (Phoenix.Router.NoRouteError) no route found for GET /phoenix/live_reload/socket/websocket (HelloWeb.Router)
(hello) lib/hello_web/router.ex:1: HelloWeb.Router.__match_route__/4
(hello) lib/phoenix/router.ex:303: HelloWeb.Router.call/2
(hello) lib/hello_web/endpoint.ex:1: HelloWeb.Endpoint.plug_builder_call/2
(hello) lib/plug/debugger.ex:99: HelloWeb.Endpoint."call (overridable 3)"/2
(hello) lib/hello_web/endpoint.ex:1: HelloWeb.Endpoint.call/2
(plug) lib/plug/adapters/cowboy/handler.ex:15: Plug.Adapters.Cowboy.Handler.upgrade/4
(cowboy) /media/iampeterbanjo/data/programs/elixir/portfolio/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4
The endpoint for the hello app are boilerplate:
plug Plug.Static,
at: "/", from: :hello, gzip: false,
only: ~w(css fonts images js favicon.ico robots.txt)
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
end
Since there is only one app at the moment, my proxy plug.ex is:
defmodule Proxy.Plug do
def init(options) do
options
end
def call(conn, _opts) do
cond do
false ->
HelloWeb.Endpoint.call(conn, [])
true ->
HelloWeb.Endpoint.call(conn, [])
end
end
end
This happens when I start the phoenix web server for the umbrella app and not the sub-app. A repo of my code is at https://github.com/iampeterbanjo/hello-proxy
Is there a way to proxy the websockets as well in the master proxy?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.