goncalotomas

goncalotomas

I’m working on a Phoenix LiveView app that uses boundary, and we’ve bumped into warnings writing out a way to do notifications. There are a couple of tricks that would remove these warnings, but I care about the right way to model data and to organize the code. :slight_smile:

Here’s a rough example of the code I’m working with:

defmodule MyApp.Notifications do
  @moduledoc """
  The Notifications module is responsible for managing user notifications - both in-app and email.
  """
  use Boundary, top_level?: true
  # importing verified routes 
  use MyAppWeb, :verified_routes

  def send_invitation(invitation) do
    to =
      if invitation.user_id do
        url(~p"/app/users/invitations")
      else
        url(~p"/auth/register"))
      end

    MyApp.Mailer.deliver_invitation_email(invitation, to)
    send_invitation_notification(invitation)

    # ...
    :ok
  end

  defp send_notification(invitation) do
    # ...
  end
end

The real code is a bit more complicated, but hopefully this illustrates the issue.

We’ve configured Boundary to reject any calls to from the MyApp context to the MyAppWeb context, which makes this module not pass the boundary checks:

warning: forbidden reference to MyAppWeb
  (references from MyApp.Notifications to MyAppWeb are not allowed)
  lib/my_app/notifications.ex:17

It makes sense to prevent access to the Web context from the non-Web, but in this case we’re, were just trying to use verified routes so that the links for the invitations are verified to exist. Previously we could use Phoenix.Router.Helpers, but from the docs, it looks like the routes module helper is deprecated, so no help there.

Moving the logic that switches between the possible URLs inside the Mailer doesn’t seem helpful, since it looks like we’re leaking the logic around Invitations when Mailer should just care about sending email.

We’ve also toyed with a few ideas that basically revolve around replacing the Route Helpers module, but quickly ran into the thought of “there’s got to be a better way to do this”.

Can someone help us organize our code a bit better?

Thanks! :smiley:

Showing Posts 1 to 6

LostKobrakai

LostKobrakai

You can move what use MyAppWeb, :verified_routes does to a separate module, which you can independently scope with boundary.

You could also reverse the dependency by injecting the module for url generation e.g. using config. Works around Boundary a bit, but imo is the architectually cleaner approach.

goncalotomas

goncalotomas OP

I tried doing that, but it doesn’t change the output. Here’s how I added it:

defmodule MyApp.Notifications do
  use Boundary, top_level?: true
  # importing my custom module exposing what `verified_routes` does
  use MyApp.VerifiedRoutes
  # ...
end

The issue is that if I put this module under MyAppWeb, Boundary knows you’re still referencing the Web layer:

defmodule MyAppWeb.VerifiedRoutes do
  @moduledoc false
  def __using__(_) do
    quote do
      use Phoenix.VerifiedRoutes,
        endpoint: MyAppWeb.Endpoint,
        router: MyAppWeb.Router,
        statics: MyAppWeb.static_paths()
    end
  end
end

And I get this compile warning:

warning: forbidden reference to MyAppWeb
  (references from MyApp.Notifications to MyAppWeb are not allowed)
  lib/my_app/notifications.ex:17

If I try to put VerifiedRoutes out of the Web boundary, i.e.MyApp.VerifiedRoutes, I get these compile errors instead:

    error: undefined function url/1 (expected MyApp.Notifications to define such a function or for it to be imported, but none are available)
    │
 44 │           url(~p"/app/users/invitations")
    │           ^^^
    │
    └─ lib/my_app/notifications.ex:44:11: MyApp.Notifications.send_invitation/4

    error: undefined function sigil_p/2 (expected MyApp.Notifications to define such a function or for it to be imported, but none are available)
    │
 44 │           url(~p"/app/users/invitations")
    │               ^^^^^^^
    │
    └─ lib/my_app/notifications.ex:44:15: MyApp.Notifications.send_invitation/4

I don’t understand exactly why it fails to import the same code that works for MyAppWeb, so I couldn’t do it that way either.

The dependency inversion technique is something that I had heard of but never actually used. For now I’m seeing it as the only viable approach.

Thank you!

LostKobrakai

LostKobrakai

You could exclude the module just like it was previously done for the router helper module.

Not sure what this issue about the url function is on a quick glance.

al2o3cr

al2o3cr

I’ve used the “module in config” approach to solve this exact issue in an umbrella app.

It has a couple components:

  • a module in MyAppWeb that exposes a URL-generation API
  • code in MyApp that looks up a module and then calls functions on it
  • config to set the key for MyApp to the module from MyAppWeb
defmodule MyAppWeb.NotificationUrls
  def for_invitation(invitation) do
    if invitation.user_id do
      url(~p"/app/users/#{invitation.user_id}/invitations")
    else
      url(~p"/auth/register")
    end
  end
end
defmodule MyApp.Notifications do
  ...
  def send_invitation(invitation) do
    to = url_module().for_invitation(invitation)

    ...
  end

  defp url_module do
    Application.fetch_env!(:my_app, :notification_urls)
  end
config :my_app, notification_urls: MyAppWeb.NotificationUrls

One downside of this simple approach is that the dynamic apply is very hard for most tools to “see through”, so you probably won’t get compile-time warnings if something’s incorrect (calling for_invitation with the wrong arity, etc)

You can mitigate most of that with some additional boilerplate-y code:

  • extract the Application.fetch_env! part and the call into a separate module in MyApp
  • create a @behaviour that it implements
  • also implement the behaviour in MyAppWeb.NotificationUrls
gtcode

gtcode

Saša’s book helped to get me started long ago. So, this is clearly a solid tool built by an expert.

I’m in the middle of restructuring a large codebase, and boundary came up. My layout is monorepo (imagine lib → {foo, bar, baz}), currently via manually ensuring a directed dependency graph. The original inclination for such manual enforcement was simply to use grep to look for any circular refs, but I like the idea of boundary enforcing this programmatically.

The lib looks well-vetted. Has anyone had luck using this for larger codebases? Any feedback, tips?

goncalotomas

goncalotomas OP

There was a new talk from Saša recently published that talks about the process of both incorporating boundary into a large new codebase and also working towards removing projects from umbrella format:

I found it a great talk, and there’s no better source than the author about this library :grin:

— All posts loaded —

Where Next? Top

Trending in Questions Top

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews