query.ex (10880B)
1 # Zenflows is designed to implement the Valueflows vocabulary, 2 # written and maintained by srfsh <info@dyne.org>. 3 # Copyright (C) 2021-2023 Dyne.org foundation <foundation@dyne.org>. 4 # 5 # This program is free software: you can redistribute it and/or modify 6 # it under the terms of the GNU Affero General Public License as published by 7 # the Free Software Foundation, either version 3 of the License, or 8 # (at your option) any later version. 9 # 10 # This program is distributed in the hope that it will be useful, 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 # GNU Affero General Public License for more details. 14 # 15 # You should have received a copy of the GNU Affero General Public License 16 # along with this program. If not, see <https://www.gnu.org/licenses/>. 17 18 defmodule Zenflows.VF.Proposal.Query do 19 @moduledoc false 20 21 import Ecto.Query 22 23 alias Ecto.{Changeset, Queryable} 24 alias Zenflows.DB.{ID, Page, Schema, Validate} 25 alias Zenflows.VF.Proposal 26 27 @spec all(Page.t()) :: {:ok, Queryable.t()} | {:error, Changeset.t()} 28 def all(%{filter: nil}), do: {:ok, Proposal} 29 def all(%{filter: params}) do 30 with {:ok, filters} <- all_validate(params) do 31 {:ok, Enum.reduce(filters, Proposal, &all_f(&2, &1)) |> distinct([x], x.id)} 32 end 33 end 34 35 @spec all_f(Queryable.t(), {atom(), term()}) :: Queryable.t() 36 defp all_f(q, {:primary_intents_resource_inventoried_as_conforms_to, v}) do 37 q 38 |> join(:primary_intents_resource_inventoried_as) 39 |> where([primary_intents_resource_inventoried_as: r], r.conforms_to_id in ^v) 40 end 41 defp all_f(q, {:or_primary_intents_resource_inventoried_as_conforms_to, v}) do 42 q 43 |> join(:primary_intents_resource_inventoried_as) 44 |> or_where([primary_intents_resource_inventoried_as: r], r.conforms_to_id in ^v) 45 end 46 defp all_f(q, {:primary_intents_resource_inventoried_as_primary_accountable, v}) do 47 q 48 |> join(:primary_intents_resource_inventoried_as) 49 |> where([primary_intents_resource_inventoried_as: r], r.primary_accountable_id in ^v) 50 end 51 defp all_f(q, {:or_primary_intents_resource_inventoried_as_primary_accountable, v}) do 52 q 53 |> join(:primary_intents_resource_inventoried_as) 54 |> or_where([primary_intents_resource_inventoried_as: r], r.primary_accountable_id in ^v) 55 end 56 defp all_f(q, {:primary_intents_resource_inventoried_as_classified_as, v}) do 57 q 58 |> join(:primary_intents_resource_inventoried_as) 59 |> where([primary_intents_resource_inventoried_as: r], fragment("? @> ?", r.classified_as, ^v)) 60 end 61 defp all_f(q, {:or_primary_intents_resource_inventoried_as_classified_as, v}) do 62 q 63 |> join(:primary_intents_resource_inventoried_as) 64 |> or_where([primary_intents_resource_inventoried_as: r], fragment("? @> ?", r.classified_as, ^v)) 65 end 66 defp all_f(q, {:primary_intents_resource_inventoried_as_name, v}) do 67 q 68 |> join(:primary_intents_resource_inventoried_as) 69 |> where([primary_intents_resource_inventoried_as: r], ilike(r.name, ^"%#{v}%")) 70 end 71 defp all_f(q, {:or_primary_intents_resource_inventoried_as_name, v}) do 72 q 73 |> join(:primary_intents_resource_inventoried_as) 74 |> or_where([primary_intents_resource_inventoried_as: r], ilike(r.name, ^"%#{v}%")) 75 end 76 defp all_f(q, {:primary_intents_resource_inventoried_as_note, v}) do 77 q 78 |> join(:primary_intents_resource_inventoried_as) 79 |> where([primary_intents_resource_inventoried_as: r], ilike(r.note, ^"%#{v}%")) 80 end 81 defp all_f(q, {:or_primary_intents_resource_inventoried_as_note, v}) do 82 q 83 |> join(:primary_intents_resource_inventoried_as) 84 |> or_where([primary_intents_resource_inventoried_as: r], ilike(r.note, ^"%#{v}%")) 85 end 86 defp all_f(q, {:primary_intents_resource_inventoried_as_id, v}) do 87 q 88 |> join(:primary_intents_resource_inventoried_as) 89 |> where([primary_intents_resource_inventoried_as: r], r.id in ^v) 90 end 91 defp all_f(q, {:or_primary_intents_resource_inventoried_as_id, v}) do 92 q 93 |> join(:primary_intents_resource_inventoried_as) 94 |> or_where([primary_intents_resource_inventoried_as: r], r.id in ^v) 95 end 96 defp all_f(q, {:status, v}) do 97 cond = case v do 98 :refused -> dynamic([intents: i, satisfactions: s], fragment("every(?)", i.finished) and count(s.id) == 0) 99 :accepted -> dynamic([intents: i, satisfactions: s], count(i.id) == count(s.id)) 100 :pending -> dynamic([intents: i, satisfactions: s], not fragment("every(?)", i.finished) and count(i.id) != count(s.id)) 101 end 102 q 103 |> join(:inner, [x], pi in assoc(x, :publishes), as: :proposed_intents) 104 |> join(:inner, [proposed_intents: pi], i in assoc(pi, :publishes), as: :intents) 105 |> join(:left, [intents: i], s in assoc(i, :satisfied_by), as: :satisfactions) 106 |> group_by([x], x.id) 107 |> having(^cond) 108 end 109 defp all_f(q, {:or_status, v}) do 110 cond = case v do 111 :refused -> dynamic([intents: i, satisfactions: s], fragment("every(?)", i.finished) and count(s.id) == 0) 112 :accepted -> dynamic([intents: i, satisfactions: s], count(i.id) == count(s.id)) 113 :pending -> dynamic([intents: i, satisfactions: s], not fragment("every(?)", i.finished) and count(i.id) != count(s.id)) 114 end 115 q 116 |> join(:inner, [x], pi in assoc(x, :publishes), as: :proposed_intents) 117 |> join(:inner, [proposed_intents: pi], i in assoc(pi, :publishes), as: :intents) 118 |> join(:left, [intents: i], s in assoc(i, :satisfied_by), as: :satisfactions) 119 |> group_by([x], x.id) 120 |> or_having(^cond) 121 end 122 defp all_f(q, {:not_status, v}) do 123 cond = case v do 124 :refused -> dynamic([intents: i, satisfactions: s], not (fragment("every(?)", i.finished) and count(s.id) == 0)) 125 :accepted -> dynamic([intents: i, satisfactions: s], not (count(i.id) == count(s.id))) 126 :pending -> dynamic([intents: i, satisfactions: s], not (not fragment("every(?)", i.finished) and count(i.id) != count(s.id))) 127 end 128 q 129 |> join(:inner, [x], pi in assoc(x, :publishes), as: :proposed_intents) 130 |> join(:inner, [proposed_intents: pi], i in assoc(pi, :publishes), as: :intents) 131 |> join(:left, [intents: i], s in assoc(i, :satisfied_by), as: :satisfactions) 132 |> group_by([x], x.id) 133 |> having(^cond) 134 end 135 136 # join primary_intents 137 @spec join(Queryable.t(), atom()) :: Queryable.t() 138 defp join(q, :primary_intents) do 139 if has_named_binding?(q, :primary_intents), 140 do: q, 141 else: join(q, :inner, [x], pi in assoc(x, :primary_intents), as: :primary_intents) 142 end 143 # join resource_inventoried_as through primary_intents above 144 defp join(q, :primary_intents_resource_inventoried_as) do 145 q = join(q, :primary_intents) 146 if has_named_binding?(q, :primary_intents_resource_inventoried_as), 147 do: q, 148 else: join(q, :inner, [primary_intents: pi], r in assoc(pi, :resource_inventoried_as), 149 as: :primary_intents_resource_inventoried_as) 150 end 151 152 @spec all_validate(Schema.params()) 153 :: {:ok, Changeset.data()} | {:error, Changeset.t()} 154 defp all_validate(params) do 155 {%{}, %{ 156 primary_intents_resource_inventoried_as_conforms_to: {:array, ID}, 157 or_primary_intents_resource_inventoried_as_conforms_to: {:array, ID}, 158 primary_intents_resource_inventoried_as_primary_accountable: {:array, ID}, 159 or_primary_intents_resource_inventoried_as_primary_accountable: {:array, ID}, 160 primary_intents_resource_inventoried_as_classified_as: {:array, :string}, 161 or_primary_intents_resource_inventoried_as_classified_as: {:array, :string}, 162 primary_intents_resource_inventoried_as_name: :string, 163 or_primary_intents_resource_inventoried_as_name: :string, 164 primary_intents_resource_inventoried_as_note: :string, 165 or_primary_intents_resource_inventoried_as_note: :string, 166 primary_intents_resource_inventoried_as_id: {:array, ID}, 167 or_primary_intents_resource_inventoried_as_id: {:array, ID}, 168 status: {:parameterized, Ecto.Enum, Ecto.Enum.init(values: ~w[pending accepted refused]a)}, 169 or_status: {:parameterized, Ecto.Enum, Ecto.Enum.init(values: ~w[pending accepted refused]a)}, 170 not_status: {:parameterized, Ecto.Enum, Ecto.Enum.init(values: ~w[pending accepted refused]a)}, 171 }} 172 |> Changeset.cast(params, ~w[ 173 primary_intents_resource_inventoried_as_conforms_to 174 or_primary_intents_resource_inventoried_as_conforms_to 175 primary_intents_resource_inventoried_as_primary_accountable 176 or_primary_intents_resource_inventoried_as_primary_accountable 177 primary_intents_resource_inventoried_as_classified_as 178 or_primary_intents_resource_inventoried_as_classified_as 179 primary_intents_resource_inventoried_as_name 180 or_primary_intents_resource_inventoried_as_name 181 primary_intents_resource_inventoried_as_note 182 or_primary_intents_resource_inventoried_as_note 183 primary_intents_resource_inventoried_as_id 184 or_primary_intents_resource_inventoried_as_id 185 status or_status not_status 186 ]a) 187 |> Validate.class(:primary_intents_resource_inventoried_as_conforms_to) 188 |> Validate.class(:or_primary_intents_resource_inventoried_as_conforms_to) 189 |> Validate.exist_nand([:primary_intents_resource_inventoried_as_conforms_to, 190 :or_primary_intents_resource_inventoried_as_conforms_to]) 191 |> Validate.class(:primary_intents_resource_inventoried_as_primary_accountable) 192 |> Validate.class(:or_primary_intents_resource_inventoried_as_primary_accountable) 193 |> Validate.exist_nand([:primary_intents_resource_inventoried_as_primary_accountable, 194 :or_primary_intents_resource_inventoried_as_primary_accountable]) 195 |> Validate.class(:primary_intents_resource_inventoried_as_classified_as) 196 |> Validate.class(:or_primary_intents_resource_inventoried_as_classified_as) 197 |> Validate.exist_nand([:primary_intents_resource_inventoried_as_classified_as, 198 :or_primary_intents_resource_inventoried_as_classified_as]) 199 |> Validate.escape_like(:primary_intents_resource_inventoried_as_name) 200 |> Validate.escape_like(:or_primary_intents_resource_inventoried) 201 |> Validate.name(:primary_intents_resource_inventoried_as_name) 202 |> Validate.name(:or_primary_intents_resource_inventoried_as_name) 203 |> Validate.exist_nand([:primary_intents_resource_inventoried_as_name, 204 :or_primary_intents_resource_inventoried_as_name]) 205 |> Validate.escape_like(:primary_intents_resource_inventoried_as_note) 206 |> Validate.escape_like(:or_primary_intents_resource_inventoried) 207 |> Validate.note(:primary_intents_resource_inventoried_as_note) 208 |> Validate.note(:or_primary_intents_resource_inventoried_as_note) 209 |> Validate.exist_nand([:primary_intents_resource_inventoried_as_note, 210 :or_primary_intents_resource_inventoried_as_note]) 211 |> Validate.class(:primary_intents_resource_inventoried_as_id) 212 |> Validate.class(:or_primary_intents_resource_inventoried_as_id) 213 |> Validate.exist_nand([:primary_intents_resource_inventoried_as_id, 214 :or_primary_intents_resource_inventoried_as_id]) 215 |> Validate.exist_nand([:status, :or_status, :not_status]) 216 |> Changeset.apply_action(nil) 217 end 218 219 @spec status(Schema.id()) :: Queryable.t() 220 def status(id) do 221 from p in Proposal, 222 where: p.id == ^id, 223 join: pi in assoc(p, :publishes), 224 join: i in assoc(pi, :publishes), 225 left_join: s in assoc(i, :satisfied_by), 226 select: {fragment("every(?)", i.finished), count(i.id), count(s.id)} 227 end 228 end