zf

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

commit ef1dfbb9b2641ec026743baa6c0156fbe7f47115
parent 3a0b7e70d60ea3183e5ee8262505aa89d4473dff
Author: srfsh <dev@srf.sh>
Date:   Wed,  6 Jul 2022 23:28:27 +0200

sw_pass: add support for interacting with softwarepassport instances

Diffstat:
Msrc/zenflows/gql/schema.ex | 5+++++
Asrc/zenflows/sw_pass/domain.ex | 238+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/zenflows/sw_pass/resolv.ex | 19+++++++++++++++++++
Asrc/zenflows/sw_pass/type.ex | 30++++++++++++++++++++++++++++++
4 files changed, 292 insertions(+), 0 deletions(-)

diff --git a/src/zenflows/gql/schema.ex b/src/zenflows/gql/schema.ex @@ -7,6 +7,7 @@ alias Zenflows.VF import_types Absinthe.Type.Custom import_types Zenflows.GQL.Type +import_types Zenflows.SWPass.Type import_types VF.TimeUnit.Type import_types VF.Action.Type @@ -48,6 +49,8 @@ import_types VF.EconomicEvent.Type #import_types VF.ProposedTo.Type query do + import_fields :query_sw_pass + import_fields :query_unit import_fields :query_spatial_thing import_fields :query_process_specification @@ -85,6 +88,8 @@ query do end mutation do + import_fields :mutation_sw_pass + import_fields :mutation_unit import_fields :mutation_spatial_thing import_fields :mutation_process_specification diff --git a/src/zenflows/sw_pass/domain.ex b/src/zenflows/sw_pass/domain.ex @@ -0,0 +1,238 @@ +defmodule Zenflows.SWPass.Domain do +@moduledoc """ +Domain logic of interacting with softwarepassport instances over +HTTP. +""" + +alias Zenflows.DB.Repo +alias Zenflows.VF.{ + EconomicEvent, + EconomicResource, + Person, + Process, + ResourceSpecification, + Unit, +} +alias Ecto.Multi + +# TODO: not the best piece of code, but will do okay for now. +@doc """ +Import repos by a URL to `/repositories` route of a softwarepassport +instance, and generate necessary things to link Person Agents to +EconomicResource Procjets. +""" +@spec import_repos(String.t()) :: {:ok, term()} | {:error, term()} +def import_repos(url) do + url = to_charlist(url) + hdrs = [ + {'user-agent', useragent()}, + {'accept', 'application/json'}, + ] + http_opts = [ + {:timeout, 30000}, # 30 seconds + {:connect_timeout, 5000}, # 5 seconds + {:autoredirect, false}, + ] + with {:ok, {{_, 200, _}, _, body_charlist}} <- + :httpc.request(:get, {url, hdrs}, http_opts, []), + {:ok, map} <- body_charlist |> to_string() |> Jason.decode() do + proj = get_emails(map) + + result = Enum.map(proj, fn {url, emails} -> + now = DateTime.utc_now() + mult = + Multi.new() + |> Multi.run(:commit_spec, fn repo, _ -> + params = %{name: "committing"} + + case repo.get_by(ResourceSpecification, params) do + nil -> + params + |> ResourceSpecification.chgset() + |> repo.insert() + res_spec -> {:ok, res_spec} + end + end) + |> Multi.run(:proj_spec, fn repo, _ -> + params = %{name: "project"} + + case repo.get_by(ResourceSpecification, params) do + nil -> + params + |> ResourceSpecification.chgset() + |> repo.insert() + res_spec -> {:ok, res_spec} + end + end) + |> Multi.run(:unit, fn repo, _ -> + params = %{label: "one", symbol: "one"} + + case repo.get_by(Unit, params) do + nil -> + params + |> Unit.chgset() + |> repo.insert() + unit -> {:ok, unit} + end + end) + |> Multi.run(:proc, fn repo, _ -> + params = %{name: url} + + case repo.get_by(Process, params) do + nil -> + params + |> Process.chgset() + |> repo.insert() + proc -> {:ok, proc} + end + end) + Enum.reduce(emails, mult, fn e, mult -> + mult + |> Multi.run("per:#{e}", fn repo, _ -> + params = %{email: e, user: e, name: e} + + case repo.get_by(Person, params) do + nil -> + params + |> Person.chgset() + |> repo.insert() + per -> {:ok, per} + end + end) + |> Multi.run("evt:#{e}", fn repo, %{proc: proc, commit_spec: commit_spec} = chgs -> + per = Map.fetch!(chgs ,"per:#{e}") + params = %{ + action_id: "deliverService", + input_of_id: proc.id, + provider_id: per.id, + receiver_id: per.id, + resource_conforms_to_id: commit_spec.id, + note: url, + } + + case repo.get_by(EconomicEvent, params) do + nil -> + params + |> Map.put(:has_point_in_time, now) + |> EconomicEvent.chgset() + |> repo.insert() + evt -> {:ok, evt} + end + end) + end) + |> Multi.merge(fn changes -> + first_email = emails |> MapSet.to_list() |> List.first() + first_person = Map.fetch!(changes, "per:#{first_email}") + Multi.put(Multi.new(), :org, first_person) # TODO: make this really an organization + end) + |> Multi.run(:produce, fn repo, %{org: org, proc: proc, proj_spec: proj_spec, unit: unit} -> + params = %{ + action_id: "produce", + output_of_id: proc.id, + provider_id: org.id, + receiver_id: org.id, + resource_conforms_to_id: proj_spec.id, + note: url, + } + + case repo.get_by(EconomicEvent, + params + |> Map.put(:resource_quantity_has_unit_id, unit.id) + |> Map.put(:resource_quantity_has_numerical_value, 1) + ) do + nil -> + params + |> Map.put(:resource_quantity, %{ + has_unit_id: unit.id, + has_numerical_value: 1, + }) + |> Map.put(:has_point_in_time, now) + |> EconomicEvent.chgset() + |> repo.insert() + evt -> {:ok, evt} + end + end) + |> Multi.run(:resource, fn repo, %{unit: unit, org: org, proj_spec: proj_spec} -> + params = %{ + name: url, + primary_accountable_id: org.id, + custodian_id: org.id, + conforms_to_id: proj_spec.id, + accounting_quantity_has_unit_id: unit.id, + accounting_quantity_has_numerical_value: 1, + onhand_quantity_has_unit_id: unit.id, + onhand_quantity_has_numerical_value: 1, + } + + case repo.get_by(EconomicResource, params) do + nil -> + params + |> EconomicResource.chgset() + |> repo.insert() + res -> {:ok, res} + end + end) + |> Multi.run(:update_produce_evt, fn repo, %{produce: evt, resource: res} -> + Ecto.Changeset.change(evt, resource_inventoried_as_id: res.id) |> repo.update() + end) + |> Repo.transaction() + |> case do + {:ok, res} -> {:ok, res} + {:error, op, val, chng} -> {:error, op, val, chng} + end + end) + + if Enum.all?(result, &match?({:ok, _}, &1)) do + {:ok, result} + else + {:error, result} + end + else + {:ok, {{_, stat, _}, _, body_charlist}} -> + {:error, "the http call result in non-200 status code #{stat}: #{to_string(body_charlist)}"} + + other -> other + end +end + +@spec project_agents(String.t()) :: [Agent.t()] +def project_agents(url) do + import Ecto.Query + + from(p in Process, + join: e in assoc(p, :inputs), + join: a in assoc(e, :provider), + where: p.name == ^url, + select: a) + |> Repo.all() +end + +# Get emails from the softwarepassport repositories output. +@spec get_emails(map()) :: %{String.t() => MapSet.t()} +defp get_emails(map) do + Enum.reduce(map, %{}, fn p, acc -> + # the scanning might be in proccess, so it might be `nil` + if p["scancode_report"] do + emails = + Enum.reduce(p["scancode_report"]["files"], MapSet.new(), fn f, acc -> + Enum.reduce(f["emails"], acc, fn e, acc -> + MapSet.put(acc, String.downcase(e["email"])) + end) + end) + if emails == MapSet.new() do # empty? + acc + else + Map.put(acc, p["url"], emails) + end + else + acc + end + end) +end + +# Return the useragent to be used by the HTTP client, this module. +@spec useragent() :: charlist() +defp useragent() do + 'zenflows/' ++ Application.spec(:zenflows, :vsn) +end +end diff --git a/src/zenflows/sw_pass/resolv.ex b/src/zenflows/sw_pass/resolv.ex @@ -0,0 +1,19 @@ +defmodule Zenflows.SWPass.Resolv do +@moduledoc "Resolvers of softwarepassport-related queries." + +alias Zenflows.Admin +alias Zenflows.SWPass.Domain + +def import_repos(%{url: url, admin_key: key}, _) do + with :ok <- Admin.auth(key), + {:ok, _} <- Domain.import_repos(url) do + {:ok, "successfully imported"} + else _ -> + {:error, "something went wrong"} + end +end + +def project_agents(%{url: url}, _) do + {:ok, Domain.project_agents(url)} +end +end diff --git a/src/zenflows/sw_pass/type.ex b/src/zenflows/sw_pass/type.ex @@ -0,0 +1,30 @@ +defmodule Zenflows.SWPass.Type do +@moduledoc "GraphQL types of SWPass." + +use Absinthe.Schema.Notation + +alias Zenflows.SWPass.Resolv + +object :query_sw_pass do + @desc "List all the agents associated in a project." + field :project_agents, list_of(:agent) do + @desc "The URL to the project." + arg :url, non_null(:string) + + resolve &Resolv.project_agents/2 + end +end + +object :mutation_sw_pass do + @desc "Import repositories from a softwarepassport instance." + field :import_repos, :string do + @desc "The configuration-defined key to authenticate admin calls." + arg :admin_key, non_null(:string) + + @desc "The URL where all the repository information is listed." + arg :url, non_null(:string) + + resolve &Resolv.import_repos/2 + end +end +end