Skip to content

Commit 717ab13

Browse files
committed
Expose Project.membership(user: ID)
1 parent 464df84 commit 717ab13

File tree

6 files changed

+114
-0
lines changed

6 files changed

+114
-0
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
2+
import { type ID, IdArg } from '~/common';
3+
import { Loader, type LoaderOf } from '~/core/data-loader';
4+
import { IProject, type Project } from '../dto';
5+
import { ProjectMember } from './dto';
6+
import { MembershipByProjectAndUserLoader } from './membership-by-project-and-user.loader';
7+
8+
@Resolver(IProject)
9+
export class MemberProjectConnectionResolver {
10+
@ResolveField(() => ProjectMember)
11+
async membership(
12+
@Parent() project: Project,
13+
@IdArg({ name: 'user' }) userId: ID<'User'>,
14+
@Loader(() => MembershipByProjectAndUserLoader)
15+
loader: LoaderOf<MembershipByProjectAndUserLoader>,
16+
): Promise<ProjectMember> {
17+
const { membership } = await loader.load({
18+
project: project.id,
19+
user: userId,
20+
});
21+
return membership;
22+
}
23+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { type ID } from '~/common';
2+
import {
3+
type DataLoaderStrategy,
4+
LoaderFactory,
5+
type LoaderOptionsOf,
6+
} from '~/core/data-loader';
7+
import { type ProjectMember } from './dto';
8+
import { ProjectMemberService } from './project-member.service';
9+
10+
export interface MembershipByProjectAndUserInput {
11+
project: ID<'Project'>;
12+
user: ID<'User'>;
13+
}
14+
15+
@LoaderFactory()
16+
export class MembershipByProjectAndUserLoader
17+
implements
18+
DataLoaderStrategy<
19+
{ id: MembershipByProjectAndUserInput; membership: ProjectMember },
20+
MembershipByProjectAndUserInput,
21+
string
22+
>
23+
{
24+
constructor(private readonly service: ProjectMemberService) {}
25+
26+
getOptions() {
27+
return {
28+
cacheKeyFn: (input) => `${input.project}:${input.user}`,
29+
} satisfies LoaderOptionsOf<MembershipByProjectAndUserLoader>;
30+
}
31+
32+
async loadMany(input: readonly MembershipByProjectAndUserInput[]) {
33+
return await this.service.readManyByProjectAndUser(input);
34+
}
35+
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
ProjectMember,
88
type ProjectMemberListInput,
99
} from './dto';
10+
import type { MembershipByProjectAndUserInput } from './membership-by-project-and-user.loader';
1011
import { type ProjectMemberRepository as Neo4jRepository } from './project-member.repository';
1112

1213
@Injectable()
@@ -39,6 +40,29 @@ export class ProjectMemberGelRepository
3940
return await this.db.run(query);
4041
}
4142

43+
async readManyByProjectAndUser(
44+
input: readonly MembershipByProjectAndUserInput[],
45+
) {
46+
return await this.db.run(this.readManyByProjectAndUserQuery, { input });
47+
}
48+
private readonly readManyByProjectAndUserQuery = e.params(
49+
{
50+
input: e.array(e.tuple({ project: e.uuid, user: e.uuid })),
51+
},
52+
({ input }) =>
53+
e.select(e.Project.Member, (member) => ({
54+
...this.hydrate(member),
55+
filter: e.op(
56+
e.tuple({
57+
project: member.project.id,
58+
user: member.user.id,
59+
}),
60+
'in',
61+
e.array_unpack(input),
62+
),
63+
})),
64+
);
65+
4266
async listAsNotifiers(projectId: ID, roles?: Role[]) {
4367
const project = e.cast(e.Project, e.uuid(projectId));
4468
const members = e.select(project.members, (member) => ({

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ProjectModule } from '../project.module';
66
import { AvailableRolesToProjectResolver } from './available-roles-to-project.resolver';
77
import { DirectorChangeApplyToProjectMembersHandler } from './handlers/director-change-apply-to-project-members.handler';
88
import { RegionsZoneChangesAppliesDirectorChangeToProjectMembersHandler } from './handlers/regions-zone-changes-applies-director-change-to-project-members.handler';
9+
import { MemberProjectConnectionResolver } from './member-project-connection.resolver';
910
import { AddInactiveAtMigration } from './migrations/add-inactive-at.migration';
1011
import { ProjectMemberGelRepository } from './project-member.gel.repository';
1112
import { ProjectMemberLoader } from './project-member.loader';
@@ -22,6 +23,7 @@ import { ProjectMemberService } from './project-member.service';
2223
providers: [
2324
ProjectMemberResolver,
2425
AvailableRolesToProjectResolver,
26+
MemberProjectConnectionResolver,
2527
ProjectMemberService,
2628
splitDb(ProjectMemberRepository, ProjectMemberGelRepository),
2729
ProjectMemberLoader,

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
type ProjectMemberListInput,
4545
type UpdateProjectMember,
4646
} from './dto';
47+
import { type MembershipByProjectAndUserInput } from './membership-by-project-and-user.loader';
4748

4849
@Injectable()
4950
export class ProjectMemberRepository extends DtoRepository(ProjectMember) {
@@ -155,6 +156,24 @@ export class ProjectMemberRepository extends DtoRepository(ProjectMember) {
155156
);
156157
}
157158

159+
async readManyByProjectAndUser(
160+
input: readonly MembershipByProjectAndUserInput[],
161+
) {
162+
return await this.db
163+
.query()
164+
.unwind([...input], 'input')
165+
.match([
166+
node('project', 'Project', { id: variable('input.project') }),
167+
relation('out', '', 'member', ACTIVE),
168+
node('node', 'ProjectMember'),
169+
relation('out', '', 'user', ACTIVE),
170+
node('user', 'User', { id: variable('input.user') }),
171+
])
172+
.apply(this.hydrate())
173+
.map('dto')
174+
.run();
175+
}
176+
158177
async list({ filter, ...input }: ProjectMemberListInput) {
159178
const result = await this.db
160179
.query()

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
type ProjectMemberListOutput,
2222
type UpdateProjectMember,
2323
} from './dto';
24+
import { type MembershipByProjectAndUserInput } from './membership-by-project-and-user.loader';
2425
import { ProjectMemberRepository } from './project-member.repository';
2526

2627
@Injectable()
@@ -75,6 +76,16 @@ export class ProjectMemberService {
7576
return projectMembers.map((dto) => this.secure(dto));
7677
}
7778

79+
async readManyByProjectAndUser(
80+
input: readonly MembershipByProjectAndUserInput[],
81+
) {
82+
const dtos = await this.repo.readManyByProjectAndUser(input);
83+
return dtos.map((dto) => ({
84+
id: { project: dto.project.id, user: dto.user.id },
85+
membership: this.secure(dto),
86+
}));
87+
}
88+
7889
private secure(dto: UnsecuredDto<ProjectMember>): ProjectMember {
7990
const { user, ...secured } = this.privileges.for(ProjectMember).secure(dto);
8091
return {

0 commit comments

Comments
 (0)