# Copyright 2023 Arkemis S.r.l. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Arke.Core.Query do @moduledoc """ Struct which defines a Query """ defstruct ~w[project arke persistence filters link orders offset limit]a @type t() :: %Arke.Core.Query{} defmodule LinkFilter do @moduledoc """ Base struct of a LinkFilter: - unit => %Arke.Core.`{arke_struct}`{} => the `arke_struct` of the unit which we want to filter on. See `Arke.Struct` - depth => integer => how many results we want to have at max - direction => "child" | "parent" => the direction the query will use to search, - type => the name of the connection we want to look at \n It is used to define a common filter struct which will be applied on an arke_link Query """ defstruct ~w[unit depth direction type]a @type t() :: %Arke.Core.Query.LinkFilter{} end defmodule Filter do @moduledoc """ Base struct of a Filter: - logic => :and | :or => the logic of the filter - negate => boolean => used to figure out whether the condition is to be denied - base_filters (refer to `Arke.Core.Query.BaseFilter`).\n It is used to define a Filter which will be applied on a Query """ defstruct ~w[logic negate base_filters]a @type t() :: %Arke.Core.Query.Filter{} end defmodule BaseFilter do @moduledoc """ Base struct of a BaseFilter: - parameter => %Arke.Core.Parameter.`ParameterType` => refer to `Arke.Core.Parameter` - operator => refer to [operators](#module-operators) - value => any => the value that the query will search for - negate => boolean => used to figure out whether the condition is to be denied \n It is used to keep the same logic structure across all the Filter """ defstruct ~w[parameter operator value negate]a @type t() :: %Arke.Core.Query.BaseFilter{} @doc """ Create a new BaseParameter ## Parameters - parameter => %Arke.Core.Parameter.`ParameterType` => refer to `Arke.Core.Parameter` - operator => refer to [operators](#module-operators) - value => any => the value that the query will search for - negate => boolean => used to figure out whether the condition is to be denied \n ## Example iex> filter = Arke.Core.Query.BaseFilter.new(parameter: "name", operator: "eq", value: "John", negate: false) ...> Arke.Core.Query.BaseFilter.new(person, :default) ## Return %Arke.Core.Query.BaseFilter{} """ @spec new( parameter :: Arke.Core.Parameter.ParameterType, operator :: atom(), value :: any, negate :: boolean ) :: Arke.Core.Query.BaseFilter.t() def new(parameter, operator, value, negate) do %__MODULE__{ parameter: parameter, operator: operator, value: cast_value(parameter, value), negate: negate } end defp cast_value(%Arke.Core.Unit{arke_id: arke_id} = parameter, value) do case arke_id do :datetime -> Arke.DatetimeHandler.parse_datetime(value) |> case do {:ok, value} -> value _ -> raise ArgumentError, message: "Invalid datetime format" end _ -> value end end defp cast_value(parameter, value), do: value end defmodule Order do @moduledoc """ Base struct Order: - parameter => %Arke.Core.Parameter.`ParameterType` => refer to `Arke.Core.Parameter` - direction => "child" | "parent" => the direction the query will use to search \n It is used to define the return order of a Query """ defstruct ~w[parameter direction]a @type t() :: %Arke.Core.Query.Order{} end @doc """ Create a new Query ## Parameters - arke => %Arke.Core.`{arke_struct}`{} => the `arke_struct` of the unit which we want to filter on. See `Arke.Struct` - project => :atom => identify the `Arke.Core.Project` ## Example iex> person = Arke.Core.Arke.new(id: "person", label: "Person") ...> Arke.Core.Query.new(person, :default) ## Return %Arke.Core.Query{} """ @spec new(arke :: %Arke.Core.Arke{}, project :: atom()) :: Arke.Core.Query.t() def new(arke, project), do: %__MODULE__{ project: project, arke: arke, persistence: nil, filters: [], orders: [], offset: nil, limit: nil } @doc """ Add a new link filter ## Parameters - query => refer to `new/1` - unit => %Arke.Core.`{arke_struct}`{} => the `arke_struct` of the unit which we want to filter on. See `Arke.Struct` - depth => integer => how many results we want to have at max - direction => "child" | "parent" => the direction the query will use to search - type => the name of the link we want to look at ## Example iex> person = Arke.Core.Arke.new(id: :person, label: "Person") ...> query = Arke.Core.Query.new(person, :arke_system) ...> Arke.Core.Query.add_link_filter(query, person, 0, "child", "link") ## Return %Arke.Core.Query{... link: %Arke.Core.Query.LinkFilter{} ... } """ @spec add_link_filter( query :: Arke.Core.Query.t(), unit :: Arke.Core.Unit.t(), depth :: integer(), direction :: atom(), connection_type :: String.t() ) :: Arke.Core.Query.t() def add_link_filter(query, unit, depth, direction, type) do %{query | link: %LinkFilter{unit: unit, depth: depth, direction: direction, type: type}} end @doc """ Add a filter to a query ## Parameters - query => refer to `new/1` - filter => refer to `Arke.Core.Query.BaseFilter` ## Example iex> person = Arke.Core.Arke.new(id: :person, label: "Person") ...> query = Arke.Core.Query.new(person, :arke_system) ...> parameter = Arke.Boundary.ParameterManager.get(:id,:arke_system) ...> filter = Arke.Core.Query.new_filter(parameter,:eq, "name", false) ...> Arke.Core.Query.add_filter(query, filter) ## Return %Arke.Core.Query{... filters: [ %Arke.Core.Query.Filter{} ] ... } """ def add_filter(query, filter) do %{query | filters: [filter | query.filters]} end @doc """ Add a filter to a query ## Parameters - query => refer to `new/1` - parameter - operator - value - negate ## Example iex> person = Arke.Core.Arke.new(id: :person, label: "Person") ...> query = Arke.Core.Query.new(person, :arke_system) ...> parameter = Arke.Boundary.ParameterManager.get(:id,:arke_system) ...> base_filter = Arke.Core.Query.new_base_filter(parameter, :eq, "name", false) ...> Arke.Core.Query.add_filter(query, :and, false, base_filter) ## Return %Arke.Core.Query{... filters: [ %Arke.Core.Query.Filter{} ] ... } """ def add_filter(query, parameter, operator, value, negate) do %{query | filters: [new_filter(parameter, operator, value, negate) | query.filters]} end @doc """ Add a filter to a query ## Parameters - query => refer to `new/1` - logic => :and | :or => the logic of the filter - negate => boolean => used to figure out whether the condition is to be denied - base_filters ## Example iex> person = Arke.new(id: :person, label: "Person") ...> query = Arke.Core.Query.new(person, :arke_system) ...> parameter = Arke.Core.ParameterManager.get(:id,:arke_system) ...> Arke.Core.Query.add_filter(query, parameter, :eq, "name", false) ## Return %Arke.Core.Query{... filters: [ %Arke.Core.Query.Filter{} ] ... } """ def add_filter(query, logic, negate, base_filters) do %{query | filters: [new_filter(logic, negate, base_filters) | query.filters]} end @doc """ Create a new filter ## Parameters - parameter => %Arke.Core.Parameter.`ParameterType` => refer to `Arke.Core.Parameter` - operator => refer to [operators](#module-operators) - value => any => the value that the query will search for - negate => boolean => used to figure out whether the condition is to be denied ## Example iex> parameter = Arke.Boundary.ParameterManager.get(:id,:arke_system) ...> Arke.Core.Query.new_filter(parameter,:eq, "name", false) ## Return %Arke.Core.Query.Filter{base_filters: [ %Arke.Core.Query.BaseFilter{} ]} """ def new_filter(parameter, operator, value, negate) do %Filter{ logic: :and, negate: false, base_filters: [new_base_filter(parameter, operator, value, negate)] } end @doc """ Create a new filter ## Parameters - logic => :and | :or => the logic of the filter - negate => boolean => used to figure out whether the condition is to be denied - base_filters => refer to `Arke.Core.Query.BaseFilter` ## Example iex> base_filter = Arke.Core.Query.new_base_filter(parameter, :eq, "name", false) ...> Arke.Core.Query.new_filter(:and, false, base_filter) ## Return %Arke.Core.Query.Filter{base_filters: [ %Arke.Core.Query.BaseFilter{} ]} """ def new_filter(logic, negate, base_filters) do %Filter{base_filters: parse_base_filters(base_filters), logic: logic, negate: negate} end @doc """ Create a new base filter ## Parameters - parameter => %Arke.Core.Parameter.`ParameterType` => refer to `Arke.Core.Parameter` - operator => refer to [operators](#module-operators) - value => any => the value that the query will search for - negate => boolean => used to figure out whether the condition is to be denied ## Example iex> parameter = Arke.Boundary.ParameterManager.get(:id,:arke_system) ...> Arke.Core.Query.new_base_filter(parameter, :eq, "name", false) ## Return %Arke.Core.Query.BaseFilter{} """ # TODO: standardize parameter # if it is a string convert it to existing atom and get it from paramater manager # if it is an atom get it from paramater manaager def new_base_filter(parameter, operator, value, negate) do BaseFilter.new(parameter, operator, value, negate) end defp parse_base_filters(base_filters) when is_list(base_filters), do: base_filters defp parse_base_filters(base_filters), do: [base_filters] @doc """ Get the query result ordered by specific criteria ## Parameters - query => refer to refer to `new/1` - parameter => %Arke.Core.Parameter.`ParameterType` => refer to `Arke.Core.Parameter` - direction => "child" | "parent" => the direction the query will use to search ## Example iex> person = Arke.Core.Arke.new(id: "person", label: "Person") ...> query = Arke.Core.Query.new(person, :arke_system) ...> parameter = Arke.Boundary.ParameterManager.get(:id,:arke_system) ...> Arke.Core.Query.add_order(query, parameter, :asc) ## Return %Arke.Core.Query{ ... orders: [ %Arke.Core.Query.Order{} ] ... } """ def add_order(query, parameter, direction) do %{ query | orders: [%Order{parameter: parameter, direction: direction} | query.orders] } end @doc """ Define the offset of the query ## Parameters - query => refer to `new/1` - offset => integer => define the offset of the query ## Example iex> person = Arke.Core.Arke.new(id: :person, label: "Person") ...> query = Arke.Core.Query.new(person, :arke_system) ...> Arke.Core.Query.set_offset(query, 5) ## Return %Arke.Core.Query{... offset: value ...} """ def set_offset(query, nil), do: query def set_offset(query, offset) when is_binary(offset), do: %{query | offset: String.to_integer(offset)} def set_offset(query, offset) when is_integer(offset), do: %{query | offset: offset} # TODO Custom exception offset must be integer def set_offset(query, offset), do: nil @doc """ Define the limit of the query ## Parameters - query => refer to `new/1` - limit => integer => set the results limit of the query ## Example iex> person = Arke.Core.Arke.new(id: :person, label: "Person") ...> query = Arke.Core.Query.new(person, :arke_system) ...> Arke.Core.Query.set_limit(query, 100) ## Return %Arke.Core.Query{... limit: value ...} """ def set_limit(query, nil), do: query def set_limit(query, offset) when is_binary(offset), do: %{query | limit: String.to_integer(offset)} def set_limit(query, limit) when is_integer(limit), do: %{query | limit: limit} # TODO Custom exception limit must be integer def set_limit(query, limit), do: nil end