File

src/user/services/user.service.ts

Index

Properties
Methods

Constructor

constructor(userRepository: Repository<DbUser>, userHighlightReactionRepository: Repository<DbUserHighlightReaction>, userHighlightRepository: Repository<DbUserHighlight>, userInsightsRepository: Repository<DbInsight>, userCollaborationRepository: Repository<DbUserCollaboration>, userListRepository: Repository<DbUserList>, starSearchUserThreadRepository: Repository<DbStarSearchUserThread>, workspaceRepository: Repository<DbWorkspace>, workspaceMemberRepository: Repository, pullRequestGithubEventsService: PullRequestGithubEventsService, contribDevstatsService: ContributorDevstatsService, configService: ConfigService)
Parameters :
Name Type Optional
userRepository Repository<DbUser> No
userHighlightReactionRepository Repository<DbUserHighlightReaction> No
userHighlightRepository Repository<DbUserHighlight> No
userInsightsRepository Repository<DbInsight> No
userCollaborationRepository Repository<DbUserCollaboration> No
userListRepository Repository<DbUserList> No
starSearchUserThreadRepository Repository<DbStarSearchUserThread> No
workspaceRepository Repository<DbWorkspace> No
workspaceMemberRepository Repository<DbWorkspaceMember> No
pullRequestGithubEventsService PullRequestGithubEventsService No
contribDevstatsService ContributorDevstatsService No
configService ConfigService No

Methods

Async acceptTerms
acceptTerms(id: number)
Parameters :
Name Type Optional
id number No
Returns : any
baseQueryBuilder
baseQueryBuilder()
Async checkAddUser
checkAddUser(user: User)
Parameters :
Name Type Optional
user User No
Returns : Promise<DbUser>
Private Async createStubUser
createStubUser(username: string)
Parameters :
Name Type Optional
username string No
Returns : Promise<DbUser>
Async deleteUser
deleteUser(id: number)
Parameters :
Name Type Optional
id number No
Returns : unknown
Async filterGivenContributors
filterGivenContributors(contributors: string[], options: RepoContributorsDto)
Parameters :
Name Type Optional
contributors string[] No
options RepoContributorsDto No
Returns : Promise<DbUser[]>
Async findContributorDevstats
findContributorDevstats(username: string, options: UserDevstatsDto)
Parameters :
Name Type Optional
username string No
options UserDevstatsDto No
Async findManyByUsernames
findManyByUsernames(usernames: string[])
Parameters :
Name Type Optional
usernames string[] No
Returns : Promise<DbUser[]>
Async findOneByEmail
findOneByEmail(email: string)
Parameters :
Name Type Optional
email string No
Private Async findOneById
findOneById(undefined: literal type)
Parameters :
Name Type Optional
literal type No
Returns : Promise<DbUser>
Private Async findOneByUsername
findOneByUsername(undefined: literal type)
Parameters :
Name Type Optional
literal type No
Returns : Promise<DbUser>
Async findTopUsers
findTopUsers(pageOptionsDto: TopUsersDto)
Parameters :
Name Type Optional
pageOptionsDto TopUsersDto No
Async findUsersByFilter
findUsersByFilter(pageOptionsDto: FilteredUsersDto)
Parameters :
Name Type Optional
pageOptionsDto FilteredUsersDto No
Async getNumWaitlisted
getNumWaitlisted()
Returns : unknown
reactionsQueryBuilder
reactionsQueryBuilder()
Async refreshOneDevstatsByUsername
refreshOneDevstatsByUsername(username: string)
Parameters :
Name Type Optional
username string No
Returns : Promise<DbUser>
Async tryFindUserOrMakeStub
tryFindUserOrMakeStub(undefined: literal type)
Parameters :
Name Type Optional
literal type No
Returns : Promise<DbUser>
Async tryUpdateContributorOSCR
tryUpdateContributorOSCR(user: DbUser)
Parameters :
Name Type Optional
user DbUser No
Returns : unknown
Async updateEmailPreferences
updateEmailPreferences(id: number, user: UpdateUserEmailPreferencesDto)
Parameters :
Name Type Optional
id number No
user UpdateUserEmailPreferencesDto No
Returns : unknown
Async updateInterests
updateInterests(id: number, user: UpdateUserProfileInterestsDto)
Parameters :
Name Type Optional
id number No
user UpdateUserProfileInterestsDto No
Returns : unknown
Async updateOnboarding
updateOnboarding(id: number, user: UserOnboardingDto)
Parameters :
Name Type Optional
id number No
user UserOnboardingDto No
Returns : any
Async updateUser
updateUser(id: number, user: UpdateUserDto)
Parameters :
Name Type Optional
id number No
user UpdateUserDto No
Returns : unknown
Async updateWaitlistStatus
updateWaitlistStatus(id: number)
Parameters :
Name Type Optional
id number No
Returns : any

Properties

Private logger
Default value : new Logger("UserService")
import { BadRequestException, Inject, Injectable, Logger, NotFoundException, forwardRef } from "@nestjs/common";
import { Repository, SelectQueryBuilder } from "typeorm";
import { InjectRepository } from "@nestjs/typeorm";
import { User } from "@supabase/supabase-js";

import { Octokit } from "@octokit/rest";
import { ConfigService } from "@nestjs/config";
import { DbStarSearchUserThread } from "../../star-search/entities/user-thread.entity";
import { WaitlistedUsersDto } from "../../auth/dtos/waitlisted.dto";
import { opensaucedEmailRegex, validEmailRegex } from "../../common/util/email";
import { PullRequestGithubEventsService } from "../../timescale/pull_request_github_events.service";
import { DbUser } from "../user.entity";
import { UpdateUserDto } from "../dtos/update-user.dto";
import { UpdateUserProfileInterestsDto } from "../dtos/update-user-interests.dto";
import { UpdateUserEmailPreferencesDto } from "../dtos/update-user-email-prefs.dto";
import { UserOnboardingDto } from "../../auth/dtos/user-onboarding.dto";
import { userNotificationTypes } from "../entities/user-notification.constants";
import { DbUserHighlightReaction } from "../entities/user-highlight-reaction.entity";
import { DbTopUser } from "../entities/top-users.entity";
import { TopUsersDto } from "../dtos/top-users.dto";
import { PageDto } from "../../common/dtos/page.dto";
import { PageMetaDto } from "../../common/dtos/page-meta.dto";
import { DbFilteredUser } from "../entities/filtered-users.entity";
import { FilteredUsersDto } from "../dtos/filtered-users.dto";
import { DbUserHighlight } from "../entities/user-highlight.entity";
import { DbInsight } from "../../insight/entities/insight.entity";
import { DbUserCollaboration } from "../entities/user-collaboration.entity";
import { DbUserList } from "../../user-lists/entities/user-list.entity";
import { DbWorkspace } from "../../workspace/entities/workspace.entity";
import { DbWorkspaceMember, WorkspaceMemberRoleEnum } from "../../workspace/entities/workspace-member.entity";
import { UserDto } from "../dtos/user.dto";
import { ContributorDevstatsService } from "../../timescale/contrib-stats.service";
import { RepoContributorsDto } from "../../repo/dtos/repo-contributors.dto";
import { DbUserDevstats } from "../entities/user-devstats.entity";
import { UserDevstatsDto } from "../dtos/user-devstats.dto";

@Injectable()
export class UserService {
  private logger = new Logger("UserService");

  constructor(
    @InjectRepository(DbUser, "ApiConnection")
    private userRepository: Repository<DbUser>,
    @InjectRepository(DbUserHighlightReaction, "ApiConnection")
    private userHighlightReactionRepository: Repository<DbUserHighlightReaction>,
    @InjectRepository(DbUserHighlight, "ApiConnection")
    private userHighlightRepository: Repository<DbUserHighlight>,
    @InjectRepository(DbInsight, "ApiConnection")
    private userInsightsRepository: Repository<DbInsight>,
    @InjectRepository(DbUserCollaboration, "ApiConnection")
    private userCollaborationRepository: Repository<DbUserCollaboration>,
    @InjectRepository(DbUserList, "ApiConnection")
    private userListRepository: Repository<DbUserList>,
    @InjectRepository(DbStarSearchUserThread, "ApiConnection")
    private starSearchUserThreadRepository: Repository<DbStarSearchUserThread>,
    @InjectRepository(DbWorkspace, "ApiConnection")
    private workspaceRepository: Repository<DbWorkspace>,
    @InjectRepository(DbWorkspaceMember, "ApiConnection")
    private workspaceMemberRepository: Repository<DbWorkspaceMember>,
    @Inject(forwardRef(() => PullRequestGithubEventsService))
    private pullRequestGithubEventsService: PullRequestGithubEventsService,
    @Inject(forwardRef(() => ContributorDevstatsService))
    private contribDevstatsService: ContributorDevstatsService,
    private configService: ConfigService
  ) {}

  baseQueryBuilder(): SelectQueryBuilder<DbUser> {
    const builder = this.userRepository.createQueryBuilder("users");

    return builder;
  }

  reactionsQueryBuilder(): SelectQueryBuilder<DbUserHighlightReaction> {
    const builder = this.userHighlightReactionRepository.createQueryBuilder("user_highlight_reactions");

    return builder;
  }

  async findTopUsers(pageOptionsDto: TopUsersDto): Promise<PageDto<DbTopUser>> {
    const queryBuilder = this.reactionsQueryBuilder();

    const { userId } = pageOptionsDto;

    queryBuilder
      .select("users.login as login")
      .from(DbUser, "users")
      .innerJoin("user_highlights", "user_highlights", "user_highlights.user_id = users.id")
      .innerJoin("user_highlight_reactions", "reactions", "reactions.highlight_id = user_highlights.id")
      .where("reactions.deleted_at IS NULL");

    if (userId) {
      queryBuilder.andWhere(
        "users.id NOT IN (SELECT following_user_id FROM users_to_users_followers WHERE user_id = :userId AND deleted_at IS NULL)",
        { userId }
      );
    }

    queryBuilder.groupBy("users.login").orderBy("COUNT(reactions.user_id)", "DESC");

    queryBuilder.offset(pageOptionsDto.skip).limit(pageOptionsDto.limit);

    const [itemCount, entities] = await Promise.all([queryBuilder.getCount(), queryBuilder.getRawMany()]);
    const pageMetaDto = new PageMetaDto({ itemCount, pageOptionsDto });

    return new PageDto(entities, pageMetaDto);
  }

  private async findOneById({
    id,
    withDeleted = false,
    includeEmail = false,
  }: {
    id: number;
    withDeleted?: boolean;
    includeEmail?: boolean;
  }): Promise<DbUser> {
    const queryBuilder = this.baseQueryBuilder();

    if (withDeleted) {
      queryBuilder.withDeleted();
      queryBuilder.addSelect("users.deleted_at", "users_deleted_at");
    }

    queryBuilder
      .addSelect(
        `(
          SELECT COALESCE(COUNT("user_notifications"."id"), 0)
          FROM user_notifications
          WHERE user_id = :userId
          AND user_notifications.type IN (:...userNotificationTypes)
          AND user_notifications.read_at IS NULL
        )::INTEGER`,
        "users_notification_count"
      )
      .addSelect(
        `(
          SELECT COALESCE(COUNT("insights"."id"), 0)
          FROM insights
          LEFT JOIN insight_members ON insights.id = insight_members.insight_id
          WHERE insight_members.user_id = :userId
        )::INTEGER`,
        "users_insights_count"
      )
      .where("id = :id", { id });

    if (includeEmail) {
      queryBuilder.addSelect("users.email", "users_email");
    }

    let item: DbUser | null;

    try {
      queryBuilder.setParameters({ userId: id, userNotificationTypes });
      item = await queryBuilder.getOne();
    } catch (e) {
      // handle error
      item = null;
    }

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

    return item;
  }

  async refreshOneDevstatsByUsername(username: string): Promise<DbUser> {
    const queryBuilder = this.baseQueryBuilder().where("LOWER(login) = :username", {
      username: username.toLowerCase(),
    });

    const user: DbUser | null = await queryBuilder.getOne();

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

    const oscr = await this.tryUpdateContributorOSCR(user);

    user.oscr = oscr;

    return user;
  }

  private async findOneByUsername({
    username,
    withDeleted = false,
    options,
  }: {
    username: string;
    withDeleted?: boolean;
    options?: UserDto;
  }): Promise<DbUser> {
    const maintainerRepoIds = options?.maintainerRepoIds?.split(",");
    const queryBuilder = this.baseQueryBuilder();

    if (withDeleted) {
      queryBuilder.withDeleted();
    }

    queryBuilder
      .addSelect(
        `(
        SELECT COALESCE(COUNT("user_highlights"."id"), 0)
        FROM user_highlights
        WHERE user_id = users.id
        AND user_highlights.deleted_at IS NULL
      )::INTEGER`,
        "users_highlights_count"
      )
      .addSelect(
        `(
        SELECT COALESCE(COUNT("user_follows"."id"), 0)
        FROM users_to_users_followers user_follows
        WHERE user_id = users.id
        AND user_follows.deleted_at IS NULL
      )::INTEGER`,
        "users_following_count"
      )
      .addSelect(
        `(
        SELECT COALESCE(COUNT("user_follows"."id"), 0)
        FROM users_to_users_followers user_follows
        WHERE following_user_id = users.id
        AND user_follows.deleted_at IS NULL
      )::INTEGER`,
        "users_followers_count"
      )
      .where("LOWER(login) = :username", { username: username.toLowerCase() });

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

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

    const oscr = await this.tryUpdateContributorOSCR(item);
    const recentPrCount = await this.pullRequestGithubEventsService.findCountByPrAuthor(username, 30, 0);
    const userVelocity = await this.pullRequestGithubEventsService.findVelocityByPrAuthor(username, 30, 0);
    const isMaintainer = await this.pullRequestGithubEventsService.isMaintainer(username, maintainerRepoIds);

    item.oscr = oscr;
    item.recent_pull_request_velocity_count = userVelocity;
    item.recent_pull_requests_count = recentPrCount;
    item.is_maintainer = isMaintainer;

    return item;
  }

  async findManyByUsernames(usernames: string[]): Promise<DbUser[]> {
    const queryBuilder = this.baseQueryBuilder();
    const lowerCaseUsernames = usernames.map((username) => username.toLowerCase());

    queryBuilder
      .where("LOWER(login) IN (:...usernames)", { usernames: lowerCaseUsernames })
      .setParameters({ usernames: lowerCaseUsernames });

    const items: DbUser[] = await queryBuilder.getMany();

    const foundUsernames = items.map((user) => user.login.toLowerCase());
    const notFoundUsernames = lowerCaseUsernames.filter((username) => !foundUsernames.includes(username));

    if (notFoundUsernames.length > 0) {
      throw new NotFoundException(notFoundUsernames);
    }

    return items;
  }

  async findUsersByFilter(pageOptionsDto: FilteredUsersDto): Promise<PageDto<DbFilteredUser>> {
    const queryBuilder = this.baseQueryBuilder();

    const { username, limit } = pageOptionsDto;

    if (!username) {
      throw new BadRequestException();
    }

    queryBuilder
      .select(["users.login as login", "users.name as full_name"])
      .where(`LOWER(users.login) LIKE :username`)
      .andWhere("users.type = 'User'")
      .setParameters({ username: `%${username.toLowerCase()}%` })
      .limit(limit);

    queryBuilder.offset(pageOptionsDto.skip).limit(pageOptionsDto.limit);

    const [itemCount, entities] = await Promise.all([queryBuilder.getCount(), queryBuilder.getRawMany()]);
    const pageMetaDto = new PageMetaDto({ itemCount, pageOptionsDto });

    return new PageDto(entities, pageMetaDto);
  }

  async filterGivenContributors(contributors: string[], options: RepoContributorsDto): Promise<DbUser[]> {
    const queryBuilder = this.baseQueryBuilder();

    queryBuilder
      .where(`LOWER(users.login) IN (:...users)`, { users: contributors })
      .andWhere("users.type = 'User'")
      .orderBy(options.filter!, options.orderDirection)
      .offset(options.skip)
      .limit(options.limit);

    return queryBuilder.getMany();
  }

  async checkAddUser(user: User): Promise<DbUser> {
    const {
      user_metadata: { user_name, email, name },
      identities,
      confirmed_at,
    } = user;
    const github = identities!.filter((identity) => identity.provider === "github")[0];
    const id = parseInt(github.id, 10);

    try {
      const user = await this.findOneById({ id, includeEmail: true, withDeleted: true });

      if (!user.is_open_sauced_member) {
        // create new workspace for user
        const newWorkspace = await this.workspaceRepository.save({
          name: "Your workspace",
          description: "Your personal workspace",
        });

        if (user.deleted_at) {
          await this.userRepository.restore(user.id);
        }

        await this.userRepository.update(user.id, {
          is_open_sauced_member: true,
          connected_at: new Date(),
          campaign_start_date: new Date(),
          personal_workspace_id: newWorkspace.id,
        });

        await this.workspaceMemberRepository.save({
          role: WorkspaceMemberRoleEnum.Owner,
          workspace: newWorkspace,
          member: user,
        });
      }

      return user;
    } catch (e) {
      try {
        // if we got here but the user exists
        const user = await this.tryFindUserOrMakeStub({ username: user_name as string });

        if (user) {
          this.logger.error(`Found existing user in UserService.checkAddUser fallback: ${user_name as string}`);
          return user;
        }
      } catch (e) {
        this.logger.error(`User ${user_name as string} not found, creating new user/personal workspace`);
      }

      // create new workspace for user
      const newWorkspace = await this.workspaceRepository.save({
        name: "Your workspace",
        description: "Your personal workspace",
      });

      // create new user
      const newUser = await this.userRepository.save({
        id,
        name: name as string,
        is_open_sauced_member: true,
        login: user_name as string,
        email: email as string,
        created_at: new Date(github.created_at),
        updated_at: new Date(github.updated_at ?? github.created_at),
        connected_at: confirmed_at ? new Date(confirmed_at) : new Date(),
        campaign_start_date: confirmed_at ? new Date(confirmed_at) : new Date(),
        personal_workspace_id: newWorkspace.id,
      });

      await this.workspaceMemberRepository.save({
        role: WorkspaceMemberRoleEnum.Owner,
        workspace: newWorkspace,
        member: newUser,
      });

      return newUser;
    }
  }

  /*
   * this function is a special "escape" hatch function that will return the
   * queried user by ID or username if they exist in our database. If not, we use
   * github's octokit to go stub them out. This should be used in any paths
   * where there is a critical possibility of encountering a user that requires
   * that requires github metadata in our database.
   */
  async tryFindUserOrMakeStub({
    userId,
    username,
    dto,
    withDeleted = false,
  }: {
    userId?: number;
    username?: string;
    dto?: UserDto;
    withDeleted?: boolean;
  }): Promise<DbUser> {
    if (!userId && !username) {
      throw new BadRequestException("either user id or username must be provided");
    }

    let user;

    try {
      if (userId) {
        user = await this.findOneById({ id: userId, withDeleted });
      } else if (username) {
        user = await this.findOneByUsername({ username, options: dto, withDeleted });
      }
    } catch (e) {
      // could not find user being added to workspace in our database. Add it.
      if (userId) {
        throw new BadRequestException("no user by user ID found: must provide user owner/name to create stub user");
      } else if (username) {
        user = await this.createStubUser(username);
      }
    }

    if (!user) {
      throw new NotFoundException("could not find nor create user");
    }

    return user;
  }

  async findContributorDevstats(username: string, options: UserDevstatsDto): Promise<DbUserDevstats> {
    const range = options.range!;
    const user = await this.tryFindUserOrMakeStub({ username, withDeleted: true });

    const devstats = await this.contribDevstatsService.findAllContributorStats(
      {
        range,
        skip: 0,
      },
      [user.login]
    );

    if (devstats.length !== 1) {
      throw new BadRequestException("Error fetching user:", username);
    }

    return {
      ...user,
      commits: devstats[0].commits,
      prs_created: devstats[0].prs_created,
      prs_reviewed: devstats[0].prs_reviewed,
      issues_created: devstats[0].issues_created,
      commit_comments: devstats[0].commit_comments,
      issue_comments: devstats[0].issue_comments,
      pr_review_comments: devstats[0].pr_review_comments,
      comments: devstats[0].comments,
      total_contributions: devstats[0].total_contributions,
    } as DbUserDevstats;
  }

  private async createStubUser(username: string): Promise<DbUser> {
    const ghAuthToken: string = this.configService.get("github.authToken")!;

    // using octokit and GitHub's API, go fetch the user
    const octokit = new Octokit({
      auth: ghAuthToken,
    });

    let octoResponse;

    try {
      octoResponse = await octokit.users.getByUsername({ username });
    } catch (error: unknown) {
      console.error(error);
      throw new BadRequestException("Error fetching user:", username);
    }

    /*
     * create a new, minimum partial user based on GitHub data (primarily, the ID).
     * Because our first party databases uses the GitHub IDs as primary keys,
     * we can't use an auto generated ID for a stub user.
     * This stub user will eventually get picked up by the ETL and more data will get backfilled on them.
     */

    return this.userRepository.save({
      id: octoResponse.data.id,
      type: octoResponse.data.type,
      is_open_sauced_member: false,
      login: octoResponse.data.login,
    });
  }

  async updateUser(id: number, user: UpdateUserDto) {
    try {
      if (!validEmailRegex.test(user.email) || opensaucedEmailRegex.test(user.email)) {
        throw new BadRequestException("user provided email is not valid");
      }

      await this.findOneById({ id });

      await this.userRepository.update(id, {
        name: user.name,
        email: user.email,
        bio: user.bio ?? "",
        url: user.url ?? "",
        twitter_username: user.twitter_username ?? "",
        company: user.company ?? "",
        location: user.location ?? "",
        display_local_time: !!user.display_local_time,
        timezone: user.timezone,
        github_sponsors_url: user.github_sponsors_url ?? "",
        linkedin_url: user.linkedin_url ?? "",
        discord_url: user.discord_url ?? "",
      });

      return this.findOneById({ id });
    } catch (e) {
      if (e instanceof Error) {
        throw e;
      }

      throw new NotFoundException("Unable to update user");
    }
  }

  async updateOnboarding(id: number, user: UserOnboardingDto) {
    try {
      await this.findOneById({ id });

      await this.userRepository.update(id, {
        is_onboarded: true,
        is_waitlisted: false,
        timezone: user.timezone,
        interests: user.interests.join(","),
      });
    } catch (e) {
      throw new NotFoundException("Unable to update user onboarding status");
    }
  }

  async updateWaitlistStatus(id: number) {
    try {
      await this.findOneById({ id });

      await this.userRepository.update(id, { is_waitlisted: true });
    } catch (e) {
      throw new NotFoundException("Unable to update user waitlist status");
    }
  }

  async getNumWaitlisted() {
    const queryBuilder = this.userRepository.manager
      .createQueryBuilder()
      .select("COUNT(*)", "waitlisted_users")
      .from("users", "users")
      .where("is_waitlisted = true")
      .andWhere("is_open_sauced_member = true");

    const item = await queryBuilder.getRawOne<WaitlistedUsersDto>();

    if (!item) {
      throw new NotFoundException("could not derive number of waitlisted_users");
    }

    return item;
  }

  async acceptTerms(id: number) {
    try {
      await this.findOneById({ id });

      await this.userRepository.update(id, { accepted_usage_terms: true });
    } catch (e) {
      throw new NotFoundException("Unable to update accepting usage terms and conditions");
    }
  }

  async updateInterests(id: number, user: UpdateUserProfileInterestsDto) {
    return this.userRepository.update(id, { interests: user.interests.join(",") });
  }

  async updateEmailPreferences(id: number, user: UpdateUserEmailPreferencesDto) {
    return this.userRepository.update(id, {
      display_email: user.display_email,
      receive_collaboration: user.receive_collaboration,
      receive_product_updates: user.receive_product_updates,
    });
  }

  async findOneByEmail(email: string): Promise<DbUser | null> {
    const queryBuilder = this.baseQueryBuilder();

    queryBuilder.where(`users.email = :email`, { email: email.toLowerCase() });

    let item: DbUser | null;

    try {
      item = await queryBuilder.getOne();
    } catch (e) {
      // handle error
      item = null;
    }

    return item;
  }

  async deleteUser(id: number) {
    try {
      const user = await this.findOneById({ id });

      /*
       * typeORM doesn't play well with soft deletes and foreign key constraints.
       * so, we capture all the users's relations and soft delete them manually
       * without disrupting the foreign key constraint back to the user
       */
      const userAndRelations = await this.userRepository.findOneOrFail({
        where: {
          id,
        },
        relations: [
          "highlights",
          "insights",
          "collaborations",
          "request_collaborations",
          "from_user_notifications",
          "lists",
          "starsearch_thread",
        ],
      });

      await this.userRepository.softDelete(id);

      // need to reset these as we're only doing a soft delete.
      await this.userRepository.update(id, {
        is_onboarded: false,
        is_open_sauced_member: false,
      });

      await Promise.all([
        // soft delete the user's highlights
        Promise.all(
          userAndRelations.highlights.map(async (highlight) => {
            await this.userHighlightRepository.softDelete(highlight.id);
          })
        ),

        // soft delete the user's insight pages
        Promise.all(
          userAndRelations.insights.map(async (insight) => {
            await this.userInsightsRepository.softDelete(insight.id);
          })
        ),

        // soft delete the user's collaborations
        Promise.all(
          userAndRelations.collaborations.map(async (collab) => {
            await this.userCollaborationRepository.softDelete(collab.id);
          })
        ),
        // soft delete the user's collaboration requests
        Promise.all(
          userAndRelations.request_collaborations.map(async (req_collab) => {
            await this.userCollaborationRepository.softDelete(req_collab.id);
          })
        ),

        // soft delete the user's lists
        Promise.all(
          userAndRelations.lists.map(async (list) => {
            await this.userListRepository.softDelete(list.id);
          })
        ),

        // soft delete the user's StarSearch threads
        Promise.all(
          userAndRelations.starsearch_thread.map(async (thread) => {
            await this.starSearchUserThreadRepository.softDelete(thread.id);
          })
        ),
      ]);

      return user;
    } catch (e) {
      throw new NotFoundException("Unable to delete user");
    }
  }

  async tryUpdateContributorOSCR(user: DbUser) {
    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
    if ((user as DbUser | undefined)?.login?.endsWith("[bot]")) {
      return 0;
    }

    const now = new Date(
      Date.UTC(
        new Date().getUTCFullYear(),
        new Date().getUTCMonth(),
        new Date().getUTCDate(),
        new Date().getUTCHours(),
        new Date().getUTCMinutes(),
        new Date().getUTCSeconds()
      )
    );

    const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);

    /*
     * returns the OSRC early if the last time the devstats was updated was less than 24 hours ago.
     * dates are expected to be in UTC, without a time zone
     */

    if (user.devstats_updated_at! > twentyFourHoursAgo) {
      return user.oscr;
    }

    const oscr = await this.contribDevstatsService.calculateOpenSourceContributorRating(user.login);

    await this.userRepository.update(user.id, {
      oscr,
      devstats_updated_at: now.toUTCString(),
    });

    return oscr;
  }
}

results matching ""

    No results matching ""