Skip to content

Commit 5c4728a

Browse files
authored
Merge pull request #3243 from SeedCompany/eng-nested-project-filters
2 parents ed22b23 + 6fa2680 commit 5c4728a

File tree

9 files changed

+312
-172
lines changed

9 files changed

+312
-172
lines changed

src/components/engagement/dto/list-engagements.dto.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@ import {
66
SecuredList,
77
SortablePaginationInput,
88
} from '~/common';
9+
import { LanguageFilters } from '../../language/dto';
10+
import { ProjectFilters } from '../../project/dto';
911
import {
1012
Engagement,
1113
IEngagement,
1214
InternshipEngagement,
1315
LanguageEngagement,
1416
} from './engagement.dto';
17+
import { EngagementStatus } from './status.enum';
1518

1619
@InputType()
1720
export abstract class EngagementFilters {
@@ -21,9 +24,18 @@ export abstract class EngagementFilters {
2124
})
2225
readonly type?: 'language' | 'internship';
2326

27+
@Field(() => [EngagementStatus], {
28+
nullable: true,
29+
})
30+
readonly status?: readonly EngagementStatus[];
31+
2432
readonly projectId?: ID;
33+
@FilterField(() => ProjectFilters)
34+
readonly project?: ProjectFilters & {};
2535

2636
readonly languageId?: ID;
37+
@FilterField(() => LanguageFilters)
38+
readonly language?: LanguageFilters & {};
2739

2840
readonly partnerId?: ID<'Partner'>;
2941
}

src/components/engagement/engagement.repository.ts

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import { Injectable } from '@nestjs/common';
22
import { cleanJoin, mapValues, simpleSwitch } from '@seedcompany/common';
3-
import { inArray, node, Node, Query, relation } from 'cypher-query-builder';
3+
import {
4+
hasLabel,
5+
inArray,
6+
node,
7+
Node,
8+
Query,
9+
relation,
10+
} from 'cypher-query-builder';
411
import { difference, pickBy } from 'lodash';
512
import { DateTime } from 'luxon';
613
import { MergeExclusive } from 'type-fest';
@@ -39,17 +46,22 @@ import {
3946
} from '~/core/database/query';
4047
import { Privileges } from '../authorization';
4148
import { FileId } from '../file/dto';
42-
import { languageSorters } from '../language/language.repository';
49+
import {
50+
languageFilters,
51+
languageSorters,
52+
} from '../language/language.repository';
4353
import {
4454
matchCurrentDue,
4555
progressReportSorters,
4656
} from '../periodic-report/periodic-report.repository';
4757
import { ProjectType } from '../project/dto';
58+
import { projectFilters } from '../project/project-filters.query';
4859
import { projectSorters } from '../project/project.repository';
4960
import {
5061
CreateInternshipEngagement,
5162
CreateLanguageEngagement,
5263
Engagement,
64+
EngagementFilters,
5365
EngagementListInput,
5466
EngagementStatus,
5567
IEngagement,
@@ -335,20 +347,14 @@ export class EngagementRepository extends CommonRepository {
335347
// LIST ///////////////////////////////////////////////////////////
336348

337349
async list(input: EngagementListInput, session: Session, changeset?: ID) {
338-
const label =
339-
simpleSwitch(input.filter?.type, {
340-
language: 'LanguageEngagement',
341-
internship: 'InternshipEngagement',
342-
}) ?? 'Engagement';
343-
344350
const result = await this.db
345351
.query()
346352
.subQuery((sub) =>
347353
sub
348354
.match([
349355
node('project', 'Project', pickBy({ id: input.filter?.projectId })),
350356
relation('out', '', 'engagement', ACTIVE),
351-
node('node', label),
357+
node('node', 'Engagement'),
352358
])
353359
.apply(whereNotDeletedInChangeset(changeset))
354360
.return(['node', 'project'])
@@ -359,7 +365,7 @@ export class EngagementRepository extends CommonRepository {
359365
.match([
360366
node('project', 'Project', { id: input.filter.projectId }),
361367
relation('out', '', 'engagement', INACTIVE),
362-
node('node', label),
368+
node('node', 'Engagement'),
363369
relation('in', '', 'changeset', ACTIVE),
364370
node('changeset', 'Changeset', { id: changeset }),
365371
])
@@ -368,26 +374,7 @@ export class EngagementRepository extends CommonRepository {
368374
),
369375
)
370376
.match(requestingUser(session))
371-
.apply(
372-
filter.builder(input.filter ?? {}, {
373-
type: filter.skip,
374-
projectId: filter.skip,
375-
partnerId: filter.pathExists((id) => [
376-
node('node'),
377-
relation('in', '', 'engagement'),
378-
node('', 'Project'),
379-
relation('out', '', 'partnership', ACTIVE),
380-
node('', 'Partnership'),
381-
relation('out', '', 'partner'),
382-
node('', 'Partner', { id }),
383-
]),
384-
languageId: filter.pathExists((id) => [
385-
node('node'),
386-
relation('out', '', 'language'),
387-
node('', 'Language', { id }),
388-
]),
389-
}),
390-
)
377+
.apply(engagementFilters(input.filter))
391378
.apply(
392379
this.privileges.for(session, IEngagement).filterToReadable({
393380
wrapContext: oncePerProject,
@@ -529,6 +516,55 @@ export class EngagementRepository extends CommonRepository {
529516
}
530517
}
531518

519+
export const engagementFilters = filter.define(() => EngagementFilters, {
520+
type: ({ value }) => ({
521+
node: hasLabel(
522+
simpleSwitch(value, {
523+
language: 'LanguageEngagement',
524+
internship: 'InternshipEngagement',
525+
})!,
526+
),
527+
}),
528+
status: filter.stringListProp(),
529+
projectId: filter.pathExists((id) => [
530+
node('node'),
531+
relation('in', '', 'engagement'),
532+
node('project', 'Project', { id }),
533+
]),
534+
partnerId: filter.pathExists((id) => [
535+
node('node'),
536+
relation('in', '', 'engagement'),
537+
node('', 'Project'),
538+
relation('out', '', 'partnership', ACTIVE),
539+
node('', 'Partnership'),
540+
relation('out', '', 'partner'),
541+
node('', 'Partner', { id }),
542+
]),
543+
languageId: filter.pathExists((id) => [
544+
node('node'),
545+
relation('out', '', 'language'),
546+
node('', 'Language', { id }),
547+
]),
548+
project: filter.sub(() => projectFilters)((sub) =>
549+
sub
550+
.with('node as eng')
551+
.match([
552+
node('eng'),
553+
relation('in', '', 'engagement'),
554+
node('node', 'Project'),
555+
]),
556+
),
557+
language: filter.sub(() => languageFilters)((sub) =>
558+
sub
559+
.with('node as eng')
560+
.match([
561+
node('eng'),
562+
relation('out', '', 'language'),
563+
node('node', 'Language'),
564+
]),
565+
),
566+
});
567+
532568
export const engagementSorters = defineSorters(IEngagement, {
533569
nameProjectFirst: (query) =>
534570
query

src/components/language/dto/list-language.dto.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@ import {
1010
} from '~/common';
1111
import { Language } from './language.dto';
1212

13+
@InputType()
14+
export abstract class EthnologueLanguageFilters {
15+
@Field({
16+
nullable: true,
17+
})
18+
readonly code?: string;
19+
20+
@Field({
21+
nullable: true,
22+
})
23+
readonly provisionalCode?: string;
24+
25+
@Field({
26+
nullable: true,
27+
})
28+
readonly name?: string;
29+
}
30+
1331
@InputType()
1432
export abstract class LanguageFilters {
1533
@Field(() => [Sensitivity], {
@@ -48,7 +66,15 @@ export abstract class LanguageFilters {
4866
})
4967
readonly pinned?: boolean;
5068

69+
@Field({
70+
nullable: true,
71+
})
72+
readonly registryOfDialectsCode?: string;
73+
5174
readonly partnerId?: ID;
75+
76+
@FilterField(() => EthnologueLanguageFilters)
77+
readonly ethnologue?: EthnologueLanguageFilters & {};
5278
}
5379

5480
@InputType()

src/components/language/language.repository.ts

Lines changed: 75 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ import { ProjectStatus } from '../project/dto';
4343
import {
4444
CreateLanguage,
4545
EthnologueLanguage,
46+
EthnologueLanguageFilters,
4647
Language,
48+
LanguageFilters,
4749
LanguageListInput,
4850
UpdateLanguage,
4951
} from './dto';
@@ -184,7 +186,7 @@ export class LanguageRepository extends DtoRepository<
184186
node('eth', 'EthnologueLanguage'),
185187
])
186188
.apply(matchProps({ nodeName: 'eth', outputVar: 'ethProps' }))
187-
.apply(this.isPresetInventory())
189+
.apply(isPresetInventory)
188190
.optionalMatch([
189191
node('node'),
190192
relation('in', '', 'language', ACTIVE),
@@ -217,31 +219,7 @@ export class LanguageRepository extends DtoRepository<
217219
])
218220
// match requesting user once (instead of once per row)
219221
.match(requestingUser(session))
220-
.apply(
221-
filter.builder(input.filter ?? {}, {
222-
sensitivity: filter.stringListProp(),
223-
leastOfThese: filter.propVal(),
224-
isSignLanguage: filter.propVal(),
225-
isDialect: filter.propVal(),
226-
partnerId: filter.pathExists((id) => [
227-
node('node'),
228-
relation('in', '', 'language', ACTIVE),
229-
node('', 'LanguageEngagement'),
230-
relation('in', '', 'engagement', ACTIVE),
231-
node('', 'Project'),
232-
relation('out', '', 'partnership', ACTIVE),
233-
node('', 'Partnership'),
234-
relation('out', '', 'partner', ACTIVE),
235-
node('', 'Partner', { id }),
236-
]),
237-
presetInventory: ({ value, query }) => {
238-
query.apply(this.isPresetInventory()).with('*');
239-
const condition = equals('true', true);
240-
return { presetInventory: value ? condition : not(condition) };
241-
},
242-
pinned: filter.isPinned,
243-
}),
244-
)
222+
.apply(languageFilters(input.filter))
245223
.apply(
246224
this.privileges.forUser(session).filterToReadable({
247225
wrapContext: oncePerProject,
@@ -290,41 +268,79 @@ export class LanguageRepository extends DtoRepository<
290268
.first();
291269
return !!res;
292270
}
293-
294-
private isPresetInventory() {
295-
return (query: Query) =>
296-
query.subQuery('node', (sub) =>
297-
sub
298-
.optionalMatch([
299-
node('node'),
300-
relation('in', '', 'language', ACTIVE),
301-
node('', 'LanguageEngagement'),
302-
relation('in', '', 'engagement', ACTIVE),
303-
node('project', 'Project'),
304-
relation('out', '', 'status', ACTIVE),
305-
node('status', 'ProjectStatus'),
306-
])
307-
.where({
308-
'status.value': inArray(
309-
`['${ProjectStatus.InDevelopment}', '${ProjectStatus.Active}']`,
310-
true,
311-
),
312-
})
313-
.return(
314-
any(
315-
'project',
316-
collect('project'),
317-
exp.path([
318-
node('project'),
319-
relation('out', '', 'presetInventory', ACTIVE),
320-
node('', 'Property', { value: variable('true') }),
321-
]),
322-
).as('presetInventory'),
323-
),
324-
);
325-
}
326271
}
327272

273+
export const languageFilters = filter.define(() => LanguageFilters, {
274+
sensitivity: filter.stringListProp(),
275+
leastOfThese: filter.propVal(),
276+
isSignLanguage: filter.propVal(),
277+
isDialect: filter.propVal(),
278+
registryOfDialectsCode: filter.propPartialVal(),
279+
partnerId: filter.pathExists((id) => [
280+
node('node'),
281+
relation('in', '', 'language', ACTIVE),
282+
node('', 'LanguageEngagement'),
283+
relation('in', '', 'engagement', ACTIVE),
284+
node('', 'Project'),
285+
relation('out', '', 'partnership', ACTIVE),
286+
node('', 'Partnership'),
287+
relation('out', '', 'partner', ACTIVE),
288+
node('', 'Partner', { id }),
289+
]),
290+
presetInventory: ({ value, query }) => {
291+
query.apply(isPresetInventory).with('*');
292+
const condition = equals('true', true);
293+
return { presetInventory: value ? condition : not(condition) };
294+
},
295+
pinned: filter.isPinned,
296+
ethnologue: filter.sub(() => ethnologueFilters)((sub) =>
297+
sub
298+
.with('node as lang')
299+
.match([
300+
node('lang'),
301+
relation('out', '', 'ethnologue'),
302+
node('node', 'EthnologueLanguage'),
303+
]),
304+
),
305+
});
306+
307+
const ethnologueFilters = filter.define(() => EthnologueLanguageFilters, {
308+
code: filter.propPartialVal(),
309+
provisionalCode: filter.propPartialVal(),
310+
name: filter.propPartialVal(),
311+
});
312+
313+
const isPresetInventory = (query: Query) =>
314+
query.subQuery('node', (sub) =>
315+
sub
316+
.optionalMatch([
317+
node('node'),
318+
relation('in', '', 'language', ACTIVE),
319+
node('', 'LanguageEngagement'),
320+
relation('in', '', 'engagement', ACTIVE),
321+
node('project', 'Project'),
322+
relation('out', '', 'status', ACTIVE),
323+
node('status', 'ProjectStatus'),
324+
])
325+
.where({
326+
'status.value': inArray(
327+
`['${ProjectStatus.InDevelopment}', '${ProjectStatus.Active}']`,
328+
true,
329+
),
330+
})
331+
.return(
332+
any(
333+
'project',
334+
collect('project'),
335+
exp.path([
336+
node('project'),
337+
relation('out', '', 'presetInventory', ACTIVE),
338+
node('', 'Property', { value: variable('true') }),
339+
]),
340+
).as('presetInventory'),
341+
),
342+
);
343+
328344
export const languageSorters = defineSorters(Language, {
329345
// eslint-disable-next-line @typescript-eslint/naming-convention
330346
'ethnologue.*': (query, input) =>

src/components/project/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
export * from './project.service';
22
export * from './project-member';
3-
export * from './list-filter.query';
43
export * from './project.loader';

0 commit comments

Comments
 (0)