Skip to content
Merged
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
12 changes: 12 additions & 0 deletions apps/backend/src/customTypes/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
25 changes: 9 additions & 16 deletions apps/backend/src/datasources/postgres/ProposalDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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}%`])
Expand All @@ -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}`
)
);
}

Expand Down Expand Up @@ -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}`
)
);
}
Expand Down
22 changes: 22 additions & 0 deletions apps/backend/src/datasources/postgres/databaseExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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;
8 changes: 4 additions & 4 deletions apps/backend/src/datasources/stfc/StfcProposalDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
);
});
}
Expand Down