Skip to content

Expose Project.membership(user: ID) #3508

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
type UnsecuredDto,
} from '~/common';
import { e } from '~/core/gel';
import { RegisterResource } from '~/core/resources';
import { type LinkTo, RegisterResource } from '~/core/resources';
import { SecuredUser, type User } from '../../../user/dto';

@RegisterResource({ db: e.Project.Member })
Expand All @@ -21,6 +21,8 @@ import { SecuredUser, type User } from '../../../user/dto';
export class ProjectMember extends Resource {
static readonly Parent = () => import('../../dto').then((m) => m.IProject);

readonly project: LinkTo<'Project'>;

@Field(() => SecuredUser)
readonly user: SecuredUser & SetUnsecuredType<UnsecuredDto<User>>;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { type ID, IdArg } from '~/common';
import { Loader, type LoaderOf } from '~/core/data-loader';
import { IProject, type Project } from '../dto';
import { ProjectMember } from './dto';
import { MembershipByProjectAndUserLoader } from './membership-by-project-and-user.loader';

@Resolver(IProject)
export class MemberProjectConnectionResolver {
@ResolveField(() => ProjectMember)
async membership(
@Parent() project: Project,
@IdArg({ name: 'user' }) userId: ID<'User'>,
@Loader(() => MembershipByProjectAndUserLoader)
loader: LoaderOf<MembershipByProjectAndUserLoader>,
): Promise<ProjectMember> {
const { membership } = await loader.load({
project: project.id,
user: userId,
});
return membership;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { type ID } from '~/common';
import {
type DataLoaderStrategy,
LoaderFactory,
type LoaderOptionsOf,
} from '~/core/data-loader';
import { type ProjectMember } from './dto';
import { ProjectMemberService } from './project-member.service';

export interface MembershipByProjectAndUserInput {
project: ID<'Project'>;
user: ID<'User'>;
}

@LoaderFactory()
export class MembershipByProjectAndUserLoader
implements
DataLoaderStrategy<
{ id: MembershipByProjectAndUserInput; membership: ProjectMember },
MembershipByProjectAndUserInput,
string
>
{
constructor(private readonly service: ProjectMemberService) {}

getOptions() {
return {
cacheKeyFn: (input) => `${input.project}:${input.user}`,
} satisfies LoaderOptionsOf<MembershipByProjectAndUserLoader>;
}

async loadMany(input: readonly MembershipByProjectAndUserInput[]) {
return await this.service.readManyByProjectAndUser(input);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import {
ProjectMember,
type ProjectMemberListInput,
} from './dto';
import type { MembershipByProjectAndUserInput } from './membership-by-project-and-user.loader';
import { type ProjectMemberRepository as Neo4jRepository } from './project-member.repository';

@Injectable()
export class ProjectMemberGelRepository
extends RepoFor(ProjectMember, {
hydrate: (member) => ({
...member['*'],
project: true,
user: hydrateUser(member.user),
}),
omit: ['create'],
Expand All @@ -38,6 +40,29 @@ export class ProjectMemberGelRepository
return await this.db.run(query);
}

async readManyByProjectAndUser(
input: readonly MembershipByProjectAndUserInput[],
) {
return await this.db.run(this.readManyByProjectAndUserQuery, { input });
}
private readonly readManyByProjectAndUserQuery = e.params(
{
input: e.array(e.tuple({ project: e.uuid, user: e.uuid })),
},
({ input }) =>
e.select(e.Project.Member, (member) => ({
...this.hydrate(member),
filter: e.op(
e.tuple({
project: member.project.id,
user: member.user.id,
}),
'in',
e.array_unpack(input),
),
})),
);

async listAsNotifiers(projectId: ID, roles?: Role[]) {
const project = e.cast(e.Project, e.uuid(projectId));
const members = e.select(project.members, (member) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ProjectModule } from '../project.module';
import { AvailableRolesToProjectResolver } from './available-roles-to-project.resolver';
import { DirectorChangeApplyToProjectMembersHandler } from './handlers/director-change-apply-to-project-members.handler';
import { RegionsZoneChangesAppliesDirectorChangeToProjectMembersHandler } from './handlers/regions-zone-changes-applies-director-change-to-project-members.handler';
import { MemberProjectConnectionResolver } from './member-project-connection.resolver';
import { AddInactiveAtMigration } from './migrations/add-inactive-at.migration';
import { ProjectMemberGelRepository } from './project-member.gel.repository';
import { ProjectMemberLoader } from './project-member.loader';
Expand All @@ -22,6 +23,7 @@ import { ProjectMemberService } from './project-member.service';
providers: [
ProjectMemberResolver,
AvailableRolesToProjectResolver,
MemberProjectConnectionResolver,
ProjectMemberService,
splitDb(ProjectMemberRepository, ProjectMemberGelRepository),
ProjectMemberLoader,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
type ProjectMemberListInput,
type UpdateProjectMember,
} from './dto';
import { type MembershipByProjectAndUserInput } from './membership-by-project-and-user.loader';

@Injectable()
export class ProjectMemberRepository extends DtoRepository(ProjectMember) {
Expand Down Expand Up @@ -148,10 +149,31 @@ export class ProjectMemberRepository extends DtoRepository(ProjectMember) {
sub.with('user as node').apply(this.users.hydrateAsNeo4j()),
)
.return<{ dto: UnsecuredDto<ProjectMember> }>(
merge('props', { user: 'dto' }).as('dto'),
merge('props', {
project: 'project { .id }',
user: 'dto',
}).as('dto'),
);
}

async readManyByProjectAndUser(
input: readonly MembershipByProjectAndUserInput[],
) {
return await this.db
.query()
.unwind([...input], 'input')
.match([
node('project', 'Project', { id: variable('input.project') }),
relation('out', '', 'member', ACTIVE),
node('node', 'ProjectMember'),
relation('out', '', 'user', ACTIVE),
node('user', 'User', { id: variable('input.user') }),
])
.apply(this.hydrate())
.map('dto')
.run();
}

async list({ filter, ...input }: ProjectMemberListInput) {
const result = await this.db
.query()
Expand Down
11 changes: 11 additions & 0 deletions src/components/project/project-member/project-member.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
type ProjectMemberListOutput,
type UpdateProjectMember,
} from './dto';
import { type MembershipByProjectAndUserInput } from './membership-by-project-and-user.loader';
import { ProjectMemberRepository } from './project-member.repository';

@Injectable()
Expand Down Expand Up @@ -75,6 +76,16 @@ export class ProjectMemberService {
return projectMembers.map((dto) => this.secure(dto));
}

async readManyByProjectAndUser(
input: readonly MembershipByProjectAndUserInput[],
) {
const dtos = await this.repo.readManyByProjectAndUser(input);
return dtos.map((dto) => ({
id: { project: dto.project.id, user: dto.user.id },
membership: this.secure(dto),
}));
}

private secure(dto: UnsecuredDto<ProjectMember>): ProjectMember {
const { user, ...secured } = this.privileges.for(ProjectMember).secure(dto);
return {
Expand Down
2 changes: 2 additions & 0 deletions src/components/user/dto/list-users.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { InputType, ObjectType } from '@nestjs/graphql';
import {
FilterField,
type ID,
IdField,
ListField,
OptionalField,
PaginatedList,
Expand All @@ -12,6 +13,7 @@ import { User } from './user.dto';

@InputType()
export abstract class UserFilters {
@IdField({ optional: true })
readonly id?: ID<'User'>;

@OptionalField()
Expand Down