File

src/workspace/workspace.service.ts

Index

Methods

Constructor

constructor(workspaceRepository: Repository<DbWorkspace>, workspaceRepoRepository: Repository, workspaceContributorRepository: Repository<DbWorkspaceContributor>, userRepository: Repository<DbUser>, repoService: RepoService, userService: UserService)
Parameters :
Name Type Optional
workspaceRepository Repository<DbWorkspace> No
workspaceRepoRepository Repository<DbWorkspaceRepo> No
workspaceContributorRepository Repository<DbWorkspaceContributor> No
userRepository Repository<DbUser> No
repoService RepoService No
userService UserService No

Methods

baseQueryBuilder
baseQueryBuilder()
Async createWorkspace
createWorkspace(dto: CreateWorkspaceDto, userId: number)
Parameters :
Name Type Optional
dto CreateWorkspaceDto No
userId number No
Async deleteWorkspace
deleteWorkspace(id: string, userId: number)
Parameters :
Name Type Optional
id string No
userId number No
Returns : unknown
Async findAllByUserId
findAllByUserId(pageOptionsDto: PageOptionsDto, userId: number)
Parameters :
Name Type Optional
pageOptionsDto PageOptionsDto No
userId number No
Async findOneById
findOneById(id: string)
Parameters :
Name Type Optional
id string No
Async findOneByIdAndUserId
findOneByIdAndUserId(id: string, userId: number)
Parameters :
Name Type Optional
id string No
userId number No
Async findOneByIdGuarded
findOneByIdGuarded(id: string, userId: number | undefined)
Parameters :
Name Type Optional
id string No
userId number | undefined No
Async findPersonalWorkspaceByUserId
findPersonalWorkspaceByUserId(userId: number)
Parameters :
Name Type Optional
userId number No
Async updateWorkspace
updateWorkspace(id: string, dto: UpdateWorkspaceDto, userId: number)
Parameters :
Name Type Optional
id string No
dto UpdateWorkspaceDto No
userId number No
import {
  BadRequestException,
  Inject,
  Injectable,
  NotFoundException,
  UnauthorizedException,
  forwardRef,
} from "@nestjs/common";
import { Repository, SelectQueryBuilder } from "typeorm";
import { InjectRepository } from "@nestjs/typeorm";

import { PageOptionsDto } from "../common/dtos/page-options.dto";
import { PageDto } from "../common/dtos/page.dto";
import { RepoService } from "../repo/repo.service";
import { DbUser } from "../user/user.entity";
import { UserService } from "../user/services/user.service";
import { PageMetaDto } from "../common/dtos/page-meta.dto";
import { DbWorkspaceMember, WorkspaceMemberRoleEnum } from "./entities/workspace-member.entity";
import { DbWorkspace } from "./entities/workspace.entity";
import { CreateWorkspaceDto } from "./dtos/create-workspace.dto";
import { UpdateWorkspaceDto } from "./dtos/update-workspace.dto";
import { canUserEditWorkspace, canUserManageWorkspace, canUserViewWorkspace } from "./common/memberAccess";
import { DbWorkspaceRepo } from "./entities/workspace-repos.entity";
import { DbWorkspaceContributor } from "./entities/workspace-contributors.entity";

@Injectable()
export class WorkspaceService {
  constructor(
    @InjectRepository(DbWorkspace, "ApiConnection")
    private workspaceRepository: Repository<DbWorkspace>,
    @InjectRepository(DbWorkspaceRepo, "ApiConnection")
    private workspaceRepoRepository: Repository<DbWorkspaceRepo>,
    @InjectRepository(DbWorkspaceContributor, "ApiConnection")
    private workspaceContributorRepository: Repository<DbWorkspaceContributor>,
    @InjectRepository(DbUser, "ApiConnection")
    private userRepository: Repository<DbUser>,
    @Inject(forwardRef(() => RepoService))
    private repoService: RepoService,
    private userService: UserService
  ) {}

  baseQueryBuilder(): SelectQueryBuilder<DbWorkspace> {
    const builder = this.workspaceRepository.createQueryBuilder("workspaces");

    return builder;
  }

  async findOneById(id: string): Promise<DbWorkspace> {
    const queryBuilder = this.baseQueryBuilder();

    queryBuilder
      .leftJoinAndSelect(`workspaces.members`, `workspace_members`, `workspaces.id=workspace_members.workspace_id`)
      .where("workspaces.id = :id", { id });

    const item: DbWorkspace | null = await queryBuilder.getOne();

    if (!item) {
      throw new NotFoundException();
    }

    return item;
  }

  async findOneByIdGuarded(id: string, userId: number | undefined): Promise<DbWorkspace> {
    const queryBuilder = this.baseQueryBuilder();

    queryBuilder
      .leftJoinAndSelect(`workspaces.members`, `workspace_members`, `workspaces.id=workspace_members.workspace_id`)
      .where("workspaces.id = :id", { id });

    const item: DbWorkspace | null = await queryBuilder.getOne();

    if (!item) {
      throw new NotFoundException();
    }

    const canView = canUserViewWorkspace(item, userId);

    if (!canView) {
      throw new NotFoundException();
    }

    return item;
  }

  async findOneByIdAndUserId(id: string, userId: number): Promise<DbWorkspace> {
    const queryBuilder = this.baseQueryBuilder();

    queryBuilder
      .leftJoinAndSelect(
        `workspaces.members`,
        `workspace_members`,
        `workspaces.id=workspace_members.workspace_id AND workspace_members.user_id = :userId`,
        { userId }
      )
      .where("workspaces.id = :id", { id });

    const item: DbWorkspace | null = await queryBuilder.getOne();

    if (!item) {
      throw new NotFoundException();
    }

    return item;
  }

  async findPersonalWorkspaceByUserId(userId: number): Promise<DbWorkspace> {
    const workspaceIdQb = this.userRepository
      .createQueryBuilder()
      .select("personal_workspace_id")
      .where("id = :userId", { userId });

    const workspaceId = await workspaceIdQb.getRawOne<{ personal_workspace_id: string }>();

    if (!workspaceId?.personal_workspace_id) {
      throw new NotFoundException();
    }

    const queryBuilder = this.baseQueryBuilder();

    queryBuilder
      .leftJoinAndSelect(`workspaces.members`, `workspace_members`, `workspaces.id=workspace_members.workspace_id`)
      .where("workspaces.id = :id", { id: workspaceId.personal_workspace_id });

    const item: DbWorkspace | null = await queryBuilder.getOne();

    if (!item) {
      throw new NotFoundException();
    }

    return item;
  }

  async findAllByUserId(pageOptionsDto: PageOptionsDto, userId: number): Promise<PageDto<DbWorkspace>> {
    const queryBuilder = this.baseQueryBuilder();

    queryBuilder.innerJoinAndSelect("workspaces.members", "workspace_members").where(
      `
      "workspaces"."id" IN (
        SELECT "workspace_id" FROM "workspace_members" WHERE "user_id" = :userId
      )`,
      { userId }
    );

    queryBuilder.skip(pageOptionsDto.skip).take(pageOptionsDto.limit);
    queryBuilder.orderBy("workspaces.updated_at", "DESC");

    const [itemCount, entities] = await Promise.all([queryBuilder.getCount(), queryBuilder.getMany()]);

    const pageMetaDto = new PageMetaDto({ itemCount, pageOptionsDto });

    return new PageDto(entities, pageMetaDto);
  }

  async createWorkspace(dto: CreateWorkspaceDto, userId: number): Promise<DbWorkspace> {
    return this.workspaceRepository.manager.transaction(async (entityManager) => {
      const newWorkspace = entityManager.create(DbWorkspace, {
        name: dto.name,
        description: dto.description,
      });

      const savedWorkspace = await entityManager.save(DbWorkspace, newWorkspace);

      /* set the calling creator as the owner (so there's always at least 1 owner by default)*/
      const callerIsOwner = entityManager.create(DbWorkspaceMember, {
        user_id: userId,
        workspace_id: savedWorkspace.id,
        role: WorkspaceMemberRoleEnum.Owner,
      });

      await entityManager.save(DbWorkspaceMember, callerIsOwner);

      const memberPromises = dto.members.map(async (member) => {
        const newMember = entityManager.create(DbWorkspaceMember, {
          user_id: member.id,
          workspace_id: savedWorkspace.id,
          role: member.role,
        });

        return entityManager.save(DbWorkspaceMember, newMember);
      });

      await Promise.all(memberPromises);

      const repoPromises = dto.repos.map(async (dtoRepo) => {
        const parts = dtoRepo.full_name.split("/");

        if (parts.length !== 2) {
          throw new NotFoundException("invalid repo input: must be of form 'owner/name'");
        }

        const repo = await this.repoService.tryFindRepoOrMakeStub({ repoOwner: parts[0], repoName: parts[1] });
        const existingWorkspaceRepo = await this.workspaceRepoRepository.findOne({
          where: {
            workspace_id: savedWorkspace.id,
            repo_id: repo.id,
          },
          withDeleted: true,
        });

        if (existingWorkspaceRepo) {
          await entityManager.restore(DbWorkspaceRepo, existingWorkspaceRepo.id);
        } else {
          const newWorkspaceRepo = new DbWorkspaceRepo();

          newWorkspaceRepo.workspace = savedWorkspace;
          newWorkspaceRepo.repo = repo;

          await entityManager.save(DbWorkspaceRepo, newWorkspaceRepo);
        }
      });

      await Promise.all(repoPromises);

      const contribPromises = dto.contributors.map(async (contributor) => {
        const user = await this.userService.tryFindUserOrMakeStub({
          userId: contributor.id,
          username: contributor.login,
        });

        const existingContributor = await this.workspaceContributorRepository.findOne({
          where: {
            workspace_id: savedWorkspace.id,
            contributor_id: user.id,
          },
          withDeleted: true,
        });

        if (existingContributor) {
          await entityManager.restore(DbWorkspaceContributor, existingContributor.id);
        } else {
          if (user.type.toLowerCase() === "organization") {
            throw new NotFoundException("not an contributor, is an org");
          }

          const newContributor = new DbWorkspaceContributor();

          newContributor.workspace = savedWorkspace;
          newContributor.contributor = user;

          await entityManager.save(DbWorkspaceContributor, newContributor);
        }
      });

      await Promise.all(contribPromises);

      return savedWorkspace;
    });
  }

  async updateWorkspace(id: string, dto: UpdateWorkspaceDto, userId: number): Promise<DbWorkspace> {
    const workspace = await this.findOneById(id);

    /*
     * editors and owners can update the workspace details
     * membership modification is left to owners on different endpoints
     */

    const canUpdate = canUserEditWorkspace(workspace, userId);

    if (!canUpdate) {
      throw new UnauthorizedException();
    }

    if (dto.is_public) {
      const isPublic = dto.is_public === "true" || dto.is_public === "1";

      await this.workspaceRepository.update(id, {
        is_public: isPublic,
      });
    }

    await this.workspaceRepository.update(id, {
      name: dto.name,
      description: dto.description,
    });

    return this.findOneById(id);
  }

  async deleteWorkspace(id: string, userId: number) {
    const workspace = await this.findOneByIdAndUserId(id, userId);

    /*
     * only owners can delete an entire workspace
     */
    const canDelete = canUserManageWorkspace(workspace, userId);

    if (!canDelete) {
      throw new UnauthorizedException();
    }

    const personalWorkspace = await this.findPersonalWorkspaceByUserId(userId);

    if (workspace.id === personalWorkspace.id) {
      throw new BadRequestException("cannot delete personal workspace");
    }

    return this.workspaceRepository.softDelete(id);
  }
}

results matching ""

    No results matching ""