Skip to content

Commit

Permalink
feat: add detour notifications to notifications module
Browse files Browse the repository at this point in the history
feat(ex/notifications/detour): add virtual fields to detour notification schema
feat(ex/notifications): query detour info from notifications

Co-authored-by: Josh Larson <[email protected]>
  • Loading branch information
firestack and joshlarson committed Sep 26, 2024
1 parent 1248985 commit b3bd45d
Show file tree
Hide file tree
Showing 8 changed files with 379 additions and 3 deletions.
113 changes: 112 additions & 1 deletion lib/notifications/db/detour.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,24 @@ defmodule Notifications.Db.Detour do
@derive {Jason.Encoder,
only: [
:__struct__,
:status
:status,
:headsign,
:route,
:direction,
:origin
]}

typed_schema "detour_notifications" do
belongs_to :detour, Skate.Detours.Db.Detour
has_one :notification, Notifications.Db.Notification

field :status, Ecto.Enum, values: [:activated]

# Derived from the associated detour
field :headsign, :any, virtual: true
field :route, :any, virtual: true
field :direction, :any, virtual: true
field :origin, :any, virtual: true
end

def changeset(
Expand All @@ -33,4 +43,105 @@ defmodule Notifications.Db.Detour do
:status
])
end

defmodule Queries do
@moduledoc """
Defines composable queries for retrieving `Notifications.Db.Detour` info.
"""

import Ecto.Query

@doc """
The "base" query that queries `Notifications.Db.Detour`'s without restriction
## Examples
The `base` query returns all Detour Notifications
iex> :detour
...> |> insert()
...> |> Notifications.Notification.create_activated_detour_notification_from_detour()
...>
iex> all_detour_notifications =
...> Notifications.Db.Detour.Queries.base()
...> |> Skate.Repo.all()
...>
iex> match?(
...> [
...> %Notifications.Db.Detour{
...> }
...> ],
...> all_detour_notifications
...> )
true
"""
def base() do
from(d in Notifications.Db.Detour, as: :detour_notification, select_merge: d)
end

@doc """
Retrieves detour information for notifications from the `Notifications.Db.Detour` table
## Examples
iex> :detour
...> |> insert()
...> |> Notifications.Notification.create_activated_detour_notification_from_detour()
...>
iex> all_detour_notifications =
...> Notifications.Db.Detour.Queries.get_derived_info()
...> |> Skate.Repo.all()
...>
iex> [
...> %Notifications.Db.Detour{
...> route: route,
...> origin: origin,
...> headsign: headsign,
...> direction: direction
...> }
...> ] = all_detour_notifications
iex> Enum.any?([route, origin, headsign, direction], &is_nil/1)
false
"""
def get_derived_info(query \\ base()) do
from(
[detour_notification: dn] in query,
left_join: ad in assoc(dn, :detour),
as: :associated_detour,
select_merge: %{
route: ad.state["context"]["route"]["name"],
origin: ad.state["context"]["routePattern"]["name"],
headsign: ad.state["context"]["routePattern"]["headsign"],

# Ecto can't figure out how to index a JSON map via another JSON value
# because (in the ways it was tried) Ecto won't allow us to use the
# value from the associated detour, `ad`, as a value in the
# ["JSON path"](https://hexdocs.pm/ecto/Ecto.Query.API.html#json_extract_path/2).
#
# i.e., this
# ad.state["context"]["route"]["directionNames"][
# ad.state["context"]["routePattern"]["directionId"]
# ]
#
# But, Postgres _is_ able to do this, _if_ we get the types correct.
# A JSON value in Postgres is either of type JSON or JSONB, but
# - indexing a JSON array requires an `INTEGER`,
# - accessing a JSON map, requires Postgres's `TEXT` type.
#
# So because we know the `directionId` will correspond to the keys in
# `directionNames`, casting the `directionId` to `TEXT` allows us to
# access the `directionNames` JSON map
direction:
fragment(
"? -> CAST(? AS TEXT)",
ad.state["context"]["route"]["directionNames"],
ad.state["context"]["routePattern"]["directionId"]
)
}
)
end
end
end
64 changes: 64 additions & 0 deletions lib/notifications/db/notification.ex
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,70 @@ defmodule Notifications.Db.Notification do
}
)
end

@doc """
Joins associated `Notifications.Db.Detour`'s on
`Notifications.Db.Notification`'s and retrieves the Detour's
associated info.
## Examples
There is a `base` query struct that can be provided at the
beginning of a query:
iex> :detour
...> |> insert()
...> |> Notifications.Notification.create_activated_detour_notification_from_detour()
...>
iex> all_detour_notifications =
...> Notifications.Db.Notification.Queries.base()
...> |> Notifications.Db.Notification.Queries.select_detour_info()
...> |> Skate.Repo.all()
...> |> Skate.Repo.preload(:detour)
...>
iex> match?(
...> [
...> %Notifications.Db.Notification{
...> detour: %Notifications.Db.Detour{}
...> }
...> ], all_detour_notifications
...> )
true
If `base` is omitted, then it's inferred:
iex> :detour
...> |> insert()
...> |> Notifications.Notification.create_activated_detour_notification_from_detour()
...>
iex> all_detour_notifications =
...> Notifications.Db.Notification.Queries.select_detour_info()
...> |> Skate.Repo.all()
...> |> Skate.Repo.preload(:detour)
...>
iex> match?(
...> [
...> %Notifications.Db.Notification{
...> detour: %Notifications.Db.Detour{}
...> }
...> ],
...> all_detour_notifications
...> )
true
"""
@spec select_detour_info(Ecto.Query.t()) :: Ecto.Query.t()
@spec select_detour_info() :: Ecto.Query.t()
def select_detour_info(query \\ base()) do
from([notification: n] in query,
left_join: detour in subquery(Notifications.Db.Detour.Queries.get_derived_info()),
on: detour.id == n.detour_id,
select_merge: %{
detour: detour
}
)
end

@doc """
Joins associated `Notifications.Db.BridgeMovement`'s on
`Notifications.Db.Notification`'s
Expand Down
24 changes: 24 additions & 0 deletions lib/notifications/detour.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule Notifications.Detour do
@moduledoc """
Context for working with Detour notifications
"""

@doc """
Creates a detour notification struct from a detour to insert into the database
"""
def detour_notification(%Skate.Detours.Db.Detour{} = detour) do
%Notifications.Db.Detour{
detour: detour
}
end

@doc """
Creates a activated detour notification struct to insert into the database
"""
def activated_detour(%Skate.Detours.Db.Detour{} = detour) do
%{
detour_notification(detour)
| status: :activated
}
end
end
67 changes: 67 additions & 0 deletions lib/notifications/notification.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,66 @@ defmodule Notifications.Notification do
:content
]

@doc """
Inserts a new notification for an activated detour into the database
and returns the detour notification with notification info.
"""
def create_activated_detour_notification_from_detour(%Skate.Detours.Db.Detour{} = detour) do
import Notifications.Db.Notification.Queries

notification =
activated_detour_notification(detour)
|> unread_notifications_for_users(Skate.Settings.User.get_all())
|> Skate.Repo.insert!()

# We need the associated values in the Detour JSON, so query the DB with the
# id to load the extra data.
select_detour_info()
|> where([notification: n], n.id == ^notification.id)
|> Skate.Repo.one!()
|> from_db_notification()
end

# Creates a new notification set to the current time
defp new_notification_now() do
%Notifications.Db.Notification{
created_at: DateTime.to_unix(DateTime.utc_now())
}
end

# Adds a activated detour notification relation to a `Notifications.Db.Notification`
defp activated_detour_notification(%Skate.Detours.Db.Detour{} = detour) do
%Notifications.Db.Notification{
new_notification_now()
| detour: Notifications.Detour.activated_detour(detour)
}
end

defp notification_for_user(%Skate.Settings.Db.User{} = user) do
%Notifications.Db.NotificationUser{
user: user
}
end

defp unread_notification(%Notifications.Db.NotificationUser{} = user_notification) do
%{
user_notification
| state: :unread
}
end

defp unread_notifications_for_users(%Notifications.Db.Notification{} = notification, users) do
%{
notification
| notification_users:
for user <- users do
user
|> notification_for_user()
|> unread_notification()
end
}
end

@spec get_or_create_from_block_waiver(map()) :: t()
def get_or_create_from_block_waiver(block_waiver_values) do
changeset =
Expand Down Expand Up @@ -149,6 +209,7 @@ defmodule Notifications.Notification do
|> select_user_read_state(user_id)
|> select_bridge_movements()
|> select_block_waivers()
|> select_detour_info()
|> where([notification: n], n.created_at > ^cutoff_time)
|> order_by([notification: n], desc: n.created_at)
|> Skate.Repo.all()
Expand Down Expand Up @@ -220,4 +281,10 @@ defmodule Notifications.Notification do
}) do
bm
end

defp content_from_db_notification(%DbNotification{
detour: %Notifications.Db.Detour{} = detour
}) do
detour
end
end
7 changes: 7 additions & 0 deletions test/notifications/db/detour_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Notifications.Db.DetourTest do
use Skate.DataCase

import Skate.Factory

doctest Notifications.Db.Detour.Queries
end
7 changes: 7 additions & 0 deletions test/notifications/db/notification_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule Notifications.Db.NotificationTest do
use Skate.DataCase

import Skate.Factory

doctest Notifications.Db.Notification.Queries
end
Loading

0 comments on commit b3bd45d

Please sign in to comment.