teableio/teable

View on GitHub
apps/nestjs-backend/src/features/space/space.service.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
import type { IRole } from '@teable/core';
import { Role, generateSpaceId, getUniqName } from '@teable/core';
import type { Prisma } from '@teable/db-main-prisma';
import { PrismaService } from '@teable/db-main-prisma';
import type { ICreateSpaceRo, IUpdateSpaceRo } from '@teable/openapi';
import { ResourceType, CollaboratorType } from '@teable/openapi';
import { keyBy, map } from 'lodash';
import { ClsService } from 'nestjs-cls';
import { ThresholdConfig, IThresholdConfig } from '../../configs/threshold.config';
import type { IClsStore } from '../../types/cls';
import { PermissionService } from '../auth/permission.service';
import { BaseService } from '../base/base.service';
import { CollaboratorService } from '../collaborator/collaborator.service';

@Injectable()
export class SpaceService {
  constructor(
    private readonly prismaService: PrismaService,
    private readonly cls: ClsService<IClsStore>,
    private readonly baseService: BaseService,
    private readonly collaboratorService: CollaboratorService,
    private readonly permissionService: PermissionService,
    @ThresholdConfig() private readonly thresholdConfig: IThresholdConfig
  ) {}

  async createSpaceByParams(spaceCreateInput: Prisma.SpaceCreateInput) {
    return await this.prismaService.$tx(async () => {
      const result = await this.prismaService.txClient().space.create({
        select: {
          id: true,
          name: true,
        },
        data: spaceCreateInput,
      });
      await this.collaboratorService.createSpaceCollaborator(
        spaceCreateInput.createdBy,
        result.id,
        Role.Owner
      );
      return result;
    });
  }

  async getSpaceById(spaceId: string) {
    const userId = this.cls.get('user.id');

    const space = await this.prismaService.space.findFirst({
      select: {
        id: true,
        name: true,
      },
      where: {
        id: spaceId,
        deletedTime: null,
      },
    });
    if (!space) {
      throw new NotFoundException('Space not found');
    }
    const collaborator = await this.prismaService.collaborator.findFirst({
      select: {
        roleName: true,
      },
      where: {
        resourceId: spaceId,
        userId,
      },
    });
    if (!collaborator) {
      throw new ForbiddenException();
    }
    return {
      ...space,
      role: collaborator.roleName as IRole,
    };
  }

  async getSpaceList() {
    const userId = this.cls.get('user.id');

    const collaboratorSpaceList = await this.prismaService.collaborator.findMany({
      select: {
        resourceId: true,
        roleName: true,
      },
      where: {
        userId,
        resourceType: CollaboratorType.Space,
      },
    });
    const spaceIds = map(collaboratorSpaceList, 'resourceId') as string[];
    const spaceList = await this.prismaService.space.findMany({
      where: { id: { in: spaceIds }, deletedTime: null },
      select: { id: true, name: true },
      orderBy: { createdTime: 'asc' },
    });
    const roleMap = keyBy(collaboratorSpaceList, 'resourceId');
    return spaceList.map((space) => ({
      ...space,
      role: roleMap[space.id].roleName as IRole,
    }));
  }

  async createSpace(createSpaceRo: ICreateSpaceRo) {
    const userId = this.cls.get('user.id');
    const isAdmin = this.cls.get('user.isAdmin');

    if (!isAdmin) {
      const setting = await this.prismaService.setting.findFirst({
        select: {
          disallowSpaceCreation: true,
        },
      });

      if (setting?.disallowSpaceCreation) {
        throw new ForbiddenException(
          'The current instance disallow space creation by the administrator'
        );
      }
    }

    const spaceList = await this.prismaService.space.findMany({
      where: { deletedTime: null, createdBy: userId },
      select: { name: true },
    });

    const names = spaceList.map((space) => space.name);
    const uniqName = getUniqName(createSpaceRo.name ?? 'Space', names);
    return await this.createSpaceByParams({
      id: generateSpaceId(),
      name: uniqName,
      createdBy: userId,
    });
  }

  async updateSpace(spaceId: string, updateSpaceRo: IUpdateSpaceRo) {
    const userId = this.cls.get('user.id');

    return await this.prismaService.space.update({
      select: {
        id: true,
        name: true,
      },
      data: {
        ...updateSpaceRo,
        lastModifiedBy: userId,
      },
      where: {
        id: spaceId,
        deletedTime: null,
      },
    });
  }

  async deleteSpace(spaceId: string) {
    const userId = this.cls.get('user.id');

    await this.prismaService.$tx(async () => {
      await this.prismaService
        .txClient()
        .space.update({
          data: {
            deletedTime: new Date(),
            lastModifiedBy: userId,
          },
          where: {
            id: spaceId,
            deletedTime: null,
          },
        })
        .catch(() => {
          throw new NotFoundException('Space not found');
        });
    });
  }

  async getBaseListBySpaceId(spaceId: string) {
    const userId = this.cls.get('user.id');
    const { spaceIds, roleMap } =
      await this.collaboratorService.getCollaboratorsBaseAndSpaceArray(userId);
    if (!spaceIds.includes(spaceId)) {
      throw new ForbiddenException();
    }
    const baseList = await this.prismaService.base.findMany({
      select: {
        id: true,
        name: true,
        order: true,
        spaceId: true,
        icon: true,
      },
      where: {
        spaceId,
        deletedTime: null,
      },
      orderBy: {
        order: 'asc',
      },
    });

    return baseList.map((base) => ({ ...base, role: roleMap[base.id] || roleMap[base.spaceId] }));
  }

  async permanentDeleteSpace(spaceId: string) {
    const accessTokenId = this.cls.get('accessTokenId');
    await this.permissionService.validPermissions(spaceId, ['space|delete'], accessTokenId, true);

    await this.prismaService.space.findUniqueOrThrow({
      where: { id: spaceId },
    });

    await this.prismaService.$tx(
      async (prisma) => {
        const bases = await prisma.base.findMany({
          where: { spaceId },
          select: { id: true },
        });

        for (const { id } of bases) {
          await this.baseService.permanentDeleteBase(id);
        }

        await this.cleanSpaceRelatedData(spaceId);
      },
      {
        timeout: this.thresholdConfig.bigTransactionTimeout,
      }
    );
  }

  async cleanSpaceRelatedData(spaceId: string) {
    // delete collaborators for space
    await this.prismaService.txClient().collaborator.deleteMany({
      where: { resourceId: spaceId, resourceType: CollaboratorType.Space },
    });

    // delete invitation for space
    await this.prismaService.txClient().invitation.deleteMany({
      where: { spaceId },
    });

    // delete invitation record for space
    await this.prismaService.txClient().invitationRecord.deleteMany({
      where: { spaceId },
    });

    // delete space
    await this.prismaService.txClient().space.delete({
      where: { id: spaceId },
    });

    // delete trash for space
    await this.prismaService.txClient().trash.deleteMany({
      where: {
        resourceId: spaceId,
        resourceType: ResourceType.Space,
      },
    });
  }
}