commit 4ff13842464691001fbe98354b23828108efabc7
parent 1d9bb41b9792f9c912799bfb3d2c931acd8bae55
Author: sir fish <dev@srf.sh>
Date: Fri, 23 Sep 2022 14:00:52 +0000
Merge pull request #18 from dyne/srfsh/filter
Add more filters and improve filtering
Diffstat:
16 files changed, 431 insertions(+), 34 deletions(-)
diff --git a/src/zenflows/db/filter.ex b/src/zenflows/db/filter.ex
@@ -0,0 +1,28 @@
+# Zenflows is designed to implement the Valueflows vocabulary,
+# written and maintained by srfsh <info@dyne.org>.
+# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+defmodule Zenflows.DB.Filter do
+@moduledoc "Filtering helpers for Filter modules."
+
+@type params() :: %{atom() => term()}
+@type error() :: {:error, Changeset.t()}
+@type result() :: {:ok, Query.t()} | error()
+
+def escape_like(v) do
+ Regex.replace(~r/\\|%|_/, v, &"\\#{&1}")
+end
+end
diff --git a/src/zenflows/db/paging.ex b/src/zenflows/db/paging.ex
@@ -43,8 +43,7 @@ alias Zenflows.DB.{ID, Repo}
node: struct(),
}
-@type params() :: %{first: non_neg_integer(), after: ID.t()}
- | %{last: non_neg_integer, before: ID.t()}
+@type params() :: %{atom() => term()}
@spec def_page_size() :: non_neg_integer()
def def_page_size() do
diff --git a/src/zenflows/vf/agent/domain.ex b/src/zenflows/vf/agent/domain.ex
@@ -19,7 +19,7 @@ defmodule Zenflows.VF.Agent.Domain do
@moduledoc "Domain logic of Agents."
alias Zenflows.DB.{Paging, Repo}
-alias Zenflows.VF.Agent
+alias Zenflows.VF.{Agent, Agent.Filter}
@typep repo() :: Ecto.Repo.t()
@typep id() :: Zenflows.DB.Schema.id()
@@ -34,9 +34,11 @@ def one(repo, clauses) do
end
end
-@spec all(Paging.params()) :: Paging.result()
-def all(params) do
- Paging.page(Agent, params)
+@spec all(Paging.params()) :: Filter.error() | Paging.result()
+def all(params \\ %{}) do
+ with {:ok, q} <- Filter.filter(params[:filter] || %{}) do
+ Paging.page(q, params)
+ end
end
@spec preload(Agent.t(), :images | :primary_location) :: Agent.t()
diff --git a/src/zenflows/vf/agent/filter.ex b/src/zenflows/vf/agent/filter.ex
@@ -0,0 +1,55 @@
+# Zenflows is designed to implement the Valueflows vocabulary,
+# written and maintained by srfsh <info@dyne.org>.
+# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+defmodule Zenflows.VF.Agent.Filter do
+@moduledoc "Filtering logic of Agents."
+
+use Zenflows.DB.Schema
+
+import Ecto.Query
+
+alias Ecto.Query
+alias Zenflows.DB.Filter
+alias Zenflows.VF.{Agent, Validate}
+
+@type error() :: Filter.error()
+
+@spec filter(Filter.params()) :: Filter.result()
+def filter(params) do
+ case chgset(params) do
+ %{valid?: true, changes: c} ->
+ {:ok, Enum.reduce(c, Agent, &f(&2, &1))}
+ %{valid?: false} = cset ->
+ {:error, cset}
+ end
+end
+
+@spec f(Query.t(), {atom(), term()}) :: Query.t()
+defp f(q, {:name, v}),
+ do: where(q, [x], ilike(x.name, ^"%#{Filter.escape_like(v)}%"))
+
+embedded_schema do
+ field :name, :string
+end
+
+@spec chgset(params()) :: Changeset.t()
+defp chgset(params) do
+ %__MODULE__{}
+ |> Changeset.cast(params, [:name])
+ |> Validate.name(:name)
+end
+end
diff --git a/src/zenflows/vf/agent/type.ex b/src/zenflows/vf/agent/type.ex
@@ -70,6 +70,10 @@ object :agent_connection do
field :edges, non_null(list_of(non_null(:agent_edge)))
end
+input_object :agent_filter_params do
+ field :name, :string
+end
+
object :query_agent do
@desc "Loads details of the currently authenticated agent."
field :my_agent, :agent do
@@ -91,6 +95,7 @@ object :query_agent do
arg :after, :id
arg :last, :integer
arg :before, :id
+ arg :filter, :agent_filter_params
resolve &Resolv.agents/2
end
end
diff --git a/src/zenflows/vf/economic_resource/domain.ex b/src/zenflows/vf/economic_resource/domain.ex
@@ -18,13 +18,12 @@
defmodule Zenflows.VF.EconomicResource.Domain do
@moduledoc "Domain logic of EconomicResources."
-import Ecto.Query
-
alias Ecto.Multi
alias Zenflows.DB.{Paging, Repo}
alias Zenflows.VF.{
Action,
EconomicResource,
+ EconomicResource.Filter,
Measure,
}
@@ -44,20 +43,13 @@ def one(repo, clauses) do
end
end
-@spec all(Paging.params()) :: Paging.result()
-def all(params) do
- Paging.page(filter(params[:filter]), params)
-end
-
-defp filter(params) do
- Enum.reduce(params || %{}, EconomicResource, &filt(&2, &1))
+@spec all(Paging.params()) :: Filter.error() | Paging.result()
+def all(params \\ %{}) do
+ with {:ok, q} <- Filter.filter(params[:filter] || %{}) do
+ Paging.page(q, params)
+ end
end
-defp filt(q, {:classified_as, v}), do: where(q, [x], fragment("? @> ?", x.classified_as, ^v))
-defp filt(q, {:primary_accountable, v}), do: where(q, [x], x.primary_accountable_id in ^v)
-defp filt(q, {:custodian, v}), do: where(q, [x], x.custodian_id in ^v)
-defp filt(q, {:conforms_to, v}), do: where(q, [x], x.conforms_to_id in ^v)
-
@spec update(id(), params()) :: {:ok, EconomicResource.t()} | {:error, error()}
def update(id, params) do
Multi.new()
diff --git a/src/zenflows/vf/economic_resource/filter.ex b/src/zenflows/vf/economic_resource/filter.ex
@@ -0,0 +1,69 @@
+# Zenflows is designed to implement the Valueflows vocabulary,
+# written and maintained by srfsh <info@dyne.org>.
+# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+defmodule Zenflows.VF.EconomicResource.Filter do
+@moduledoc "Filtering logic of EconomicResources."
+
+use Zenflows.DB.Schema
+
+import Ecto.Query
+
+alias Ecto.Query
+alias Zenflows.DB.{Filter, ID}
+alias Zenflows.VF.{EconomicResource, Validate}
+
+@type error() :: Filter.error()
+
+@spec filter(Filter.params()) :: Filter.result()
+def filter(params) do
+ case chgset(params) do
+ %{valid?: true, changes: c} ->
+ {:ok, Enum.reduce(c, EconomicResource, &f(&2, &1))}
+ %{valid?: false} = cset ->
+ {:error, cset}
+ end
+end
+
+@spec f(Query.t(), {atom(), term()}) :: Query.t()
+defp f(q, {:classified_as, v}),
+ do: where(q, [x], fragment("? @> ?", x.classified_as, ^v))
+defp f(q, {:primary_accountable, v}),
+ do: where(q, [x], x.primary_accountable_id in ^v)
+defp f(q, {:custodian, v}),
+ do: where(q, [x], x.custodian_id in ^v)
+defp f(q, {:conforms_to, v}),
+ do: where(q, [x], x.conforms_to_id in ^v)
+
+embedded_schema do
+ field :classified_as, {:array, :string}
+ field :primary_accountable, {:array, ID}
+ field :custodian, {:array, ID}
+ field :conforms_to, {:array, ID}
+end
+
+@cast ~w[classified_as primary_accountable custodian conforms_to]a
+
+@spec chgset(params()) :: Changeset.t()
+defp chgset(params) do
+ %__MODULE__{}
+ |> Changeset.cast(params, @cast)
+ |> Validate.class(:classified_as)
+ |> Validate.class(:primary_accountable)
+ |> Validate.class(:custodian)
+ |> Validate.class(:conforms_to)
+end
+end
diff --git a/src/zenflows/vf/organization/domain.ex b/src/zenflows/vf/organization/domain.ex
@@ -18,11 +18,9 @@
defmodule Zenflows.VF.Organization.Domain do
@moduledoc "Domain logic of Organizations."
-import Ecto.Query
-
alias Ecto.Multi
alias Zenflows.DB.{Paging, Repo}
-alias Zenflows.VF.Organization
+alias Zenflows.VF.{Organization, Organization.Filter}
@typep repo() :: Ecto.Repo.t()
@typep chgset() :: Ecto.Changeset.t()
@@ -43,9 +41,11 @@ def one(repo, clauses) do
end
end
-@spec all(Paging.params()) :: Paging.result()
-def all(params) do
- Paging.page(where(Organization, type: :org), params)
+@spec all(Paging.params()) :: Filter.error() | Paging.result()
+def all(params \\ %{}) do
+ with {:ok, q} <- Filter.filter(params[:filter] || %{}) do
+ Paging.page(q, params)
+ end
end
@spec create(params()) :: {:ok, Organization.t()} | {:error, chgset()}
diff --git a/src/zenflows/vf/organization/filter.ex b/src/zenflows/vf/organization/filter.ex
@@ -0,0 +1,55 @@
+# Zenflows is designed to implement the Valueflows vocabulary,
+# written and maintained by srfsh <info@dyne.org>.
+# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+defmodule Zenflows.VF.Organization.Filter do
+@moduledoc "Filtering logic of Organizations."
+
+use Zenflows.DB.Schema
+
+import Ecto.Query
+
+alias Ecto.Query
+alias Zenflows.DB.Filter
+alias Zenflows.VF.{Organization, Validate}
+
+@type error() :: Filter.error()
+
+@spec filter(Filter.params()) :: Filter.result()
+def filter(params) do
+ case chgset(params) do
+ %{valid?: true, changes: c} ->
+ {:ok, Enum.reduce(c, where(Organization, type: :org), &f(&2, &1))}
+ %{valid?: false} = cset ->
+ {:error, cset}
+ end
+end
+
+@spec f(Query.t(), {atom(), term()}) :: Query.t()
+defp f(q, {:name, v}),
+ do: where(q, [x], ilike(x.name, ^"%#{Filter.escape_like(v)}%"))
+
+embedded_schema do
+ field :name, :string
+end
+
+@spec chgset(params()) :: Changeset.t()
+defp chgset(params) do
+ %__MODULE__{}
+ |> Changeset.cast(params, [:name])
+ |> Validate.name(:name)
+end
+end
diff --git a/src/zenflows/vf/organization/type.ex b/src/zenflows/vf/organization/type.ex
@@ -109,6 +109,10 @@ object :organization_connection do
field :edges, non_null(list_of(non_null(:organization_edge)))
end
+input_object :organization_filter_params do
+ field :name, :string
+end
+
object :query_organization do
@desc "Find an organization (group) agent by its ID."
field :organization, :organization do
@@ -125,6 +129,7 @@ object :query_organization do
arg :after, :id
arg :last, :integer
arg :before, :id
+ arg :filter, :organization_filter_params
resolve &Resolv.organizations/2
end
end
diff --git a/src/zenflows/vf/person/domain.ex b/src/zenflows/vf/person/domain.ex
@@ -22,7 +22,7 @@ import Ecto.Query
alias Ecto.Multi
alias Zenflows.DB.{Paging, Repo}
-alias Zenflows.VF.Person
+alias Zenflows.VF.{Person, Person.Filter}
@typep repo() :: Ecto.Repo.t()
@typep chgset() :: Ecto.Changeset.t()
@@ -42,9 +42,11 @@ def one(repo, clauses) do
end
end
-@spec all(Paging.params()) :: Paging.result()
-def all(params) do
- Paging.page(where(Person, type: :per), params)
+@spec all(Paging.params()) :: Filter.error() | Paging.result()
+def all(params \\ %{}) do
+ with {:ok, q} <- Filter.filter(params[:filter] || %{}) do
+ Paging.page(q, params)
+ end
end
@spec exists?(Keyword.t()) :: boolean()
diff --git a/src/zenflows/vf/person/filter.ex b/src/zenflows/vf/person/filter.ex
@@ -0,0 +1,92 @@
+# Zenflows is designed to implement the Valueflows vocabulary,
+# written and maintained by srfsh <info@dyne.org>.
+# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+defmodule Zenflows.VF.Person.Filter do
+@moduledoc "Filtering logic of Persons."
+
+use Zenflows.DB.Schema
+
+import Ecto.Query
+
+alias Ecto.Query
+alias Zenflows.DB.Filter
+alias Zenflows.VF.{Person, Validate}
+
+@type error() :: Filter.error()
+
+@spec filter(Filter.params()) :: Filter.result()
+def filter(params) do
+ case chgset(params) do
+ %{valid?: true, changes: c} ->
+ {:ok, Enum.reduce(c, where(Person, type: :per), &f(&2, &1))}
+ %{valid?: false} = cset ->
+ {:error, cset}
+ end
+end
+
+@spec f(Query.t(), {atom(), term()}) :: Query.t()
+defp f(q, {:name, v}),
+ do: where(q, [x], ilike(x.name, ^"%#{Filter.escape_like(v)}%"))
+defp f(q, {:user, v}),
+ do: where(q, [x], ilike(x.user, ^"%#{Filter.escape_like(v)}%"))
+defp f(q, {:user_or_name, v}) do
+ v = "%#{Filter.escape_like(v)}%"
+ where(q, [x], ilike(x.user, ^v) or ilike(x.name, ^v))
+end
+
+embedded_schema do
+ field :name, :string
+ field :user, :string
+ field :user_or_name, :string
+end
+
+@spec chgset(params()) :: Changeset.t()
+defp chgset(params) do
+ %__MODULE__{}
+ |> Changeset.cast(params, ~w[name user user_or_name]a)
+ |> Validate.name(:name)
+ |> Validate.name(:user)
+ |> Validate.name(:user_or_name)
+ |> user_or_name_mutex()
+end
+
+# Validate that `user_or_name` is mutually exclusive with either `user`
+# or `name`.
+@spec user_or_name_mutex(Changeset.t()) :: Changeset.t()
+defp user_or_name_mutex(cset) do
+ name = Changeset.get_change(cset, :name)
+ user = Changeset.get_change(cset, :user)
+ user_or_name = Changeset.get_change(cset, :user_or_name)
+
+ cond do
+ user_or_name && user ->
+ msg = "user-or-name and user can't be used together"
+ cset
+ |> Changeset.add_error(:user_or_name, msg)
+ |> Changeset.add_error(:user, msg)
+
+ user_or_name && name ->
+ msg = "user-or-name and name can't be used together"
+ cset
+ |> Changeset.add_error(:user_or_name, msg)
+ |> Changeset.add_error(:name, msg)
+
+ true ->
+ cset
+ end
+end
+end
diff --git a/src/zenflows/vf/person/type.ex b/src/zenflows/vf/person/type.ex
@@ -148,6 +148,12 @@ object :person_connection do
field :edges, non_null(list_of(non_null(:person_edge)))
end
+input_object :person_filter_params do
+ field :name, :string
+ field :user, :string
+ field :user_or_name, :string
+end
+
object :query_person do
@desc "Find a person by their ID."
field :person, :person do
@@ -164,6 +170,7 @@ object :query_person do
arg :after, :id
arg :last, :integer
arg :before, :id
+ arg :filter, :person_filter_params
resolve &Resolv.people/2
end
diff --git a/src/zenflows/vf/proposal/domain.ex b/src/zenflows/vf/proposal/domain.ex
@@ -20,7 +20,7 @@ defmodule Zenflows.VF.Proposal.Domain do
alias Ecto.Multi
alias Zenflows.DB.{Paging, Repo}
-alias Zenflows.VF.Proposal
+alias Zenflows.VF.{Proposal, Proposal.Filter}
@typep repo() :: Ecto.Repo.t()
@typep chgset() :: Ecto.Changeset.t()
@@ -38,9 +38,11 @@ def one(repo, clauses) do
end
end
-@spec all(Paging.params()) :: Paging.result()
-def all(params) do
- Paging.page(Proposal, params)
+@spec all(Paging.params()) :: Filter.error() | Paging.result()
+def all(params \\ %{}) do
+ with {:ok, q} <- Filter.filter(params[:filter] || %{}) do
+ Paging.page(q, params)
+ end
end
@spec create(params()) :: {:ok, Proposal.t()} | {:error, chgset()}
diff --git a/src/zenflows/vf/proposal/filter.ex b/src/zenflows/vf/proposal/filter.ex
@@ -0,0 +1,78 @@
+# Zenflows is designed to implement the Valueflows vocabulary,
+# written and maintained by srfsh <info@dyne.org>.
+# Copyright (C) 2021-2022 Dyne.org foundation <foundation@dyne.org>.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+defmodule Zenflows.VF.Proposal.Filter do
+@moduledoc "Filtering logic of Proposals."
+
+use Zenflows.DB.Schema
+
+import Ecto.Query
+
+alias Ecto.Query
+alias Zenflows.DB.{Filter, ID}
+alias Zenflows.VF.{Proposal, Validate}
+
+@type error() :: Filter.error()
+
+@spec filter(Filter.params()) :: Filter.result()
+def filter(params) do
+ case chgset(params) do
+ %{valid?: true, changes: c} ->
+ {:ok, Enum.reduce(c, Proposal, &f(&2, &1))}
+ %{valid?: false} = cset ->
+ {:error, cset}
+ end
+end
+
+@spec f(Query.t(), {atom(), term()}) :: Query.t()
+defp f(q, {:primary_intents_resource_inventoried_as_conforms_to, v}) do
+ q = if has_named_binding?(q, :pi),
+ do: q,
+ else: join(q, :inner, [x], pi in assoc(x, :primary_intents), as: :pi)
+ q = if has_named_binding?(q, :r),
+ do: q,
+ else: join(q, :inner, [pi: pi], r in assoc(pi, :resource_inventoried_as), as: :r)
+ where(q, [r: r], r.conforms_to_id in ^v)
+end
+defp f(q, {:primary_intents_resource_inventoried_as_primary_accountable, v}) do
+ q = if has_named_binding?(q, :pi),
+ do: q,
+ else: join(q, :inner, [x], pi in assoc(x, :primary_intents), as: :pi)
+ q = if has_named_binding?(q, :r),
+ do: q,
+ else: join(q, :inner, [pi: pi], r in assoc(pi, :resource_inventoried_as), as: :r)
+ where(q, [r: r], r.primary_accountable_id in ^v)
+end
+
+embedded_schema do
+ field :primary_intents_resource_inventoried_as_conforms_to, {:array, ID}
+ field :primary_intents_resource_inventoried_as_primary_accountable, {:array, ID}
+end
+
+@cast ~w[
+ primary_intents_resource_inventoried_as_conforms_to
+ primary_intents_resource_inventoried_as_primary_accountable
+]a
+
+@spec chgset(params()) :: Changeset.t()
+defp chgset(params) do
+ %__MODULE__{}
+ |> Changeset.cast(params, @cast)
+ |> Validate.class(:primary_intents_resource_inventoried_as_conforms_to)
+ |> Validate.class(:primary_intents_resource_inventoried_as_primary_accountable)
+end
+end
diff --git a/src/zenflows/vf/proposal/type.ex b/src/zenflows/vf/proposal/type.ex
@@ -130,6 +130,11 @@ object :proposal_connection do
field :edges, non_null(list_of(non_null(:proposal_edge)))
end
+input_object :proposal_filter_params do
+ field :primary_intents_resource_inventoried_as_conforms_to, list_of(non_null(:id))
+ field :primary_intents_resource_inventoried_as_primary_accountable, list_of(non_null(:id))
+end
+
object :query_proposal do
field :proposal, :proposal do
arg :id, non_null(:id)
@@ -142,6 +147,7 @@ object :query_proposal do
arg :after, :id
arg :last, :integer
arg :before, :id
+ arg :filter, :proposal_filter_params
resolve &Resolv.proposals/2
end