From 4a9cb34f9cd572dc61fb3f8672730f9baacad398 Mon Sep 17 00:00:00 2001 From: "aime.rolandi" Date: Wed, 28 May 2025 14:40:15 -0300 Subject: [PATCH] Se crea el parser para customfilter. Agregamos variables de entorno para manejar url de adjuntos con o sin clave de encriptacion. --- .env | 7 +- lib/api/api_consultas/studies.ex | 71 ++++++----- lib/api/api_consultas/study.ex | 129 +++++++++++++------- lib/api/application.ex | 2 +- lib/api/emailsender/schemas/Emailstosend.ex | 1 + lib/api/parser/expression_parser.ex | 124 +++++++++++++++++++ lib/api/parser/expression_to_ecto.ex | 59 +++++++++ mix.exs | 3 +- 8 files changed, 318 insertions(+), 78 deletions(-) create mode 100644 lib/api/parser/expression_parser.ex create mode 100644 lib/api/parser/expression_to_ecto.ex diff --git a/.env b/.env index f2a2cc4..fdc4f91 100644 --- a/.env +++ b/.env @@ -4,7 +4,6 @@ DATABASE_URL=ecto://postgres:1nf0rm3@127.0.0.1/dicomscp CHECK_ORIGIN=https://estudios.nobissalud.com.ar PHX_PORT=4001 PHX_HOST=127.0.0.1 -#PHX_HOST=pacientes.sdlc.com.ar ROOT_PATH=/api SCHEME=https #si es true debe estar escrito siempre en mayuscula (por que llega como string no como boolean) @@ -16,8 +15,10 @@ PYTHON_SCRIPT=/home/informemedico/impacs/cgi/soffice/generar_pdf_api.py TEMPLATE_NAME=Con Membrete pr2_statusname=FINAL IDSITE=86 -#ESCANEADOS=https://cedialcom.informemedico.com.ar:4443/cgi-bin/imageasjpeg.bf/imageasjpeg/ -ESCANEADOS=https://estudios.nobissalud.com.ar/# +ESCANEADOS=https://estudios.nobissalud.com.ar/ +# ESCANEADOS_CLAVE= +ADJUNTOS=https://estudios.nobissalud.com.ar/api/attachment/ +ADJUNTOS_CLAVE=1nf0rm3 acceso_ed=IDSTUDY IDENTIFIERFIELD=IDSTUDY diff --git a/lib/api/api_consultas/studies.ex b/lib/api/api_consultas/studies.ex index ffe8fe4..137033b 100644 --- a/lib/api/api_consultas/studies.ex +++ b/lib/api/api_consultas/studies.ex @@ -3,14 +3,23 @@ defmodule Api.Studies do alias Api.Repo def studies_sql_query(filters) do + customfilter = + case Map.get(filters, "customfilter") do + nil -> dynamic([_], true) + "" -> dynamic([_], true) + expr -> + case ExpressionParser.parse(expr) do + {:ok, ast} -> ExpressionToEcto.to_dynamic(ast) + {:error, _reason} -> raise "Error al parsear customfilter" + end + end + page = filters["page"] || 1 size = filters["size"] || 24 filter = filters["filter"] || [] sort = filters["sort"] || [%{"dir" => "desc", "field" => "studydate"}] - - # Construcción de condiciones de filtro dinámicas filter_conditions = Enum.reduce(filter, dynamic(true), fn f, acc -> @@ -23,7 +32,7 @@ defmodule Api.Studies do dynamic([s], ilike(s.modality, ^"%#{value}%")) "idstudy" -> - dynamic([s], s.idstudy == ^String.to_integer(value)) + dynamic([s], like(fragment("CAST(? AS TEXT)", s.idstudy), ^"#{value}%")) "studydate" -> dynamic([s], fragment("CAST(? AS DATE)::text LIKE ?", s.studydate, ^"%#{value}%")) @@ -82,35 +91,37 @@ defmodule Api.Studies do {direction, field} end) + combined_filter = + dynamic([q], ^filter_conditions and ^customfilter) query = - from s in "study", - join: p in "patient", - on: p.idpatient == s.idpatient, - left_join: sr in "studyreport", - on: sr.idstudy == s.idstudy, - left_join: st in "statuses", - on: st.idstatus == sr.idstudyreport, - where: ^filter_conditions, - select: %{ - recordstotal: fragment("count(*) over()"), - idstudy: fragment("substring(encrypt(?::text::bytea, '1nf0rm3', 'aes')::text from 3)", s.idstudy), - #idstudy: s.idstudy, - accessionnumber: s.accessionnumber, - studydate: s.studydate, - studytime: s.studytime, - patientname: fragment("select replace(?, '^', ' ')", p.patientname), - proceduredescription: s.studydescription, - modality: s.modality, - sitename: s.institutionname, - insurer: s.insurer, - nrodocumento: fragment("substring(encrypt(?::text::bytea, '1nf0rm3', 'aes')::text from 3)", p.patientid), - #nrodocumento: p.patientid, - hasaudio: fragment("CASE WHEN ? IS NOT NULL THEN true ELSE false END", s.audiofile) - }, - order_by: ^sort_conditions, - limit: ^size, - offset: ^((page - 1) * size) + from s in "study", + join: p in "patient", + on: p.idpatient == s.idpatient, + left_join: sr in "studyreport", + on: sr.idstudy == s.idstudy, + left_join: st in "statuses", + on: st.idstatus == sr.idstudyreport, + where: ^combined_filter, + select: %{ + recordstotal: fragment("count(*) over()"), + idstudy: fragment("substring(encrypt(?::text::bytea, '1nf0rm3', 'aes')::text from 3)", s.idstudy), + #idstudy: s.idstudy, + accessionnumber: s.accessionnumber, + studydate: s.studydate, + studytime: s.studytime, + patientname: fragment("select replace(?, '^', ' ')", p.patientname), + proceduredescription: s.studydescription, + modality: s.modality, + sitename: s.institutionname, + insurer: s.insurer, + nrodocumento: fragment("substring(encrypt(?::text::bytea, '1nf0rm3', 'aes')::text from 3)", p.patientid), + #nrodocumento: p.patientid, + hasaudio: fragment("CASE WHEN ? IS NOT NULL THEN true ELSE false END", s.audiofile) + }, + order_by: ^sort_conditions, + limit: ^size, + offset: ^((page - 1) * size) result = Repo.one( diff --git a/lib/api/api_consultas/study.ex b/lib/api/api_consultas/study.ex index eebe74f..f445cf8 100644 --- a/lib/api/api_consultas/study.ex +++ b/lib/api/api_consultas/study.ex @@ -9,7 +9,6 @@ defmodule Api.Study do idsite = Envar.get("IDSITE") |> String.to_integer domain = Envar.get("CHECK_ORIGIN") - escaneados = Envar.get("ESCANEADOS") id_study = if Envar.get("IDENTIFIERFIELD") == "ACCESSIONNUMBER" do query = @@ -25,15 +24,6 @@ defmodule Api.Study do id_study = if is_integer(id_study), do: id_study, else: String.to_integer(id_study) - - # accession_fragment = - # if Envar.get("IDENTIFIERFIELD") == "ACCESSIONNUMBER" do - # fragment("?::text", ^accession) - # else - # fragment("?::integer", ^id_study) - # end - - Logger.info("id_study -------> #{inspect(id_study)}") # Las siguientes consultas obtienen la información completa @@ -70,56 +60,109 @@ defmodule Api.Study do } ) + # escaneados - subquery2 = subquery( - from s in "study", - join: ss in "studyscans", on: ss.idstudy == s.idstudy, - join: sc in "scanclasses", on: sc.idscanclass == ss.idscanclass, - join: p in "patient", on: s.idpatient == p.idpatient, - where: s.idstudy == ^id_study, - select: %{ - idsite: type(^idsite, :integer), + escaneados_clave = Envar.get("ESCANEADOS_CLAVE") + base_escaneados = Envar.get("ESCANEADOS") + + escaneados_expr = + if escaneados_clave do + dynamic([_, ss, _, _], + fragment( + "concat(?::text, substring(encrypt(?::text::bytea, ?, 'aes')::text from 3))", + ^base_escaneados, + ss.idstudyscan, + ^escaneados_clave + ) + ) + else + dynamic([_, ss, _, _], + fragment( + "concat(?::text, ?::text)", + ^base_escaneados, + ss.idstudyscan + ) + ) + end + + select_expr_es = + dynamic([s, ss, sc, p], %{ + idsite: type(^idsite, :integer), iddocument: fragment("substring(encrypt(?::text::bytea, '1nf0rm3', 'aes')::text from 3)", ss.idstudyscan), document_name: sc.scanclass, document_type: "url", - url: fragment( - "concat(?::text, substring(encrypt(?::text::bytea, '1nf0rm3', 'aes')::text from 3))", - ^escaneados, - ss.idstudyscan - ), + url: ^escaneados_expr, patientname: p.patientname, proceduredescription: s.studydescription, studydate: fragment("TO_CHAR(?, 'YYYY-MM-DD')", s.studydate), studytime: fragment("TO_CHAR(?, 'HH24:MI:SS')", s.studytime), accessionnumber: s.idstudy, patientid: p.patientid - } + }) + + subquery2 = subquery( + from s in "study", + join: ss in "studyscans", on: ss.idstudy == s.idstudy, + join: sc in "scanclasses", on: sc.idscanclass == ss.idscanclass, + join: p in "patient", on: s.idpatient == p.idpatient, + where: s.idstudy == ^id_study, + select: ^select_expr_es ) # adjuntos - subquery3 = + adjuntos_clave = Envar.get("ADJUNTOS_CLAVE") + base_adjuntos = Envar.get("ADJUNTOS") + + adjuntos_expr = + if adjuntos_clave do + dynamic([_, sa, _], + fragment( + "concat(?::text, substring(encrypt(?::text::bytea, ?, 'aes')::text from 3))", + ^base_adjuntos, + sa.idstudyattachment, + ^adjuntos_clave + ) + ) + else + dynamic([_, sa, _], + fragment( + "concat(?::text, ?::text)", + ^base_adjuntos, + sa.idstudyattachment + ) + ) + end + + select_expr_ad = + dynamic([s, sa, p], %{ + idsite: type(^idsite, :integer), + iddocument: + fragment( + "substring(encrypt(?::text::bytea, '1nf0rm3', 'aes')::text from 3)", + sa.idstudyattachment + ), + document_name: sa.name, + document_type: + fragment( + "CASE WHEN ? NOT IN ('jpg', 'jpeg', 'png') THEN 'attachment' ELSE 'url' END", + sa.format + ), + url: ^adjuntos_expr, + patientname: p.patientname, + proceduredescription: s.studydescription, + studydate: fragment("TO_CHAR(?, 'YYYY-MM-DD')", s.studydate), + studytime: fragment("TO_CHAR(?, 'HH24:MI:SS')", s.studytime), + accessionnumber: s.idstudy, + patientid: "" + }) + + subquery3 = subquery( from s in "study", join: sa in "studyattachments", on: sa.idstudy == s.idstudy, join: p in "patient", on: p.idpatient == s.idpatient, where: s.idstudy == ^id_study, - select: %{ - idsite: type(^idsite, :integer), - iddocument: fragment("substring(encrypt(?::text::bytea, '1nf0rm3', 'aes')::text from 3)", sa.idstudyattachment), - document_name: sa.name, - document_type: fragment("CASE WHEN ? NOT IN ('jpg', 'jpeg', 'png') THEN 'attachment' ELSE 'url' END", sa.format), - url: fragment( - "concat(?::text, ?::text, substring(encrypt(?::text::bytea, '1nf0rm3', 'aes')::text from 3))", - ^domain, - "/api/attachment/", - sa.idstudyattachment - ), - patientname: p.patientname, - proceduredescription: s.studydescription, - studydate: fragment("TO_CHAR(?, 'YYYY-MM-DD')", s.studydate), - studytime: fragment("TO_CHAR(?, 'HH24:MI:SS')", s.studytime), - accessionnumber: s.idstudy, - patientid: "" - } + select: ^select_expr_ad + ) # Si no encontró informes, ni adjuntos, ni nada # se traen sólo los datos básicos del estudio diff --git a/lib/api/application.ex b/lib/api/application.ex index 7bd8032..583399b 100644 --- a/lib/api/application.ex +++ b/lib/api/application.ex @@ -10,7 +10,7 @@ defmodule Api.Application do children = [ ApiWeb.Telemetry, Api.Repo, - {Api.Autosender, 1000 * 62 * 4}, + # {Api.Autosender, 1000 * 60}, # {DNSCluster, query: Application.get_env(:api, :dns_cluster_query) || :ignore}, {Phoenix.PubSub, name: Api.PubSub}, # Start the Finch HTTP client for sending emails diff --git a/lib/api/emailsender/schemas/Emailstosend.ex b/lib/api/emailsender/schemas/Emailstosend.ex index 978a9da..7e16ea7 100644 --- a/lib/api/emailsender/schemas/Emailstosend.ex +++ b/lib/api/emailsender/schemas/Emailstosend.ex @@ -13,6 +13,7 @@ defmodule Api.Emailtosend do field :errormsg, :string field :forcereprocess, :boolean field :sentdatetime, :naive_datetime + field :registered, :utc_datetime end def changeset(emailtosend, attrs) do diff --git a/lib/api/parser/expression_parser.ex b/lib/api/parser/expression_parser.ex new file mode 100644 index 0000000..c8887f0 --- /dev/null +++ b/lib/api/parser/expression_parser.ex @@ -0,0 +1,124 @@ +defmodule ExpressionParser do + import NimbleParsec + + whitespace = ignore(optional(ascii_string([?\s, ?\t, ?\n, ?\r], min: 1))) + + identifier = + ascii_string([?a..?z, ?A..?Z, ?_, ?0..?9], min: 1) + |> label("identifier") + |> unwrap_and_tag(:field) + + quoted_string = + ignore(string("\"")) + |> repeat(utf8_char(not: ?")) + |> reduce({List, :to_string, []}) + |> ignore(string("\"")) + + number = + ascii_string([?0..?9], min: 1) + |> map({String, :to_integer, []}) + + atom_value = + ignore(string("'")) + |> ascii_string([?a..?z, ?A..?Z], min: 1) + |> ignore(string("'")) + |> map({__MODULE__, :identity, []}) + + list_item = + choice([ + number, + atom_value, + quoted_string |> unwrap_and_tag(:string) + ]) + + atom_list = + ignore(string("[")) + |> optional(whitespace) + |> repeat( + list_item + |> optional(ignore(string(","))) + |> optional(whitespace) + ) + |> ignore(string("]")) + |> tag(:list) + + + + operator = + choice([ + string("ilike") |> replace(:ilike), + string(">=") |> replace(:>=), + string("<=") |> replace(:<=), + string("!=") |> replace(:!=), + string(">") |> replace(:>), + string("<") |> replace(:<), + string("=") |> replace(:==), + string("in") |> replace(:in) + ]) + + value = + choice([ + quoted_string |> unwrap_and_tag(:string), + number |> unwrap_and_tag(:number), + atom_list + ]) + + comparison = + identifier + |> ignore(whitespace) + |> concat(operator) + |> ignore(whitespace) + |> concat(value) + |> tag(:comparison) + + defcombinatorp :expr, parsec(:logic_expr) + + paren_expr = + ignore(string("(")) + |> ignore(whitespace) + |> parsec(:expr) + |> ignore(whitespace) + |> ignore(string(")")) + + logic_term = + choice([ + paren_expr, + comparison + ]) + + logical_op = + ignore(whitespace) + |> choice([ + string("and") |> replace(:and), + string("or") |> replace(:or) + ]) + |> ignore(whitespace) + + logic_expr = + logic_term + |> repeat( + logical_op + |> concat(logic_term) + ) + |> reduce({__MODULE__, :reduce_logic, []}) + + defparsec :logic_expr, logic_expr + + def parse(input) do + case logic_expr(input) do + {:ok, result, "", _, _, _} -> {:ok, result} + {:ok, _, rest, _, _, _} -> {:error, "Unexpected remaining input: #{inspect(rest)}"} + {:error, reason, _, _, _, _} -> {:error, reason} + end + end + + def reduce_logic([head | tail]) do + Enum.chunk_every(tail, 2) + |> Enum.reduce(head, fn [op, right], acc -> + {op, acc, right} + end) + end + + def identity(value), do: value + +end diff --git a/lib/api/parser/expression_to_ecto.ex b/lib/api/parser/expression_to_ecto.ex new file mode 100644 index 0000000..4618f82 --- /dev/null +++ b/lib/api/parser/expression_to_ecto.ex @@ -0,0 +1,59 @@ +defmodule ExpressionToEcto do + import Ecto.Query + + def to_dynamic([]), do: true + + def to_dynamic([single]), do: build_dynamic(single) + + def to_dynamic(ast), do: build_dynamic(ast) + + # Operadores lógicos + defp build_dynamic({:or, left, right}) do + dynamic([q], ^build_dynamic(left) or ^build_dynamic(right)) + end + + defp build_dynamic({:and, left, right}) do + dynamic([q], ^build_dynamic(left) and ^build_dynamic(right)) + end + + # Comparaciones con número + defp build_dynamic({:comparison, [{:field, field}, op, {:number, val}]}) do + field_atom = String.to_atom(field) + + case op do + :== -> dynamic([q], field(q, ^field_atom) == ^val) + :!= -> dynamic([q], field(q, ^field_atom) != ^val) + :> -> dynamic([q], field(q, ^field_atom) > ^val) + :< -> dynamic([q], field(q, ^field_atom) < ^val) + :>= -> dynamic([q], field(q, ^field_atom) >= ^val) + :<= -> dynamic([q], field(q, ^field_atom) <= ^val) + other -> raise "Operador no soportado: #{inspect(other)}" + end + end + + # Comparaciones con string + defp build_dynamic({:comparison, [{:field, field}, op, {:string, val}]}) when op in [:==, :!=] do + field_atom = String.to_atom(field) + case op do + :== -> dynamic([q], field(q, ^field_atom) == ^val) + :!= -> dynamic([q], field(q, ^field_atom) != ^val) + end + end + + # ILIKE + defp build_dynamic({:comparison, [{:field, field}, :ilike, {:string, val}]}) do + field_atom = String.to_atom(field) + pattern = "#{val}" + dynamic([q], ilike(field(q, ^field_atom), ^pattern)) + end + + # IN + defp build_dynamic({:comparison, [{:field, field}, :in, {:list, vals}]}) do + field_atom = String.to_atom(field) + dynamic([q], field(q, ^field_atom) in ^vals) + end + + defp build_dynamic(other) do + raise "AST no soportado o mal formado: #{inspect(other)}" + end +end diff --git a/mix.exs b/mix.exs index e31a853..902a169 100644 --- a/mix.exs +++ b/mix.exs @@ -64,7 +64,8 @@ defmodule Api.MixProject do {:ex_heroicons, "~> 2.0.0"}, {:castore, "~> 1.0.10"}, {:dotenv, "~> 3.1"}, - {:httpoison, "~> 2.2"} + {:httpoison, "~> 2.2"}, + {:nimble_parsec, "~> 1.4"} ] end