Skip to content

Commit 99a121c

Browse files
authored
Merge branch 'develop' into 1526_Instrument_Contact_Visibility
2 parents bb2fbf5 + 60ac891 commit 99a121c

46 files changed

Lines changed: 1020 additions & 402 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/backend/docker-compose-stfc.e2e.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ services:
55
- "3310:3310"
66

77
db:
8-
image: postgres:16-alpine
8+
image: postgres:18-alpine
99
restart: always
1010
environment:
1111
POSTGRES_PASSWORD: duopassword

apps/backend/docker-compose.e2e.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ services:
66
API_AUTH_TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoyLCJ1c2VyX3RpdGxlIjoiTXIuIiwiZmlyc3RuYW1lIjoiQW5kZXJzIiwibWlkZGxlbmFtZSI6IkFkYW0iLCJsYXN0bmFtZSI6IkFuZGVyc3NvbiIsInVzZXJuYW1lIjoidGVzdG9mZmljZXIiLCJwcmVmZXJyZWRuYW1lIjoiUmhpYW5ub24iLCJvcmNpZCI6Ijg3ODMyMTg5NyIsImdlbmRlciI6Im1hbGUiLCJuYXRpb25hbGl0eSI6IkZyZW5jaCIsImJpcnRoZGF0ZSI6IjE5ODEtMDgtMDRUMjI6MDA6MDAuMDAwWiIsIm9yZ2FuaXNhdGlvbiI6IlBmYW5uZXJzdGlsbCBhbmQgU29ucyIsImRlcGFydG1lbnQiOiJJVCBkZXBhcnRtZW50Iiwib3JnYW5pc2F0aW9uX2FkZHJlc3MiOiJDb25nbywgQWxsZW5ldmlsbGUsIDM1ODIzIE11ZWxsZXIgR2xlbnMiLCJwb3NpdGlvbiI6IkxpYWlzb24iLCJlbWFpbCI6IkFhcm9uX0hhcnJpczQ5QGdtYWlsLmNvbSIsImVtYWlsVmVyaWZpZWQiOnRydWUsInRlbGVwaG9uZSI6IjcxMS0zMTYtNTcyOCIsInRlbGVwaG9uZV9hbHQiOiIxLTM1OS04NjQtMzQ4OSB4NzM5MCIsImNyZWF0ZWQiOiIyMDE5LTEwLTE3VDEwOjU4OjM4LjczNVoiLCJ1cGRhdGVkIjoiMjAxOS0xMC0xN1QxMDo1ODozOC43MzVaIn0sInJvbGVzIjpbeyJpZCI6Miwic2hvcnRDb2RlIjoidXNlcl9vZmZpY2VyIiwidGl0bGUiOiJVc2VyIE9mZmljZXIifV0sImlhdCI6MTU3MTMyNzQ2Mn0.NinmUuwuu0D6syqwd2z5J1BaqhwRPlFaxtML8sA2Ang
77

88
db:
9-
image: postgres:16-alpine
9+
image: postgres:18-alpine
1010
restart: always
1111
environment:
1212
POSTGRES_PASSWORD: duopassword

apps/backend/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
services:
22
db:
3-
image: postgres:16-alpine
3+
image: postgres:18-alpine
44
restart: always
55
environment:
66
POSTGRES_PASSWORD: duopassword

apps/backend/package-lock.json

Lines changed: 11 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/backend/src/datasources/ProposalDataSource.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { PaginationSortDirection } from '../utils/pagination';
99
import { ProposalsFilter } from './../resolvers/queries/ProposalsQuery';
1010

1111
export interface ProposalDataSource {
12+
getRequestedTime(proposalPk: number, instrumentId: number): Promise<number>;
1213
getProposalsFromView(
1314
filter?: ProposalsFilter,
1415
first?: number,

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,4 +374,11 @@ export class ProposalDataSourceMock implements ProposalDataSource {
374374
getInvitedProposal(inviteId: number): Promise<InvitedProposal | null> {
375375
throw new Error('Method not implemented.');
376376
}
377+
378+
async getRequestedTime(
379+
proposalPk: number,
380+
instrumentId: number
381+
): Promise<number> {
382+
return 32;
383+
}
377384
}

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

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { ProposalDataSource } from '../ProposalDataSource';
2323
import { TagDataSource } from '../TagDataSource';
2424
import { WorkflowDataSource } from '../WorkflowDataSource';
2525
import {
26+
InstrumentFilterInput,
2627
ProposalsFilter,
2728
QuestionFilterInput,
2829
} from './../../resolvers/queries/ProposalsQuery';
@@ -94,6 +95,29 @@ export async function calculateReferenceNumber(
9495
return prefix + paddedSequence;
9596
}
9697

98+
/**
99+
* Resolves instrument IDs from an InstrumentFilterInput.
100+
* Supports both `instrumentIds` and `instrumentId`(deprecated).
101+
*/
102+
export function resolveInstrumentIds(
103+
instrumentFilter?: InstrumentFilterInput
104+
): number[] | undefined {
105+
if (!instrumentFilter) {
106+
return undefined;
107+
}
108+
if (
109+
instrumentFilter.instrumentIds &&
110+
instrumentFilter.instrumentIds.length > 0
111+
) {
112+
return instrumentFilter.instrumentIds;
113+
}
114+
if (instrumentFilter.instrumentId) {
115+
return [instrumentFilter.instrumentId];
116+
}
117+
118+
return undefined;
119+
}
120+
97121
@injectable()
98122
export default class PostgresProposalDataSource implements ProposalDataSource {
99123
constructor(
@@ -312,6 +336,32 @@ export default class PostgresProposalDataSource implements ProposalDataSource {
312336
});
313337
}
314338

339+
async getRequestedTime(
340+
proposalPk: number,
341+
instrumentId: number
342+
): Promise<number> {
343+
//Non-nullable, time is either requested for the instrument or it is zero.
344+
const result = await database('proposals as p')
345+
.sum({
346+
total_time_requested: database.raw(
347+
"(a.answer->'value'->>'timeRequested')::numeric"
348+
),
349+
})
350+
.innerJoin('questionaries as q2', 'q2.questionary_id', 'p.questionary_id')
351+
.innerJoin('answers as a', 'a.questionary_id', 'q2.questionary_id')
352+
.innerJoin('questions as q', 'q.question_id', 'a.question_id')
353+
.where('p.proposal_pk', proposalPk)
354+
.where('q.data_type', 'INSTRUMENT_PICKER')
355+
.whereRaw("a.answer->'value'->>'instrumentId' = ?", [
356+
instrumentId.toString(),
357+
])
358+
.first();
359+
360+
return result?.total_time_requested
361+
? Number(result.total_time_requested)
362+
: 0;
363+
}
364+
315365
async create(
316366
proposer_id: number,
317367
call_id: number,
@@ -471,11 +521,20 @@ export default class PostgresProposalDataSource implements ProposalDataSource {
471521

472522
if (filter?.instrumentFilter?.showMultiInstrumentProposals) {
473523
query.whereRaw('jsonb_array_length(instruments) > 1');
474-
} else if (filter?.instrumentFilter?.instrumentId) {
475-
query.whereRaw(
476-
'jsonb_path_exists(instruments, \'$[*].id \\? (@.type() == "number" && @ == :instrumentId:)\')',
477-
{ instrumentId: filter.instrumentFilter.instrumentId }
524+
} else {
525+
const effectiveInstrumentIds = resolveInstrumentIds(
526+
filter?.instrumentFilter
478527
);
528+
if (effectiveInstrumentIds && effectiveInstrumentIds.length > 0) {
529+
query.where(function () {
530+
effectiveInstrumentIds.forEach((id) => {
531+
this.orWhereRaw(
532+
'jsonb_path_exists(instruments, \'$[*].id \\? (@.type() == "number" && @ == :instrumentId:)\')',
533+
{ instrumentId: id }
534+
);
535+
});
536+
});
537+
}
479538
}
480539

481540
if (filter?.proposalStatusId) {
@@ -629,16 +688,19 @@ export default class PostgresProposalDataSource implements ProposalDataSource {
629688
);
630689
}
631690

632-
if (filter?.instrumentFilter?.instrumentId) {
691+
const effectiveInstrumentIds = resolveInstrumentIds(
692+
filter?.instrumentFilter
693+
);
694+
if (effectiveInstrumentIds && effectiveInstrumentIds.length > 0) {
633695
query
634696
.leftJoin(
635697
'instrument_has_proposals',
636698
'instrument_has_proposals.proposal_pk',
637699
'proposals.proposal_pk'
638700
)
639-
.where(
701+
.whereIn(
640702
'instrument_has_proposals.instrument_id',
641-
filter.instrumentFilter.instrumentId
703+
effectiveInstrumentIds
642704
);
643705
}
644706

@@ -788,12 +850,21 @@ export default class PostgresProposalDataSource implements ProposalDataSource {
788850

789851
if (filter?.instrumentFilter?.showMultiInstrumentProposals) {
790852
query.whereRaw('jsonb_array_length(instruments) > 1');
791-
} else if (filter?.instrumentFilter?.instrumentId) {
792-
// NOTE: Using jsonpath we check the jsonb (instruments) field if it contains object with id equal to filter.instrumentId
793-
query.whereRaw(
794-
'jsonb_path_exists(instruments, \'$[*].id \\? (@.type() == "number" && @ == :instrumentId:)\')',
795-
{ instrumentId: filter.instrumentFilter?.instrumentId }
853+
} else {
854+
const effectiveInstrumentIds = resolveInstrumentIds(
855+
filter?.instrumentFilter
796856
);
857+
if (effectiveInstrumentIds && effectiveInstrumentIds.length > 0) {
858+
// NOTE: Using jsonpath we check the jsonb (instruments) field if it contains object with id equal to filter.instrumentId
859+
query.where(function () {
860+
effectiveInstrumentIds.forEach((id) => {
861+
this.orWhereRaw(
862+
'jsonb_path_exists(instruments, \'$[*].id \\? (@.type() == "number" && @ == :instrumentId:)\')',
863+
{ instrumentId: id }
864+
);
865+
});
866+
});
867+
}
797868
}
798869

799870
if (filter?.proposalStatusId) {
@@ -1117,14 +1188,15 @@ export default class PostgresProposalDataSource implements ProposalDataSource {
11171188
'ins.instrument_id'
11181189
)
11191190
.modify((query) => {
1120-
const instrumentId = filter?.instrumentFilter?.instrumentId;
1191+
const effectiveInstrumentIds = resolveInstrumentIds(
1192+
filter?.instrumentFilter
1193+
);
11211194

1122-
if (instrumentId && !isNaN(instrumentId)) {
1195+
if (effectiveInstrumentIds && effectiveInstrumentIds.length > 0) {
11231196
query.join('instrument_has_proposals as ihp', function () {
1124-
this.on('ihp.proposal_pk', '=', 'proposals.proposal_pk').andOnVal(
1197+
this.on('ihp.proposal_pk', '=', 'proposals.proposal_pk').andOnIn(
11251198
'ihp.instrument_id',
1126-
'=',
1127-
instrumentId
1199+
effectiveInstrumentIds
11281200
);
11291201
});
11301202
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function defaultConfig(shortCode: string): unknown {
2323
hasAdminAccess: false,
2424
};
2525
default:
26-
return {};
26+
return { note: '' };
2727
}
2828
}
2929

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

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { container, injectable } from 'tsyringe';
22

3+
import { StfcUserDataSource } from './StfcUserDataSource';
34
import { Tokens } from '../../config/Tokens';
45
import { Call } from '../../models/Call';
56
import { Proposal } from '../../models/Proposal';
@@ -13,6 +14,9 @@ import { PaginationSortDirection } from '../../utils/pagination';
1314
import PostgresAdminDataSource from '../postgres/AdminDataSource';
1415
import PostgresCallDataSource from '../postgres/CallDataSource';
1516
import database from '../postgres/database';
17+
import PostgresProposalDataSource, {
18+
resolveInstrumentIds,
19+
} from '../postgres/ProposalDataSource';
1620
import {
1721
CallRecord,
1822
createCallObject,
@@ -24,8 +28,6 @@ import PostgresTagDataSource from '../postgres/TagDataSource';
2428
import PostgresUserDataSource from '../postgres/UserDataSource';
2529
import PostgresWorkflowDataSource from '../postgres/WorkflowDataSource';
2630
import { ProposalsFilter } from './../../resolvers/queries/ProposalsQuery';
27-
import PostgresProposalDataSource from './../postgres/ProposalDataSource';
28-
import { StfcUserDataSource } from './StfcUserDataSource';
2931

3032
const postgresProposalDataSource = new PostgresProposalDataSource(
3133
new PostgresWorkflowDataSource(new PostgresStatusDataSource()),
@@ -163,12 +165,22 @@ export default class StfcProposalDataSource extends PostgresProposalDataSource {
163165
}
164166
if (filter?.instrumentFilter?.showMultiInstrumentProposals) {
165167
query.whereRaw('jsonb_array_length(instruments) > 1');
166-
} else if (filter?.instrumentFilter?.instrumentId) {
167-
// NOTE: Using jsonpath we check the jsonb (instruments) field if it contains object with id equal to filter.instrumentId
168-
query.whereRaw(
169-
'jsonb_path_exists(instruments, \'$[*].id \\? (@.type() == "number" && @ == :instrumentId:)\')',
170-
{ instrumentId: filter?.instrumentFilter?.instrumentId }
168+
} else {
169+
const effectiveInstrumentIds = resolveInstrumentIds(
170+
filter?.instrumentFilter
171171
);
172+
173+
if (effectiveInstrumentIds && effectiveInstrumentIds.length > 0) {
174+
// NOTE: Using jsonpath we check the jsonb (instruments) field if it contains object with id equal to filter.instrumentId
175+
query.where(function () {
176+
effectiveInstrumentIds.forEach((id) => {
177+
this.orWhereRaw(
178+
'jsonb_path_exists(instruments, \'$[*].id \\? (@.type() == "number" && @ == :instrumentId:)\')',
179+
{ instrumentId: id }
180+
);
181+
});
182+
});
183+
}
172184
}
173185

174186
if (filter?.proposalStatusId) {

apps/backend/src/resolvers/queries/ProposalQuery.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
import { container } from 'tsyringe';
12
import { Query, Ctx, Resolver, Arg, Int } from 'type-graphql';
23

4+
import { Tokens } from '../../config/Tokens';
35
import { ResolverContext } from '../../context';
6+
import { ProposalDataSource } from '../../datasources/ProposalDataSource';
47
import { Proposal } from '../types/Proposal';
8+
59
@Resolver()
610
export class ProposalQuery {
711
@Query(() => Proposal, { nullable: true })
@@ -19,4 +23,21 @@ export class ProposalQuery {
1923
): Promise<boolean> {
2024
return context.queries.proposal.get(context.user, proposalPk) !== null;
2125
}
26+
27+
@Query(() => Number, { nullable: false })
28+
async proposalTimeRequested(
29+
@Arg('proposalPk', () => Int) proposalPk: number,
30+
@Arg('instrumentId', () => Int) instrumentId: number,
31+
@Ctx() context: ResolverContext
32+
): Promise<number> {
33+
const proposalDataSource = container.resolve<ProposalDataSource>(
34+
Tokens.ProposalDataSource
35+
);
36+
const timeRequested = await proposalDataSource.getRequestedTime(
37+
proposalPk,
38+
instrumentId
39+
);
40+
41+
return timeRequested || 0;
42+
}
2243
}

0 commit comments

Comments
 (0)