diff --git a/apps/backend/src/customTypes/index.d.ts b/apps/backend/src/customTypes/index.d.ts index 66322d7c4d..7b4a2ead3e 100644 --- a/apps/backend/src/customTypes/index.d.ts +++ b/apps/backend/src/customTypes/index.d.ts @@ -34,6 +34,18 @@ declare module 'knex' { query: string, userInput: string ): QueryBuilder; + + whereJsonFieldLikeEscaped( + column: string, + field: string, + userInput: string + ) : QueryBuilder; + + orWhereJsonFieldLikeEscaped( + column: string, + field: string, + userInput: string + ) : QueryBuilder; } } } diff --git a/apps/backend/src/datasources/postgres/ProposalDataSource.ts b/apps/backend/src/datasources/postgres/ProposalDataSource.ts index 42af7bc8d3..e1609b21f0 100644 --- a/apps/backend/src/datasources/postgres/ProposalDataSource.ts +++ b/apps/backend/src/datasources/postgres/ProposalDataSource.ts @@ -377,11 +377,6 @@ export default class PostgresProposalDataSource implements ProposalDataSource { return query; } - safeEscapeRegexExpression(expression: string): string { - const escaped = expression.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - - return escaped; - } async getProposalsFromView( filter?: ProposalsFilter, first?: number, @@ -442,10 +437,6 @@ export default class PostgresProposalDataSource implements ProposalDataSource { searchText !== null && searchText !== undefined ) { - const regexLiteral = this.safeEscapeRegexExpression(searchText); - // NOTE: Using jsonpath we check the jsonb (instruments) field if it contains object with name equal to searchText case insensitive - const jsonPathPattern = `$[*] ? (@.name.type() == "string" && @.name like_regex "${regexLiteral}" flag "i")`; - query.andWhere((qb) => qb .orWhereRaw('proposal_id ILIKE ?', [`%${searchText}%`]) @@ -455,9 +446,11 @@ export default class PostgresProposalDataSource implements ProposalDataSource { .orWhere('users.firstname', 'ilike', `%${searchText}%`) .orWhere('users.lastname', 'ilike', `%${searchText}%`) .orWhere('principal_investigator', 'in', principalInvestigator) - .orWhereRaw('jsonb_path_exists(instruments, ?)', [ - jsonPathPattern, - ]) + .orWhereJsonFieldLikeEscaped( + 'instruments', + 'name', + `${searchText}` + ) ); } @@ -634,10 +627,10 @@ export default class PostgresProposalDataSource implements ProposalDataSource { .orWhereRaw('users.email ILIKE ?', `%${filter.text}%`) .orWhereRaw('users.firstname ILIKE ?', `%${filter.text}%`) .orWhereRaw('users.lastname ILIKE ?', `%${filter.text}%`) - // NOTE: Using jsonpath we check the jsonb (instruments) field if it contains object with name equal to searchText case insensitive - .orWhereRaw( - 'jsonb_path_exists(instruments, \'$[*].name \\? (@.type() == "string" && @ like_regex :searchText: flag "i")\')', - { searchText: filter.text } + .orWhereJsonFieldLikeEscaped( + 'instruments', + 'name', + `${filter.text}` ) ); } diff --git a/apps/backend/src/datasources/postgres/databaseExtensions.ts b/apps/backend/src/datasources/postgres/databaseExtensions.ts index b4c3d8990c..2957a1ebc9 100644 --- a/apps/backend/src/datasources/postgres/databaseExtensions.ts +++ b/apps/backend/src/datasources/postgres/databaseExtensions.ts @@ -3,6 +3,10 @@ import Knex from 'knex'; const escapeLike = (s: string) => s.replace(/\\/g, '\\\\').replace(/[%_]/g, '\\$&'); +function safeJsonPath(input: string): string { + return input.replace(/[.*+?^${}()|[\]\\"]/g, '\\$&').trim(); +} + const addExtensions = () => { // Add the custom methods Knex.QueryBuilder.extend( @@ -23,6 +27,24 @@ const addExtensions = () => { return this.orWhereILike(column, finalQuery); } ); + Knex.QueryBuilder.extend( + 'whereJsonFieldLikeEscaped', + function (column: string, field: string, userInput: string) { + const escapedInput = safeJsonPath(userInput); + const jsonPath = `$[*].${field}? (@ like_regex "${escapedInput}" flag "i")`; + + return this.whereRaw('jsonb_path_exists(??, ?)', [column, jsonPath]); + } + ); + Knex.QueryBuilder.extend( + 'orWhereJsonFieldLikeEscaped', + function (column: string, field: string, userInput: string) { + const escapedInput = safeJsonPath(userInput); + const jsonPath = `$[*].${field}? (@ like_regex "${escapedInput}" flag "i")`; + + return this.orWhereRaw('jsonb_path_exists(??, ?)', [column, jsonPath]); + } + ); }; export default addExtensions; diff --git a/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts b/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts index 298292001e..0303aa7b6e 100644 --- a/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts +++ b/apps/backend/src/datasources/stfc/StfcProposalDataSource.ts @@ -129,10 +129,10 @@ export default class StfcProposalDataSource extends PostgresProposalDataSource { .orWhere('users.firstname', 'ilike', `%${filter.text}%`) .orWhere('users.lastname', 'ilike', `%${filter.text}%`) .orWhere('principal_investigator', 'in', stfcUserIds) - // NOTE: Using jsonpath we check the jsonb (instruments) field if it contains object with name equal to searchText case insensitive - .orWhereRaw( - 'jsonb_path_exists(instruments, \'$[*].name \\? (@.type() == "string" && @ like_regex :searchText: flag "i")\')', - { searchText: filter.text } + .orWhereJsonFieldLikeEscaped( + 'instruments', + 'name', + `${filter.text}` ); }); }