From 0fba2c9ee7d285bc8b5a180a6dbf2a52fabecab0 Mon Sep 17 00:00:00 2001 From: arcticfly <41524992+arcticfly@users.noreply.github.com> Date: Tue, 15 Aug 2023 16:43:59 -0700 Subject: [PATCH] Add NOT_CONTAINS, fix bugs (#160) * Fix null case for tag comparisons * Change debounce time to 500ms * Add NOT_CONTAINS * Avoid sql injection * Store filters by id * Fix chained NOT_CONTAINS --- .../LogFilters/AddFilterButton.tsx | 9 ++- .../requestLogs/LogFilters/LogFilter.tsx | 23 +++---- .../requestLogs/LogFilters/LogFilters.tsx | 4 +- .../LogFilters/SelectComparatorDropdown.tsx | 4 +- .../LogFilters/SelectFieldDropdown.tsx | 4 +- .../server/api/routers/loggedCalls.router.ts | 68 ++++++++----------- app/src/state/logFiltersSlice.ts | 15 ++-- 7 files changed, 59 insertions(+), 68 deletions(-) diff --git a/app/src/components/requestLogs/LogFilters/AddFilterButton.tsx b/app/src/components/requestLogs/LogFilters/AddFilterButton.tsx index 7f0ce6a..4c71fcf 100644 --- a/app/src/components/requestLogs/LogFilters/AddFilterButton.tsx +++ b/app/src/components/requestLogs/LogFilters/AddFilterButton.tsx @@ -10,7 +10,14 @@ const AddFilterButton = () => { addFilter({ field: defaultFilterableFields[0], comparator: comparators[0] })} + onClick={() => + addFilter({ + id: Date.now().toString(), + field: defaultFilterableFields[0], + comparator: comparators[0], + value: "", + }) + } spacing={0} fontSize="sm" > diff --git a/app/src/components/requestLogs/LogFilters/LogFilter.tsx b/app/src/components/requestLogs/LogFilters/LogFilter.tsx index 65f9fd0..44d349b 100644 --- a/app/src/components/requestLogs/LogFilters/LogFilter.tsx +++ b/app/src/components/requestLogs/LogFilters/LogFilter.tsx @@ -8,39 +8,34 @@ import { debounce } from "lodash-es"; import SelectFieldDropdown from "./SelectFieldDropdown"; import SelectComparatorDropdown from "./SelectComparatorDropdown"; -const LogFilter = ({ filter, index }: { filter: LogFilter; index: number }) => { +const LogFilter = ({ filter }: { filter: LogFilter }) => { const updateFilter = useAppStore((s) => s.logFilters.updateFilter); const deleteFilter = useAppStore((s) => s.logFilters.deleteFilter); - const [editedValue, setEditedValue] = useState(""); + const [editedValue, setEditedValue] = useState(filter.value); const debouncedUpdateFilter = useCallback( - debounce( - (index: number, filter: LogFilter) => { - console.log("updating filter!!!"); - updateFilter(index, filter); - }, - 200, - { leading: true }, - ), + debounce((filter: LogFilter) => updateFilter(filter), 500, { + leading: true, + }), [updateFilter], ); return ( - - + + { setEditedValue(e.target.value); - debouncedUpdateFilter(index, { ...filter, value: e.target.value }); + debouncedUpdateFilter({ ...filter, value: e.target.value }); }} /> } - onClick={() => deleteFilter(index)} + onClick={() => deleteFilter(filter.id)} /> ); diff --git a/app/src/components/requestLogs/LogFilters/LogFilters.tsx b/app/src/components/requestLogs/LogFilters/LogFilters.tsx index 98d7ca5..17fb381 100644 --- a/app/src/components/requestLogs/LogFilters/LogFilters.tsx +++ b/app/src/components/requestLogs/LogFilters/LogFilters.tsx @@ -19,8 +19,8 @@ const LogFilters = () => { Filters - {filters.map((filter, index) => ( - + {filters.map((filter) => ( + ))} diff --git a/app/src/components/requestLogs/LogFilters/SelectComparatorDropdown.tsx b/app/src/components/requestLogs/LogFilters/SelectComparatorDropdown.tsx index bb7d00c..761d620 100644 --- a/app/src/components/requestLogs/LogFilters/SelectComparatorDropdown.tsx +++ b/app/src/components/requestLogs/LogFilters/SelectComparatorDropdown.tsx @@ -2,7 +2,7 @@ import { comparators, type LogFilter } from "~/state/logFiltersSlice"; import { useAppStore } from "~/state/store"; import InputDropdown from "~/components/InputDropdown"; -const SelectComparatorDropdown = ({ filter, index }: { filter: LogFilter; index: number }) => { +const SelectComparatorDropdown = ({ filter }: { filter: LogFilter }) => { const updateFilter = useAppStore((s) => s.logFilters.updateFilter); const { comparator } = filter; @@ -11,7 +11,7 @@ const SelectComparatorDropdown = ({ filter, index }: { filter: LogFilter; index: updateFilter(index, { ...filter, comparator: option })} + onSelect={(option) => updateFilter({ ...filter, comparator: option })} /> ); }; diff --git a/app/src/components/requestLogs/LogFilters/SelectFieldDropdown.tsx b/app/src/components/requestLogs/LogFilters/SelectFieldDropdown.tsx index 5d217d3..c8c128f 100644 --- a/app/src/components/requestLogs/LogFilters/SelectFieldDropdown.tsx +++ b/app/src/components/requestLogs/LogFilters/SelectFieldDropdown.tsx @@ -3,7 +3,7 @@ import { useAppStore } from "~/state/store"; import { useTagNames } from "~/utils/hooks"; import InputDropdown from "~/components/InputDropdown"; -const SelectFieldDropdown = ({ filter, index }: { filter: LogFilter; index: number }) => { +const SelectFieldDropdown = ({ filter }: { filter: LogFilter }) => { const tagNames = useTagNames().data; const updateFilter = useAppStore((s) => s.logFilters.updateFilter); @@ -14,7 +14,7 @@ const SelectFieldDropdown = ({ filter, index }: { filter: LogFilter; index: numb updateFilter(index, { ...filter, field: option })} + onSelect={(option) => updateFilter({ ...filter, field: option })} /> ); }; diff --git a/app/src/server/api/routers/loggedCalls.router.ts b/app/src/server/api/routers/loggedCalls.router.ts index a837b5c..4798b1f 100644 --- a/app/src/server/api/routers/loggedCalls.router.ts +++ b/app/src/server/api/routers/loggedCalls.router.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { type Expression, type SqlBool, sql } from "kysely"; +import { type Expression, type SqlBool, sql, type RawBuilder } from "kysely"; import { jsonArrayFrom } from "kysely/helpers/postgres"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; @@ -8,15 +8,22 @@ import { comparators, defaultFilterableFields } from "~/state/logFiltersSlice"; import { requireCanViewProject } from "~/utils/accessControl"; // create comparator type based off of comparators -const comparatorToSqlValue = (comparator: (typeof comparators)[number], value: string) => { - switch (comparator) { - case "=": - return `= '${value}'`; - case "!=": - return `!= '${value}'`; - case "CONTAINS": - return `like '%${value}%'`; - } +const comparatorToSqlExpression = (comparator: (typeof comparators)[number], value: string) => { + return (reference: RawBuilder): Expression => { + switch (comparator) { + case "=": + return sql`${reference} = ${value}`; + case "!=": + // Handle NULL values + return sql`${reference} IS DISTINCT FROM ${value}`; + case "CONTAINS": + return sql`${reference} LIKE ${"%" + value + "%"}`; + case "NOT_CONTAINS": + return sql`(${reference} NOT LIKE ${"%" + value + "%"} OR ${reference} IS NULL)`; + default: + throw new Error("Unknown comparator"); + } + }; }; export const loggedCallsRouter = createTRPCRouter({ @@ -30,7 +37,7 @@ export const loggedCallsRouter = createTRPCRouter({ z.object({ field: z.string(), comparator: z.enum(comparators), - value: z.string().optional(), + value: z.string(), }), ), }), @@ -48,40 +55,19 @@ export const loggedCallsRouter = createTRPCRouter({ for (const filter of input.filters) { if (!filter.value) continue; + const filterExpression = comparatorToSqlExpression(filter.comparator, filter.value); + if (filter.field === "Request") { - wheres.push( - sql.raw( - `lcmr."reqPayload"::text ${comparatorToSqlValue( - filter.comparator, - filter.value, - )}`, - ), - ); + wheres.push(filterExpression(sql.raw(`lcmr."reqPayload"::text`))); } if (filter.field === "Response") { - wheres.push( - sql.raw( - `lcmr."respPayload"::text ${comparatorToSqlValue( - filter.comparator, - filter.value, - )}`, - ), - ); + wheres.push(filterExpression(sql.raw(`lcmr."respPayload"::text`))); } if (filter.field === "Model") { - wheres.push( - sql.raw(`lc."model" ${comparatorToSqlValue(filter.comparator, filter.value)}`), - ); + wheres.push(filterExpression(sql.raw(`lc."model"`))); } if (filter.field === "Status Code") { - wheres.push( - sql.raw( - `lcmr."statusCode"::text ${comparatorToSqlValue( - filter.comparator, - filter.value, - )}`, - ), - ); + wheres.push(filterExpression(sql.raw(`lcmr."statusCode"::text`))); } } @@ -101,15 +87,15 @@ export const loggedCallsRouter = createTRPCRouter({ const filter = tagFilters[i]; if (!filter?.value) continue; const tableAlias = `lct${i}`; + const filterExpression = comparatorToSqlExpression(filter.comparator, filter.value); + updatedBaseQuery = updatedBaseQuery .leftJoin(`LoggedCallTag as ${tableAlias}`, (join) => join .onRef("lc.id", "=", `${tableAlias}.loggedCallId`) .on(`${tableAlias}.name`, "=", filter.field), ) - .where( - sql.raw(`${tableAlias}.value ${comparatorToSqlValue(filter.comparator, filter.value)}`), - ) as unknown as typeof baseQuery; + .where(filterExpression(sql.raw(`${tableAlias}.value`))) as unknown as typeof baseQuery; } const rawCalls = await updatedBaseQuery diff --git a/app/src/state/logFiltersSlice.ts b/app/src/state/logFiltersSlice.ts index 1af0f21..2528fd6 100644 --- a/app/src/state/logFiltersSlice.ts +++ b/app/src/state/logFiltersSlice.ts @@ -1,20 +1,21 @@ import { type SliceCreator } from "./store"; -export const comparators = ["=", "!=", "CONTAINS"] as const; +export const comparators = ["=", "!=", "CONTAINS", "NOT_CONTAINS"] as const; export const defaultFilterableFields = ["Request", "Response", "Model", "Status Code"] as const; export interface LogFilter { + id: string; field: string; comparator: (typeof comparators)[number]; - value?: string; + value: string; } export type LogFiltersSlice = { filters: LogFilter[]; addFilter: (filter: LogFilter) => void; - updateFilter: (index: number, filter: LogFilter) => void; - deleteFilter: (index: number) => void; + updateFilter: (filter: LogFilter) => void; + deleteFilter: (id: string) => void; clearSelectedLogIds: () => void; }; @@ -24,12 +25,14 @@ export const createLogFiltersSlice: SliceCreator = (set, get) = set((state) => { state.logFilters.filters.push(filter); }), - updateFilter: (index: number, filter: LogFilter) => + updateFilter: (filter: LogFilter) => set((state) => { + const index = state.logFilters.filters.findIndex((f) => f.id === filter.id); state.logFilters.filters[index] = filter; }), - deleteFilter: (index: number) => + deleteFilter: (id: string) => set((state) => { + const index = state.logFilters.filters.findIndex((f) => f.id === id); state.logFilters.filters.splice(index, 1); }), clearSelectedLogIds: () =>