Skip to content

Commit 0a468e8

Browse files
committed
Working API
1 parent ca8c2b9 commit 0a468e8

File tree

7 files changed

+189
-106
lines changed

7 files changed

+189
-106
lines changed

lib/sanbase/menu/menus.ex

Lines changed: 72 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,7 @@ defmodule Sanbase.Menus do
4646

4747
@type update_menu_item_params :: %{
4848
optional(:parent_id) => menu_id,
49-
optional(:position) => integer() | nil,
50-
optional(:query_id) => Sanbase.Queries.Query.query_id(),
51-
optional(:dashboard_id) => Sanbase.Queries.Dashboard.dashboard_id(),
52-
optional(:menu_id) => menu_id
49+
optional(:position) => integer() | nil
5350
}
5451

5552
@doc ~s"""
@@ -77,6 +74,10 @@ defmodule Sanbase.Menus do
7774
"""
7875
def menu_to_simple_map(%Menu{} = menu) do
7976
%{
77+
# If this menu is a sub-menu, then the caller from get_menu_items/1 will
78+
# additionally set the menu_item_id. If this is the top-level menu, then
79+
# this is not a sub-menu and it does not have a menu_item_id
80+
menu_item_id: nil,
8081
type: :menu,
8182
id: menu.id,
8283
name: menu.name,
@@ -95,7 +96,9 @@ defmodule Sanbase.Menus do
9596
"""
9697
@spec create_menu(create_menu_params, user_id) :: {:ok, Menu.t()} | {:error, String.t()}
9798
def create_menu(params, user_id) do
98-
params = params |> Map.merge(%{user_id: user_id})
99+
params =
100+
params
101+
|> Map.merge(%{user_id: user_id})
99102

100103
Ecto.Multi.new()
101104
|> Ecto.Multi.run(:create_menu, fn _repo, _changes ->
@@ -121,19 +124,13 @@ defmodule Sanbase.Menus do
121124
)
122125
end
123126
end)
124-
# |> Ecto.Multi.run(
125-
# :get_menu_with_preloads,
126-
# fn _repo, %{create_menu: menu, maybe_create_menu_item: menu_or_nil} ->
127-
# # If the menu was created as a sub-menu, then the `maybe_create_menu_item` step
128-
# # has already returned the menu with preloads. Otherwise, we need to preload it here.
129-
# case menu_or_nil do
130-
# %Menu{} = m -> {:ok, m}
131-
# nil -> get_menu(menu.id, user_id)
132-
# end
133-
# end
134-
# )
127+
|> Ecto.Multi.run(:get_menu_with_preloads, fn _repo, %{create_menu: menu} ->
128+
# There would be no menu items, but this will help to set the menu items to []
129+
# instead of getting an error when trying to iterate them because they're set to <not preloaded>
130+
get_menu(menu.id, user_id)
131+
end)
135132
|> Repo.transaction()
136-
|> process_transaction_result(:create_menu)
133+
|> process_transaction_result(:get_menu_with_preloads)
137134
end
138135

139136
@doc ~s"""
@@ -152,7 +149,7 @@ defmodule Sanbase.Menus do
152149
query = Menu.update(menu, params)
153150
Repo.update(query)
154151
end)
155-
|> Ecto.Multi.run(:get_menu_with_preloads, fn _repo, %{create_menu: menu} ->
152+
|> Ecto.Multi.run(:get_menu_with_preloads, fn _repo, %{update_menu: menu} ->
156153
get_menu(menu.id, user_id)
157154
end)
158155
|> Repo.transaction()
@@ -168,11 +165,19 @@ defmodule Sanbase.Menus do
168165
|> Ecto.Multi.run(:get_menu_for_update, fn _repo, _changes ->
169166
get_menu_for_update(menu_id, user_id)
170167
end)
168+
|> Ecto.Multi.run(:get_menu_with_preloads, fn _repo, _changes ->
169+
# Call this so we can return the menu with its menu items after it is
170+
# successfully deleted
171+
get_menu(menu_id, user_id)
172+
end)
171173
|> Ecto.Multi.run(:delete_menu, fn _repo, %{get_menu_for_update: menu} ->
172174
Repo.delete(menu)
173175
end)
174176
|> Repo.transaction()
175-
|> process_transaction_result(:delete_menu)
177+
# Purposefully do not return the result of the last Ecto.Multi.run call,
178+
# but from the get_menu_with_preloads call, so we can return the menu with
179+
# its items.
180+
|> process_transaction_result(:get_menu_with_preloads)
176181
end
177182

178183
@doc ~s"""
@@ -234,45 +239,63 @@ defmodule Sanbase.Menus do
234239

235240
@doc ~s"""
236241
Update an existing menu item.
242+
243+
A menu item can have the follwing fields updated:
244+
- position - change the position of the item in the menu
245+
- parent_id - change the parent menu of the item. On the frontend this is done
246+
by dragging and dropping the item in the menu tree (this can also update the position)
247+
248+
The entity (query, dashboard, etc.) cannot be changed. Delete a menu item and insert a new
249+
one instead.
237250
"""
238-
@spec update_menu_item(menu_id, menu_item_id, update_menu_item_params, user_id) ::
251+
@spec update_menu_item(menu_item_id, update_menu_item_params, user_id) ::
239252
{:ok, Menu.t()} | {:error, String.t()}
240-
def update_menu_item(menu_id, menu_item_id, params, user_id) do
253+
def update_menu_item(menu_item_id, params, user_id) do
241254
Ecto.Multi.new()
242-
|> Ecto.Multi.run(:get_menu_for_update, fn _repo, _changes ->
243-
# Just check that the current user can update the parent menu
244-
get_menu_for_update(menu_id, user_id)
245-
end)
246255
|> Ecto.Multi.run(:get_menu_item_for_update, fn _repo, _changes ->
247-
get_menu_item_for_update(menu_item_id)
256+
get_menu_item_for_update(menu_item_id, user_id)
248257
end)
258+
|> Ecto.Multi.run(
259+
:maybe_update_items_positions,
260+
fn _repo, %{get_menu_item_for_update: menu_item} ->
261+
case Map.get(params, :position) do
262+
nil ->
263+
{:ok, nil}
264+
265+
position when is_integer(position) ->
266+
# If `position` is specified, bump all the positions bigger than it by 1 in
267+
# order to avoid having multiple items with the same position.
268+
{:ok, {_, nil}} = inc_all_positions_after(menu_item.parent_id, position)
269+
{:ok, position}
270+
end
271+
end
272+
)
249273
|> Ecto.Multi.run(:update_menu_item, fn _repo, %{get_menu_item_for_update: menu_item} ->
250-
# Handle change of entity
251-
params = MenuItem.process_update_params(params)
252274
query = MenuItem.update(menu_item, params)
253275
Repo.update(query)
254276
end)
255-
|> Ecto.Multi.run(:get_menu_with_preloads, fn _repo, %{get_menu_for_update: menu} ->
256-
get_menu(menu.id, user_id)
277+
|> Ecto.Multi.run(:get_menu_with_preloads, fn _repo, %{update_menu_item: menu_item} ->
278+
get_menu(menu_item.parent_id, user_id)
257279
end)
258280
|> Repo.transaction()
259281
|> process_transaction_result(:get_menu_with_preloads)
260282
end
261283

262-
def delete_menu_item(menu_id, menu_item_id, user_id) do
284+
@doc ~s"""
285+
Delete a menu item.
286+
"""
287+
@spec delete_menu_item(menu_item_id, user_id) ::
288+
{:ok, Menu.t()} | {:error, String.t()}
289+
def delete_menu_item(menu_item_id, user_id) do
263290
Ecto.Multi.new()
264-
|> Ecto.Multi.run(:check_user_has_write_access, fn _repo, _changes ->
265-
# Just check that the current user can update the parent menu
266-
get_menu_for_update(menu_id, user_id)
267-
end)
268291
|> Ecto.Multi.run(:get_menu_item, fn _repo, _changes ->
269-
get_menu_item_for_update(menu_item_id)
292+
get_menu_item_for_update(menu_item_id, user_id)
270293
end)
271294
|> Ecto.Multi.run(:delete_menu_item, fn _repo, %{get_menu_item: menu_item} ->
272295
Repo.delete(menu_item)
273296
end)
274-
|> Ecto.Multi.run(:get_menu_with_preloads, fn _repo, %{create_menu: menu} ->
275-
get_menu(menu.id, user_id)
297+
|> Ecto.Multi.run(:get_menu_with_preloads, fn _repo, %{delete_menu_item: menu_item} ->
298+
get_menu(menu_item.parent_id, user_id)
276299
end)
277300
|> Repo.transaction()
278301
|> process_transaction_result(:get_menu_with_preloads)
@@ -289,11 +312,8 @@ defmodule Sanbase.Menus do
289312
end
290313
end
291314

292-
defp get_menu_item_for_update(menu_item_id) do
293-
# This should be called only after checking that the user
294-
# has write access to the menu. This is done by calling
295-
# get_menu_for_update/2 first.
296-
query = MenuItem.get_for_update(menu_item_id)
315+
defp get_menu_item_for_update(menu_item_id, user_id) do
316+
query = MenuItem.get_for_update(menu_item_id, user_id)
297317

298318
case Repo.one(query) do
299319
nil -> {:error, "Menu item does not exist"}
@@ -339,14 +359,17 @@ defmodule Sanbase.Menus do
339359
defp get_menu_items(%Menu{menu_items: list}) when is_list(list) do
340360
list
341361
|> Enum.map(fn
342-
%{query: %{id: _} = map, position: p} ->
343-
Map.take(map, [:id, :name, :description]) |> Map.merge(%{type: :query, position: p})
362+
%{id: menu_item_id, query: %{id: _} = map, position: position} ->
363+
Map.take(map, [:id, :name, :description])
364+
|> Map.merge(%{type: :query, position: position, menu_item_id: menu_item_id})
344365

345-
%{dashboard: %{id: _} = map, position: p} ->
346-
Map.take(map, [:id, :name, :description]) |> Map.merge(%{type: :dashboard, position: p})
366+
%{id: menu_item_id, dashboard: %{id: _} = map, position: position} ->
367+
Map.take(map, [:id, :name, :description])
368+
|> Map.merge(%{type: :dashboard, position: position, menu_item_id: menu_item_id})
347369

348-
%{menu: %{id: _} = map, position: p} ->
349-
menu_to_simple_map(map) |> Map.put(:position, p)
370+
%{id: menu_item_id, menu: %{id: _} = map, position: position} ->
371+
menu_to_simple_map(map)
372+
|> Map.merge(%{type: :menu, position: position, menu_item_id: menu_item_id})
350373
end)
351374
end
352375
end

lib/sanbase/menu/schema/menu_item.ex

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ defmodule Sanbase.Menus.MenuItem do
1818
updated_at: DateTime.t()
1919
}
2020

21-
@enitity_fields [:query_id, :dashboard_id, :menu_id]
22-
@enitity_fields_nil_map Map.new(@enitity_fields, fn k -> {k, nil} end)
23-
def entity_fields(), do: @enitity_fields
24-
2521
@timestamps_opts [type: :utc_datetime]
2622
schema "menu_items" do
2723
belongs_to(:parent, Menu)
@@ -35,9 +31,10 @@ defmodule Sanbase.Menus.MenuItem do
3531
timestamps()
3632
end
3733

38-
def get_for_update(id) do
34+
def get_for_update(id, user_id) do
3935
base_query()
40-
|> where([m], m.id == ^id)
36+
|> join(:left, [mi], p in Menu, on: mi.parent_id == p.id, as: :parent)
37+
|> where([mi, parent: p], mi.id == ^id and p.user_id == ^user_id)
4138
|> lock("FOR UPDATE")
4239
end
4340

@@ -77,31 +74,10 @@ defmodule Sanbase.Menus.MenuItem do
7774
|> validate_required([:parent_id, :position])
7875
end
7976

80-
def process_update_params(params) do
81-
case Enum.find(params, fn {k, v} -> k in @enitity_fields and not is_nil(v) end) do
82-
# If a new entity is provided, we need to nullify the old entities by
83-
# explicitly setting them to nil. We can set all of them to nil because
84-
# when Ecto generates the update query, it will explicitly update only the
85-
# existing field as it will differ from the value in the struct.
86-
# Put back the provided entity id in the params after nullifying.
87-
{k, v} -> params |> Map.merge(@enitity_fields_nil_map) |> Map.put(k, v)
88-
nil -> params
89-
end
90-
end
91-
9277
def update(menu, attrs) do
93-
%__MODULE__{}
94-
|> cast(attrs, [
95-
# Who this item belongs to
96-
:parent_id,
97-
# What is the item. There's check constraint on the DB level that only one
98-
# of these can be set
99-
:menu_id,
100-
:query_id,
101-
:dashboard_id,
102-
# The position of the item in the menu
103-
:position
104-
])
78+
menu
79+
# Do not allow to change the entity. Prefer deleting and adding a new item instead.
80+
|> cast(attrs, [:parent_id, :position])
10581
end
10682

10783
# Private functions

lib/sanbase/run_examples.ex

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,7 @@ defmodule Sanbase.RunExamples do
784784

785785
defp do_run(:menus) do
786786
user = Sanbase.Factory.insert(:user)
787+
user2 = Sanbase.Factory.insert(:user)
787788

788789
{:ok, query} = Sanbase.Queries.create_query(%{name: "Query"}, user.id)
789790
{:ok, dashboard} = Sanbase.Dashboards.create_dashboard(%{name: "Dashboard"}, user.id)
@@ -803,6 +804,13 @@ defmodule Sanbase.RunExamples do
803804
user.id
804805
)
805806

807+
# Cannot create item on non-owner menu
808+
{:error, _} =
809+
Sanbase.Menus.create_menu_item(
810+
%{parent_id: menu.id, dashboard_id: dashboard.id, position: 2},
811+
user2.id
812+
)
813+
806814
{:ok, sub_menu} =
807815
Sanbase.Menus.create_menu(
808816
%{
@@ -814,6 +822,9 @@ defmodule Sanbase.RunExamples do
814822
user.id
815823
)
816824

825+
{:ok, _} = Sanbase.Menus.update_menu(sub_menu.id, %{name: "MySubMenuNewName"}, user.id)
826+
# Cannot update non-owner menu
827+
{:error, _} = Sanbase.Menus.update_menu(sub_menu.id, %{name: "hehe"}, user2.id)
817828
{:ok, fetched_menu} = Sanbase.Menus.get_menu(menu.id, user.id)
818829

819830
menu_id = menu.id
@@ -829,7 +840,7 @@ defmodule Sanbase.RunExamples do
829840
description: "MySubDescription",
830841
id: ^sub_menu_id,
831842
menu_items: [],
832-
name: "MySubMenu",
843+
name: "MySubMenuNewName",
833844
position: 1,
834845
type: :menu
835846
},

0 commit comments

Comments
 (0)