Skip to content

Commit bf1ecba

Browse files
Merge branch 'develop' into dependabot-npm_and_yarn-lint-staged-16.2.4
2 parents 7255a50 + f9defb6 commit bf1ecba

8 files changed

Lines changed: 66 additions & 25 deletions

File tree

apps/backend/src/customTypes/index.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ declare module 'knex' {
3434
query: string,
3535
userInput: string
3636
): QueryBuilder;
37+
38+
whereJsonFieldLikeEscaped(
39+
column: string,
40+
field: string,
41+
userInput: string
42+
) : QueryBuilder;
43+
44+
orWhereJsonFieldLikeEscaped(
45+
column: string,
46+
field: string,
47+
userInput: string
48+
) : QueryBuilder;
3749
}
3850
}
3951
}

apps/backend/src/datasources/postgres/ProposalDataSource.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -377,11 +377,6 @@ export default class PostgresProposalDataSource implements ProposalDataSource {
377377
return query;
378378
}
379379

380-
safeEscapeRegexExpression(expression: string): string {
381-
const escaped = expression.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
382-
383-
return escaped;
384-
}
385380
async getProposalsFromView(
386381
filter?: ProposalsFilter,
387382
first?: number,
@@ -442,10 +437,6 @@ export default class PostgresProposalDataSource implements ProposalDataSource {
442437
searchText !== null &&
443438
searchText !== undefined
444439
) {
445-
const regexLiteral = this.safeEscapeRegexExpression(searchText);
446-
// NOTE: Using jsonpath we check the jsonb (instruments) field if it contains object with name equal to searchText case insensitive
447-
const jsonPathPattern = `$[*] ? (@.name.type() == "string" && @.name like_regex "${regexLiteral}" flag "i")`;
448-
449440
query.andWhere((qb) =>
450441
qb
451442
.orWhereRaw('proposal_id ILIKE ?', [`%${searchText}%`])
@@ -455,9 +446,11 @@ export default class PostgresProposalDataSource implements ProposalDataSource {
455446
.orWhere('users.firstname', 'ilike', `%${searchText}%`)
456447
.orWhere('users.lastname', 'ilike', `%${searchText}%`)
457448
.orWhere('principal_investigator', 'in', principalInvestigator)
458-
.orWhereRaw('jsonb_path_exists(instruments, ?)', [
459-
jsonPathPattern,
460-
])
449+
.orWhereJsonFieldLikeEscaped(
450+
'instruments',
451+
'name',
452+
`${searchText}`
453+
)
461454
);
462455
}
463456

@@ -634,10 +627,10 @@ export default class PostgresProposalDataSource implements ProposalDataSource {
634627
.orWhereRaw('users.email ILIKE ?', `%${filter.text}%`)
635628
.orWhereRaw('users.firstname ILIKE ?', `%${filter.text}%`)
636629
.orWhereRaw('users.lastname ILIKE ?', `%${filter.text}%`)
637-
// NOTE: Using jsonpath we check the jsonb (instruments) field if it contains object with name equal to searchText case insensitive
638-
.orWhereRaw(
639-
'jsonb_path_exists(instruments, \'$[*].name \\? (@.type() == "string" && @ like_regex :searchText: flag "i")\')',
640-
{ searchText: filter.text }
630+
.orWhereJsonFieldLikeEscaped(
631+
'instruments',
632+
'name',
633+
`${filter.text}`
641634
)
642635
);
643636
}

apps/backend/src/datasources/postgres/databaseExtensions.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import Knex from 'knex';
33
const escapeLike = (s: string) =>
44
s.replace(/\\/g, '\\\\').replace(/[%_]/g, '\\$&');
55

6+
function safeJsonPath(input: string): string {
7+
return input.replace(/[.*+?^${}()|[\]\\"]/g, '\\$&').trim();
8+
}
9+
610
const addExtensions = () => {
711
// Add the custom methods
812
Knex.QueryBuilder.extend(
@@ -23,6 +27,24 @@ const addExtensions = () => {
2327
return this.orWhereILike(column, finalQuery);
2428
}
2529
);
30+
Knex.QueryBuilder.extend(
31+
'whereJsonFieldLikeEscaped',
32+
function (column: string, field: string, userInput: string) {
33+
const escapedInput = safeJsonPath(userInput);
34+
const jsonPath = `$[*].${field}? (@ like_regex "${escapedInput}" flag "i")`;
35+
36+
return this.whereRaw('jsonb_path_exists(??, ?)', [column, jsonPath]);
37+
}
38+
);
39+
Knex.QueryBuilder.extend(
40+
'orWhereJsonFieldLikeEscaped',
41+
function (column: string, field: string, userInput: string) {
42+
const escapedInput = safeJsonPath(userInput);
43+
const jsonPath = `$[*].${field}? (@ like_regex "${escapedInput}" flag "i")`;
44+
45+
return this.orWhereRaw('jsonb_path_exists(??, ?)', [column, jsonPath]);
46+
}
47+
);
2648
};
2749

2850
export default addExtensions;

apps/backend/src/datasources/stfc/StfcProposalDataSource.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,10 @@ export default class StfcProposalDataSource extends PostgresProposalDataSource {
129129
.orWhere('users.firstname', 'ilike', `%${filter.text}%`)
130130
.orWhere('users.lastname', 'ilike', `%${filter.text}%`)
131131
.orWhere('principal_investigator', 'in', stfcUserIds)
132-
// NOTE: Using jsonpath we check the jsonb (instruments) field if it contains object with name equal to searchText case insensitive
133-
.orWhereRaw(
134-
'jsonb_path_exists(instruments, \'$[*].name \\? (@.type() == "string" && @ like_regex :searchText: flag "i")\')',
135-
{ searchText: filter.text }
132+
.orWhereJsonFieldLikeEscaped(
133+
'instruments',
134+
'name',
135+
`${filter.text}`
136136
);
137137
});
138138
}

apps/e2e/cypress/e2e/proposals.cy.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,11 @@ context('Proposal tests', () => {
317317

318318
cy.contains('OK').click();
319319

320+
cy.get('[data-cy="questionary-stepper"').contains('New proposal').click();
321+
322+
cy.get('#title-input').should('have.value', title);
323+
cy.get('#abstract-input').should('have.value', modifiedAbstract);
324+
320325
cy.contains('Dashboard').click();
321326
cy.contains(title);
322327
cy.contains('submitted');

apps/frontend/src/components/proposal/ProposalContainer.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ export default function ProposalContainer(props: ProposalContainerProps) {
129129
}
130130
draftState.isDirty = true;
131131
break;
132+
133+
case 'ITEM_WITH_QUESTIONARY_SUBMITTED':
134+
draftState.proposal = {
135+
...draftState.proposal,
136+
...action.itemWithQuestionary,
137+
};
138+
139+
break;
132140
}
133141

134142
return draftState;

apps/frontend/src/components/proposal/ProposalSummary.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,13 +164,13 @@ function ProposalReview({ confirm }: ProposalSummaryProps) {
164164
proposalPk: state.proposal.primaryKey,
165165
});
166166

167-
dispatch({
168-
type: 'ITEM_WITH_QUESTIONARY_MODIFIED',
169-
itemWithQuestionary: submitProposal,
170-
});
167+
const { proposal } = await api().getProposal({
168+
primaryKey: submitProposal.primaryKey,
169+
}); // refetching proposal after event handling is done in backend
170+
171171
dispatch({
172172
type: 'ITEM_WITH_QUESTIONARY_SUBMITTED',
173-
itemWithQuestionary: submitProposal,
173+
itemWithQuestionary: proposal!,
174174
});
175175
} finally {
176176
setSubmitDisabled(true);

apps/frontend/src/components/questionary/NavigationFragment.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const NavigationFragment = (props: {
1919
marginTop={3}
2020
spacing={1}
2121
alignItems="center"
22+
data-cy="navigation-fragment"
2223
>
2324
{props.isLoading && <UOLoader size="2em" />}
2425
<>{props.children}</>

0 commit comments

Comments
 (0)