zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

commit 476534d0f1ac91d59344eb7d6429dbffb9d37b1f
parent 4e8240dbd809a3d04f26cdae2554c3c9325bad17
Author: sir fish <dev@srf.sh>
Date:   Tue, 16 Aug 2022 13:33:59 +0300

Merge pull request #5 from dyne/srfsh/price


Diffstat:
Msrc/zenflows/vf/intent.ex | 5+++++
Asrc/zenflows/vf/intent/domain.ex | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/zenflows/vf/intent/resolv.ex | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/zenflows/vf/intent/type.ex | 355+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/zenflows/vf/proposal.ex | 8++++++++
Asrc/zenflows/vf/proposal/domain.ex | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/zenflows/vf/proposal/resolv.ex | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/zenflows/vf/proposed_intent/domain.ex | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/zenflows/vf/proposed_intent/resolv.ex | 44++++++++++++++++++++++++++++++++++++++++++++
Atest/vf/intent/domain.test.exs | 320+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/vf/intent/type.test.exs | 299+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/vf/proposal/domain.test.exs | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/vf/proposal/type.test.exs | 214+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/vf/proposed_intent/domain.test.exs | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/vf/proposed_intent/type.test.exs | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
15 files changed, 2094 insertions(+), 0 deletions(-)

diff --git a/src/zenflows/vf/intent.ex b/src/zenflows/vf/intent.ex @@ -28,6 +28,7 @@ alias Zenflows.VF.{ EconomicResource, Measure, Process, + ProposedIntent, ResourceSpecification, SpatialThing, Unit, @@ -57,6 +58,8 @@ alias Zenflows.VF.{ note: String.t() | nil, # in_scope_of: agreed_in: String.t() | nil, + + published_in: [ProposedIntent.t()], } schema "vf_intent" do @@ -90,6 +93,8 @@ schema "vf_intent" do # field :in_scope_of field :agreed_in, :string timestamps() + + has_many :published_in, ProposedIntent, foreign_key: :publishes_id end @reqr [:action_id] diff --git a/src/zenflows/vf/intent/domain.ex b/src/zenflows/vf/intent/domain.ex @@ -0,0 +1,139 @@ +# 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.Intent.Domain do +@moduledoc "Domain logic of Intents." + +alias Ecto.Multi +alias Zenflows.DB.Repo +alias Zenflows.VF.{ + Action, + Intent, + Measure, +} + +@typep repo() :: Ecto.Repo.t() +@typep chgset() :: Ecto.Changeset.t() +@typep id() :: Zenflows.DB.Schema.id() +@typep params() :: Zenflows.DB.Schema.params() + +@spec one(repo(), id()) :: {:ok, Intent.t()} | {:error, String.t()} +def one(repo \\ Repo, id) do + one_by(repo, id: id) +end + +@spec one_by(repo(), map() | Keyword.t()) + :: {:ok, Intent.t()} | {:error, String.t()} +def one_by(repo \\ Repo, clauses) do + case repo.get_by(Intent, clauses) do + nil -> {:error, "not found"} + found -> {:ok, found} + end +end + +@spec create(params()) :: {:ok, Intent.t()} | {:error, chgset()} +def create(params) do + Multi.new() + |> Multi.insert(:insert, Intent.chgset(params)) + |> Repo.transaction() + |> case do + {:ok, %{insert: i}} -> {:ok, i} + {:error, _, cset, _} -> {:error, cset} + end +end + +@spec update(id(), params()) :: + {:ok, Intent.t()} | {:error, String.t() | chgset()} +def update(id, params) do + Multi.new() + |> Multi.put(:id, id) + |> Multi.run(:one, &one_by/2) + |> Multi.update(:update, &Intent.chgset(&1.one, params)) + |> Repo.transaction() + |> case do + {:ok, %{update: i}} -> {:ok, i} + {:error, _, msg_or_cset, _} -> {:error, msg_or_cset} + end +end + +@spec delete(id()) :: {:ok, Intent.t()} | {:error, String.t() | chgset()} +def delete(id) do + Multi.new() + |> Multi.put(:id, id) + |> Multi.run(:one, &one_by/2) + |> Multi.delete(:delete, &(&1.one)) + |> Repo.transaction() + |> case do + {:ok, %{delete: i}} -> {:ok, i} + {:error, _, msg_or_cset, _} -> {:error, msg_or_cset} + end +end + +@spec preload(Intent.t(), :action | :input_of | :output_of + | :provider | :receiver + | :resource_inventoried_as | :resource_conforms_to + | :resource_quantity | :effort_quantity | :available_quantity + | :at_location | :published_in) + :: Intent.t() +def preload(int, :action) do + Action.preload(int, :action) +end + +def preload(int, :input_of) do + Repo.preload(int, :input_of) +end + +def preload(int, :output_of) do + Repo.preload(int, :output_of) +end + +def preload(int, :provider) do + Repo.preload(int, :provider) +end + +def preload(int, :receiver) do + Repo.preload(int, :receiver) +end + +def preload(int, :resource_inventoried_as) do + Repo.preload(int, :resource_inventoried_as) +end + +def preload(int, :resource_conforms_to) do + Repo.preload(int, :resource_conforms_to) +end + +def preload(int, :resource_quantity) do + Measure.preload(int, :resource_quantity) +end + +def preload(int, :effort_quantity) do + Measure.preload(int, :effort_quantity) +end + +def preload(int, :available_quantity) do + Measure.preload(int, :available_quantity) +end + +def preload(int, :at_location) do + Repo.preload(int, :at_location) +end + +def preload(int, :published_in) do + Repo.preload(int, :published_in) +end +end diff --git a/src/zenflows/vf/intent/resolv.ex b/src/zenflows/vf/intent/resolv.ex @@ -0,0 +1,114 @@ +# 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.Intent.Resolv do +@moduledoc "Resolvers of Intent." + +alias Zenflows.VF.Intent.Domain + +def action(int, _, _) do + int = Domain.preload(int, :action) + {:ok, int.action} +end + +def input_of(int, _, _) do + int = Domain.preload(int, :input_of) + {:ok, int.input_of} +end + +def output_of(int, _, _) do + int = Domain.preload(int, :output_of) + {:ok, int.output_of} +end + +def provider(int, _, _) do + int = Domain.preload(int, :provider) + {:ok, int.provider} +end + +def receiver(int, _, _) do + int = Domain.preload(int, :receiver) + {:ok, int.receiver} +end + +def resource_inventoried_as(int, _, _) do + int = Domain.preload(int, :resource_inventoried_as) + {:ok, int.resource_inventoried_as} +end + +def resource_conforms_to(int, _, _) do + int = Domain.preload(int, :resource_conforms_to) + {:ok, int.resource_conforms_to} +end + +def resource_quantity(int, _, _) do + int = Domain.preload(int, :resource_quantity) + {:ok, int.resource_quantity} +end + +def effort_quantity(int, _, _) do + int = Domain.preload(int, :effort_quantity) + {:ok, int.effort_quantity} +end + +def available_quantity(int, _, _) do + int = Domain.preload(int, :available_quantity) + {:ok, int.available_quantity} +end + +def at_location(int, _, _) do + int = Domain.preload(int, :at_location) + {:ok, int.at_location} +end + +def published_in(int, _, _) do + int = Domain.preload(int, :published_in) + {:ok, int.published_in} +end + +def intent(%{id: id}, _) do + Domain.one(id) +end + +def intents(_, _) do + {:ok, %{ + edges: [], + page_info: %{ + has_previous_page: false, + has_next_page: false, + }, + }} +end + +def create_intent(%{intent: params}, _) do + with {:ok, int} <- Domain.create(params) do + {:ok, %{intent: int}} + end +end + +def update_intent(%{intent: %{id: id} = params}, _) do + with {:ok, int} <- Domain.update(id, params) do + {:ok, %{intent: int}} + end +end + +def delete_intent(%{id: id}, _) do + with {:ok, _} <- Domain.delete(id) do + {:ok, true} + end +end +end diff --git a/src/zenflows/vf/intent/type.ex b/src/zenflows/vf/intent/type.ex @@ -0,0 +1,355 @@ +# 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.Intent.Type do +@moduledoc "GraphQL types of Intents." + +use Absinthe.Schema.Notation + +alias Zenflows.VF.Intent.Resolv + +@action """ +Relates an intent to a verb, such as consume, produce, work, improve, etc. +""" +@action_id "(`Action`) #{@action}" +@input_of "Defines the process to which this intent is an input." +@input_of_id "(`Process`) #{@input_of}" +@output_of "Defines the process to which this intent is an output." +@output_of_id "(`Process`) #{@output_of}" +@provider """ +The economic agent from whom the intent is initiated. This implies that +the intent is an offer. +""" +@provider_id "(`Agent`) #{@provider}" +@receiver """ +The economic agent whom the intent is for. This implies that the intent +is a request. +""" +@receiver_id "(`Agent`) #{@receiver}" +@resource_inventoried_as """ +When a specific `EconomicResource` is known which can service the +`Intent`, this defines that resource. +""" +@resource_inventoried_as_id "(`EconomicResource`) #{@resource_inventoried_as}" +@resource_conforms_to """ +The primary resource specification or definition of an existing or +potential economic resource. A resource will have only one, as this +specifies exactly what the resource is. +""" +@resource_conforms_to_id "(`ResourceSpecification`) #{@resource_conforms_to}" +@resource_classified_as """ +References a concept in a common taxonomy or other classification scheme +for purposes of categorization or grouping. +""" +@resource_quantity """ +The amount and unit of the economic resource counted or inventoried. This +is the quantity that could be used to increment or decrement a resource, +depending on the type of resource and resource effect of action. +""" +@effort_quantity """ +The amount and unit of the work or use or citation effort-based action. +This is often a time duration, but also could be cycle counts or other +measures of effort or usefulness. +""" +@available_quantity "The total quantity of the offered resource available." +@has_beginning "The planned beginning of the intent." +@has_end "The planned end of the intent." +@has_point_in_time """ +The planned date/time for the intent. Can be used instead of beginning +and end. +""" +@due "The time something is expected to be complete." +@finished """ +The intent is complete or not. This is irrespective of if the original +goal has been met, and indicates that no more will be done. +""" +@at_location "The place where an intent would occur. Usually mappable." +@at_location_id "(`SpatialThing`) #{@at_location}" +@image """ +The base64-encoded image binary relevant to the intent, such as a photo. +""" +@name """ +An informal or formal textual identifier for an intent. Does not imply +uniqueness. +""" +@note "A textual description or comment." +@agreed_in """ +Reference to an agreement between agents which specifies the rules or +policies or calculations which govern this intent. +""" +@deletable "The intent can be safely deleted, has no dependent information." + +@desc """ +A planned economic flow which has not been committed to, which can lead +to EconomicEvents (sometimes through Commitments). +""" +object :intent do + field :id, non_null(:id) + + @desc @action + field :action, non_null(:action), resolve: &Resolv.action/3 + + @desc @input_of + field :input_of, :process, resolve: &Resolv.input_of/3 + + @desc @output_of + field :output_of, :process, resolve: &Resolv.output_of/3 + + @desc @provider + field :provider, :agent, resolve: &Resolv.provider/3 + + @desc @receiver + field :receiver, :agent, resolve: &Resolv.receiver/3 + + @desc @resource_inventoried_as + field :resource_inventoried_as, :economic_resource, + resolve: &Resolv.resource_inventoried_as/3 + + @desc @resource_conforms_to + field :resource_conforms_to, :economic_resource, + resolve: &Resolv.resource_conforms_to/3 + + @desc @resource_classified_as + field :resource_classified_as, list_of(non_null(:uri)) + + @desc @resource_quantity + field :resource_quantity, :measure, + resolve: &Resolv.resource_quantity/3 + + @desc @effort_quantity + field :effort_quantity, :measure, + resolve: &Resolv.effort_quantity/3 + + @desc @available_quantity + field :available_quantity, :measure, + resolve: &Resolv.available_quantity/3 + + @desc @has_beginning + field :has_beginning, :datetime + + @desc @has_end + field :has_end, :datetime + + @desc @has_point_in_time + field :has_point_in_time, :datetime + + @desc @due + field :due, :datetime + + @desc @finished + field :finished, non_null(:boolean) + + @desc @at_location + field :at_location, :spatial_thing, resolve: &Resolv.at_location/3 + + @desc @image + field :image, :base64 + + @desc @name + field :name, :string + + @desc @note + field :note, :string + + @desc @agreed_in + field :agreed_in, :uri + + @desc @deletable + field :deletable, non_null(:boolean) + + field :published_in, list_of(non_null(:proposed_intent)), + resolve: &Resolv.published_in/3 +end + +object :intent_response do + field :intent, non_null(:intent) +end + +object :intent_edge do + field :cursor, non_null(:string) + field :node, non_null(:intent) +end + +object :intent_connection do + field :page_info, non_null(:page_info) + field :edges, non_null(list_of(non_null(:intent_edge))) +end + +input_object :intent_create_params do + @desc @action_id + field :action_id, non_null(:string), name: "action" + + @desc @input_of_id + field :input_of_id, :id, name: "input_of" + + @desc @output_of_id + field :output_of_id, :id, name: "output_of" + + @desc @provider_id + field :provider_id, :id, name: "provider" + + @desc @receiver_id + field :receiver_id, :id, name: "receiver" + + @desc @resource_inventoried_as_id + field :resource_inventoried_as_id, :id, name: "resource_inventoried_as" + + @desc @resource_conforms_to_id + field :resource_conforms_to_id, :id, name: "resource_conforms_to" + + @desc @resource_classified_as + field :resource_classified_as, list_of(non_null(:uri)) + + @desc @resource_quantity + field :resource_quantity, :imeasure + + @desc @effort_quantity + field :effort_quantity, :imeasure + + @desc @available_quantity + field :available_quantity, :imeasure + + @desc @has_beginning + field :has_beginning, :datetime + + @desc @has_end + field :has_end, :datetime + + @desc @has_point_in_time + field :has_point_in_time, :datetime + + @desc @due + field :due, :datetime + + @desc @finished + field :finished, :boolean + + @desc @at_location_id + field :at_location_id, :id, name: "at_location" + + @desc @image + field :image, :base64 + + @desc @name + field :name, :string + + @desc @note + field :note, :string + + @desc @agreed_in + field :agreed_in, :uri +end + +input_object :intent_update_params do + field :id, non_null(:id) + + @desc @action_id + field :action_id, :string, name: "action" + + @desc @input_of_id + field :input_of_id, :id, name: "input_of" + + @desc @output_of_id + field :output_of_id, :id, name: "output_of" + + @desc @provider_id + field :provider_id, :id, name: "provider" + + @desc @receiver_id + field :receiver_id, :id, name: "receiver" + + @desc @resource_inventoried_as_id + field :resource_inventoried_as_id, :id, name: "resource_inventoried_as" + + @desc @resource_conforms_to_id + field :resource_conforms_to_id, :id, name: "resource_conforms_to" + + @desc @resource_classified_as + field :resource_classified_as, list_of(non_null(:uri)) + + @desc @resource_quantity + field :resource_quantity, :imeasure + + @desc @effort_quantity + field :effort_quantity, :imeasure + + @desc @available_quantity + field :available_quantity, :imeasure + + @desc @has_beginning + field :has_beginning, :datetime + + @desc @has_end + field :has_end, :datetime + + @desc @has_point_in_time + field :has_point_in_time, :datetime + + @desc @due + field :due, :datetime + + @desc @finished + field :finished, :boolean + + @desc @at_location_id + field :at_location_id, :id, name: "at_location" + + @desc @image + field :image, :base64 + + @desc @name + field :name, :string + + @desc @note + field :note, :string + + @desc @agreed_in + field :agreed_in, :uri +end + +object :query_intent do + field :intent, :intent do + arg :id, non_null(:id) + resolve &Resolv.intent/2 + end + + field :intents, non_null(:intent_connection) do + arg :first, :integer + arg :after, :string + arg :last, :integer + arg :before, :string + resolve &Resolv.intents/2 + end +end + +object :mutation_intent do + field :create_intent, non_null(:intent_response) do + arg :intent, non_null(:intent_create_params) + resolve &Resolv.create_intent/2 + end + + field :update_intent, non_null(:intent_response) do + arg :intent, non_null(:intent_update_params) + resolve &Resolv.update_intent/2 + end + + field :delete_intent, non_null(:boolean) do + arg :id, non_null(:id) + resolve &Resolv.delete_intent/2 + end +end +end diff --git a/src/zenflows/vf/proposal.ex b/src/zenflows/vf/proposal.ex @@ -23,6 +23,7 @@ Published requests or offers, sometimes with what is expected in return. use Zenflows.DB.Schema alias Zenflows.VF.{ + Intent, ProposedIntent, SpatialThing, Validate, @@ -35,6 +36,9 @@ alias Zenflows.VF.{ unit_based: boolean(), note: String.t() | nil, eligible_location: SpatialThing.t() | nil, + publishes: [ProposedIntent.t()], + primary_intents: [Intent.t()], + reciprocal_intents: [Intent.t()], } schema "vf_proposal" do @@ -47,6 +51,10 @@ schema "vf_proposal" do timestamps() has_many :publishes, ProposedIntent, foreign_key: :published_in_id + many_to_many :primary_intents, Intent, join_through: ProposedIntent, + join_keys: [published_in_id: :id, publishes_id: :id], join_where: [reciprocal: false] + many_to_many :reciprocal_intents, Intent, join_through: ProposedIntent, + join_keys: [published_in_id: :id, publishes_id: :id], join_where: [reciprocal: true] end @cast ~w[name has_beginning has_end unit_based note eligible_location_id]a diff --git a/src/zenflows/vf/proposal/domain.ex b/src/zenflows/vf/proposal/domain.ex @@ -0,0 +1,101 @@ +# 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.Domain do +@moduledoc "Domain logic of Proposals." + +alias Ecto.Multi +alias Zenflows.DB.Repo +alias Zenflows.VF.Proposal + +@typep repo() :: Ecto.Repo.t() +@typep chgset() :: Ecto.Changeset.t() +@typep id() :: Zenflows.DB.Schema.id() +@typep params() :: Zenflows.DB.Schema.params() + +@spec one(repo(), id()) :: {:ok, Proposal.t()} | {:error, String.t()} +def one(repo \\ Repo, id) do + one_by(repo, id: id) +end + +@spec one_by(repo(), map() | Keyword.t()) + :: {:ok, Proposal.t()} | {:error, String.t()} +def one_by(repo \\ Repo, clauses) do + case repo.get_by(Proposal, clauses) do + nil -> {:error, "not found"} + found -> {:ok, found} + end +end + +@spec create(params()) :: {:ok, Proposal.t()} | {:error, chgset()} +def create(params) do + Multi.new() + |> Multi.insert(:insert, Proposal.chgset(params)) + |> Repo.transaction() + |> case do + {:ok, %{insert: p}} -> {:ok, p} + {:error, _, cset, _} -> {:error, cset} + end +end + +@spec update(id(), params()) :: + {:ok, Proposal.t()} | {:error, String.t() | chgset()} +def update(id, params) do + Multi.new() + |> Multi.put(:id, id) + |> Multi.run(:one, &one_by/2) + |> Multi.update(:update, &Proposal.chgset(&1.one, params)) + |> Repo.transaction() + |> case do + {:ok, %{update: p}} -> {:ok, p} + {:error, _, msg_or_cset, _} -> {:error, msg_or_cset} + end +end + +@spec delete(id()) + :: {:ok, Proposal.t()} | {:error, String.t() | chgset()} +def delete(id) do + Multi.new() + |> Multi.put(:id, id) + |> Multi.run(:one, &one_by/2) + |> Multi.delete(:delete, &(&1.one)) + |> Repo.transaction() + |> case do + {:ok, %{delete: pi}} -> {:ok, pi} + {:error, _, msg_or_cset, _} -> {:error, msg_or_cset} + end +end + +@spec preload(Proposal.t(), :eligible_location | :publishes + | :primary_intents | :reciprocal_intents) + :: Proposal.t() +def preload(prop, :eligible_location) do + Repo.preload(prop, :eligible_location) +end + +def preload(prop, :publishes) do + Repo.preload(prop, :publishes) +end + +def preload(prop, :primary_intents) do + Repo.preload(prop, :primary_intents) +end + +def preload(prop, :reciprocal_intents) do + Repo.preload(prop, :reciprocal_intents) +end +end diff --git a/src/zenflows/vf/proposal/resolv.ex b/src/zenflows/vf/proposal/resolv.ex @@ -0,0 +1,94 @@ +# 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.Resolv do +@moduledoc "Resolvers of Proposal." + +alias Zenflows.VF.Proposal.Domain + +def eligible_location(prop, _, _) do + prop = Domain.preload(prop, :eligible_location) + {:ok, prop.eligible_location} +end + +def publishes(prop, _, _) do + prop = Domain.preload(prop, :publishes) + {:ok, prop.publishes} +end + +def primary_intents(prop, _, _) do + prop = Domain.preload(prop, :primary_intents) + {:ok, prop.primary_intents} +end + +def reciprocal_intents(prop, _, _) do + prop = Domain.preload(prop, :reciprocal_intents) + {:ok, prop.reciprocal_intents} +end + +def proposal(%{id: id}, _) do + Domain.one(id) +end + +def proposals(_params, _) do + {:ok, %{ + edges: [], + page_info: %{ + has_previous_page: false, + has_next_page: false, + }, + }} +end + +def offers(_params, _) do + {:ok, %{ + edges: [], + page_info: %{ + has_previous_page: false, + has_next_page: false, + }, + }} +end + +def requests(_params, _) do + {:ok, %{ + edges: [], + page_info: %{ + has_previous_page: false, + has_next_page: false, + }, + }} +end + +def create_proposal(%{proposal: params}, _) do + with {:ok, prop} <- Domain.create(params) do + {:ok, %{proposal: prop}} + end +end + +def update_proposal(%{proposal: %{id: id} = params}, _) do + with {:ok, prop} <- Domain.update(id, params) do + {:ok, %{proposal: prop}} + end +end + +def delete_proposal(%{id: id}, _) do + with {:ok, _} <- Domain.delete(id) do + {:ok, true} + end +end +end diff --git a/src/zenflows/vf/proposed_intent/domain.ex b/src/zenflows/vf/proposed_intent/domain.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.ProposedIntent.Domain do +@moduledoc "Domain logic of ProposedIntents." + +alias Ecto.Multi +alias Zenflows.DB.Repo +alias Zenflows.VF.ProposedIntent + +@typep repo() :: Ecto.Repo.t() +@typep chgset() :: Ecto.Changeset.t() +@typep id() :: Zenflows.DB.Schema.id() +@typep params() :: Zenflows.DB.Schema.params() + +@spec one(repo(), id()) :: {:ok, ProposedIntent.t()} | {:error, String.t()} +def one(repo \\ Repo, id) do + one_by(repo, id: id) +end + +@spec one_by(repo(), map() | Keyword.t()) + :: {:ok, ProposedIntent.t()} | {:error, String.t()} +def one_by(repo \\ Repo, clauses) do + case repo.get_by(ProposedIntent, clauses) do + nil -> {:error, "not found"} + found -> {:ok, found} + end +end + +@spec create(params()) :: {:ok, ProposedIntent.t()} | {:error, chgset()} +def create(params) do + Multi.new() + |> Multi.insert(:insert, ProposedIntent.chgset(params)) + |> Repo.transaction() + |> case do + {:ok, %{insert: pi}} -> {:ok, pi} + {:error, _, cset, _} -> {:error, cset} + end +end + +@spec delete(id()) + :: {:ok, ProposedIntent.t()} | {:error, String.t() | chgset()} +def delete(id) do + Multi.new() + |> Multi.put(:id, id) + |> Multi.run(:one, &one_by/2) + |> Multi.delete(:delete, &(&1.one)) + |> Repo.transaction() + |> case do + {:ok, %{delete: pi}} -> {:ok, pi} + {:error, _, msg_or_cset, _} -> {:error, msg_or_cset} + end +end + +@spec preload(ProposedIntent.t(), :published_in | :publishes) + :: ProposedIntent.t() +def preload(prop_int, :published_in) do + Repo.preload(prop_int, :published_in) +end + +def preload(prop_int, :publishes) do + Repo.preload(prop_int, :publishes) +end +end diff --git a/src/zenflows/vf/proposed_intent/resolv.ex b/src/zenflows/vf/proposed_intent/resolv.ex @@ -0,0 +1,44 @@ +# 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.ProposedIntent.Resolv do +@moduledoc "Resolvers of ProposedIntent." + +alias Zenflows.VF.ProposedIntent.Domain + +def published_in(prop_int, _, _) do + prop_int = Domain.preload(prop_int, :published_in) + {:ok, prop_int.published_in} +end + +def publishes(prop_int, _, _) do + prop_int = Domain.preload(prop_int, :publishes) + {:ok, prop_int.publishes} +end + +def propose_intent(params, _) do + with {:ok, prop_int} <- Domain.create(params) do + {:ok, %{proposed_intent: prop_int}} + end +end + +def delete_proposed_intent(%{id: id}, _) do + with {:ok, _} <- Domain.delete(id) do + {:ok, true} + end +end +end diff --git a/test/vf/intent/domain.test.exs b/test/vf/intent/domain.test.exs @@ -0,0 +1,320 @@ +# 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 ZenflowsTest.VF.Intent do +use ZenflowsTest.Help.EctoCase, async: true + +alias Ecto.Changeset +alias Zenflows.VF.{ + Action, + Agent, + EconomicResource, + Intent, + Intent.Domain, + Measure, + Process, + ResourceSpecification, + SpatialThing, +} + +setup do + %{ + params: %{ + action_id: Factory.build(:action_id), + input_of_id: Factory.insert!(:process).id, + output_of_id: Factory.insert!(:process).id, + provider_id: Factory.insert!(:agent).id, + receiver_id: Factory.insert!(:agent).id, + resource_inventoried_as_id: Factory.insert!(:economic_resource).id, + resource_conforms_to_id: Factory.insert!(:resource_specification).id, + resource_classified_as: Factory.uniq_list("uri"), + resource_quantity: %{ + has_unit_id: Factory.insert!(:unit).id, + has_numerical_value: Factory.float(), + }, + effort_quantity: %{ + has_unit_id: Factory.insert!(:unit).id, + has_numerical_value: Factory.float(), + }, + available_quantity: %{ + has_unit_id: Factory.insert!(:unit).id, + has_numerical_value: Factory.float(), + }, + has_beginning: Factory.now(), + has_end: Factory.now(), + has_point_in_time: Factory.now(), + due: Factory.now(), + finished: Factory.bool(), + at_location_id: Factory.insert!(:spatial_thing).id, + image: Factory.img(), + name: Factory.uniq("name"), + note: Factory.uniq("note"), + # in_scope_of_id: + agreed_in: Factory.uniq("uri"), + }, + inserted: Factory.insert!(:intent), + id: Factory.id(), + } +end + +describe "one/1" do + test "with good id: finds the Intent", %{inserted: %{id: id}} do + assert {:ok, %Intent{}} = Domain.one(id) + end + + test "with bad id: doesn't find the Intent", %{id: id} do + assert {:error, "not found"} = Domain.one(id) + end +end + +describe "create/1" do + test "with good params (with only :provider): creates an Intent", %{params: params} do + params = Map.put(params, :receiver_id, nil) + assert {:ok, %Intent{} = int} = Domain.create(params) + + keys = ~w[ + action_id + provider_id receiver_id + input_of_id output_of_id + resource_inventoried_as_id resource_conforms_to_id resource_classified_as + has_beginning has_end has_point_in_time due + finished image name note agreed_in at_location_id + ]a # in_scope_of_id + assert Map.take(int, keys) == Map.take(params, keys) + + assert int.resource_quantity_has_unit_id == params.resource_quantity.has_unit_id + assert int.resource_quantity_has_numerical_value == params.resource_quantity.has_numerical_value + assert int.effort_quantity_has_unit_id == params.effort_quantity.has_unit_id + assert int.effort_quantity_has_numerical_value == params.effort_quantity.has_numerical_value + assert int.available_quantity_has_unit_id == params.available_quantity.has_unit_id + assert int.available_quantity_has_numerical_value == params.available_quantity.has_numerical_value + end + + test "with good params (with only :receiver): creates an Intent", %{params: params} do + params = Map.put(params, :provider_id, nil) + assert {:ok, %Intent{} = int} = Domain.create(params) + + keys = ~w[ + action_id + provider_id receiver_id + input_of_id output_of_id + resource_inventoried_as_id resource_conforms_to_id resource_classified_as + has_beginning has_end has_point_in_time due + finished image name note agreed_in at_location_id + ]a # in_scope_of_id + assert Map.take(int, keys) == Map.take(params, keys) + + assert int.resource_quantity_has_unit_id == params.resource_quantity.has_unit_id + assert int.resource_quantity_has_numerical_value == params.resource_quantity.has_numerical_value + assert int.effort_quantity_has_unit_id == params.effort_quantity.has_unit_id + assert int.effort_quantity_has_numerical_value == params.effort_quantity.has_numerical_value + assert int.available_quantity_has_unit_id == params.available_quantity.has_unit_id + assert int.available_quantity_has_numerical_value == params.available_quantity.has_numerical_value + end + + test "with bad params (with both :provider and :receiver): doesn't create an Intent", %{params: params} do + assert {:error, %Changeset{errors: errs}} = Domain.create(params) + assert {:ok, _} = Keyword.fetch(errs, :provider_id) + assert {:ok, _} = Keyword.fetch(errs, :receiver_id) + end + + test "with bad params: doesn't create an Intent" do + assert {:error, %Changeset{}} = Domain.create(%{}) + end +end + +describe "update/2" do + test "with good params (with only :provider): updates the Intent", %{params: params} do + params = Map.put(params, :receiver_id, nil) + old = Factory.insert!(:intent, %{receiver: nil, provider: Factory.build(:agent)}) + assert {:ok, %Intent{} = new} = Domain.update(old.id, params) + + keys = ~w[ + action_id + provider_id receiver_id + input_of_id output_of_id + resource_inventoried_as_id resource_conforms_to_id resource_classified_as + has_beginning has_end has_point_in_time due + finished image name note agreed_in at_location_id + ]a # in_scope_of_id + assert Map.take(new, keys) == Map.take(params, keys) + + assert new.resource_quantity_has_unit_id == params.resource_quantity.has_unit_id + assert new.resource_quantity_has_numerical_value == params.resource_quantity.has_numerical_value + assert new.effort_quantity_has_unit_id == params.effort_quantity.has_unit_id + assert new.effort_quantity_has_numerical_value == params.effort_quantity.has_numerical_value + assert new.available_quantity_has_unit_id == params.available_quantity.has_unit_id + assert new.available_quantity_has_numerical_value == params.available_quantity.has_numerical_value + end + + test "with good params (with only :receiver): updates the Intent", %{params: params} do + params = Map.put(params, :provider_id, nil) + old = Factory.insert!(:intent, %{provider: nil, receiver: Factory.build(:agent)}) + assert {:ok, %Intent{} = new} = Domain.update(old.id, params) + + keys = ~w[ + action_id + provider_id receiver_id + input_of_id output_of_id + resource_inventoried_as_id resource_conforms_to_id resource_classified_as + has_beginning has_end has_point_in_time due + finished image name note agreed_in at_location_id + ]a # in_scope_of_id + assert Map.take(new, keys) == Map.take(params, keys) + + assert new.resource_quantity_has_unit_id == params.resource_quantity.has_unit_id + assert new.resource_quantity_has_numerical_value == params.resource_quantity.has_numerical_value + assert new.effort_quantity_has_unit_id == params.effort_quantity.has_unit_id + assert new.effort_quantity_has_numerical_value == params.effort_quantity.has_numerical_value + assert new.available_quantity_has_unit_id == params.available_quantity.has_unit_id + assert new.available_quantity_has_numerical_value == params.available_quantity.has_numerical_value + end + + test "with bad params (with both :provider and :receiver): updates the Intent", %{params: params} do + int = Factory.insert!(:intent, %{provider: Factory.build(:agent), receiver: nil}) + assert {:error, %Changeset{errors: errs}} = Domain.update(int.id, params) + + assert {:ok, _} = Keyword.fetch(errs, :provider_id) + assert {:ok, _} = Keyword.fetch(errs, :receiver_id) + end + + test "with bad params (with both :receiver and :provider): updates the Intent", %{params: params} do + int = Factory.insert!(:intent, %{provider: nil, receiver: Factory.build(:agent)}) + assert {:error, %Changeset{errors: errs}} = Domain.update(int.id, params) + + assert {:ok, _} = Keyword.fetch(errs, :provider_id) + assert {:ok, _} = Keyword.fetch(errs, :receiver_id) + end + + test "with bad params: doesn't update the Intent", %{inserted: old} do + assert {:ok, %Intent{} = new} = Domain.update(old.id, %{}) + keys = ~w[ + action_id + provider_id receiver_id + input_of_id output_of_id + resource_inventoried_as_id resource_conforms_to_id resource_classified_as + resource_quantity_has_unit_id resource_quantity_has_numerical_value + effort_quantity_has_unit_id effort_quantity_has_numerical_value + available_quantity_has_unit_id available_quantity_has_numerical_value + has_beginning has_end has_point_in_time due + finished image name note agreed_in at_location_id + ]a # in_scope_of_id + assert Map.take(new, keys) == Map.take(old, keys) + end +end + +describe "delete/1" do + test "with good id: deletes the Intent", %{inserted: %{id: id}} do + assert {:ok, %Intent{id: ^id}} = Domain.delete(id) + assert {:error, "not found"} = Domain.one(id) + end + + test "with bad id: doesn't delete the Intent", %{id: id} do + assert {:error, "not found"} = Domain.delete(id) + end +end + +describe "preload/2" do + test "preloads `:action`", %{inserted: %{id: id}} do + assert {:ok, int} = Domain.one(id) + int = Domain.preload(int, :action) + assert %Action{} = int.action + end + + test "preloads `:input_of`", %{inserted: %{id: id}} do + assert {:ok, int} = Domain.one(id) + int = Domain.preload(int, :input_of) + assert %Process{} = int.input_of + end + + test "preloads `:output_of`", %{inserted: %{id: id}} do + assert {:ok, int} = Domain.one(id) + int = Domain.preload(int, :output_of) + assert %Process{} = int.output_of + end + + test "preloads `:provider`" do + %{id: id} = + Factory.insert!(:intent, %{provider: Factory.build(:agent), receiver: nil}) + + assert {:ok, int} = Domain.one(id) + int = Domain.preload(int, :provider) + assert %Agent{} = int.provider + end + + test "preloads `:receiver`" do + %{id: id} = + Factory.insert!(:intent, %{provider: nil, receiver: Factory.build(:agent)}) + + assert {:ok, int} = Domain.one(id) + int = Domain.preload(int, :receiver) + assert %Agent{} = int.receiver + end + + test "preloads `:resource_inventoried_as`", %{inserted: %{id: id}} do + assert {:ok, int} = Domain.one(id) + int = Domain.preload(int, :resource_inventoried_as) + assert %EconomicResource{} = int.resource_inventoried_as + end + + test "preloads `:resource_conforms_to`", %{inserted: %{id: id}} do + assert {:ok, int} = Domain.one(id) + int = Domain.preload(int, :resource_conforms_to) + assert %ResourceSpecification{} = int.resource_conforms_to + end + + test "preloads `:resource_quantity`", %{inserted: %{id: id}} do + assert {:ok, int} = Domain.one(id) + int = Domain.preload(int, :resource_quantity) + assert %Measure{} = int.resource_quantity + end + + test "preloads `:effort_quantity`", %{inserted: %{id: id}} do + assert {:ok, int} = Domain.one(id) + int = Domain.preload(int, :effort_quantity) + assert %Measure{} = int.effort_quantity + end + + test "preloads `:available_quantity`", %{inserted: %{id: id}} do + assert {:ok, int} = Domain.one(id) + int = Domain.preload(int, :available_quantity) + assert %Measure{} = int.available_quantity + end + + test "preloads `:at_location`", %{inserted: %{id: id}} do + assert {:ok, int} = Domain.one(id) + int = Domain.preload(int, :at_location) + assert %SpatialThing{} = int.at_location + end + + test "preloads `:published_in`", %{inserted: %{id: id}} do + assert {:ok, int} = Domain.one(id) + + assert [] = Domain.preload(int, :published_in).published_in + + left = + Enum.map(0..9, fn _ -> + Factory.insert!(:proposed_intent, %{publishes: int}).id + end) + right = + Domain.preload(int, :published_in) + |> Map.fetch!(:published_in) + |> Enum.map(& &1.id) + assert left -- right == [] + end +end +end diff --git a/test/vf/intent/type.test.exs b/test/vf/intent/type.test.exs @@ -0,0 +1,299 @@ +# 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 ZenflowsTest.VF.Intent.Type do +use ZenflowsTest.Help.AbsinCase, async: true + +alias Zenflows.DB.ID + +setup do + %{ + params: %{ + "action" => Factory.build(:action_id), + "inputOf" => Factory.insert!(:process).id, + "outputOf" => Factory.insert!(:process).id, + "provider" => Factory.insert!(:agent).id, + "receiver" => Factory.insert!(:agent).id, + "resourceInventoriedAs" => Factory.insert!(:economic_resource).id, + "resourceConformsTo" => Factory.insert!(:resource_specification).id, + "resourceClassifiedAs" => Factory.uniq_list("uri"), + "resourceQuantity" => %{ + "hasUnit" => Factory.insert!(:unit).id, + "hasNumericalValue" => Factory.float(), + }, + "effortQuantity" => %{ + "hasUnit" => Factory.insert!(:unit).id, + "hasNumericalValue" => Factory.float(), + }, + "availableQuantity" => %{ + "hasUnit" => Factory.insert!(:unit).id, + "hasNumericalValue" => Factory.float(), + }, + "hasBeginning" => Factory.iso_now(), + "hasEnd" => Factory.iso_now(), + "hasPointInTime" => Factory.iso_now(), + "due" => Factory.iso_now(), + "finished" => Factory.bool(), + "atLocation" => Factory.insert!(:spatial_thing).id, + "image" => Factory.img(), + "name" => Factory.uniq("name"), + "note" => Factory.uniq("note"), + # inScopeOf: + "agreedIn" => Factory.uniq("uri"), + }, + inserted: Factory.insert!(:intent), + id: Factory.id(), + } +end + +@frag """ +fragment measure on Measure { + hasNumericalValue + hasUnit {id} +} + +fragment intent on Intent { + id + action {id} + inputOf {id} + outputOf {id} + provider {id} + receiver {id} + resourceInventoriedAs {id} + resourceConformsTo {id} + resourceClassifiedAs + resourceQuantity {...measure} + effortQuantity {...measure} + availableQuantity {...measure} + hasBeginning + hasEnd + hasPointInTime + due + finished + atLocation {id} + image + name + note + agreedIn +} +""" + +describe "Query" do + test "intent", %{inserted: int} do + assert %{data: %{"intent" => data}} = + run!(""" + #{@frag} + query ($id: ID!) { + intent(id: $id) {...intent} + } + """, vars: %{"id" => int.id}) + + assert data["id"] == int.id + assert data["action"]["id"] == int.action_id + assert data["inputOf"]["id"] == int.input_of_id + assert data["outputOf"]["id"] == int.output_of_id + assert data["provider"]["id"] == int.provider_id + assert data["receiver"]["id"] == int.receiver_id + assert data["resourceInventoriedAs"]["id"] == int.resource_inventoried_as_id + assert data["resourceConformsTo"]["id"] == int.resource_conforms_to_id + assert data["resourceClassifiedAs"] == int.resource_classified_as + assert data["resourceQuantity"]["hasNumericalValue"] == int.resource_quantity_has_numerical_value + assert data["resourceQuantity"]["hasUnit"]["id"] == int.resource_quantity_has_unit_id + assert data["effortQuantity"]["hasNumericalValue"] == int.effort_quantity_has_numerical_value + assert data["effortQuantity"]["hasUnit"]["id"] == int.effort_quantity_has_unit_id + assert data["availableQuantity"]["hasNumericalValue"] == int.available_quantity_has_numerical_value + assert data["availableQuantity"]["hasUnit"]["id"] == int.available_quantity_has_unit_id + assert data["hasBeginning"] == DateTime.to_iso8601(int.has_beginning) + assert data["hasEnd"] == DateTime.to_iso8601(int.has_end) + assert data["hasPointInTime"] == DateTime.to_iso8601(int.has_point_in_time) + assert data["due"] == DateTime.to_iso8601(int.due) + assert data["finished"] == int.finished + assert data["atLocation"]["id"] == int.at_location_id + assert data["image"] == int.image + assert data["name"] == int.name + assert data["note"] == int.note + assert data["agreedIn"] == int.agreed_in + end +end + +describe "Mutation" do + test "createIntent with only provider", %{params: params} do + params = Map.put(params, "receiver", nil) + + assert %{data: %{"createIntent" => %{"intent" => data}}} = + run!(""" + #{@frag} + mutation ($intent: IntentCreateParams!) { + createIntent(intent: $intent) { + intent {...intent} + } + } + """, vars: %{"intent" => params}) + + assert {:ok, _} = ID.cast(data["id"]) + assert data["action"]["id"] == params["action"] + assert data["inputOf"]["id"] == params["inputOf"] + assert data["outputOf"]["id"] == params["outputOf"] + assert data["provider"]["id"] == params["provider"] + assert data["receiver"]["id"] == params["receiver"] + assert data["resourceInventoriedAs"]["id"] == params["resourceInventoriedAs"] + assert data["resourceConformsTo"]["id"] == params["resourceConformsTo"] + assert data["resourceClassifiedAs"] == params["resourceClassifiedAs"] + assert data["resourceQuantity"]["hasNumericalValue"] == params["resourceQuantity"]["hasNumericalValue"] + assert data["resourceQuantity"]["hasUnit"]["id"] == params["resourceQuantity"]["hasUnit"] + assert data["effortQuantity"]["hasNumericalValue"] == params["effortQuantity"]["hasNumericalValue"] + assert data["effortQuantity"]["hasUnit"]["id"] == params["effortQuantity"]["hasUnit"] + assert data["availableQuantity"]["hasNumericalValue"] == params["availableQuantity"]["hasNumericalValue"] + assert data["availableQuantity"]["hasUnit"]["id"] == params["availableQuantity"]["hasUnit"] + assert data["atLocation"]["id"] == params["atLocation"] + + Enum.each(~w[ + hasBeginning hasEnd hasPointInTime due + finished image name note agreedIn + ], fn x -> + assert data[x] == params[x] + end) + end + + test "createIntent with only receiver", %{params: params} do + params = Map.put(params, "provider", nil) + + assert %{data: %{"createIntent" => %{"intent" => data}}} = + run!(""" + #{@frag} + mutation ($intent: IntentCreateParams!) { + createIntent(intent: $intent) { + intent {...intent} + } + } + """, vars: %{"intent" => params}) + + assert {:ok, _} = ID.cast(data["id"]) + assert data["action"]["id"] == params["action"] + assert data["inputOf"]["id"] == params["inputOf"] + assert data["outputOf"]["id"] == params["outputOf"] + assert data["provider"]["id"] == params["provider"] + assert data["receiver"]["id"] == params["receiver"] + assert data["resourceInventoriedAs"]["id"] == params["resourceInventoriedAs"] + assert data["resourceConformsTo"]["id"] == params["resourceConformsTo"] + assert data["resourceClassifiedAs"] == params["resourceClassifiedAs"] + assert data["resourceQuantity"]["hasNumericalValue"] == params["resourceQuantity"]["hasNumericalValue"] + assert data["resourceQuantity"]["hasUnit"]["id"] == params["resourceQuantity"]["hasUnit"] + assert data["effortQuantity"]["hasNumericalValue"] == params["effortQuantity"]["hasNumericalValue"] + assert data["effortQuantity"]["hasUnit"]["id"] == params["effortQuantity"]["hasUnit"] + assert data["availableQuantity"]["hasNumericalValue"] == params["availableQuantity"]["hasNumericalValue"] + assert data["availableQuantity"]["hasUnit"]["id"] == params["availableQuantity"]["hasUnit"] + assert data["atLocation"]["id"] == params["atLocation"] + + Enum.each(~w[ + hasBeginning hasEnd hasPointInTime due + finished image name note agreedIn + ], fn x -> + assert data[x] == params[x] + end) + end + + test "updateIntent with only provider", %{params: params} do + params = Map.put(params, "receiver", nil) + %{id: id} = Factory.insert!(:intent, %{provider: Factory.build(:agent), receiver: nil}) + assert %{data: %{"updateIntent" => %{"intent" => data}}} = + run!(""" + #{@frag} + mutation ($intent: IntentUpdateParams!) { + updateIntent(intent: $intent) { + intent {...intent} + } + } + """, vars: %{ + "intent" => Map.put(params, "id", id), + }) + + assert data["id"] == id + assert data["action"]["id"] == params["action"] + assert data["inputOf"]["id"] == params["inputOf"] + assert data["outputOf"]["id"] == params["outputOf"] + assert data["provider"]["id"] == params["provider"] + assert data["receiver"]["id"] == params["receiver"] + assert data["resourceInventoriedAs"]["id"] == params["resourceInventoriedAs"] + assert data["resourceConformsTo"]["id"] == params["resourceConformsTo"] + assert data["resourceClassifiedAs"] == params["resourceClassifiedAs"] + assert data["resourceQuantity"]["hasNumericalValue"] == params["resourceQuantity"]["hasNumericalValue"] + assert data["resourceQuantity"]["hasUnit"]["id"] == params["resourceQuantity"]["hasUnit"] + assert data["effortQuantity"]["hasNumericalValue"] == params["effortQuantity"]["hasNumericalValue"] + assert data["effortQuantity"]["hasUnit"]["id"] == params["effortQuantity"]["hasUnit"] + assert data["availableQuantity"]["hasNumericalValue"] == params["availableQuantity"]["hasNumericalValue"] + assert data["availableQuantity"]["hasUnit"]["id"] == params["availableQuantity"]["hasUnit"] + assert data["atLocation"]["id"] == params["atLocation"] + + Enum.each(~w[ + hasBeginning hasEnd hasPointInTime due + finished image name note agreedIn + ], fn x -> + assert data[x] == params[x] + end) + end + + test "updateIntent with only receiver", %{params: params} do + params = Map.put(params, "provider", nil) + %{id: id} = Factory.insert!(:intent, %{provider: nil, receiver: Factory.build(:agent)}) + assert %{data: %{"updateIntent" => %{"intent" => data}}} = + run!(""" + #{@frag} + mutation ($intent: IntentUpdateParams!) { + updateIntent(intent: $intent) { + intent {...intent} + } + } + """, vars: %{ + "intent" => Map.put(params, "id", id), + }) + + assert data["id"] == id + assert data["action"]["id"] == params["action"] + assert data["inputOf"]["id"] == params["inputOf"] + assert data["outputOf"]["id"] == params["outputOf"] + assert data["provider"]["id"] == params["provider"] + assert data["receiver"]["id"] == params["receiver"] + assert data["resourceInventoriedAs"]["id"] == params["resourceInventoriedAs"] + assert data["resourceConformsTo"]["id"] == params["resourceConformsTo"] + assert data["resourceClassifiedAs"] == params["resourceClassifiedAs"] + assert data["resourceQuantity"]["hasNumericalValue"] == params["resourceQuantity"]["hasNumericalValue"] + assert data["resourceQuantity"]["hasUnit"]["id"] == params["resourceQuantity"]["hasUnit"] + assert data["effortQuantity"]["hasNumericalValue"] == params["effortQuantity"]["hasNumericalValue"] + assert data["effortQuantity"]["hasUnit"]["id"] == params["effortQuantity"]["hasUnit"] + assert data["availableQuantity"]["hasNumericalValue"] == params["availableQuantity"]["hasNumericalValue"] + assert data["availableQuantity"]["hasUnit"]["id"] == params["availableQuantity"]["hasUnit"] + assert data["atLocation"]["id"] == params["atLocation"] + + Enum.each(~w[ + hasBeginning hasEnd hasPointInTime due + finished image name note agreedIn + ], fn x -> + assert data[x] == params[x] + end) + end + + test "deleteIntent", %{inserted: %{id: id}} do + assert %{data: %{"deleteIntent" => true}} = + run!(""" + mutation ($id: ID!) { + deleteIntent(id: $id) + } + """, vars: %{"id" => id}) + end +end +end diff --git a/test/vf/proposal/domain.test.exs b/test/vf/proposal/domain.test.exs @@ -0,0 +1,164 @@ +# 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 ZenflowsTest.VF.Proposal.Domain do +use ZenflowsTest.Help.EctoCase, async: true + +alias Zenflows.VF.{ + Proposal, + Proposal.Domain, + SpatialThing, +} + +setup do + %{ + params: %{ + name: Factory.str("name"), + note: Factory.str("note"), + has_beginning: Factory.now(), + has_end: Factory.now(), + unit_based: Factory.bool(), + eligible_location_id: Factory.insert!(:spatial_thing).id, + }, + inserted: Factory.insert!(:proposal), + id: Factory.id(), + } +end + +describe "one/1" do + test "with good id: finds the Proposal", %{inserted: %{id: id}} do + assert {:ok, %Proposal{}} = Domain.one(id) + end + + test "with bad id: doesn't find the Proposal", %{id: id} do + assert {:error, "not found"} = Domain.one(id) + end +end + +describe "create/1" do + test "with good params: creates a Proposal", %{params: params} do + assert {:ok, %Proposal{} = prop} = Domain.create(params) + assert prop.name == params.name + assert prop.note == params.note + assert prop.has_beginning == params.has_beginning + assert prop.has_end == params.has_end + assert prop.unit_based == params.unit_based + assert prop.eligible_location_id == params.eligible_location_id + end + + test "with empty params: creates a Proposal" do + assert {:ok, %Proposal{} = prop} = Domain.create(%{}) + assert prop.name == nil + assert prop.note == nil + assert prop.has_beginning == nil + assert prop.has_end == nil + assert prop.unit_based == false # since it defaults + assert prop.eligible_location_id == nil + end +end + +describe "update/2" do + test "with good params: updates the Proposal", %{params: params, inserted: old} do + assert {:ok, %Proposal{} = new} = Domain.update(old.id, params) + + assert new.name == params.name + assert new.note == params.note + assert new.has_beginning == params.has_beginning + assert new.has_end == params.has_end + assert new.unit_based == params.unit_based + assert new.eligible_location_id == params.eligible_location_id + end + + test "with bad params: doesn't update the Proposal", %{inserted: old} do + assert {:ok, %Proposal{} = new} = Domain.update(old.id, %{}) + + assert new.name == old.name + assert new.note == old.note + assert new.has_beginning == old.has_beginning + assert new.has_end == old.has_end + assert new.unit_based == old.unit_based + assert new.eligible_location_id == old.eligible_location_id + end +end + +describe "delete/1" do + test "with good id: deletes the Proposal", %{inserted: %{id: id}} do + assert {:ok, %Proposal{id: ^id}} = Domain.delete(id) + assert {:error, "not found"} = Domain.one(id) + end + + test "with bad id: doesn't delete the Proposal", %{id: id} do + assert {:error, "not found"} = Domain.delete(id) + end +end + +describe "preload/2" do + test "preloads `:eligible_location`", %{inserted: %{id: id}} do + assert {:ok, prop} = Domain.one(id) + prop = Domain.preload(prop, :eligible_location) + assert %SpatialThing{} = prop.eligible_location + end + + test "preloads `:publishes`", %{inserted: %{id: id}} do + assert {:ok, prop} = Domain.one(id) + + assert [] = Domain.preload(prop, :publishes).publishes + + left = + Enum.map(0..9, fn _ -> + Factory.insert!(:proposed_intent, %{published_in: prop}).id + end) + right = + Domain.preload(prop, :publishes) + |> Map.fetch!(:publishes) + |> Enum.map(& &1.id) + assert left -- right == [] + end + + test "preloads `:primary_intents`", %{inserted: %{id: id}} do + assert {:ok, prop} = Domain.one(id) + + assert [] = Domain.preload(prop, :primary_intents).primary_intents + + left = + Enum.map(0..9, fn _ -> + Factory.insert!(:proposed_intent, %{published_in: prop, reciprocal: false}).publishes_id + end) + right = + Domain.preload(prop, :primary_intents) + |> Map.fetch!(:primary_intents) + |> Enum.map(& &1.id) + assert left -- right == [] + end + + test "preloads `:reciprocal_intents`", %{inserted: %{id: id}} do + assert {:ok, prop} = Domain.one(id) + + assert [] = Domain.preload(prop, :reciprocal_intents).reciprocal_intents + + left = + Enum.map(0..9, fn _ -> + Factory.insert!(:proposed_intent, %{published_in: prop, reciprocal: true}).publishes_id + end) + right = + Domain.preload(prop, :reciprocal_intents) + |> Map.fetch!(:reciprocal_intents) + |> Enum.map(& &1.id) + assert left -- right == [] + end +end +end diff --git a/test/vf/proposal/type.test.exs b/test/vf/proposal/type.test.exs @@ -0,0 +1,214 @@ +# 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 ZenflowsTest.VF.Proposal.Type do +use ZenflowsTest.Help.AbsinCase, async: true + +setup do + %{ + params: %{ + "name" => Factory.str("name"), + "note" => Factory.str("note"), + "hasBeginning" => Factory.iso_now(), + "hasEnd" => Factory.iso_now(), + "unitBased" => Factory.bool(), + "eligibleLocation" => Factory.insert!(:spatial_thing).id, + }, + inserted: Factory.insert!(:proposal), + } +end + +@frag """ +fragment proposal on Proposal { + id + name + note + hasBeginning + hasEnd + unitBased + eligibleLocation {id} +} +""" +describe "Query" do + test "proposal", %{inserted: prop} do + assert %{data: %{"proposal" => data}} = + run!(""" + #{@frag} + query ($id: ID!) { + proposal(id: $id) {...proposal} + } + """, vars: %{"id" => prop.id}) + + assert data["id"] == prop.id + assert data["name"] == prop.name + assert data["note"] == prop.note + assert data["unitBased"] == prop.unit_based + assert data["eligibleLocation"]["id"] == prop.eligible_location_id + assert {:ok, has_beginning, 0} = DateTime.from_iso8601(data["hasBeginning"]) + assert DateTime.compare(DateTime.utc_now(), has_beginning) != :lt + assert {:ok, has_end, 0} = DateTime.from_iso8601(data["hasEnd"]) + assert DateTime.compare(DateTime.utc_now(), has_end) != :lt + end + + test "proposals" do + assert %{data: %{"proposals" => data}} = + run!(""" + #{@frag} + query { + proposals { + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage + totalCount + pageLimit + } + edges { + cursor + node {...proposal} + } + } + } + """) + assert %{ + "pageInfo" => %{ + "startCursor" => nil, + "endCursor" => nil, + "hasPreviousPage" => false, + "hasNextPage" => false, + "totalCount" => nil, + "pageLimit" => nil, + }, + "edges" => [], + } = data + end + + test "offers" do + assert %{data: %{"offers" => data}} = + run!(""" + #{@frag} + query { + offers { + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage + totalCount + pageLimit + } + edges { + cursor + node {...proposal} + } + } + } + """) + assert %{ + "pageInfo" => %{ + "startCursor" => nil, + "endCursor" => nil, + "hasPreviousPage" => false, + "hasNextPage" => false, + "totalCount" => nil, + "pageLimit" => nil, + }, + "edges" => [], + } = data + end + + test "requests" do + assert %{data: %{"requests" => data}} = + run!(""" + #{@frag} + query { + requests { + pageInfo { + startCursor + endCursor + hasPreviousPage + hasNextPage + totalCount + pageLimit + } + edges { + cursor + node {...proposal} + } + } + } + """) + assert %{ + "pageInfo" => %{ + "startCursor" => nil, + "endCursor" => nil, + "hasPreviousPage" => false, + "hasNextPage" => false, + "totalCount" => nil, + "pageLimit" => nil, + }, + "edges" => [], + } = data + end +end + +describe "Mutation" do + test "createProposal", %{params: params} do + assert %{data: %{"createProposal" => %{"proposal" => data}}} = + run!(""" + #{@frag} + mutation ($proposal: ProposalCreateParams!) { + createProposal(proposal: $proposal) { + proposal {...proposal} + } + } + """, vars: %{"proposal" => params}) + + assert {:ok, _} = Zenflows.DB.ID.cast(data["id"]) + keys = ~w[name note unitBased hasBeginning hasEnd] + assert Map.take(data, keys) == Map.take(params, keys) + assert data["eligibleLocation"]["id"] == params["eligibleLocation"] + end + + test "updateProposal", %{params: params, inserted: prop} do + assert %{data: %{"updateProposal" => %{"proposal" => data}}} = + run!(""" + #{@frag} + mutation ($proposal: ProposalUpdateParams!) { + updateProposal(proposal: $proposal) { + proposal {...proposal} + } + } + """, vars: %{"proposal" => params |> Map.put("id", prop.id)}) + + assert data["id"] == prop.id + keys = ~w[name note unitBased hasBeginning hasEnd] + assert Map.take(data, keys) == Map.take(params, keys) + assert data["eligibleLocation"]["id"] == params["eligibleLocation"] + end + + test "deleteProposal", %{inserted: %{id: id}} do + assert %{data: %{"deleteProposal" => true}} = + run!(""" + mutation ($id: ID!) { + deleteProposal(id: $id) + } + """, vars: %{"id" => id}) + end +end +end diff --git a/test/vf/proposed_intent/domain.test.exs b/test/vf/proposed_intent/domain.test.exs @@ -0,0 +1,88 @@ +# 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 ZenflowsTest.VF.ProposedIntent.Domain do +use ZenflowsTest.Help.EctoCase, async: true + +alias Ecto.Changeset +alias Zenflows.VF.{ + Intent, + Proposal, + ProposedIntent, + ProposedIntent.Domain, +} + +setup do + %{ + params: %{ + reciprocal: Factory.bool(), + publishes_id: Factory.insert!(:intent).id, + published_in_id: Factory.insert!(:proposal).id, + }, + inserted: Factory.insert!(:proposed_intent), + id: Factory.id(), + } +end + +describe "one/1" do + test "with good id: finds the ProposedIntent", %{inserted: %{id: id}} do + assert {:ok, %ProposedIntent{}} = Domain.one(id) + end + + test "with bad id: doesn't find the ProposedIntent", %{id: id} do + assert {:error, "not found"} = Domain.one(id) + end +end + +describe "create/1" do + test "with good params: creates a ProposedIntent", %{params: params} do + assert {:ok, %ProposedIntent{} = prop_int} = Domain.create(params) + assert prop_int.reciprocal == params.reciprocal + assert prop_int.publishes_id == params.publishes_id + assert prop_int.published_in_id == params.published_in_id + end + + test "with bad params: doesn't create a ProposedIntent" do + assert {:error, %Changeset{}} = Domain.create(%{}) + end +end + +describe "delete/1" do + test "with good id: deletes the ProposedIntent", %{inserted: %{id: id}} do + assert {:ok, %ProposedIntent{id: ^id}} = Domain.delete(id) + assert {:error, "not found"} = Domain.one(id) + end + + test "with bad id: doesn't delete the ProposedIntent", %{id: id} do + assert {:error, "not found"} = Domain.delete(id) + end +end + +describe "preload/2" do + test "preloads `:published_in`", %{inserted: %{id: id}} do + assert {:ok, prop} = Domain.one(id) + prop = Domain.preload(prop, :published_in) + assert %Proposal{} = prop.published_in + end + + test "preloads `:publishes`", %{inserted: %{id: id}} do + assert {:ok, prop} = Domain.one(id) + prop = Domain.preload(prop, :publishes) + assert %Intent{} = prop.publishes + end +end +end diff --git a/test/vf/proposed_intent/type.test.exs b/test/vf/proposed_intent/type.test.exs @@ -0,0 +1,71 @@ +# 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 ZenflowsTest.VF.ProposedIntent.Type do +use ZenflowsTest.Help.AbsinCase, async: true + +setup do + %{ + params: %{ + "reciprocal" => Factory.bool(), + "publishedIn" => Factory.insert!(:proposal).id, + "publishes" => Factory.insert!(:intent).id + }, + inserted: Factory.insert!(:proposed_intent), + } +end + +describe "Mutation" do + test "proposeIntent", %{params: params} do + assert %{data: %{"proposeIntent" => %{"proposedIntent" => data}}} = + run!(""" + mutation ( + $reciprocal: Boolean! + $publishedIn: ID! + $publishes: ID! + ) { + proposeIntent( + reciprocal: $reciprocal + publishedIn: $publishedIn + publishes: $publishes + ) { + proposedIntent { + id + reciprocal + publishedIn {id} + publishes {id} + } + } + } + """, vars: params) + + assert {:ok, _} = Zenflows.DB.ID.cast(data["id"]) + assert data["reciprocal"] == params["reciprocal"] + assert data["publishedIn"]["id"] == params["publishedIn"] + assert data["publishes"]["id"] == params["publishes"] + end + + test "deleteProposedIntent", %{inserted: %{id: id}} do + assert %{data: %{"deleteProposedIntent" => true}} = + run!(""" + mutation ($id: ID!) { + deleteProposedIntent(id: $id) + } + """, vars: %{"id" => id}) + end +end +end