diff --git a/backend/package-lock.json b/backend/package-lock.json index 77ce0f52..f1444f21 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -3998,9 +3998,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.634", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.634.tgz", - "integrity": "sha512-gQNahJfF5AE4MZo+pMSwmnwkzVZ+F4ZGGj4Z/MMddOXVQM0y9OHy6ts3W9SDzAJaiZM3p6eixn5ABCQ+AfXzcQ==", + "version": "1.4.637", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.637.tgz", + "integrity": "sha512-G7j3UCOukFtxVO1vWrPQUoDk3kL70mtvjc/DC/k2o7lE0wAdq+Vwp1ipagOow+BH0uVztFysLWbkM/RTIrbK3w==", "dev": true }, "node_modules/emittery": { @@ -7227,9 +7227,9 @@ } }, "node_modules/prettier": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.3.tgz", - "integrity": "sha512-QNhUTBq+mqt1oH1dTfY3phOKNhcDdJkfttHI6u0kj7M2+c+7fmNKlgh2GhnHiqMcbxJ+a0j2igz/2jfl9QKLuw==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", + "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -8290,9 +8290,9 @@ } }, "node_modules/terser": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", - "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", + "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index bcefcb5c..f00adbd7 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -11,13 +11,13 @@ datasource db { } model User { - id String @id @default(auto()) @map("_id") @db.ObjectId - socialProvider String @map("social_provider") - socialUid String @unique @map("social_uid") - nickname String - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - UserWorkspace UserWorkspace[] + id String @id @default(auto()) @map("_id") @db.ObjectId + socialProvider String @map("social_provider") + socialUid String @unique @map("social_uid") + nickname String + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + userWorkspaceList UserWorkspace[] @@map("users") } diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 8e8dc6fd..73567ed1 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -6,9 +6,16 @@ import { ConfigModule } from "@nestjs/config"; import { APP_GUARD } from "@nestjs/core/constants"; import { JwtAuthGuard } from "./auth/jwt.guard"; import { WorkspacesModule } from "./workspaces/workspaces.module"; +import { WorkspaceUsersModule } from "./workspace-users/workspace-users.module"; @Module({ - imports: [ConfigModule.forRoot({ isGlobal: true }), UsersModule, AuthModule, WorkspacesModule], + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + UsersModule, + AuthModule, + WorkspacesModule, + WorkspaceUsersModule, + ], controllers: [], providers: [ PrismaService, diff --git a/backend/src/workspace-users/types/find-workspace-users-response.type.ts b/backend/src/workspace-users/types/find-workspace-users-response.type.ts new file mode 100644 index 00000000..ee39236f --- /dev/null +++ b/backend/src/workspace-users/types/find-workspace-users-response.type.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { WorkspaceUserDomain } from "./workspace-user.domain"; + +export class FindWorkspaceUsersResponse { + @ApiProperty({ type: [WorkspaceUserDomain], description: "List of found workspace users" }) + workspaceUsers: Array; + + @ApiProperty({ type: String, description: "The ID of last workspace user" }) + cursor: string | null; +} diff --git a/backend/src/workspace-users/types/workspace-user.domain.ts b/backend/src/workspace-users/types/workspace-user.domain.ts new file mode 100644 index 00000000..5fa47a92 --- /dev/null +++ b/backend/src/workspace-users/types/workspace-user.domain.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from "@nestjs/swagger"; + +export class WorkspaceUserDomain { + @ApiProperty({ type: String, description: "ID of the user" }) + id: string; + @ApiProperty({ type: String, description: "Nickname of the user" }) + nickname: string; + @ApiProperty({ type: Date, description: "Created date of the user" }) + createdAt: Date; + @ApiProperty({ type: Date, description: "Updated date of the user" }) + updatedAt: Date; +} diff --git a/backend/src/workspace-users/workspace-users.controller.spec.ts b/backend/src/workspace-users/workspace-users.controller.spec.ts new file mode 100644 index 00000000..34073155 --- /dev/null +++ b/backend/src/workspace-users/workspace-users.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { WorkspaceUsersController } from "./workspace-users.controller"; + +describe("WorkspaceUsersController", () => { + let controller: WorkspaceUsersController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [WorkspaceUsersController], + }).compile(); + + controller = module.get(WorkspaceUsersController); + }); + + it("should be defined", () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/workspace-users/workspace-users.controller.ts b/backend/src/workspace-users/workspace-users.controller.ts new file mode 100644 index 00000000..a73ab22f --- /dev/null +++ b/backend/src/workspace-users/workspace-users.controller.ts @@ -0,0 +1,40 @@ +import { Controller, DefaultValuePipe, Get, Param, ParseIntPipe, Query, Req } from "@nestjs/common"; +import { ApiBearerAuth, ApiFoundResponse, ApiOperation, ApiQuery, ApiTags } from "@nestjs/swagger"; +import { FindWorkspaceUsersResponse } from "./types/find-workspace-users-response.type"; +import { AuthroizedRequest } from "src/utils/types/req.type"; +import { WorkspaceUsersService } from "./workspace-users.service"; + +@ApiTags("Workspace.Users") +@ApiBearerAuth() +@Controller("workspaces/:workspace_id/users") +export class WorkspaceUsersController { + constructor(private workspaceUsersService: WorkspaceUsersService) {} + + @Get("") + @ApiOperation({ + summary: "Retrieve the Users in Workspace", + description: "Return the users in the workspace. This API supports KeySet pagination.", + }) + @ApiFoundResponse({ type: FindWorkspaceUsersResponse }) + @ApiQuery({ + name: "page_size", + type: Number, + description: "Page size to fetch (Default to 10)", + required: false, + }) + @ApiQuery({ + name: "cursor", + type: String, + description: + "API returns a limited set of results after a given cursor. If no value is provided, it returns the first page.", + required: false, + }) + async findMany( + @Req() req: AuthroizedRequest, + @Param("workspace_id") workspaceId: string, + @Query("page_size", new DefaultValuePipe(10), ParseIntPipe) pageSize: number, + @Query("cursor", new DefaultValuePipe(undefined)) cursor?: string + ): Promise { + return this.workspaceUsersService.findMany(req.user.id, workspaceId, pageSize, cursor); + } +} diff --git a/backend/src/workspace-users/workspace-users.module.ts b/backend/src/workspace-users/workspace-users.module.ts new file mode 100644 index 00000000..7e2a5033 --- /dev/null +++ b/backend/src/workspace-users/workspace-users.module.ts @@ -0,0 +1,10 @@ +import { Module } from "@nestjs/common"; +import { WorkspaceUsersController } from "./workspace-users.controller"; +import { WorkspaceUsersService } from "./workspace-users.service"; +import { PrismaService } from "src/db/prisma.service"; + +@Module({ + controllers: [WorkspaceUsersController], + providers: [WorkspaceUsersService, PrismaService], +}) +export class WorkspaceUsersModule {} diff --git a/backend/src/workspace-users/workspace-users.service.spec.ts b/backend/src/workspace-users/workspace-users.service.spec.ts new file mode 100644 index 00000000..af9ddf17 --- /dev/null +++ b/backend/src/workspace-users/workspace-users.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { WorkspaceUsersService } from "./workspace-users.service"; + +describe("WorkspaceUsersService", () => { + let service: WorkspaceUsersService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [WorkspaceUsersService], + }).compile(); + + service = module.get(WorkspaceUsersService); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/workspace-users/workspace-users.service.ts b/backend/src/workspace-users/workspace-users.service.ts new file mode 100644 index 00000000..5723aba1 --- /dev/null +++ b/backend/src/workspace-users/workspace-users.service.ts @@ -0,0 +1,61 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { Prisma } from "@prisma/client"; +import { PrismaService } from "src/db/prisma.service"; +import { FindWorkspaceUsersResponse } from "./types/find-workspace-users-response.type"; + +@Injectable() +export class WorkspaceUsersService { + constructor(private prismaService: PrismaService) {} + + async findMany( + userId: string, + workspaceId: string, + pageSize: number, + cursor?: string + ): Promise { + try { + await this.prismaService.userWorkspace.findFirstOrThrow({ + where: { + userId, + workspaceId, + }, + }); + } catch (e) { + throw new NotFoundException(); + } + + const additionalOptions: Prisma.UserFindManyArgs = {}; + + if (cursor) { + additionalOptions.cursor = { id: cursor }; + } + + const workspaceUserList = await this.prismaService.user.findMany({ + take: pageSize + 1, + select: { + id: true, + nickname: true, + updatedAt: true, + createdAt: true, + }, + where: { + userWorkspaceList: { + some: { + workspaceId: { + equals: workspaceId, + }, + }, + }, + }, + orderBy: { + id: "desc", + }, + ...additionalOptions, + }); + + return { + workspaceUsers: workspaceUserList.slice(0, pageSize), + cursor: workspaceUserList.length > pageSize ? workspaceUserList[pageSize].id : null, + }; + } +}