Skip to content

Commit

Permalink
Remove unsupported dot-notation in partial requests
Browse files Browse the repository at this point in the history
  • Loading branch information
derrickreimer committed Jul 25, 2024
1 parent 1f9463f commit 33f6db8
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 140 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Bug Fixes

- Remove unsupported dot-notation in partial requests (related to [inertiajs/inertia-laravel#641](https://github.com/inertiajs/inertia-laravel/pull/641))

## 0.9.0

### Bug Fixes
Expand Down
133 changes: 39 additions & 94 deletions lib/inertia/controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ defmodule Inertia.Controller do
props =
shared
|> Map.merge(props)
|> resolve_props(only: only, except: except)
|> apply_filters(only, except)
|> resolve_props()
|> maybe_put_flash(conn)

conn
Expand All @@ -167,108 +168,52 @@ defmodule Inertia.Controller do

# Private helpers

defp resolve_props(map, opts) when is_map(map) and not is_struct(map) do
map =
map
|> Map.to_list()
|> Enum.reduce([], fn {key, value}, acc ->
path = if opts[:path], do: "#{opts[:path]}.#{key}", else: to_string(key)
opts = Keyword.put(opts, :path, path)
resolved_value = resolve_props(value, opts)

if resolved_value == :skip do
acc
else
[{key, resolved_value} | acc]
end
end)
|> Map.new()

if keep_map?(opts, map) do
map
else
:skip
end
end

defp resolve_props({:lazy, value}, opts) do
if Enum.member?(opts[:only], opts[:path]) do
resolve_props(value, opts)
else
:skip
end
end

defp resolve_props({:keep, value}, opts) do
opts = Keyword.put(opts, :keep, true)
resolve_props(value, opts)
end

defp resolve_props(fun, opts) when is_function(fun, 0) do
if skip?(opts) do
:skip
else
fun.()
end
end

defp resolve_props(value, opts) do
if skip?(opts) do
:skip
else
value
end
defp apply_filters(props, only, _except) when length(only) > 0 do
props
|> Enum.filter(fn {key, value} ->
case value do
{:keep, _} -> true
_ -> Enum.member?(only, to_string(key))
end
end)
|> Map.new()
end

defp keep_map?(opts, map) do
path = opts[:path]
only = opts[:only]
except = opts[:except]
keep = opts[:keep]

cond do
# KEEP if the value is an "always" prop
keep -> true
# KEEP if this is the root props object
is_nil(path) -> true
# KEEP if the map is not empty
!Enum.empty?(map) -> true
# KEEP if this is a full page load (not a partial load)
Enum.empty?(only) && Enum.empty?(except) -> true
# If restricted by `only`, KEEP if explicitly included
!Enum.empty?(only) -> only_covers_path?(only, path)
# If restricted by `except`, KEEP unless explicitly excluded
!Enum.empty?(except) -> !Enum.member?(except, path)
# Otherwise, eliminate the object
true -> false
end
defp apply_filters(props, _only, except) when length(except) > 0 do
props
|> Enum.filter(fn {key, value} ->
case value do
{:keep, _} -> true
_ -> !Enum.member?(except, to_string(key))
end
end)
|> Map.new()
end

defp skip?(opts) do
path = opts[:path]
only = opts[:only]
except = opts[:except]
keep = opts[:keep]

cond do
keep -> false
length(only) > 0 && !only_covers_path?(only, path) -> true
length(except) > 0 && Enum.member?(except, path) -> true
true -> false
end
defp apply_filters(props, _only, _except) do
props
|> Enum.filter(fn {_key, value} ->
case value do
{:lazy, _} -> false
_ -> true
end
end)
|> Map.new()
end

# This helper determines if the list of "only" paths includes
# a given path. For example, if only is ["a"] and path is "a.b.c",
# this returns true, because "a.b.c" is nested under "a".
defp only_covers_path?(only, path) do
parts = String.split(path, ".")

Enum.any?(1..length(parts), fn amount ->
Enum.member?(only, Enum.join(Enum.take(parts, amount), "."))
defp resolve_props(map) when is_map(map) and not is_struct(map) do
map
|> Enum.reduce([], fn {key, value}, acc ->
[{key, resolve_props(value)} | acc]
end)
|> Map.new()
end

defp resolve_props({:lazy, value}), do: resolve_props(value)
defp resolve_props({:keep, value}), do: resolve_props(value)
defp resolve_props(fun) when is_function(fun, 0), do: fun.()
defp resolve_props(value), do: value

# Skip putting flash in the props if there's already `:flash` key assigned.
# Otherwise, put the flash in the props.
defp maybe_put_flash(%{flash: _} = props, _conn), do: props
Expand Down
61 changes: 15 additions & 46 deletions test/inertia_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -218,40 +218,13 @@ defmodule InertiaTest do
|> put_req_header("x-inertia", "true")
|> put_req_header("x-inertia-version", @current_version)
|> put_req_header("x-inertia-partial-component", "Home")
|> put_req_header("x-inertia-partial-data", "a")
|> get(~p"/nested")

assert json_response(conn, 200) == %{
"component" => "Home",
"props" => %{
"a" => %{
"b" => %{"c" => "c", "d" => "d", "e" => %{"f" => "f", "g" => "g", "h" => %{}}}
},
"errors" => %{},
"flash" => %{}
},
"url" => "/nested",
"version" => @current_version
}
end

test "deep partial 'only' reloads", %{conn: conn} do
conn =
conn
|> put_req_header("x-inertia", "true")
|> put_req_header("x-inertia-version", @current_version)
|> put_req_header("x-inertia-partial-component", "Home")
|> put_req_header("x-inertia-partial-data", "a.b.c,a.b.e.f")
|> get(~p"/nested")
|> put_req_header("x-inertia-partial-data", "b")
|> get(~p"/always")

assert json_response(conn, 200) == %{
"component" => "Home",
"props" => %{
"a" => %{"b" => %{"c" => "c", "e" => %{"f" => "f"}}},
"errors" => %{},
"flash" => %{}
},
"url" => "/nested",
"props" => %{"errors" => %{}, "flash" => %{}, "b" => "b", "important" => "stuff"},
"url" => "/always",
"version" => @current_version
}
end
Expand All @@ -262,17 +235,13 @@ defmodule InertiaTest do
|> put_req_header("x-inertia", "true")
|> put_req_header("x-inertia-version", @current_version)
|> put_req_header("x-inertia-partial-component", "Home")
|> put_req_header("x-inertia-partial-except", "a.b.d,a.b.e.f,a.b.e.g,a.b.e.h")
|> get(~p"/nested")
|> put_req_header("x-inertia-partial-except", "b")
|> get(~p"/always")

assert json_response(conn, 200) == %{
"component" => "Home",
"props" => %{
"a" => %{"b" => %{"c" => "c", "e" => %{}}},
"errors" => %{},
"flash" => %{}
},
"url" => "/nested",
"props" => %{"a" => "a", "errors" => %{}, "flash" => %{}, "important" => "stuff"},
"url" => "/always",
"version" => @current_version
}
end
Expand Down Expand Up @@ -300,19 +269,19 @@ defmodule InertiaTest do
|> put_req_header("x-inertia", "true")
|> put_req_header("x-inertia-version", @current_version)
|> put_req_header("x-inertia-partial-component", "NonMatchingComponent")
|> put_req_header("x-inertia-partial-data", "a.b.c")
|> get(~p"/nested")
|> put_req_header("x-inertia-partial-data", "a")
|> get(~p"/always")

assert json_response(conn, 200) == %{
"component" => "Home",
"props" => %{
"a" => %{
"b" => %{"c" => "c", "e" => %{"f" => "f", "g" => "g", "h" => %{}}, "d" => "d"}
},
"a" => "a",
"errors" => %{},
"flash" => %{}
"flash" => %{},
"b" => "b",
"important" => "stuff"
},
"url" => "/nested",
"url" => "/always",
"version" => @current_version
}
end
Expand Down

0 comments on commit 33f6db8

Please sign in to comment.