diff --git a/backend/src/definitions/user-query.ts b/backend/src/definitions/user-query.ts index 37fd731..641f8fb 100644 --- a/backend/src/definitions/user-query.ts +++ b/backend/src/definitions/user-query.ts @@ -12,6 +12,7 @@ export const UserQuerySchema = z.object({ description: z.string().optional(), sql: z.string().optional(), pipeline: z.any().optional(), // TODO replace with actual pipeline schema + parameters: z.any().optional(), // TODO replace with actual parameter schema scope: z.enum(["private", "organization"]), // TODO turn this into an enum permissions: z.record(z.any()), // TODO update favorited_by: z.array(z.string()), diff --git a/backend/src/user-queries/user-queries.controller.ts b/backend/src/user-queries/user-queries.controller.ts index e17d7b5..437febc 100644 --- a/backend/src/user-queries/user-queries.controller.ts +++ b/backend/src/user-queries/user-queries.controller.ts @@ -10,6 +10,7 @@ import { UseInterceptors, HttpException, HttpStatus, + Query, } from "@nestjs/common"; import { UserQueriesService } from "./user-queries.service"; import { OrgGuard } from "@/auth/organizations.guard"; @@ -67,9 +68,12 @@ export class UserQueriesController { @Get("/:id/run") @UseGuards(OrgGuard("query")) @UseInterceptors(TrackingInterceptor) - async run(@Param("id") id: string): Promise> { + async run( + @Param("id") id: string, + @Query() query: any, + ): Promise> { try { - const results = await this.userQueriesService.run(id); + const results = await this.userQueriesService.run(id, query); return results; } catch (e) { throw new HttpException( diff --git a/backend/src/user-queries/user-queries.service.ts b/backend/src/user-queries/user-queries.service.ts index 71ac39b..7b7185d 100644 --- a/backend/src/user-queries/user-queries.service.ts +++ b/backend/src/user-queries/user-queries.service.ts @@ -129,7 +129,7 @@ export class UserQueriesService { } // TODO decide on the correct return type here - async run(id: string): Promise> { + async run(id: string, query): Promise> { const userQuery = await this.findOne(id); assert(userQuery, new NotFoundException(`Query not found with id ${id}`)); @@ -142,13 +142,37 @@ export class UserQueriesService { userQuery.sql, `SQL statement not found for query ${userQuery.name}`, ); - + const { sql, params } = parseParametersFromSQL(userQuery.sql, query); const results = await this.postgresAdapterService.run({ databaseId: userQuery.database_id, - sql: userQuery.sql, + sql: sql, + values: params, }); return results; } } } + +const parseParametersFromSQL = ( + sql: string, + parameters: Record, +): { sql: string; params: any[] } => { + const matches = sql.match(/{{(.*?)}}/g); + let paramIndex = 1; + const params: any[] = []; + console.log("Matches", matches); + if (!matches) { + return { sql, params: [] }; + } + matches.forEach((match) => { + const paramName = match.replace("{{", "").replace("}}", ""); + if (parameters[paramName] !== undefined) { + sql = sql.replace(match, `$${paramIndex}`); + params.push(parameters[paramName]); + paramIndex++; + } + }); + + return { sql, params }; +}; diff --git a/frontend/src/app/queries/[userQueryId]/page.tsx b/frontend/src/app/queries/[userQueryId]/page.tsx index 4d71911..6e632ac 100644 --- a/frontend/src/app/queries/[userQueryId]/page.tsx +++ b/frontend/src/app/queries/[userQueryId]/page.tsx @@ -29,21 +29,23 @@ const Page: React.FC = ({ params: { userQueryId } }) => { isLoading: isLoadingUserQuery, error: userQueryError, } = useUserQuery(userQueryId); + const [parameters, setParameters] = useState([]); + const [savedParameters, setSavedParameters] = useState([]); useEffect(() => { setSqlQuery(userQuery?.sql || ""); - }, [userQuery, setSqlQuery]); + setParameters(userQuery?.parameters || []); + }, [userQuery, setSqlQuery, setParameters]); const { data: results, isLoading: isLoadingResults, error: resultsError, - } = useUserQueryResults(userQueryId, userQuery?.sql); - - const [parameters, setParameters] = useState([]); + } = useUserQueryResults(userQueryId, userQuery?.sql, savedParameters); const handleSaveQuery = () => { - updateUserQueryTrigger({ sql: sqlQuery }); + setSavedParameters(parameters); + updateUserQueryTrigger({ sql: sqlQuery, parameters: parameters }); }; const { trigger: updateUserQueryTrigger, isMutating: isUpdatingUserQuery } = diff --git a/frontend/src/data/use-user-query.ts b/frontend/src/data/use-user-query.ts index 077074c..65ace95 100644 --- a/frontend/src/data/use-user-query.ts +++ b/frontend/src/data/use-user-query.ts @@ -3,6 +3,7 @@ import { backendCreate, backendGet, backendUpdate } from "./client"; import { CreateUserQuery, UserQuery } from "@/definitions"; import useSWRMutation from "swr/mutation"; import { useSelectedDatabase } from "@/stores"; +import { Parameter } from "@/components/query/query-parameters"; export const useUserQuery = (queryId?: string) => { const { data, error, isLoading } = useSWR( @@ -48,9 +49,20 @@ export const useCreateUserQuery = () => { return { data, error, trigger, isMutating }; }; -export const useUserQueryResults = (queryId: string, sql?: string) => { - const { data, error, isLoading } = useSWR(sql, () => { - return backendGet(`/user-queries/${queryId}/run`); +export const useUserQueryResults = ( + queryId: string, + sql?: string, + params?: Parameter[], +) => { + const reducedParams: Record = {}; + if (params) { + for (const param of params) { + reducedParams[param.name] = param.value || param.defaultValue; + } + } + const paramsString = new URLSearchParams(reducedParams).toString(); + const { data, error, isLoading } = useSWR(sql + paramsString, () => { + return backendGet(`/user-queries/${queryId}/run?${paramsString}`); }); return { data, error, isLoading }; diff --git a/frontend/src/definitions/table.ts b/frontend/src/definitions/table.ts index 2ac9232..700ee87 100644 --- a/frontend/src/definitions/table.ts +++ b/frontend/src/definitions/table.ts @@ -5,7 +5,7 @@ export const TableConfigurationSchema = z.object({ primary_key: z.string().optional(), title_property: z.string().optional(), hidden_columns: z.array(z.string()).optional(), - ordered_columns: z.array(z.string()).optional() + ordered_columns: z.array(z.string()).optional(), }); export type TableConfiguration = z.infer; @@ -60,3 +60,11 @@ export const UpdateTableSchema = TableSchema.omit({ deleted_at: true, }).partial(); export type UpdateTable = z.infer; + +export const UpdateTableRowSchema = z.object({ + row_id: z.union([z.string(), z.number()]), + column_name: z.string(), + value: z.any(), + pk_column: z.string(), +}); +export type UpdateTableRow = z.infer; diff --git a/frontend/src/definitions/user-query.ts b/frontend/src/definitions/user-query.ts index 37fd731..641f8fb 100644 --- a/frontend/src/definitions/user-query.ts +++ b/frontend/src/definitions/user-query.ts @@ -12,6 +12,7 @@ export const UserQuerySchema = z.object({ description: z.string().optional(), sql: z.string().optional(), pipeline: z.any().optional(), // TODO replace with actual pipeline schema + parameters: z.any().optional(), // TODO replace with actual parameter schema scope: z.enum(["private", "organization"]), // TODO turn this into an enum permissions: z.record(z.any()), // TODO update favorited_by: z.array(z.string()),