diff --git a/docker-compose.yml b/docker-compose.yml index c9d5d6684..fef148ffc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,7 +20,7 @@ services: - PORT=3170 volumes: # Uncomment the line below when modifying code. Only applicable when using the "dev" target. - # - ./packages/hoppscotch-backend/:/usr/src/app + - ./packages/hoppscotch-backend/:/usr/src/app/packages/hoppscotch-backend - /usr/src/app/node_modules/ depends_on: hoppscotch-db: diff --git a/packages/hoppscotch-backend/package.json b/packages/hoppscotch-backend/package.json index 1e8fb4023..185ccdd2f 100644 --- a/packages/hoppscotch-backend/package.json +++ b/packages/hoppscotch-backend/package.json @@ -21,7 +21,8 @@ "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json", - "do-test": "pnpm run test" + "do-test": "pnpm run test", + "seed": "node --loader ts-node/esm prisma/seed.ts" }, "dependencies": { "@nestjs-modules/mailer": "^1.8.1", @@ -57,6 +58,7 @@ "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "passport-microsoft": "^1.0.0", + "pg": "^8.11.3", "prisma": "^4.16.2", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", diff --git a/packages/hoppscotch-backend/prisma/seed.ts b/packages/hoppscotch-backend/prisma/seed.ts new file mode 100644 index 000000000..1c06d68fa --- /dev/null +++ b/packages/hoppscotch-backend/prisma/seed.ts @@ -0,0 +1,58 @@ +import { PrismaClient, TeamMemberRole } from '@prisma/client'; +const prisma = new PrismaClient(); + +const noOfUsers = 600000; + +const getAllUser = async () => { + const users = await prisma.user.findMany(); + return users; +}; + +const createUsers = async () => { + for (let i = 1; i <= noOfUsers; i++) { + try { + await prisma.user.create({ + data: { + email: `${i}@gmail.com`, + }, + }); + } catch (_) {} + } +}; + +const createTeams = async () => { + const users = await getAllUser(); + + for (let i = 0; i < users.length; i++) { + try { + await prisma.team.create({ + data: { + name: `Team ${i + 1}`, + members: { + create: { + userUid: users[i].uid, + role: TeamMemberRole.OWNER, + }, + }, + }, + }); + } catch (_) {} + } +}; + +async function main() { + console.log('Seeding...'); + + await createUsers(); + await createTeams(); +} + +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/packages/hoppscotch-backend/src/app.module.ts b/packages/hoppscotch-backend/src/app.module.ts index 167a8c927..f2068217e 100644 --- a/packages/hoppscotch-backend/src/app.module.ts +++ b/packages/hoppscotch-backend/src/app.module.ts @@ -20,6 +20,7 @@ import { ShortcodeModule } from './shortcode/shortcode.module'; import { COOKIES_NOT_FOUND } from './errors'; import { ThrottlerModule } from '@nestjs/throttler'; import { AppController } from './app.controller'; +import { DbModule } from './db/db.module'; @Module({ imports: [ @@ -66,6 +67,7 @@ import { AppController } from './app.controller'; ttl: +process.env.RATE_LIMIT_TTL, limit: +process.env.RATE_LIMIT_MAX, }), + DbModule, UserModule, AuthModule, AdminModule, diff --git a/packages/hoppscotch-backend/src/constants.ts b/packages/hoppscotch-backend/src/constants.ts new file mode 100644 index 000000000..49e64b5d9 --- /dev/null +++ b/packages/hoppscotch-backend/src/constants.ts @@ -0,0 +1 @@ +export const PG_CONNECTION = 'PG_CONNECTION'; diff --git a/packages/hoppscotch-backend/src/db/db.module.ts b/packages/hoppscotch-backend/src/db/db.module.ts new file mode 100644 index 000000000..a403a0ae5 --- /dev/null +++ b/packages/hoppscotch-backend/src/db/db.module.ts @@ -0,0 +1,21 @@ +import { Global, Module } from '@nestjs/common'; +import { Pool } from 'pg'; +import { PG_CONNECTION } from 'src/constants'; + +const dbProvider = { + provide: PG_CONNECTION, + useValue: new Pool({ + user: 'postgres', + host: 'hoppscotch-db', + database: 'hoppscotch', + password: 'testpass', + port: 5432, + }), +}; + +@Global() +@Module({ + providers: [dbProvider], + exports: [dbProvider], +}) +export class DbModule {} \ No newline at end of file diff --git a/packages/hoppscotch-backend/src/prisma/prisma.service.ts b/packages/hoppscotch-backend/src/prisma/prisma.service.ts index 8febf1b5b..97e4cde21 100644 --- a/packages/hoppscotch-backend/src/prisma/prisma.service.ts +++ b/packages/hoppscotch-backend/src/prisma/prisma.service.ts @@ -7,7 +7,11 @@ export class PrismaService implements OnModuleInit, OnModuleDestroy { constructor() { - super(); + super( + { + log: ['query', 'info', 'warn', 'error'], + } + ); } async onModuleInit() { await this.$connect(); diff --git a/packages/hoppscotch-backend/src/team/team.resolver.ts b/packages/hoppscotch-backend/src/team/team.resolver.ts index aeb4bc228..3ee4ec01d 100644 --- a/packages/hoppscotch-backend/src/team/team.resolver.ts +++ b/packages/hoppscotch-backend/src/team/team.resolver.ts @@ -22,6 +22,7 @@ import { throwErr } from 'src/utils'; import { AuthUser } from 'src/types/AuthUser'; import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard'; import { SkipThrottle } from '@nestjs/throttler'; +import { cons } from 'fp-ts/lib/ReadonlyNonEmptyArray'; @UseGuards(GqlThrottlerGuard) @Resolver(() => Team) @@ -55,8 +56,13 @@ export class TeamResolver { description: 'Returns the list of members of a team', complexity: 10, }) - teamMembers(@Parent() team: Team): Promise { - return this.teamService.getTeamMembers(team.id); + async teamMembers(@Parent() team: Team): Promise { + const startR = Date.now(); + const members = await this.teamService.getTeamMembers(team.id); + const endR = Date.now(); + console.log('response generation: (teamMembers)', endR - startR, 'ms'); + + return members; } @ResolveField(() => TeamMemberRole, { @@ -64,41 +70,61 @@ export class TeamResolver { nullable: true, }) @UseGuards(GqlAuthGuard) - myRole( + async myRole( @Parent() team: Team, @GqlUser() user: AuthUser, ): Promise { - return this.teamService.getRoleOfUserInTeam(team.id, user.uid); + const startR = Date.now(); + const role = await this.teamService.getRoleOfUserInTeam(team.id, user.uid); + const endR = Date.now(); + console.log('response generation: (myRole)', endR - startR, 'ms'); + + return role; } @ResolveField(() => Int, { description: 'The number of users with the OWNER role in the team', }) - ownersCount(@Parent() team: Team): Promise { - return this.teamService.getCountOfUsersWithRoleInTeam( + async ownersCount(@Parent() team: Team): Promise { + const startR = Date.now(); + const count = await this.teamService.getCountOfUsersWithRoleInTeam( team.id, TeamMemberRole.OWNER, ); + const endR = Date.now(); + console.log('response generation: (ownersCount)', endR - startR, 'ms'); + + return count; } @ResolveField(() => Int, { description: 'The number of users with the EDITOR role in the team', }) - editorsCount(@Parent() team: Team): Promise { - return this.teamService.getCountOfUsersWithRoleInTeam( + async editorsCount(@Parent() team: Team): Promise { + const startR = Date.now(); + const count = await this.teamService.getCountOfUsersWithRoleInTeam( team.id, TeamMemberRole.EDITOR, ); + const endR = Date.now(); + console.log('response generation: (editorsCount)', endR - startR, 'ms'); + + return count; } @ResolveField(() => Int, { description: 'The number of users with the VIEWER role in the team', }) - viewersCount(@Parent() team: Team): Promise { - return this.teamService.getCountOfUsersWithRoleInTeam( + async viewersCount(@Parent() team: Team): Promise { + const startR = Date.now(); + const count = await this.teamService.getCountOfUsersWithRoleInTeam( team.id, TeamMemberRole.VIEWER, ); + const endR = Date.now(); + console.log('response generation: (viewersCount)', endR - startR, 'ms'); + + return count; } // Query @@ -106,7 +132,7 @@ export class TeamResolver { description: 'List of teams that the executing user belongs to.', }) @UseGuards(GqlAuthGuard) - myTeams( + async myTeams( @GqlUser() user: AuthUser, @Args({ name: 'cursor', @@ -117,7 +143,15 @@ export class TeamResolver { }) cursor?: string, ): Promise { - return this.teamService.getTeamsOfUser(user.uid, cursor ?? null); + const startR = Date.now(); + const teams = await this.teamService.getTeamsOfUser( + user.uid, + cursor ?? null, + ); + const endR = Date.now(); + console.log('response generation: (myTeams)', endR - startR, 'ms'); + + return teams; } @Query(() => Team, { @@ -130,7 +164,7 @@ export class TeamResolver { TeamMemberRole.EDITOR, TeamMemberRole.OWNER, ) - team( + async team( @Args({ name: 'teamID', type: () => ID, @@ -138,7 +172,12 @@ export class TeamResolver { }) teamID: string, ): Promise { - return this.teamService.getTeamWithID(teamID); + const startR = Date.now(); + const team = await this.teamService.getTeamWithID(teamID); + const endR = Date.now(); + console.log('response generation: (team)', endR - startR, 'ms'); + + return team; } // Mutation @@ -151,7 +190,11 @@ export class TeamResolver { @Args({ name: 'name', description: 'Displayed name of the team' }) name: string, ): Promise { + const startR = Date.now(); const team = await this.teamService.createTeam(name, user.uid); + const endR = Date.now(); + console.log('response generation: (createTeam)', endR - startR, 'ms'); + if (E.isLeft(team)) throwErr(team.left); return team.right; } @@ -169,7 +212,11 @@ export class TeamResolver { }) teamID: string, ): Promise { + const startR = Date.now(); const isUserLeft = await this.teamService.leaveTeam(teamID, user.uid); + const endR = Date.now(); + console.log('response generation: (leaveTeam)', endR - startR, 'ms'); + if (E.isLeft(isUserLeft)) throwErr(isUserLeft.left); return isUserLeft.right; } @@ -194,7 +241,11 @@ export class TeamResolver { }) userUid: string, ): Promise { + const startR = Date.now(); const isRemoved = await this.teamService.leaveTeam(teamID, userUid); + const endR = Date.now(); + console.log('response generation: (removeTeamMember)', endR - startR, 'ms'); + if (E.isLeft(isRemoved)) throwErr(isRemoved.left); return isRemoved.right; } @@ -210,7 +261,11 @@ export class TeamResolver { @Args({ name: 'newName', description: 'The updated name of the team' }) newName: string, ): Promise { + const startR = Date.now(); const team = await this.teamService.renameTeam(teamID, newName); + const endR = Date.now(); + console.log('response generation: (renameTeam)', endR - startR, 'ms'); + if (E.isLeft(team)) throwErr(team.left); return team.right; } @@ -224,7 +279,11 @@ export class TeamResolver { @Args({ name: 'teamID', description: 'ID of the team', type: () => ID }) teamID: string, ): Promise { + const startR = Date.now(); const isDeleted = await this.teamService.deleteTeam(teamID); + const endR = Date.now(); + console.log('response generation: (deleteTeam)', endR - startR, 'ms'); + if (E.isLeft(isDeleted)) throwErr(isDeleted.left); return isDeleted.right; } @@ -254,11 +313,19 @@ export class TeamResolver { }) newRole: TeamMemberRole, ): Promise { + const startR = Date.now(); const teamMember = await this.teamService.updateTeamMemberRole( teamID, userUid, newRole, ); + const endR = Date.now(); + console.log( + 'response generation: (updateTeamMemberRole)', + endR - startR, + 'ms', + ); + if (E.isLeft(teamMember)) throwErr(teamMember.left); return teamMember.right; } diff --git a/packages/hoppscotch-backend/src/team/team.service.ts b/packages/hoppscotch-backend/src/team/team.service.ts index f1afcf908..0315adf65 100644 --- a/packages/hoppscotch-backend/src/team/team.service.ts +++ b/packages/hoppscotch-backend/src/team/team.service.ts @@ -1,4 +1,4 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; +import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; import { TeamMember, TeamMemberRole, Team } from './team.model'; import { PrismaService } from '../prisma/prisma.service'; import { TeamMember as DbTeamMember } from '@prisma/client'; @@ -23,6 +23,8 @@ import * as T from 'fp-ts/Task'; import * as A from 'fp-ts/Array'; import { throwErr } from 'src/utils'; import { AuthUser } from '../types/AuthUser'; +import { PG_CONNECTION } from 'src/constants'; +import { Client } from 'pg'; @Injectable() export class TeamService implements UserDataHandler, OnModuleInit { @@ -30,8 +32,11 @@ export class TeamService implements UserDataHandler, OnModuleInit { private readonly prisma: PrismaService, private readonly userService: UserService, private readonly pubsub: PubSubService, + @Inject(PG_CONNECTION) private conn: Client, ) {} + enableRawSql: boolean = false; + onModuleInit() { this.userService.registerUserDataHandler(this); } @@ -52,12 +57,37 @@ export class TeamService implements UserDataHandler, OnModuleInit { teamID: string, role: TeamMemberRole, ): Promise { - return await this.prisma.teamMember.count({ + if (this.enableRawSql) { + const startQ = Date.now(); + const count = await this.conn.query( + `SELECT COUNT(*) FROM "TeamMember" WHERE "teamID" = '${teamID}' AND "role" = '${role}';`, + ); + const endQ = Date.now(); + console.log( + 'getCountOfUsersWithRoleInTeam >>>>>>>>>>', + endQ - startQ, + 'ms >>>>>', + count.rows, + ); + return count.rows[0].count; + } + + const startQ = Date.now(); + const count = await this.prisma.teamMember.count({ where: { teamID, role, }, }); + const endQ = Date.now(); + console.log( + 'getCountOfUsersWithRoleInTeam >>>>>>>>>>', + endQ - startQ, + 'ms >>>>>', + count, + ); + + return count; } async addMemberToTeamWithEmail( @@ -77,6 +107,11 @@ export class TeamService implements UserDataHandler, OnModuleInit { uid: string, role: TeamMemberRole, ): Promise { + const tm = await this.conn.query( + `INSERT INTO "TeamMember" (id, userUid, teamID, role) VALUES ('${new Date().toISOString()}', '${uid}', '${teamID}', '${role}') RETURNING *;`, + ); + console.log('addMemberToTeam >>>>>>>>>>', tm.rows[0]); + const teamMember = await this.prisma.teamMember.create({ data: { userUid: uid, @@ -101,6 +136,31 @@ export class TeamService implements UserDataHandler, OnModuleInit { } async deleteTeam(teamID: string): Promise | E.Right> { + if (this.enableRawSql) { + const startQ = Date.now(); + const t = await this.conn.query( + `SELECT * FROM "Team" WHERE "id" = '${teamID}'`, + ); + if (t.rows.length === 0) return E.left(TEAM_INVALID_ID); + + await this.conn.query( + `DELETE FROM "TeamMember" WHERE "teamID" = '${teamID}' RETURNING *`, + ); + + await this.conn.query(`DELETE FROM "Team" WHERE "id" = '${teamID}'`); + const endQ = Date.now(); + console.log( + 'deleteTeam >>>>>>>>>>', + endQ - startQ, + 'ms', + '>>>>>', + t.rows[0], + ); + + return E.right(true); + } + + const startQ = Date.now(); const team = await this.prisma.team.findUnique({ where: { id: teamID, @@ -119,6 +179,8 @@ export class TeamService implements UserDataHandler, OnModuleInit { id: teamID, }, }); + const endQ = Date.now(); + console.log('deleteTeam >>>>>>>>>>', endQ - startQ, 'ms', '>>>>>', team); return E.right(true); } @@ -135,6 +197,26 @@ export class TeamService implements UserDataHandler, OnModuleInit { const isValidTitle = this.validateTeamName(newName); if (E.isLeft(isValidTitle)) return isValidTitle; + if (this.enableRawSql) { + const startQ = Date.now(); + const ut = await this.conn.query( + `UPDATE "Team" SET "name" = '${newName}' WHERE "id" = '${teamID}' RETURNING *`, + ); + const endQ = Date.now(); + console.log( + 'renameTeam >>>>>>>>>>', + endQ - startQ, + 'ms', + '>>>>>', + ut.rows[0], + ); + + return E.right({ + id: ut.rows[0].id, + name: ut.rows[0].name, + }); + } + try { const updatedTeam = await this.prisma.team.update({ where: { @@ -156,6 +238,48 @@ export class TeamService implements UserDataHandler, OnModuleInit { userUid: string, newRole: TeamMemberRole, ): Promise | E.Right> { + if (this.enableRawSql) { + const startQ = Date.now(); + const oc = await this.conn.query( + `SELECT COUNT(*) FROM "TeamMember" WHERE "teamID" = '${teamID}' AND "role" = '${TeamMemberRole.OWNER}';`, + ); + const tm = await this.conn.query( + `SELECT * FROM "TeamMember" WHERE "teamID" = '${teamID}' AND "userUid" = '${userUid}'`, + ); + if (tm.rows.length === 0) return E.left(TEAM_MEMBER_NOT_FOUND); + + const ownerCount = oc.rows[0].count; + if ( + tm.rows[0].role === TeamMemberRole.OWNER && + newRole != TeamMemberRole.OWNER && + ownerCount === 1 + ) { + return E.left(TEAM_ONLY_ONE_OWNER); + } + + const utm = await this.conn.query( + `UPDATE "teamMember" SET "role" = '${newRole}' WHERE "teamID" = '${teamID}' AND "userUid" = '${userUid}'`, + ); + const endQ = Date.now(); + console.log( + 'updateTeamMemberRole >>>>>>>>>>', + endQ - startQ, + 'ms', + '>>>>>', + utm.rows[0], + ); + + const updatedMember: TeamMember = { + membershipID: utm.rows[0].id, + userUid: utm.rows[0].userUid, + role: TeamMemberRole[utm.rows[0].role], + }; + this.pubsub.publish(`team/${teamID}/member_updated`, updatedMember); + + return E.right(updatedMember); + } + + const startQ = Date.now(); const ownerCount = await this.prisma.teamMember.count({ where: { teamID, @@ -192,6 +316,14 @@ export class TeamService implements UserDataHandler, OnModuleInit { role: newRole, }, }); + const endQ = Date.now(); + console.log( + 'updateTeamMemberRole >>>>>>>>>>', + endQ - startQ, + 'ms', + '>>>>>', + result, + ); const updatedMember: TeamMember = { membershipID: result.id, @@ -208,6 +340,30 @@ export class TeamService implements UserDataHandler, OnModuleInit { teamID: string, userUid: string, ): Promise | E.Right> { + if (this.enableRawSql) { + const oc = await this.conn.query( + `SELECT COUNT(*) FROM "TeamMember" WHERE "teamID" = '${teamID}' AND "role" = '${TeamMemberRole.OWNER}';`, + ); + const ownerCount = oc.rows[0].count; + console.log('leaveTeam >>>>>>>>>>', oc.rows); + + const member = await this.getTeamMember(teamID, userUid); + if (!member) return E.left(TEAM_INVALID_ID_OR_USER); + + if (ownerCount === 1 && member.role === TeamMemberRole.OWNER) { + return E.left(TEAM_ONLY_ONE_OWNER); + } + + const dtm = await this.conn.query( + `DELETE FROM "TeamMember" WHERE "teamID" = '${teamID}' AND "userUid" = '${userUid}'`, + ); + console.log('leaveTeam >>>>>>>>>>', dtm); + + this.pubsub.publish(`team/${teamID}/member_removed`, userUid); + + return E.right(true); + } + const ownerCount = await this.prisma.teamMember.count({ where: { teamID, @@ -248,6 +404,34 @@ export class TeamService implements UserDataHandler, OnModuleInit { const isValidName = this.validateTeamName(name); if (E.isLeft(isValidName)) return isValidName; + if (this.enableRawSql) { + const startQ = Date.now(); + const t = await this.conn.query( + `INSERT INTO "Team" (id, name) VALUES ('${new Date().toISOString()}', '${name}') RETURNING *`, + ); + const tm = await this.conn.query( + `INSERT INTO "TeamMember" ("id", "userUid", "teamID", "role") VALUES ('${new Date().toISOString()}', '${creatorUid}' , '${ + t.rows[0].id + }', '${TeamMemberRole.OWNER}') RETURNING *`, + ); + const endQ = Date.now(); + ``; + console.log( + 'createTeam >>>>>>>>>>', + endQ - startQ, + 'ms', + '>>>>>', + t.rows[0], + tm.rows[0], + ); + + return E.right({ + id: t.rows[0].id, + name: t.rows[0].name, + }); + } + + const startQ = Date.now(); const team = await this.prisma.team.create({ data: { name: name, @@ -259,12 +443,42 @@ export class TeamService implements UserDataHandler, OnModuleInit { }, }, }); + const endQ = Date.now(); + console.log('createTeam >>>>>>>>>> ', endQ - startQ, 'ms', '>>>>>', team); return E.right(team); } async getTeamsOfUser(uid: string, cursor: string | null): Promise { + if (this.enableRawSql) { + const startQ = Date.now(); + let users; + if (cursor) { + users = await this.conn.query( + `SELECT * FROM "TeamMember" LEFT JOIN "Team" ON "TeamMember"."teamID" = "Team"."id" WHERE "TeamMember"."userUid" = '${uid}' and "TeamMember"."teamID" > '${cursor}' LIMIT 10`, + ); + } else { + users = await this.conn.query( + `SELECT * FROM "TeamMember" LEFT JOIN "Team" ON "TeamMember"."teamID" = "Team"."id" WHERE "TeamMember"."userUid" = '${uid}' LIMIT 10`, + ); + } + const endQ = Date.now(); + console.log( + 'getTeamsOfUser >>>>>>>>>>', + endQ - startQ, + 'ms', + '>>>>>', + users.rows, + ); + + return users.rows.map((entry) => ({ + id: entry.teamID, + name: entry.name, + })); + } + if (!cursor) { + const startQ = Date.now(); const entries = await this.prisma.teamMember.findMany({ take: 10, where: { @@ -274,9 +488,18 @@ export class TeamService implements UserDataHandler, OnModuleInit { team: true, }, }); + const endQ = Date.now(); + console.log( + 'getTeamsOfUser >>>>>>>>>>', + endQ - startQ, + 'ms', + '>>>>>', + entries, + ); return entries.map((entry) => entry.team); } else { + const startQ = Date.now(); const entries = await this.prisma.teamMember.findMany({ take: 10, skip: 1, @@ -293,17 +516,56 @@ export class TeamService implements UserDataHandler, OnModuleInit { team: true, }, }); + const endQ = Date.now(); + console.log( + 'getTeamsOfUser >>>>>>>>>>', + endQ - startQ, + 'ms', + '>>>>>', + entries, + ); + return entries.map((entry) => entry.team); } } async getTeamWithID(teamID: string): Promise { + if (this.enableRawSql) { + const startQ = Date.now(); + const team = await this.conn.query( + `SELECT * FROM "Team" WHERE "id" = '${teamID}'`, + ); + const endQ = Date.now(); + console.log( + 'getTeamWithID >>>>>>>>>>', + endQ - startQ, + 'ms', + '>>>>>', + team.rows, + ); + + if (team.rows.length === 0) return null; + return { + id: team.rows[0].id, + name: team.rows[0].name, + }; + } + try { + const startQ = Date.now(); const team = await this.prisma.team.findUnique({ where: { id: teamID, }, }); + const endQ = Date.now(); + console.log( + 'getTeamWithID >>>>>>>>>>', + endQ - startQ, + 'ms', + '>>>>>', + team, + ); return team; } catch (_e) { @@ -353,7 +615,30 @@ export class TeamService implements UserDataHandler, OnModuleInit { teamID: string, userUid: string, ): Promise { + if (this.enableRawSql) { + const startQ = Date.now(); + const member = await this.conn.query( + `SELECT * FROM "TeamMember" WHERE "teamID" = '${teamID}' AND "userUid" = '${userUid}'`, + ); + const endQ = Date.now(); + console.log( + 'getTeamMember >>>>>>>>>>', + endQ - startQ, + 'ms', + '>>>>>', + member.rows, + ); + + if (member.rows.length === 0) return null; + return { + membershipID: member.rows[0].id, + userUid: member.rows[0].userUid, + role: TeamMemberRole[member.rows[0].role], + }; + } + try { + const startQ = Date.now(); const teamMember = await this.prisma.teamMember.findUnique({ where: { teamID_userUid: { @@ -362,6 +647,14 @@ export class TeamService implements UserDataHandler, OnModuleInit { }, }, }); + const endQ = Date.now(); + console.log( + 'getTeamMember >>>>>>>>>>', + endQ - startQ, + 'ms', + '>>>>>', + teamMember, + ); if (!teamMember) return null; @@ -433,11 +726,44 @@ export class TeamService implements UserDataHandler, OnModuleInit { } async getTeamMembers(teamID: string): Promise { + if (this.enableRawSql) { + const startQ = Date.now(); + const members = await this.conn.query( + `SELECT * FROM "TeamMember" WHERE "teamID" = '${teamID}'`, + ); + const endQ = Date.now(); + + console.log( + 'getTeamMembers >>>>>>>>>>', + endQ - startQ, + 'ms', + '>>>>>', + members.rows, + ); + + return members.rows.map((entry) => { + return { + membershipID: entry.id, + userUid: entry.userUid, + role: TeamMemberRole[entry.role], + }; + }); + } + + const startQ = Date.now(); const dbTeamMembers = await this.prisma.teamMember.findMany({ where: { teamID, }, }); + const endQ = Date.now(); + console.log( + 'getTeamMembers >>>>>>>>>>', + endQ - startQ, + 'ms', + '>>>>>', + dbTeamMembers, + ); const members = dbTeamMembers.map( (entry) => @@ -470,8 +796,39 @@ export class TeamService implements UserDataHandler, OnModuleInit { teamID: string, cursor: string | null, ): Promise { + if (this.enableRawSql) { + const startQ = Date.now(); + let members; + if (cursor) { + members = await this.conn.query( + `SELECT * FROM "TeamMember" WHERE "teamID" = '${teamID}' AND "id" > '${cursor}' LIMIT 10`, + ); + } + members = await this.conn.query( + `SELECT * FROM "TeamMember" WHERE "teamID" = '${teamID}' LIMIT 10`, + ); + const endQ = Date.now(); + console.log( + 'getMembersOfTeam >>>>>>>>>>', + endQ - startQ, + 'ms', + '>>>>>', + members.rows, + ); + + return members.rows.map( + (entry) => + { + membershipID: entry.id, + userUid: entry.userUid, + role: TeamMemberRole[entry.role], + }, + ); + } + let teamMembers: DbTeamMember[]; + const startQ = Date.now(); if (!cursor) { teamMembers = await this.prisma.teamMember.findMany({ take: 10, @@ -491,6 +848,14 @@ export class TeamService implements UserDataHandler, OnModuleInit { }, }); } + const endQ = Date.now(); + console.log( + 'getMembersOfTeam >>>>>>>>>>', + endQ - startQ, + 'ms', + '>>>>>', + teamMembers, + ); const members = teamMembers.map( (entry) => diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbb14ad09..c37fa80ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,6 +184,9 @@ importers: passport-microsoft: specifier: ^1.0.0 version: 1.0.0 + pg: + specifier: ^8.11.3 + version: 8.11.3 prisma: specifier: ^4.16.2 version: 4.16.2 @@ -1339,7 +1342,7 @@ importers: version: 0.13.1(vite@3.2.4)(workbox-build@6.6.0)(workbox-window@6.6.0) vite-plugin-vue-layouts: specifier: ^0.7.0 - version: 0.7.0(vite@3.2.4)(vue-router@4.1.0)(vue@3.2.45) + version: 0.7.0(vite@3.2.4)(vue-router@4.2.4)(vue@3.2.45) vite-plugin-windicss: specifier: ^1.8.8 version: 1.8.8(vite@3.2.4) @@ -6062,8 +6065,8 @@ packages: vue-i18n: optional: true dependencies: - '@intlify/message-compiler': 9.3.0-beta.26 - '@intlify/shared': 9.3.0-beta.26 + '@intlify/message-compiler': 9.3.0 + '@intlify/shared': 9.3.0 jsonc-eslint-parser: 1.4.1 source-map: 0.6.1 vue-i18n: 9.2.2(vue@3.3.4) @@ -6116,6 +6119,14 @@ packages: '@intlify/shared': 9.2.2 source-map: 0.6.1 + /@intlify/message-compiler@9.3.0: + resolution: {integrity: sha512-D8tSJEhTCSFcCzkThjE4Sbk1tIdvzkYa1FaVIzUtZ8hKPATvokNrOiDw1i/h671m8A80l9Ywq594i/LPTB6EJA==} + engines: {node: '>= 16'} + dependencies: + '@intlify/shared': 9.3.0 + source-map-js: 1.0.2 + dev: true + /@intlify/message-compiler@9.3.0-beta.20: resolution: {integrity: sha512-hwqQXyTnDzAVZ300SU31jO0+3OJbpOdfVU6iBkrmNpS7t2HRnVACo0EwcEXzJa++4EVDreqz5OeqJbt+PeSGGA==} engines: {node: '>= 16'} @@ -6124,25 +6135,17 @@ packages: source-map-js: 1.0.2 dev: true - /@intlify/message-compiler@9.3.0-beta.26: - resolution: {integrity: sha512-qsfU6Lca7mI80ts1vgy+pfNvGm2gHy0nERpT/K1GYgnECmsKwud0e8SG1PPxKPEHKa5Mdngzs4pS7X1wH0SCGA==} - engines: {node: '>= 16'} - dependencies: - '@intlify/shared': 9.3.0-beta.26 - source-map-js: 1.0.2 - dev: true - /@intlify/shared@9.2.2: resolution: {integrity: sha512-wRwTpsslgZS5HNyM7uDQYZtxnbI12aGiBZURX3BTR9RFIKKRWpllTsgzHWvj3HKm3Y2Sh5LPC1r0PDCKEhVn9Q==} engines: {node: '>= 14'} - /@intlify/shared@9.3.0-beta.20: - resolution: {integrity: sha512-RucSPqh8O9FFxlYUysQTerSw0b9HIRpyoN1Zjogpm0qLiHK+lBNSa5sh1nCJ4wSsNcjphzgpLQCyR60GZlRV8g==} + /@intlify/shared@9.3.0: + resolution: {integrity: sha512-MMGRz6zWxtz7rHtxIIdnyb8SYOIaaseN1IvUhAEs9tOW4u77RD4DFp4qgPXesp2Gxo/5QitH9kwSs0jnxGUNEw==} engines: {node: '>= 16'} dev: true - /@intlify/shared@9.3.0-beta.26: - resolution: {integrity: sha512-RpCtfSYIg4tSskrazTr5+WCHyw6qpgwdIxC+x3nCnrPGxyk+en9FoSbadVfx/w7uDTdyhKslEw4d2+qhNc0s4Q==} + /@intlify/shared@9.3.0-beta.20: + resolution: {integrity: sha512-RucSPqh8O9FFxlYUysQTerSw0b9HIRpyoN1Zjogpm0qLiHK+lBNSa5sh1nCJ4wSsNcjphzgpLQCyR60GZlRV8g==} engines: {node: '>= 16'} dev: true @@ -6162,7 +6165,7 @@ packages: optional: true dependencies: '@intlify/bundle-utils': 7.0.0 - '@intlify/shared': 9.3.0-beta.26 + '@intlify/shared': 9.3.0 '@rollup/pluginutils': 4.2.1 debug: 4.3.4(supports-color@9.2.2) fast-glob: 3.2.11 @@ -6189,7 +6192,7 @@ packages: optional: true dependencies: '@intlify/bundle-utils': 3.4.0(vue-i18n@9.2.2) - '@intlify/shared': 9.3.0-beta.26 + '@intlify/shared': 9.3.0 '@rollup/pluginutils': 4.2.1 debug: 4.3.4(supports-color@9.2.2) fast-glob: 3.3.1 @@ -6217,7 +6220,7 @@ packages: optional: true dependencies: '@intlify/bundle-utils': 3.4.0(vue-i18n@9.2.2) - '@intlify/shared': 9.3.0-beta.26 + '@intlify/shared': 9.3.0 '@rollup/pluginutils': 4.2.1 debug: 4.3.4(supports-color@9.2.2) fast-glob: 3.3.1 @@ -6244,7 +6247,7 @@ packages: optional: true dependencies: '@intlify/bundle-utils': 3.4.0(vue-i18n@9.2.2) - '@intlify/shared': 9.3.0-beta.26 + '@intlify/shared': 9.3.0 '@rollup/pluginutils': 4.2.1 debug: 4.3.4(supports-color@9.2.2) fast-glob: 3.3.1 @@ -11138,6 +11141,11 @@ packages: /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + /buffer-writer@2.0.0: + resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} + engines: {node: '>=4'} + dev: false + /buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} dependencies: @@ -18937,6 +18945,10 @@ packages: netmask: 2.0.2 dev: false + /packet-reader@1.0.0: + resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} + dev: false + /paho-mqtt@1.1.0: resolution: {integrity: sha512-KPbL9KAB0ASvhSDbOrZBaccXS+/s7/LIofbPyERww8hM5Ko71GUJQ6Nmg0BWqj8phAIT8zdf/Sd/RftHU9i2HA==} dev: false @@ -19218,6 +19230,70 @@ packages: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} dev: false + /pg-cloudflare@1.1.1: + resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + requiresBuild: true + dev: false + optional: true + + /pg-connection-string@2.6.2: + resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==} + dev: false + + /pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + dev: false + + /pg-pool@3.6.1(pg@8.11.3): + resolution: {integrity: sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==} + peerDependencies: + pg: '>=8.0' + dependencies: + pg: 8.11.3 + dev: false + + /pg-protocol@1.6.0: + resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==} + dev: false + + /pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + dev: false + + /pg@8.11.3: + resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + dependencies: + buffer-writer: 2.0.0 + packet-reader: 1.0.0 + pg-connection-string: 2.6.2 + pg-pool: 3.6.1(pg@8.11.3) + pg-protocol: 1.6.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.1 + dev: false + + /pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + dependencies: + split2: 4.2.0 + dev: false + /pick-util@1.1.5: resolution: {integrity: sha512-H0MaM8T7wpQ/azvB12ChZw7kpSFzjsgv3Z+N7fUWnL1McTGSEeroCngcK4eOPiFQq08rAyKX3hadcAB1kUqfXA==} dependencies: @@ -19327,6 +19403,28 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 + /postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + dev: false + + /postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + dev: false + + /postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + dev: false + + /postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + dependencies: + xtend: 4.0.2 + dev: false + /postman-collection@4.2.0: resolution: {integrity: sha512-tvOLgN1h6Kab6dt43PmBoV5kYO/YUta3x0C2QqfmbzmHZe47VTpZ/+gIkGlbNhjKNPUUub5X6ehxYKoaTYdy1w==} engines: {node: '>=10'} @@ -20741,6 +20839,11 @@ packages: readable-stream: 3.6.0 dev: true + /split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + dev: false + /split@1.0.1: resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} dependencies: @@ -23071,6 +23174,23 @@ packages: - supports-color dev: true + /vite-plugin-vue-layouts@0.7.0(vite@3.2.4)(vue-router@4.2.4)(vue@3.2.45): + resolution: {integrity: sha512-k5XDmRNFo4M/GmUjhbRXj2WmJiFcGoVI8l/uZ72RHyRDQr4wE/6Zq/KFq0lqXomWQxTSzakQRUswzNwtvZLE8A==} + peerDependencies: + vite: ^2.5.0 || ^3.0.0-0 + vue: ^2.6.12 || ^3.2.4 + vue-router: ^3.5.1 || ^ 4.0.11 + dependencies: + '@vue/compiler-sfc': 3.3.4 + debug: 4.3.4(supports-color@9.2.2) + fast-glob: 3.3.1 + vite: 3.2.4(@types/node@17.0.45)(sass@1.53.0)(terser@5.19.2) + vue: 3.2.45 + vue-router: 4.2.4(vue@3.2.45) + transitivePeerDependencies: + - supports-color + dev: true + /vite-plugin-vue-layouts@0.8.0(vite@4.4.9)(vue-router@4.2.4)(vue@3.3.4): resolution: {integrity: sha512-UZW2nSV2LraTSe7gsAL46hfdi7a0X1RvkGGoJVtA2O8beu7anzpXFwQLou8+kHy31CzVycT4gIPySBsHhtBN5g==} peerDependencies: @@ -23774,6 +23894,15 @@ packages: '@vue/devtools-api': 6.5.0 vue: 3.2.45 + /vue-router@4.2.4(vue@3.2.45): + resolution: {integrity: sha512-9PISkmaCO02OzPVOMq2w82ilty6+xJmQrarYZDkjZBfl4RvYAlt4PKnEX21oW4KTtWfa9OuO/b3qk1Od3AEdCQ==} + peerDependencies: + vue: ^3.2.0 + dependencies: + '@vue/devtools-api': 6.5.0 + vue: 3.2.45 + dev: true + /vue-router@4.2.4(vue@3.3.4): resolution: {integrity: sha512-9PISkmaCO02OzPVOMq2w82ilty6+xJmQrarYZDkjZBfl4RvYAlt4PKnEX21oW4KTtWfa9OuO/b3qk1Od3AEdCQ==} peerDependencies: diff --git a/prod.Dockerfile b/prod.Dockerfile index 28cbab316..44ccf80c9 100644 --- a/prod.Dockerfile +++ b/prod.Dockerfile @@ -18,11 +18,11 @@ RUN pnpm exec prisma generate RUN pnpm run build # Remove the env file to avoid backend copying it in and using it RUN rm "../../.env" -ENV PRODUCTION="true" +ENV PRODUCTION="false" ENV PORT=3170 ENV APP_PORT=${PORT} ENV DB_URL=${DATABASE_URL} -CMD ["pnpm", "run", "start:prod"] +CMD ["pnpm", "run", "start:dev"] EXPOSE 3170 FROM base_builder as fe_builder