Skip to content

Commit bfb9c9e

Browse files
committed
Working API
1 parent d894371 commit bfb9c9e

File tree

7 files changed

+166
-104
lines changed

7 files changed

+166
-104
lines changed

lib/sanbase/menu/menus.ex

Lines changed: 51 additions & 48 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,10 @@ 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})
102+
|> IO.inspect(label: "100", limit: :infinity)
99103

100104
Ecto.Multi.new()
101105
|> Ecto.Multi.run(:create_menu, fn _repo, _changes ->
@@ -121,19 +125,13 @@ defmodule Sanbase.Menus do
121125
)
122126
end
123127
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-
# )
128+
|> Ecto.Multi.run(:get_menu_with_preloads, fn _repo, %{create_menu: menu} ->
129+
# There would be no menu items, but this will help to set the menu items to []
130+
# instead of getting an error when trying to iterate them because they're set to <not preloaded>
131+
get_menu(menu.id, user_id)
132+
end)
135133
|> Repo.transaction()
136-
|> process_transaction_result(:create_menu)
134+
|> process_transaction_result(:get_menu_with_preloads)
137135
end
138136

139137
@doc ~s"""
@@ -152,7 +150,7 @@ defmodule Sanbase.Menus do
152150
query = Menu.update(menu, params)
153151
Repo.update(query)
154152
end)
155-
|> Ecto.Multi.run(:get_menu_with_preloads, fn _repo, %{create_menu: menu} ->
153+
|> Ecto.Multi.run(:get_menu_with_preloads, fn _repo, %{update_menu: menu} ->
156154
get_menu(menu.id, user_id)
157155
end)
158156
|> Repo.transaction()
@@ -235,44 +233,49 @@ defmodule Sanbase.Menus do
235233
@doc ~s"""
236234
Update an existing menu item.
237235
"""
238-
@spec update_menu_item(menu_id, menu_item_id, update_menu_item_params, user_id) ::
236+
@spec update_menu_item(menu_item_id, update_menu_item_params, user_id) ::
239237
{:ok, Menu.t()} | {:error, String.t()}
240-
def update_menu_item(menu_id, menu_item_id, params, user_id) do
238+
def update_menu_item(menu_item_id, params, user_id) do
241239
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)
246240
|> Ecto.Multi.run(:get_menu_item_for_update, fn _repo, _changes ->
247-
get_menu_item_for_update(menu_item_id)
241+
get_menu_item_for_update(menu_item_id, user_id)
248242
end)
243+
|> Ecto.Multi.run(
244+
:maybe_update_items_positions,
245+
fn _repo, %{get_menu_item_for_update: menu_item} ->
246+
case Map.get(params, :position) do
247+
nil ->
248+
{:ok, nil}
249+
250+
position when is_integer(position) ->
251+
# If `position` is specified, bump all the positions bigger than it by 1 in
252+
# order to avoid having multiple items with the same position.
253+
{:ok, {_, nil}} = inc_all_positions_after(menu_item.parent_id, position)
254+
{:ok, position}
255+
end
256+
end
257+
)
249258
|> 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)
252259
query = MenuItem.update(menu_item, params)
253260
Repo.update(query)
254261
end)
255-
|> Ecto.Multi.run(:get_menu_with_preloads, fn _repo, %{get_menu_for_update: menu} ->
256-
get_menu(menu.id, user_id)
262+
|> Ecto.Multi.run(:get_menu_with_preloads, fn _repo, %{update_menu_item: menu_item} ->
263+
get_menu(menu_item.parent_id, user_id)
257264
end)
258265
|> Repo.transaction()
259266
|> process_transaction_result(:get_menu_with_preloads)
260267
end
261268

262-
def delete_menu_item(menu_id, menu_item_id, user_id) do
269+
def delete_menu_item(menu_item_id, user_id) do
263270
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)
268271
|> Ecto.Multi.run(:get_menu_item, fn _repo, _changes ->
269-
get_menu_item_for_update(menu_item_id)
272+
get_menu_item_for_update(menu_item_id, user_id)
270273
end)
271274
|> Ecto.Multi.run(:delete_menu_item, fn _repo, %{get_menu_item: menu_item} ->
272275
Repo.delete(menu_item)
273276
end)
274-
|> Ecto.Multi.run(:get_menu_with_preloads, fn _repo, %{create_menu: menu} ->
275-
get_menu(menu.id, user_id)
277+
|> Ecto.Multi.run(:get_menu_with_preloads, fn _repo, %{delete_menu_item: menu_item} ->
278+
get_menu(menu_item.parent_id, user_id)
276279
end)
277280
|> Repo.transaction()
278281
|> process_transaction_result(:get_menu_with_preloads)
@@ -289,11 +292,8 @@ defmodule Sanbase.Menus do
289292
end
290293
end
291294

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)
295+
defp get_menu_item_for_update(menu_item_id, user_id) do
296+
query = MenuItem.get_for_update(menu_item_id, user_id)
297297

298298
case Repo.one(query) do
299299
nil -> {:error, "Menu item does not exist"}
@@ -339,14 +339,17 @@ defmodule Sanbase.Menus do
339339
defp get_menu_items(%Menu{menu_items: list}) when is_list(list) do
340340
list
341341
|> Enum.map(fn
342-
%{query: %{id: _} = map, position: p} ->
343-
Map.take(map, [:id, :name, :description]) |> Map.merge(%{type: :query, position: p})
342+
%{id: menu_item_id, query: %{id: _} = map, position: position} ->
343+
Map.take(map, [:id, :name, :description])
344+
|> Map.merge(%{type: :query, position: position, menu_item_id: menu_item_id})
344345

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

348-
%{menu: %{id: _} = map, position: p} ->
349-
menu_to_simple_map(map) |> Map.put(:position, p)
350+
%{id: menu_item_id, menu: %{id: _} = map, position: position} ->
351+
menu_to_simple_map(map)
352+
|> Map.merge(%{type: :menu, position: position, menu_item_id: menu_item_id})
350353
end)
351354
end
352355
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
},

lib/sanbase_web/graphql/resolvers/menu_resolver.ex.ex

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule SanbaseWeb.Graphql.Resolvers.MenuResolver do
22
alias Sanbase.Menus
33

44
# Menu CRUD
5+
56
def get_menu(
67
_root,
78
%{id: menu_id},
@@ -15,15 +16,15 @@ defmodule SanbaseWeb.Graphql.Resolvers.MenuResolver do
1516
end
1617
end
1718

18-
def create_menu(_root, %{name: _} = args, %{context: %{auth: %{current_user: current_user}}}) do
19-
case Menus.create_menu(args, current_user) do
19+
def create_menu(_root, %{name: _} = param, %{context: %{auth: %{current_user: current_user}}}) do
20+
case Menus.create_menu(param, current_user.id) do
2021
{:ok, menu} -> {:ok, Menus.menu_to_simple_map(menu)}
2122
{:error, reason} -> {:error, reason}
2223
end
2324
end
2425

25-
def update_menu(_root, %{id: id} = args, %{context: %{auth: %{current_user: current_user}}}) do
26-
case Menus.update_menu(id, args, current_user.id) do
26+
def update_menu(_root, %{id: id} = params, %{context: %{auth: %{current_user: current_user}}}) do
27+
case Menus.update_menu(id, params, current_user.id) do
2728
{:ok, menu} -> {:ok, Menus.menu_to_simple_map(menu)}
2829
{:error, reason} -> {:error, reason}
2930
end
@@ -36,15 +37,62 @@ defmodule SanbaseWeb.Graphql.Resolvers.MenuResolver do
3637
end
3738
end
3839

39-
# MenuItem CRUD
40+
# MenuItem C~R~UD
4041

41-
def create_menu_item(_root, _args, %{context: %{auth: %{current_user: current_user}}}) do
42-
Menus.create_menu_item()
42+
def create_menu_item(_root, %{} = args, %{
43+
context: %{auth: %{current_user: current_user}}
44+
}) do
45+
with {:ok, params} <- create_menu_item_params(args) do
46+
case Menus.create_menu_item(params, current_user.id) do
47+
{:ok, menu} -> {:ok, Menus.menu_to_simple_map(menu)}
48+
{:error, reason} -> {:error, reason}
49+
end
50+
end
51+
end
52+
53+
def update_menu_item(_root, %{menu_item_id: id, position: position}, %{
54+
context: %{auth: %{current_user: current_user}}
55+
}) do
56+
case Menus.update_menu_item(id, %{position: position}, current_user.id) do
57+
{:ok, menu} -> {:ok, Menus.menu_to_simple_map(menu)}
58+
{:error, reason} -> {:error, reason}
59+
end
4360
end
4461

45-
def update_menu_item(_root, _args, %{context: %{auth: %{current_user: current_user}}}) do
62+
def delete_menu_item(_root, %{id: id}, %{context: %{auth: %{current_user: current_user}}}) do
63+
case Menus.delete_menu_item(id, current_user.id) do
64+
{:ok, menu} -> {:ok, Menus.menu_to_simple_map(menu)}
65+
{:error, reason} -> {:error, reason}
66+
end
4667
end
4768

48-
def delete_menu_item(_root, _args, %{context: %{auth: %{current_user: current_user}}}) do
69+
# Private functions
70+
71+
defp create_menu_item_params(%{parent_menu_id: parent_menu_id, entity: entity} = args) do
72+
with {:ok, entity_params} <- entity_to_params(entity) do
73+
params =
74+
%{parent_id: parent_menu_id}
75+
|> Map.merge(entity_params)
76+
|> Map.merge(Map.take(args, [:position]))
77+
78+
{:ok, params}
79+
end
80+
end
81+
82+
defp create_menu_item_params(_),
83+
do:
84+
{:error,
85+
"Create menu item parameters are missing the required parent_menu_id and/or entity fields"}
86+
87+
defp entity_to_params(map) do
88+
params = Map.reject(map, fn {_k, v} -> is_nil(v) end)
89+
90+
case map_size(map) do
91+
1 ->
92+
{:ok, params}
93+
94+
_ ->
95+
{:error, "The entity field must contain exactly one key-value pair with non-null value"}
96+
end
4997
end
5098
end

lib/sanbase_web/graphql/schema.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ defmodule SanbaseWeb.Graphql.Schema do
5454
import_types(Graphql.IntercomTypes)
5555
import_types(Graphql.MarketSegmentTypes)
5656
import_types(Graphql.MarketTypes)
57+
import_types(Graphql.MenuTypes)
5758
import_types(Graphql.MetricTypes)
5859
import_types(Graphql.MonitoredTwitterHandleTypes)
5960
import_types(Graphql.NftTypes)
@@ -208,6 +209,7 @@ defmodule SanbaseWeb.Graphql.Schema do
208209
import_fields(:intercom_mutations)
209210
import_fields(:landing_emails_mutations)
210211
import_fields(:linked_user_mutations)
212+
import_fields(:menu_mutations)
211213
import_fields(:moderation_mutations)
212214
import_fields(:monitored_twitter_handle_mutations)
213215
import_fields(:project_chart_mutations)

0 commit comments

Comments
 (0)