Skip to content

Commit faedd82

Browse files
authored
Merge pull request #3446 from SeedCompany/membership-filters
2 parents f984723 + 505df9f commit faedd82

File tree

10 files changed

+105
-54
lines changed

10 files changed

+105
-54
lines changed

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ export abstract class EngagementFilters {
4545
})
4646
readonly status?: readonly EngagementStatus[];
4747

48-
readonly projectId?: ID;
4948
@FilterField(() => ProjectFilters)
5049
readonly project?: ProjectFilters & {};
5150

src/components/engagement/engagement.repository.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -496,18 +496,24 @@ export class EngagementRepository extends CommonRepository {
496496
.subQuery((sub) =>
497497
sub
498498
.match([
499-
node('project', 'Project', pickBy({ id: input.filter?.projectId })),
499+
node(
500+
'project',
501+
'Project',
502+
pickBy({ id: input.filter?.project?.id }),
503+
),
500504
relation('out', '', 'engagement', ACTIVE),
501505
node('node', 'Engagement'),
502506
])
503507
.apply(whereNotDeletedInChangeset(changeset))
504508
.return(['node', 'project'])
505509
.apply((q) =>
506-
changeset && input.filter?.projectId
510+
changeset && input.filter?.project?.id
507511
? q
508512
.union()
509513
.match([
510-
node('project', 'Project', { id: input.filter.projectId }),
514+
node('project', 'Project', {
515+
id: input.filter.project?.id,
516+
}),
511517
relation('out', '', 'engagement', INACTIVE),
512518
node('node', 'Engagement'),
513519
relation('in', '', 'changeset', ACTIVE),
@@ -739,11 +745,6 @@ export const engagementFilters = filter.define(() => EngagementFilters, {
739745
),
740746
}),
741747
status: filter.stringListProp(),
742-
projectId: filter.pathExists((id) => [
743-
node('node'),
744-
relation('in', '', 'engagement'),
745-
node('project', 'Project', { id }),
746-
]),
747748
partnerId: filter.pathExists((id) => [
748749
node('node'),
749750
relation('in', '', 'engagement'),

src/components/project/dto/list-projects.dto.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from '~/common';
1717
import { LocationFilters } from '../../location/dto';
1818
import { PartnershipFilters } from '../../partnership/dto';
19+
import { ProjectMemberFilters } from '../project-member/dto';
1920
import { ProjectStatus } from './project-status.enum';
2021
import { ProjectStep } from './project-step.enum';
2122
import { ProjectType } from './project-type.enum';
@@ -28,6 +29,8 @@ import {
2829

2930
@InputType()
3031
export abstract class ProjectFilters {
32+
readonly id?: ID<'Project'>;
33+
3134
@OptionalField()
3235
readonly name?: string;
3336

@@ -96,6 +99,17 @@ export abstract class ProjectFilters {
9699
})
97100
readonly isMember?: boolean;
98101

102+
@FilterField(() => ProjectMemberFilters, {
103+
description:
104+
"Only projects with the requesting user's membership that matches these filters",
105+
})
106+
readonly membership?: ProjectMemberFilters & {};
107+
108+
@FilterField(() => ProjectMemberFilters, {
109+
description: 'Only projects with _any_ members matching these filters',
110+
})
111+
readonly members?: ProjectMemberFilters & {};
112+
99113
@OptionalField({
100114
description: 'Filter for projects with two or more engagements.',
101115
})

src/components/project/project-filters.query.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ import {
1616
import { locationFilters } from '../location/location.repository';
1717
import { partnershipFilters } from '../partnership/partnership.repository';
1818
import { ProjectFilters } from './dto';
19+
import { projectMemberFilters } from './project-member/project-member.repository';
1920
import { ProjectNameIndex } from './project.repository';
2021

2122
export const projectFilters = filter.define(() => ProjectFilters, {
23+
id: filter.baseNodeProp(),
2224
type: filter.stringListBaseNodeProp(),
2325
pinned: filter.isPinned,
2426
status: filter.stringListProp(),
@@ -32,7 +34,7 @@ export const projectFilters = filter.define(() => ProjectFilters, {
3234
currentUser,
3335
relation('in', '', 'user'),
3436
node('', 'ProjectMember'),
35-
relation('in', '', 'member'),
37+
relation('in', '', 'member', ACTIVE),
3638
node('node'),
3739
]),
3840
languageId: filter.pathExists((id) => [
@@ -53,9 +55,25 @@ export const projectFilters = filter.define(() => ProjectFilters, {
5355
currentUser,
5456
relation('in', '', 'user'),
5557
node('', 'ProjectMember'),
56-
relation('in', '', 'member'),
58+
relation('in', '', 'member', ACTIVE),
5759
node('node'),
5860
]),
61+
membership: filter.sub(() => projectMemberFilters)((sub) =>
62+
sub.match([
63+
currentUser,
64+
relation('in', '', 'user'),
65+
node('node', 'ProjectMember'),
66+
relation('in', '', 'member', ACTIVE),
67+
node('outer'),
68+
]),
69+
),
70+
members: filter.sub(() => projectMemberFilters)((sub) =>
71+
sub.match([
72+
node('node', 'ProjectMember'),
73+
relation('in', '', 'member', ACTIVE),
74+
node('outer'),
75+
]),
76+
),
5977
userId: ({ value }) => ({
6078
userId: [
6179
// TODO We can leak if the project includes this person, if the

src/components/project/project-member/dto/list-project-members.dto.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { InputType, ObjectType } from '@nestjs/graphql';
22
import {
33
FilterField,
4-
type ID,
54
ListField,
5+
OptionalField,
66
PaginatedList,
77
Role,
88
SecuredList,
99
SortablePaginationInput,
1010
} from '~/common';
11+
import { UserFilters } from '../../../user/dto';
12+
import { ProjectFilters } from '../../dto';
1113
import { ProjectMember } from './project-member.dto';
1214

1315
@InputType()
@@ -19,7 +21,14 @@ export abstract class ProjectMemberFilters {
1921
})
2022
readonly roles?: readonly Role[];
2123

22-
readonly projectId?: ID;
24+
@OptionalField()
25+
readonly active?: boolean;
26+
27+
@FilterField(() => ProjectFilters)
28+
readonly project?: ProjectFilters & {};
29+
30+
@FilterField(() => UserFilters)
31+
readonly user?: UserFilters & {};
2332
}
2433

2534
@InputType()

src/components/project/project-member/project-member.repository.ts

Lines changed: 29 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,21 @@ import {
1515
ACTIVE,
1616
createNode,
1717
createRelationships,
18+
filter,
1819
matchPropsAndProjectSensAndScopedRoles,
1920
merge,
2021
oncePerProject,
2122
paginate,
2223
sorting,
2324
} from '~/core/database/query';
24-
import { UserRepository } from '../../user/user.repository';
25+
import { type FilterFn } from '~/core/database/query/filters';
26+
import { userFilters, UserRepository } from '../../user/user.repository';
27+
import { type ProjectFilters } from '../dto';
28+
import { projectFilters } from '../project-filters.query';
2529
import {
2630
type CreateProjectMember,
2731
ProjectMember,
32+
ProjectMemberFilters,
2833
type ProjectMemberListInput,
2934
type UpdateProjectMember,
3035
} from './dto';
@@ -140,28 +145,11 @@ export class ProjectMemberRepository extends DtoRepository(ProjectMember) {
140145
const result = await this.db
141146
.query()
142147
.match([
143-
node(
144-
'project',
145-
'Project',
146-
filter?.projectId ? { id: filter.projectId } : {},
147-
),
148+
node('project', 'Project'),
148149
relation('out', '', 'member'),
149150
node('node', 'ProjectMember'),
150151
])
151-
.apply((q) =>
152-
filter?.roles
153-
? q
154-
.match([
155-
node('node'),
156-
relation('out', '', 'roles', ACTIVE),
157-
node('role', 'Property'),
158-
])
159-
.raw(
160-
`WHERE size(apoc.coll.intersection(role.value, $filteredRoles)) > 0`,
161-
{ filteredRoles: filter.roles },
162-
)
163-
: q,
164-
)
152+
.apply(projectMemberFilters(filter))
165153
.with('*') // needed between where & where
166154
.apply(
167155
this.privileges.filterToReadable({
@@ -174,30 +162,15 @@ export class ProjectMemberRepository extends DtoRepository(ProjectMember) {
174162
return result!; // result from paginate() will always have 1 row.
175163
}
176164

177-
async listAsNotifiers(projectId: ID, roles?: Role[]) {
165+
async listAsNotifiers(project: ID<'Project'>, roles?: Role[]) {
178166
return await this.db
179167
.query()
180168
.match([
181-
node('', 'Project', { id: projectId }),
182-
relation('out', '', 'member', ACTIVE),
183169
node('node', 'ProjectMember'),
184170
relation('out', '', 'user', ACTIVE),
185171
node('user', 'User'),
186172
])
187-
.apply((q) =>
188-
roles
189-
? q
190-
.match([
191-
node('node'),
192-
relation('out', '', 'roles', ACTIVE),
193-
node('role', 'Property'),
194-
])
195-
.raw(
196-
`WHERE size(apoc.coll.intersection(role.value, $filteredRoles)) > 0`,
197-
{ filteredRoles: roles },
198-
)
199-
: q,
200-
)
173+
.apply(projectMemberFilters({ project: { id: project }, roles }))
201174
.with('user')
202175
.optionalMatch([
203176
node('user'),
@@ -211,3 +184,22 @@ export class ProjectMemberRepository extends DtoRepository(ProjectMember) {
211184
.run();
212185
}
213186
}
187+
188+
export const projectMemberFilters = filter.define(() => ProjectMemberFilters, {
189+
project: filter.sub((): FilterFn<ProjectFilters> => projectFilters)((sub) =>
190+
sub.match([
191+
node('node', 'Project'),
192+
relation('out', '', 'member', ACTIVE),
193+
node('outer'),
194+
]),
195+
),
196+
user: filter.sub(() => userFilters)((sub) =>
197+
sub.match([
198+
node('outer'),
199+
relation('out', '', 'user'),
200+
node('node', 'User'),
201+
]),
202+
),
203+
roles: filter.intersectsProp(),
204+
active: filter.isPropNotNull('inactiveAt'),
205+
});

src/components/project/project.service.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,10 @@ export class ProjectService {
354354
...input,
355355
filter: {
356356
...input.filter,
357-
projectId: project.id,
357+
project: {
358+
id: project.id,
359+
...input.filter?.project,
360+
},
358361
},
359362
},
360363
view,
@@ -379,7 +382,10 @@ export class ProjectService {
379382
...input,
380383
filter: {
381384
...input.filter,
382-
projectId: project.id,
385+
project: {
386+
id: project.id,
387+
...input.filter?.project,
388+
},
383389
},
384390
});
385391

src/components/user/dto/list-users.dto.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { InputType, ObjectType } from '@nestjs/graphql';
22
import {
33
FilterField,
4+
type ID,
45
ListField,
56
OptionalField,
67
PaginatedList,
@@ -11,6 +12,8 @@ import { User } from './user.dto';
1112

1213
@InputType()
1314
export abstract class UserFilters {
15+
readonly id?: ID<'User'>;
16+
1417
@OptionalField()
1518
readonly name?: string;
1619

src/components/user/user.repository.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ export class UserRepository extends DtoRepository(User) {
389389
}
390390

391391
export const userFilters = filter.define(() => UserFilters, {
392+
id: filter.baseNodeProp(),
392393
pinned: filter.isPinned,
393394
name: filter.fullText({
394395
index: () => NameIndex,

src/core/database/query/filters.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,14 @@ export const define =
5050
<T extends Record<string, any>>(
5151
filterClass: () => AbstractClass<T>,
5252
builders: Builders<T>,
53-
) =>
54-
(filters: T | Nil) =>
53+
): FilterFn<T> =>
54+
(filters) =>
5555
builder(filters ?? {}, builders);
5656

57+
export type FilterFn<T extends Record<string, any>> = (
58+
filters: T | Nil,
59+
) => (query: Query) => void;
60+
5761
/**
5862
* A helper to split filters given and call their respective functions.
5963
* Functions can do nothing, adjust query, return an object to add conditions to
@@ -108,6 +112,10 @@ export const propVal =
108112
return { [prop ?? key]: cond };
109113
};
110114

115+
export const baseNodeProp =
116+
<T>(prop?: string): Builder<T> =>
117+
({ key, value }) => ({ [`node.${prop ?? key}`]: value });
118+
111119
export const propPartialVal =
112120
<T, K extends ConditionalKeys<Required<T>, string>>(
113121
prop?: string,

0 commit comments

Comments
 (0)