Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Searching by metadata poc #3022

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions assets/js/common/ComposedFilter/ComposedFilter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@ import React, { useEffect, useState } from 'react';
import Button from '@common/Button';
import Filter from '@common/Filter';
import DateFilter from '@common/DateFilter';
import Input from '@common/Input';

const renderFilter = (key, { type, ...filterProps }, value, onChange) => {
switch (type) {
case 'search_input':
return (
<Input
key={key}
{...filterProps}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
);
case 'select':
return (
<Filter key={key} {...filterProps} value={value} onChange={onChange} />
Expand Down
5 changes: 5 additions & 0 deletions assets/js/pages/ActivityLogPage/ActivityLogPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ function ActivityLogPage() {
const { abilities } = useSelector(getUserProfile);

const filters = [
{
key: 'metadata',
type: 'search_input',
title: 'Metadata',
},
{
key: 'type',
type: 'select',
Expand Down
2 changes: 1 addition & 1 deletion assets/js/pages/ActivityLogPage/searchParams.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const omitUndefined = (obj) =>
);

const paginationFields = ['after', 'before', 'first', 'last'];
const scalarKeys = [...paginationFields];
const scalarKeys = [...paginationFields, 'metadata'];

const searchParamsToEntries = (searchParams) =>
pipe(Array.from, uniq, (keys) =>
Expand Down
1 change: 1 addition & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ config :trento,

config :trento, :activity_log, refresh_interval: 60_000

config :trento, Trento.Repo, types: Trento.Postgrex.Types
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"
117 changes: 111 additions & 6 deletions lib/trento/activity_log.ex
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,58 @@ defmodule Trento.ActivityLog do
def list_activity_log(params, include_all_log_types? \\ false) do
parsed_params = parse_params(params)

case ActivityLog
|> maybe_exclude_user_logs(include_all_log_types?)
|> Flop.validate_and_run(parsed_params, for: ActivityLog) do
{:ok, {activity_log_entries, meta}} ->
{:ok, activity_log_entries, meta}
ActivityLog
|> maybe_exclude_user_logs(include_all_log_types?)
|> maybe_search_by_metadata(params)
|> case do
{:ok, query} ->
case Flop.validate_and_run(query, parsed_params, for: ActivityLog) do
{:ok, {activity_log_entries, meta}} ->
{:ok, activity_log_entries, meta}

error ->
Logger.error("Activity log fetch error: #{inspect(error)}")
{:error, :activity_log_fetch_error}
end

error ->
Logger.error("Activity log fetch error: #{inspect(error)}")
Logger.error("Activity log fetch error, metadata parse failure: #{inspect(error)}")
{:error, :activity_log_fetch_error}
end
end

defp maybe_search_by_metadata(query, params) do
maybe_metadata_search_string = params[:metadata]

case parse_metadata_query_string(maybe_metadata_search_string) do
{:ok, validated_query_string} ->
{:ok,
from(
q in query,
select: %{
id: q.id,
metadata: q.metadata,
md:
fragment(
"jsonb_path_query(?, ?)",
q.metadata,
^validated_query_string
),
type: q.type,
actor: q.actor,
inserted_at: q.inserted_at,
updated_at: q.updated_at
}
)}

:noop ->
{:ok, query}

_ ->
{:ok, query}
end
end

defp maybe_exclude_user_logs(ActivityLog = q, true = _include_all_log_types?), do: q

defp maybe_exclude_user_logs(ActivityLog = q, false = _include_all_log_types?) do
Expand Down Expand Up @@ -123,6 +163,71 @@ defmodule Trento.ActivityLog do
end)
end

def parse_metadata_query_string(maybe_metadata_search) do
case is_binary(maybe_metadata_search) && maybe_metadata_search != "" do
true ->
case do_parse_metadata_query_string(maybe_metadata_search) do
{:ok, parsed_string} ->
{:ok, parsed_string}

error ->
Logger.warning("Metadata query parse failed #{inspect(error)}")
error
end

false when is_nil(maybe_metadata_search) ->
:noop

false ->
Logger.error("Not a binary #{inspect(maybe_metadata_search)}")
:error
end
end

defp do_parse_metadata_query_string(query_string) do
query_words_list =
query_string
|> String.trim()
|> String.split()

word_handler = fn
"AND" ->
"&&"

"OR" ->
"||"

<<"~"::binary, _rest::binary>> = regex ->
"(@ like_regex \"#{String.trim_leading(regex, "~")}\")"

qws ->
"(@ == \"#{qws}\")"
end

case Enum.any?(query_words_list, fn qws -> qws == "OR" or qws == "AND" end) do
true ->
{:ok,
query_words_list
|> Enum.map_join(" ", word_handler)
|> (&"$.** ? (#{&1})").()}

false ->
case length(query_words_list) do
1 ->
{:ok,
query_words_list
|> Enum.map_join("", word_handler)
|> (&"$.** ? #{&1}").()}

len when len > 1 ->
{:ok,
query_words_list
|> Enum.map_join(" || ", word_handler)
|> (&"$.** ? (#{&1})").()}
end
end
end

defp log_error({:error, _} = error, message) do
Logger.error("#{message}: #{inspect(error)}")
error
Expand Down
39 changes: 39 additions & 0 deletions lib/trento/postgrex/jsonpath.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
defmodule Trento.Postgrex.Jsonpath do
@moduledoc false

@behaviour Postgrex.Extension

@impl Postgrex.Extension
def init(opts) do
Keyword.get(opts, :decode_copy, :copy)
end

@impl Postgrex.Extension
def matching(_state), do: [type: "jsonpath"]

@impl Postgrex.Extension
def format(_state), do: :text

@impl Postgrex.Extension
def encode(_state) do
quote do
bin when is_binary(bin) ->
[<<byte_size(bin)::signed-size(32)>> | bin]
end
end

@impl Postgrex.Extension
def decode(:reference) do
quote do
<<len::signed-size(32), bin::binary-size(len)>> ->
bin
end
end

def decode(:copy) do
quote do
<<len::signed-size(32), bin::binary-size(len)>> ->
:binary.copy(bin)
end
end
end
5 changes: 5 additions & 0 deletions lib/trento/postgrex/types.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Postgrex.Types.define(
Trento.Postgrex.Types,
[Trento.Postgrex.Jsonpath],
decode_binary: :reference
)
5 changes: 5 additions & 0 deletions lib/trento_web/controllers/v1/activity_log_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ defmodule TrentoWeb.V1.ActivityLogController do
in: :query,
schema: %OpenApiSpex.Schema{type: :array},
required: false
],
metadata: [
in: :query,
schema: %OpenApiSpex.Schema{type: :string},
required: false
]
],
responses: [
Expand Down
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ defmodule Trento.MixProject do
{:ecto_commons, "~> 0.3.4"},
{:bodyguard, "~> 2.4"},
{:nimble_totp, "~> 1.0"},
{:nimble_parsec, "~> 1.0"},
{:phoenix_view, "~> 2.0"},
{:phoenix_html_helpers, "~> 1.0"},
{:pow_assent, "~> 0.4.18"},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule Trento.Repo.Migrations.AddActivityLogMetadataIndex do
use Ecto.Migration

def change do
create index("activity_logs", ["metadata jsonb_path_ops"],
name: "activity_logs_metadata_containment_idx",
using: "GIN"
)
end
end
Loading