From 3c28096b15b0bf8b539eb0c33fb917cd6b9347e6 Mon Sep 17 00:00:00 2001 From: rajdip-b Date: Mon, 9 Sep 2024 10:59:26 +0530 Subject: [PATCH] added docs --- .../src/api-key/service/api-key.service.ts | 51 ++++ apps/api/src/auth/guard/admin/admin.guard.ts | 8 + .../src/auth/guard/api-key/api-key.guard.ts | 19 ++ apps/api/src/auth/guard/auth/auth.guard.ts | 9 + apps/api/src/auth/service/auth.service.ts | 36 +++ .../service/environment.service.ts | 115 +++++++++ .../src/feedback/service/feedback.service.ts | 5 + .../service/integration.service.ts | 87 ++++++- .../src/project/service/project.service.ts | 123 ++++++++++ apps/api/src/secret/service/secret.service.ts | 73 ++++++ apps/api/src/socket/change-notifier.socket.ts | 60 +++-- .../src/variable/service/variable.service.ts | 76 ++++++ .../service/workspace-role.service.ts | 67 ++++++ .../workspace/service/workspace.service.ts | 223 ++++++++++++++++++ 14 files changed, 925 insertions(+), 27 deletions(-) diff --git a/apps/api/src/api-key/service/api-key.service.ts b/apps/api/src/api-key/service/api-key.service.ts index 021e64ae..f0ac4f1d 100644 --- a/apps/api/src/api-key/service/api-key.service.ts +++ b/apps/api/src/api-key/service/api-key.service.ts @@ -28,6 +28,14 @@ export class ApiKeyService { updatedAt: true } + /** + * Creates a new API key for the given user. + * + * @throws `ConflictException` if the API key already exists. + * @param user The user to create the API key for. + * @param dto The data to create the API key with. + * @returns The created API key. + */ async createApiKey(user: User, dto: CreateApiKey) { await this.isApiKeyUnique(user, dto.name) @@ -60,6 +68,16 @@ export class ApiKeyService { } } + /** + * Updates an existing API key of the given user. + * + * @throws `ConflictException` if the API key name already exists. + * @throws `NotFoundException` if the API key with the given slug does not exist. + * @param user The user to update the API key for. + * @param apiKeySlug The slug of the API key to update. + * @param dto The data to update the API key with. + * @returns The updated API key. + */ async updateApiKey( user: User, apiKeySlug: ApiKey['slug'], @@ -103,6 +121,13 @@ export class ApiKeyService { return updatedApiKey } + /** + * Deletes an API key of the given user. + * + * @throws `NotFoundException` if the API key with the given slug does not exist. + * @param user The user to delete the API key for. + * @param apiKeySlug The slug of the API key to delete. + */ async deleteApiKey(user: User, apiKeySlug: ApiKey['slug']) { try { await this.prisma.apiKey.delete({ @@ -118,6 +143,14 @@ export class ApiKeyService { this.logger.log(`User ${user.id} deleted API key ${apiKeySlug}`) } + /** + * Retrieves an API key of the given user by slug. + * + * @throws `NotFoundException` if the API key with the given slug does not exist. + * @param user The user to retrieve the API key for. + * @param apiKeySlug The slug of the API key to retrieve. + * @returns The API key with the given slug. + */ async getApiKeyBySlug(user: User, apiKeySlug: ApiKey['slug']) { const apiKey = await this.prisma.apiKey.findUnique({ where: { @@ -134,6 +167,17 @@ export class ApiKeyService { return apiKey } + /** + * Retrieves all API keys of the given user. + * + * @param user The user to retrieve the API keys for. + * @param page The page number to retrieve. + * @param limit The maximum number of items to retrieve per page. + * @param sort The column to sort by. + * @param order The order to sort by. + * @param search The search string to filter the API keys by. + * @returns The API keys of the given user, filtered by the search string. + */ async getAllApiKeysOfUser( user: User, page: number, @@ -158,6 +202,13 @@ export class ApiKeyService { }) } + /** + * Checks if an API key with the given name already exists for the given user. + * + * @throws `ConflictException` if the API key already exists. + * @param user The user to check for. + * @param apiKeyName The name of the API key to check. + */ private async isApiKeyUnique(user: User, apiKeyName: string) { let apiKey: ApiKey | null = null diff --git a/apps/api/src/auth/guard/admin/admin.guard.ts b/apps/api/src/auth/guard/admin/admin.guard.ts index 30ea6a80..560541fb 100644 --- a/apps/api/src/auth/guard/admin/admin.guard.ts +++ b/apps/api/src/auth/guard/admin/admin.guard.ts @@ -4,6 +4,14 @@ import { Observable } from 'rxjs' @Injectable() export class AdminGuard implements CanActivate { + /** + * This guard will check if the request's user is an admin. + * If the user is an admin, then the canActivate function will return true. + * If the user is not an admin, then the canActivate function will return false. + * + * @param context The ExecutionContext for the request. + * @returns A boolean indicating whether or not the request's user is an admin. + */ canActivate( context: ExecutionContext ): boolean | Promise | Observable { diff --git a/apps/api/src/auth/guard/api-key/api-key.guard.ts b/apps/api/src/auth/guard/api-key/api-key.guard.ts index 68ed2dfb..b32370e0 100644 --- a/apps/api/src/auth/guard/api-key/api-key.guard.ts +++ b/apps/api/src/auth/guard/api-key/api-key.guard.ts @@ -16,6 +16,25 @@ import { IS_PUBLIC_KEY } from '@/decorators/public.decorator' export class ApiKeyGuard implements CanActivate { constructor(private readonly reflector: Reflector) {} + /** + * This method will check if the user is authenticated via an API key, + * and if the API key has the required authorities for the route. + * + * If the user is not authenticated via an API key, or if the API key does not have the required authorities, + * then the canActivate method will return true. + * + * If the user is authenticated via an API key, and the API key has the required authorities, + * then the canActivate method will return true. + * + * If the user is authenticated via an API key, but the API key does not have the required authorities, + * then the canActivate method will throw an UnauthorizedException. + * + * If the user is authenticated via an API key, but the API key is forbidden for the route, + * then the canActivate method will throw an UnauthorizedException. + * + * @param context The ExecutionContext for the request. + * @returns A boolean indicating whether or not the user is authenticated via an API key and has the required authorities for the route. + */ canActivate( context: ExecutionContext ): boolean | Promise | Observable { diff --git a/apps/api/src/auth/guard/auth/auth.guard.ts b/apps/api/src/auth/guard/auth/auth.guard.ts index aa9c6c7d..0b470c9d 100644 --- a/apps/api/src/auth/guard/auth/auth.guard.ts +++ b/apps/api/src/auth/guard/auth/auth.guard.ts @@ -29,6 +29,15 @@ export class AuthGuard implements CanActivate { private readonly cache: CacheService ) {} + /** + * This method is called by NestJS every time an HTTP request is made to an endpoint + * that is protected by this guard. It checks if the request is authenticated and if + * the user is active. If the user is not active, it throws an UnauthorizedException. + * If the onboarding is not finished, it throws an UnauthorizedException. + * @param context The ExecutionContext object that contains information about the + * request. + * @returns A boolean indicating if the request is authenticated and the user is active. + */ async canActivate(context: ExecutionContext): Promise { // Get the kind of route. Routes marked with the @Public() decorator are public. const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ diff --git a/apps/api/src/auth/service/auth.service.ts b/apps/api/src/auth/service/auth.service.ts index 32c054a9..f680dfb3 100644 --- a/apps/api/src/auth/service/auth.service.ts +++ b/apps/api/src/auth/service/auth.service.ts @@ -30,6 +30,11 @@ export class AuthService { this.logger = new Logger(AuthService.name) } + /** + * Sends a login code to the given email address + * @throws {BadRequestException} If the email address is invalid + * @param email The email address to send the login code to + */ async sendOtp(email: string): Promise { if (!email || !email.includes('@')) { this.logger.error(`Invalid email address: ${email}`) @@ -45,6 +50,14 @@ export class AuthService { } /* istanbul ignore next */ + /** + * Validates a login code sent to the given email address + * @throws {NotFoundException} If the user is not found + * @throws {UnauthorizedException} If the login code is invalid + * @param email The email address the login code was sent to + * @param otp The login code to validate + * @returns An object containing the user and a JWT token + */ async validateOtp( email: string, otp: string @@ -93,6 +106,14 @@ export class AuthService { } /* istanbul ignore next */ + /** + * Handles a login with an OAuth provider + * @param email The email of the user + * @param name The name of the user + * @param profilePictureUrl The profile picture URL of the user + * @param oauthProvider The OAuth provider used + * @returns An object containing the user and a JWT token + */ async handleOAuthLogin( email: string, name: string, @@ -116,6 +137,10 @@ export class AuthService { } /* istanbul ignore next */ + /** + * Cleans up expired OTPs every hour + * @throws {PrismaError} If there is an error deleting expired OTPs + */ @Cron(CronExpression.EVERY_HOUR) async cleanUpExpiredOtps() { try { @@ -133,6 +158,17 @@ export class AuthService { } } + /** + * Creates a user if it doesn't exist yet. If the user has signed up with a + * different authentication provider, it throws an UnauthorizedException. + * @param email The email address of the user + * @param authProvider The AuthProvider used + * @param name The name of the user + * @param profilePictureUrl The profile picture URL of the user + * @returns The user + * @throws {UnauthorizedException} If the user has signed up with a different + * authentication provider + */ private async createUserIfNotExists( email: string, authProvider: AuthProvider, diff --git a/apps/api/src/environment/service/environment.service.ts b/apps/api/src/environment/service/environment.service.ts index f7644534..57a0be15 100644 --- a/apps/api/src/environment/service/environment.service.ts +++ b/apps/api/src/environment/service/environment.service.ts @@ -30,6 +30,31 @@ export class EnvironmentService { private readonly authorityCheckerService: AuthorityCheckerService ) {} + /** + * Creates a new environment in the given project. + * + * This endpoint requires the following authorities: + * - `CREATE_ENVIRONMENT` on the project + * - `READ_ENVIRONMENT` on the project + * - `READ_PROJECT` on the project + * + * If the user does not have the required authorities, a `ForbiddenException` is thrown. + * + * If an environment with the same name already exists in the project, a `ConflictException` is thrown. + * + * The created environment is returned, with the slug generated using the `name` and `ENVIRONMENT` as the entity type. + * + * An event of type `ENVIRONMENT_ADDED` is created, with the following metadata: + * - `environmentId`: The ID of the created environment + * - `name`: The name of the created environment + * - `projectId`: The ID of the project in which the environment was created + * - `projectName`: The name of the project in which the environment was created + * + * @param user The user that is creating the environment + * @param dto The data for the new environment + * @param projectSlug The slug of the project in which to create the environment + * @returns The created environment + */ async createEnvironment( user: User, dto: CreateEnvironment, @@ -96,6 +121,31 @@ export class EnvironmentService { return environment } + /** + * Updates an environment in the given project. + * + * This endpoint requires the following authorities: + * - `UPDATE_ENVIRONMENT` on the environment + * - `READ_ENVIRONMENT` on the environment + * - `READ_PROJECT` on the project + * + * If the user does not have the required authorities, a `ForbiddenException` is thrown. + * + * If an environment with the same name already exists in the project, a `ConflictException` is thrown. + * + * The updated environment is returned, with the slug generated using the `name` and `ENVIRONMENT` as the entity type. + * + * An event of type `ENVIRONMENT_UPDATED` is created, with the following metadata: + * - `environmentId`: The ID of the updated environment + * - `name`: The name of the updated environment + * - `projectId`: The ID of the project in which the environment was updated + * - `projectName`: The name of the project in which the environment was updated + * + * @param user The user that is updating the environment + * @param dto The data for the updated environment + * @param environmentSlug The slug of the environment to update + * @returns The updated environment + */ async updateEnvironment( user: User, dto: UpdateEnvironment, @@ -157,6 +207,19 @@ export class EnvironmentService { return updatedEnvironment } + /** + * Gets an environment by its slug. + * + * This endpoint requires the `READ_ENVIRONMENT` authority on the environment. + * + * If the user does not have the required authority, a `ForbiddenException` is thrown. + * + * The returned environment object does not include the project property. + * + * @param user The user that is requesting the environment + * @param environmentSlug The slug of the environment to get + * @returns The environment + */ async getEnvironment(user: User, environmentSlug: Environment['slug']) { const environment = await this.authorityCheckerService.checkAuthorityOverEnvironment({ @@ -171,6 +234,36 @@ export class EnvironmentService { return environment } + /** + * Gets a list of all environments in the given project. + * + * This endpoint requires the `READ_ENVIRONMENT` authority on the project. + * + * If the user does not have the required authority, a `ForbiddenException` is thrown. + * + * The returned list of environments is paginated and sorted according to the provided parameters. + * + * The metadata object contains the following properties: + * - `href`: The URL to the current page + * - `next`: The URL to the next page (if it exists) + * - `prev`: The URL to the previous page (if it exists) + * - `totalPages`: The total number of pages + * - `totalItems`: The total number of items + * - `limit`: The maximum number of items per page + * - `page`: The current page number + * - `sort`: The sort field + * - `order`: The sort order + * - `search`: The search query + * + * @param user The user that is requesting the environments + * @param projectSlug The slug of the project in which to get the environments + * @param page The page number + * @param limit The maximum number of items per page + * @param sort The sort field + * @param order The sort order + * @param search The search query + * @returns An object with a list of environments and metadata + */ async getEnvironmentsOfProject( user: User, projectSlug: Project['slug'], @@ -239,6 +332,23 @@ export class EnvironmentService { return { items, metadata } } + /** + * Deletes an environment in a project. + * + * This endpoint requires the `DELETE_ENVIRONMENT` authority on the environment. + * + * If the user does not have the required authority, a `ForbiddenException` is thrown. + * + * If this is the only existing environment in the project, a `BadRequestException` is thrown. + * + * An event of type `ENVIRONMENT_DELETED` is created, with the following metadata: + * - `environmentId`: The ID of the deleted environment + * - `name`: The name of the deleted environment + * - `projectId`: The ID of the project in which the environment was deleted + * + * @param user The user that is deleting the environment + * @param environmentSlug The slug of the environment to delete + */ async deleteEnvironment(user: User, environmentSlug: Environment['slug']) { const environment = await this.authorityCheckerService.checkAuthorityOverEnvironment({ @@ -289,6 +399,11 @@ export class EnvironmentService { ) } + /** + * Checks if an environment with the given name already exists in the given project. + * @throws ConflictException if an environment with the given name already exists + * @private + */ private async environmentExists(name: Environment['name'], project: Project) { const { id: projectId, slug } = project diff --git a/apps/api/src/feedback/service/feedback.service.ts b/apps/api/src/feedback/service/feedback.service.ts index d30a2c9d..19ebfac3 100644 --- a/apps/api/src/feedback/service/feedback.service.ts +++ b/apps/api/src/feedback/service/feedback.service.ts @@ -7,6 +7,11 @@ export class FeedbackService { @Inject(MAIL_SERVICE) private readonly mailService: IMailService ) {} + /** + * Registers a feedback to be sent to the admin's email. + * @param feedback The feedback to be sent. + * @throws {BadRequestException} If the feedback is null or empty. + */ async registerFeedback(feedback: string): Promise { if (!feedback || feedback.trim().length === 0) { throw new BadRequestException('Feedback cannot be null or empty') diff --git a/apps/api/src/integration/service/integration.service.ts b/apps/api/src/integration/service/integration.service.ts index 250ad614..6528bc52 100644 --- a/apps/api/src/integration/service/integration.service.ts +++ b/apps/api/src/integration/service/integration.service.ts @@ -33,6 +33,30 @@ export class IntegrationService { private readonly authorityCheckerService: AuthorityCheckerService ) {} + /** + * Creates a new integration in the given workspace. The user needs to have + * `CREATE_INTEGRATION` and `READ_WORKSPACE` authority in the workspace. + * + * If the integration is of type `PROJECT`, the user needs to have `READ_PROJECT` + * authority in the project specified by `projectSlug`. + * + * If the integration is of type `ENVIRONMENT`, the user needs to have `READ_ENVIRONMENT` + * authority in the environment specified by `environmentSlug`. + * + * If the integration is of type `PROJECT` and `environmentSlug` is provided, + * the user needs to have `READ_ENVIRONMENT` authority in the environment specified + * by `environmentSlug`. + * + * The integration is created with the given name, slug, type, metadata and + * notifyOn events. The slug is generated using the `name` and a unique + * identifier. + * + * @param user The user creating the integration + * @param dto The integration data + * @param workspaceSlug The slug of the workspace the integration is being + * created in + * @returns The created integration + */ async createIntegration( user: User, dto: CreateIntegration, @@ -127,6 +151,28 @@ export class IntegrationService { return integration } + /** + * Updates an integration. The user needs to have `UPDATE_INTEGRATION` authority + * over the integration. + * + * If the integration is of type `PROJECT`, the user needs to have `READ_PROJECT` + * authority in the project specified by `projectSlug`. + * + * If the integration is of type `ENVIRONMENT`, the user needs to have `READ_ENVIRONMENT` + * authority in the environment specified by `environmentSlug`. + * + * If the integration is of type `PROJECT` and `environmentSlug` is provided, + * the user needs to have `READ_ENVIRONMENT` authority in the environment specified + * by `environmentSlug`. + * + * The integration is updated with the given name, slug, metadata and + * notifyOn events. + * + * @param user The user updating the integration + * @param dto The integration data + * @param integrationSlug The slug of the integration to update + * @returns The updated integration + */ async updateIntegration( user: User, dto: UpdateIntegration, @@ -225,6 +271,14 @@ export class IntegrationService { return updatedIntegration } + /** + * Retrieves an integration by its slug. The user needs to have `READ_INTEGRATION` + * authority over the integration. + * + * @param user The user retrieving the integration + * @param integrationSlug The slug of the integration to retrieve + * @returns The integration with the given slug + */ async getIntegration(user: User, integrationSlug: Integration['slug']) { return this.authorityCheckerService.checkAuthorityOverIntegration({ userId: user.id, @@ -236,6 +290,22 @@ export class IntegrationService { /* istanbul ignore next */ // The e2e tests are not working, but the API calls work as expected + /** + * Retrieves all integrations in a workspace that the user has READ authority over. + * + * The user needs to have `READ_INTEGRATION` authority over the workspace. + * + * The results are paginated and can be sorted by name ascending or descending. + * + * @param user The user retrieving the integrations + * @param workspaceSlug The slug of the workspace to retrieve integrations from + * @param page The page number of the results + * @param limit The number of items per page + * @param sort The property to sort the results by (default: name) + * @param order The order to sort the results by (default: ascending) + * @param search The string to search for in the integration names + * @returns A paginated list of integrations in the workspace + */ async getAllIntegrationsOfWorkspace( user: User, workspaceSlug: Workspace['slug'], @@ -317,7 +387,7 @@ export class IntegrationService { } }) - //calculate metadata for pagination + // Calculate metadata for pagination const totalCount = await this.prisma.integration.count({ where: { name: { @@ -347,6 +417,14 @@ export class IntegrationService { return { items: integrations, metadata } } + /** + * Deletes an integration by its slug. The user needs to have `DELETE_INTEGRATION` + * authority over the integration. + * + * @param user The user deleting the integration + * @param integrationSlug The slug of the integration to delete + * @returns Nothing + */ async deleteIntegration(user: User, integrationSlug: Integration['slug']) { const integration = await this.authorityCheckerService.checkAuthorityOverIntegration({ @@ -381,6 +459,13 @@ export class IntegrationService { ) } + /** + * Checks if an integration with the same name already exists in the workspace. + * Throws a ConflictException if the integration already exists. + * + * @param name The name of the integration to check + * @param workspace The workspace to check in + */ private async existsByNameAndWorkspaceId( name: Integration['name'], workspace: Workspace diff --git a/apps/api/src/project/service/project.service.ts b/apps/api/src/project/service/project.service.ts index 69db9c08..43c991eb 100644 --- a/apps/api/src/project/service/project.service.ts +++ b/apps/api/src/project/service/project.service.ts @@ -39,6 +39,14 @@ export class ProjectService { private readonly authorityCheckerService: AuthorityCheckerService ) {} + /** + * Creates a new project in a workspace + * + * @param user The user who is creating the project + * @param workspaceSlug The slug of the workspace where the project will be created + * @param dto The data for the new project + * @returns The newly created project + */ async createProject( user: User, workspaceSlug: Workspace['slug'], @@ -201,6 +209,17 @@ export class ProjectService { return newProject } + /** + * Updates a project. + * + * @param user The user who is updating the project + * @param projectSlug The slug of the project to update + * @param dto The data to update the project with + * @returns The updated project + * + * @throws ConflictException If a project with the same name already exists for the user + * @throws BadRequestException If the private key is required but not supplied + */ async updateProject( user: User, projectSlug: Project['slug'], @@ -345,6 +364,17 @@ export class ProjectService { } } + /** + * Forks a project. + * + * @param user The user who is creating the new project + * @param projectSlug The slug of the project to fork + * @param forkMetadata The metadata for the new project + * @returns The newly forked project + * + * @throws ConflictException If a project with the same name already exists for the user + * @throws BadRequestException If the private key is required but not supplied + */ async forkProject( user: User, projectSlug: Project['slug'], @@ -481,6 +511,16 @@ export class ProjectService { return newProject } + /** + * Unlinks a forked project from its parent project. + * + * @param user The user who is unlinking the project + * @param projectSlug The slug of the project to unlink + * @returns The updated project + * + * @throws BadRequestException If the project is not a forked project + * @throws UnauthorizedException If the user does not have the authority to update the project + */ async unlinkParentOfFork(user: User, projectSlug: Project['slug']) { const project = await this.authorityCheckerService.checkAuthorityOverProject({ @@ -502,6 +542,19 @@ export class ProjectService { }) } + /** + * Syncs a forked project with its parent project. + * + * @param user The user who is syncing the project + * @param projectSlug The slug of the project to sync + * @param hardSync Whether to do a hard sync or not. If true, all items in the + * forked project will be replaced with the items from the parent project. If + * false, only items that are not present in the forked project will be added + * from the parent project. + * + * @throws BadRequestException If the project is not a forked project + * @throws UnauthorizedException If the user does not have the authority to update the project + */ async syncFork(user: User, projectSlug: Project['slug'], hardSync: boolean) { const project = await this.authorityCheckerService.checkAuthorityOverProject({ @@ -548,6 +601,13 @@ export class ProjectService { await this.prisma.$transaction(copyProjectOp) } + /** + * Deletes a project. + * @param user The user who is deleting the project + * @param projectSlug The slug of the project to delete + * + * @throws UnauthorizedException If the user does not have the authority to delete the project + */ async deleteProject(user: User, projectSlug: Project['slug']) { const project = await this.authorityCheckerService.checkAuthorityOverProject({ @@ -602,6 +662,17 @@ export class ProjectService { this.log.debug(`Deleted project ${project}`) } + /** + * Gets all the forks of a project. + * + * @param user The user who is requesting the forks + * @param projectSlug The slug of the project to get forks for + * @param page The page number to get the forks for + * @param limit The number of forks to get per page + * @returns An object with two properties: `items` and `metadata`. + * `items` is an array of project objects that are forks of the given project, + * and `metadata` is the pagination metadata for the forks. + */ async getAllProjectForks( user: User, projectSlug: Project['slug'], @@ -650,6 +721,15 @@ export class ProjectService { return { items, metadata } } + /** + * Gets a project by slug. + * + * @param user The user who is requesting the project + * @param projectSlug The slug of the project to get + * @returns The project with secrets removed + * + * @throws UnauthorizedException If the user does not have the authority to read the project + */ async getProject(user: User, projectSlug: Project['slug']) { const project = await this.authorityCheckerService.checkAuthorityOverProject({ @@ -664,6 +744,20 @@ export class ProjectService { return project } + /** + * Gets all the projects in a workspace that the user has access to. + * + * @param user The user who is requesting the projects + * @param workspaceSlug The slug of the workspace to get the projects from + * @param page The page number to get the projects for + * @param limit The number of projects to get per page + * @param sort The field to sort the projects by + * @param order The order to sort the projects in + * @param search The search string to filter the projects by + * @returns An object with two properties: `items` and `metadata`. + * `items` is an array of project objects that match the given criteria, + * and `metadata` is an object with pagination metadata. + */ async getProjectsOfWorkspace( user: User, workspaceSlug: Workspace['slug'], @@ -753,6 +847,13 @@ export class ProjectService { return { items, metadata } } + /** + * Checks if a project with a given name exists in a workspace. + * + * @param projectName The name of the project to check + * @param workspaceId The ID of the workspace to check in + * @returns true if the project exists, false otherwise + */ private async projectExists( projectName: string, workspaceId: Workspace['id'] @@ -773,6 +874,16 @@ export class ProjectService { ) } + /** + * Copies the project data from one project to another project. + * + * @param user The user who is performing the copy operation + * @param fromProject The project from which the data is being copied + * @param toProject The project to which the data is being copied + * @param hardCopy If true, replace all the data in the toProject with the fromProject, + * otherwise, only add the items in the fromProject that are not already present in the toProject. + * @returns An array of database operations that need to be performed to copy the data. + */ private async copyProjectData( user: User, fromProject: { @@ -989,6 +1100,18 @@ export class ProjectService { return [...createEnvironmentOps, ...createSecretOps, ...createVariableOps] } + /** + * Updates the key pair of a project. + * + * @param project The project to update + * @param oldPrivateKey The old private key of the project + * @param storePrivateKey Whether to store the new private key in the database + * + * @returns An object with three properties: + * - `txs`: an array of database operations that need to be performed to update the project + * - `newPrivateKey`: the new private key of the project + * - `newPublicKey`: the new public key of the project + */ private async updateProjectKeyPair( project: ProjectWithSecrets, oldPrivateKey: string, diff --git a/apps/api/src/secret/service/secret.service.ts b/apps/api/src/secret/service/secret.service.ts index a6bfc3b5..55b59fc0 100644 --- a/apps/api/src/secret/service/secret.service.ts +++ b/apps/api/src/secret/service/secret.service.ts @@ -50,6 +50,13 @@ export class SecretService { this.redis = redisClient.publisher } + /** + * Creates a new secret in a project + * @param user the user creating the secret + * @param dto the secret data + * @param projectSlug the slug of the project + * @returns the created secret + */ async createSecret( user: User, dto: CreateSecret, @@ -149,6 +156,13 @@ export class SecretService { return secret } + /** + * Updates a secret in a project + * @param user the user performing the action + * @param secretSlug the slug of the secret to update + * @param dto the new secret data + * @returns the updated secret and the updated versions + */ async updateSecret( user: User, secretSlug: Secret['slug'], @@ -298,6 +312,14 @@ export class SecretService { return result } + /** + * Rollback a secret to a specific version + * @param user the user performing the action + * @param secretSlug the slug of the secret to rollback + * @param environmentSlug the slug of the environment to rollback + * @param rollbackVersion the version to rollback to + * @returns the deleted secret versions + */ async rollbackSecret( user: User, secretSlug: Secret['slug'], @@ -394,6 +416,12 @@ export class SecretService { return result } + /** + * Deletes a secret from a project + * @param user the user performing the action + * @param secretSlug the slug of the secret to delete + * @returns void + */ async deleteSecret(user: User, secretSlug: Secret['slug']) { // Check if the user has the required role const secret = await this.authorityCheckerService.checkAuthorityOverSecret({ @@ -428,6 +456,15 @@ export class SecretService { this.logger.log(`User ${user.id} deleted secret ${secret.id}`) } + /** + * Gets all secrets of a project and environment + * @param user the user performing the action + * @param projectSlug the slug of the project + * @param environmentSlug the slug of the environment + * @returns an array of objects with the secret name and value + * @throws {NotFoundException} if the project or environment does not exist + * @throws {BadRequestException} if the user does not have the required role + */ async getAllSecretsOfProjectAndEnvironment( user: User, projectSlug: Project['slug'], @@ -495,6 +532,17 @@ export class SecretService { return response } + + /** + * Gets all revisions of a secret in an environment + * @param user the user performing the action + * @param secretSlug the slug of the secret + * @param environmentSlug the slug of the environment + * @param page the page of items to return + * @param limit the number of items to return per page + * @param order the order of the items. Default is 'desc' + * @returns an object with the items and the pagination metadata + */ async getRevisionsOfSecret( user: User, secretSlug: Secret['slug'], @@ -551,6 +599,18 @@ export class SecretService { return { items, metadata } } + /** + * Gets all secrets of a project + * @param user the user performing the action + * @param projectSlug the slug of the project + * @param decryptValue whether to decrypt the secret values or not + * @param page the page of items to return + * @param limit the number of items to return per page + * @param sort the field to sort the results by + * @param order the order of the results + * @param search the search query + * @returns an object with the items and the pagination metadata + */ async getAllSecretsOfProject( user: User, projectSlug: Project['slug'], @@ -704,6 +764,12 @@ export class SecretService { return { items, metadata } } + /** + * Checks if a secret with a given name already exists in the project + * @throws {ConflictException} if the secret already exists + * @param secretName the name of the secret to check + * @param project the project to check the secret in + */ private async secretExists(secretName: Secret['name'], project: Project) { if ( (await this.prisma.secret.findFirst({ @@ -719,6 +785,13 @@ export class SecretService { } } + /** + * Checks if the project is allowed to decrypt secret values + * @param decryptValue whether to decrypt the secret values or not + * @param project the project to check + * @throws {BadRequestException} if the project does not store the private key and decryptValue is true + * @throws {NotFoundException} if the project does not have a private key and decryptValue is true + */ private async checkAutoDecrypt(decryptValue: boolean, project: Project) { // Check if the project is allowed to store the private key if (decryptValue && !project.storePrivateKey) { diff --git a/apps/api/src/socket/change-notifier.socket.ts b/apps/api/src/socket/change-notifier.socket.ts index 38d3f201..d05d1b47 100644 --- a/apps/api/src/socket/change-notifier.socket.ts +++ b/apps/api/src/socket/change-notifier.socket.ts @@ -94,32 +94,44 @@ export default class ChangeNotifier ) @UseGuards(AuthGuard, ApiKeyGuard) @SubscribeMessage('register-client-app') + /** + * This event is emitted from the CLI to register + * itself with our services so that it can receive live updates. + * + * The CLI will send a `ChangeNotifierRegistration` object + * as the message body, containing the workspace slug, project slug, + * and environment slug that the client app wants to receive updates for. + * + * We will then check if the user has access to the workspace, + * project, and environment, and if so, add the client to the + * list of connected clients for that environment. + * + * Finally, we will send an ACK to the client with a status code of 200. + */ async handleRegister( @ConnectedSocket() client: Socket, @MessageBody() data: ChangeNotifierRegistration, @CurrentUser() user: User ) { // Check if the user has access to the workspace - const workspace = - await this.authorityCheckerService.checkAuthorityOverWorkspace({ - userId: user.id, - entity: { slug: data.workspaceSlug }, - authorities: [ - Authority.READ_WORKSPACE, - Authority.READ_VARIABLE, - Authority.READ_SECRET - ], - prisma: this.prisma - }) + await this.authorityCheckerService.checkAuthorityOverWorkspace({ + userId: user.id, + entity: { slug: data.workspaceSlug }, + authorities: [ + Authority.READ_WORKSPACE, + Authority.READ_VARIABLE, + Authority.READ_SECRET + ], + prisma: this.prisma + }) // Check if the user has access to the project - const project = - await this.authorityCheckerService.checkAuthorityOverProject({ - userId: user.id, - entity: { slug: data.projectSlug }, - authorities: [Authority.READ_PROJECT], - prisma: this.prisma - }) + await this.authorityCheckerService.checkAuthorityOverProject({ + userId: user.id, + entity: { slug: data.projectSlug }, + authorities: [Authority.READ_PROJECT], + prisma: this.prisma + }) // Check if the user has access to the environment const environment = @@ -133,18 +145,14 @@ export default class ChangeNotifier // Add the client to the environment await this.addClientToEnvironment(client, environment.id) - const clientRegisteredResponse = { - workspaceId: workspace.id, - projectId: project.id, - environmentId: environment.id - } - // Send ACK to client - client.emit('client-registered', clientRegisteredResponse) + client.emit('client-registered', { + 'status-code': 200 + }) this.logger.log( `Client registered: ${client.id} for configuration: ${JSON.stringify( - clientRegisteredResponse + data )}` ) } diff --git a/apps/api/src/variable/service/variable.service.ts b/apps/api/src/variable/service/variable.service.ts index 662fc3f3..948ac0fc 100644 --- a/apps/api/src/variable/service/variable.service.ts +++ b/apps/api/src/variable/service/variable.service.ts @@ -48,6 +48,13 @@ export class VariableService { this.redis = redisClient.publisher } + /** + * Creates a new variable in a project + * @param user the user performing the action + * @param dto the variable to create + * @param projectSlug the slug of the project to create the variable in + * @returns the newly created variable + */ async createVariable( user: User, dto: CreateVariable, @@ -143,6 +150,13 @@ export class VariableService { return variable } + /** + * Updates a variable in a project + * @param user the user performing the action + * @param variableSlug the slug of the variable to update + * @param dto the data to update the variable with + * @returns the updated variable and its new versions + */ async updateVariable( user: User, variableSlug: Variable['slug'], @@ -291,6 +305,16 @@ export class VariableService { return result } + /** + * Rollback a variable to a specific version in a given environment. + * + * Throws a NotFoundException if the variable does not exist or if the version is invalid. + * @param user the user performing the action + * @param variableSlug the slug of the variable to rollback + * @param environmentSlug the slug of the environment to rollback in + * @param rollbackVersion the version to rollback to + * @returns the deleted variable versions + */ async rollbackVariable( user: User, variableSlug: Variable['slug'], @@ -384,6 +408,14 @@ export class VariableService { return result } + /** + * Deletes a variable from a project. + * @param user the user performing the action + * @param variableSlug the slug of the variable to delete + * @returns nothing + * @throws `NotFoundException` if the variable does not exist + * @throws `ForbiddenException` if the user does not have the required authority + */ async deleteVariable(user: User, variableSlug: Variable['slug']) { const variable = await this.authorityCheckerService.checkAuthorityOverVariable({ @@ -421,6 +453,15 @@ export class VariableService { this.logger.log(`User ${user.id} deleted variable ${variable.id}`) } + /** + * Gets all variables of a project and environment. + * @param user the user performing the action + * @param projectSlug the slug of the project to get the variables from + * @param environmentSlug the slug of the environment to get the variables from + * @returns an array of objects containing the name, value and whether the value is a plaintext + * @throws `NotFoundException` if the project or environment does not exist + * @throws `ForbiddenException` if the user does not have the required authority + */ async getAllVariablesOfProjectAndEnvironment( user: User, projectSlug: Project['slug'], @@ -482,6 +523,19 @@ export class VariableService { ) } + /** + * Gets all variables of a project, paginated, sorted and filtered by search query. + * @param user the user performing the action + * @param projectSlug the slug of the project to get the variables from + * @param page the page number to fetch + * @param limit the number of items per page + * @param sort the field to sort by + * @param order the order to sort in + * @param search the search query to filter by + * @returns a paginated list of variables with their latest versions for each environment + * @throws `NotFoundException` if the project does not exist + * @throws `ForbiddenException` if the user does not have the required authority + */ async getAllVariablesOfProject( user: User, projectSlug: Project['slug'], @@ -621,6 +675,20 @@ export class VariableService { return { items, metadata } } + /** + * Gets all revisions of a variable in a given environment. + * + * The response is paginated and sorted by the version in the given order. + * @param user the user performing the action + * @param variableSlug the slug of the variable + * @param environmentSlug the slug of the environment + * @param page the page number to fetch + * @param limit the number of items per page + * @param order the order to sort in + * @returns a paginated list of variable versions with metadata + * @throws `NotFoundException` if the variable or environment does not exist + * @throws `ForbiddenException` if the user does not have the required authority + */ async getRevisionsOfVariable( user: User, variableSlug: Variable['slug'], @@ -674,6 +742,14 @@ export class VariableService { return { items, metadata } } + /** + * Checks if a variable with a given name already exists in a project. + * Throws a ConflictException if the variable already exists. + * @param variableName the name of the variable to check for + * @param project the project to check in + * @returns nothing + * @throws `ConflictException` if the variable already exists + */ private async variableExists( variableName: Variable['name'], project: Project diff --git a/apps/api/src/workspace-role/service/workspace-role.service.ts b/apps/api/src/workspace-role/service/workspace-role.service.ts index fb8dd85a..2b5367b7 100644 --- a/apps/api/src/workspace-role/service/workspace-role.service.ts +++ b/apps/api/src/workspace-role/service/workspace-role.service.ts @@ -35,6 +35,15 @@ export class WorkspaceRoleService { private readonly authorityCheckerService: AuthorityCheckerService ) {} + /** + * Creates a new workspace role + * @throws {BadRequestException} if the role has workspace admin authority + * @throws {ConflictException} if a workspace role with the same name already exists + * @param user the user that is creating the workspace role + * @param workspaceSlug the slug of the workspace + * @param dto the data for the new workspace role + * @returns the newly created workspace role + */ async createWorkspaceRole( user: User, workspaceSlug: Workspace['slug'], @@ -138,6 +147,15 @@ export class WorkspaceRoleService { return workspaceRole } + /** + * Updates a workspace role + * @throws {BadRequestException} if the role has workspace admin authority + * @throws {ConflictException} if a workspace role with the same name already exists + * @param user the user that is updating the workspace role + * @param workspaceRoleSlug the slug of the workspace role to be updated + * @param dto the data for the updated workspace role + * @returns the updated workspace role + */ async updateWorkspaceRole( user: User, workspaceRoleSlug: WorkspaceRole['slug'], @@ -238,6 +256,12 @@ export class WorkspaceRoleService { return updatedWorkspaceRole } + /** + * Deletes a workspace role + * @throws {UnauthorizedException} if the role has administrative authority + * @param user the user that is deleting the workspace role + * @param workspaceRoleSlug the slug of the workspace role to be deleted + */ async deleteWorkspaceRole( user: User, workspaceRoleSlug: WorkspaceRole['slug'] @@ -281,6 +305,14 @@ export class WorkspaceRoleService { this.logger.log(`${user.email} deleted workspace role ${workspaceRoleSlug}`) } + /** + * Checks if a workspace role with the given name exists + * @throws {UnauthorizedException} if the user does not have the required authority + * @param user the user performing the check + * @param workspaceSlug the slug of the workspace + * @param name the name of the workspace role to check + * @returns true if a workspace role with the given name exists, false otherwise + */ async checkWorkspaceRoleExists( user: User, workspaceSlug: Workspace['slug'], @@ -305,6 +337,13 @@ export class WorkspaceRoleService { ) } + /** + * Gets a workspace role by its slug + * @throws {UnauthorizedException} if the user does not have the required authority + * @param user the user performing the request + * @param workspaceRoleSlug the slug of the workspace role to get + * @returns the workspace role with the given slug + */ async getWorkspaceRole( user: User, workspaceRoleSlug: WorkspaceRole['slug'] @@ -316,6 +355,18 @@ export class WorkspaceRoleService { ) } + /** + * Gets all workspace roles of a workspace, with pagination and optional filtering by name + * @throws {UnauthorizedException} if the user does not have the required authority + * @param user the user performing the request + * @param workspaceSlug the slug of the workspace + * @param page the page to get (0-indexed) + * @param limit the maximum number of items to return + * @param sort the field to sort the results by (e.g. "name", "slug", etc.) + * @param order the order to sort the results in (e.g. "asc", "desc") + * @param search an optional search string to filter the results by + * @returns a PaginatedMetadata object containing the items and metadata + */ async getWorkspaceRolesOfWorkspace( user: User, workspaceSlug: Workspace['slug'], @@ -373,6 +424,15 @@ export class WorkspaceRoleService { return { items, metadata } } + /** + * Gets a workspace role by its slug, with additional authorities check + * @throws {NotFoundException} if the workspace role does not exist + * @throws {UnauthorizedException} if the user does not have the required authority + * @param userId the user that is performing the request + * @param workspaceRoleSlug the slug of the workspace role to get + * @param authorities the authorities to check against + * @returns the workspace role with the given slug + */ private async getWorkspaceRoleWithAuthority( userId: User['id'], workspaceRoleSlug: Workspace['slug'], @@ -411,6 +471,13 @@ export class WorkspaceRoleService { return workspaceRole } + /** + * Given an array of project slugs, returns a Map of slug to id for all projects + * found in the database. + * + * @param projectSlugs the array of project slugs + * @returns a Map of project slug to id + */ private async getProjectSlugToIdMap(projectSlugs: string[]) { const projects = await this.prisma.project.findMany({ where: { diff --git a/apps/api/src/workspace/service/workspace.service.ts b/apps/api/src/workspace/service/workspace.service.ts index 168349a6..6821139b 100644 --- a/apps/api/src/workspace/service/workspace.service.ts +++ b/apps/api/src/workspace/service/workspace.service.ts @@ -50,6 +50,13 @@ export class WorkspaceService { private readonly authorityCheckerService: AuthorityCheckerService ) {} + /** + * Creates a new workspace for the given user. + * @throws ConflictException if the workspace with the same name already exists + * @param user The user to create the workspace for + * @param dto The data to create the workspace with + * @returns The created workspace + */ async createWorkspace(user: User, dto: CreateWorkspace) { if (await this.existsByName(dto.name, user.id)) { throw new ConflictException('Workspace already exists') @@ -58,6 +65,14 @@ export class WorkspaceService { return await createWorkspace(user, dto, this.prisma) } + /** + * Updates a workspace + * @throws ConflictException if the workspace with the same name already exists + * @param user The user to update the workspace for + * @param workspaceSlug The slug of the workspace to update + * @param dto The data to update the workspace with + * @returns The updated workspace + */ async updateWorkspace( user: User, workspaceSlug: Workspace['slug'], @@ -119,6 +134,16 @@ export class WorkspaceService { return updatedWorkspace } + /** + * Transfers ownership of a workspace to another user. + * @param user The user transferring the ownership + * @param workspaceSlug The slug of the workspace to transfer + * @param otherUserEmail The email of the user to transfer the ownership to + * @throws BadRequestException if the user is already the owner of the workspace, + * or if the workspace is the default workspace + * @throws NotFoundException if the other user is not a member of the workspace + * @throws InternalServerErrorException if there is an error in the transaction + */ async transferOwnership( user: User, workspaceSlug: Workspace['slug'], @@ -244,6 +269,12 @@ export class WorkspaceService { ) } + /** + * Deletes a workspace. + * @throws BadRequestException if the workspace is the default workspace + * @param user The user to delete the workspace for + * @param workspaceSlug The slug of the workspace to delete + */ async deleteWorkspace( user: User, workspaceSlug: Workspace['slug'] @@ -273,6 +304,15 @@ export class WorkspaceService { this.log.debug(`Deleted workspace ${workspace.name} (${workspace.slug})`) } + /** + * Invites users to a workspace. + * @param user The user to invite the users for + * @param workspaceSlug The slug of the workspace to invite users to + * @param members The members to invite + * @throws BadRequestException if the user does not have the authority to add users to the workspace + * @throws NotFoundException if the workspace or any of the users to invite do not exist + * @throws InternalServerErrorException if there is an error in the transaction + */ async inviteUsersToWorkspace( user: User, workspaceSlug: Workspace['slug'], @@ -319,6 +359,16 @@ export class WorkspaceService { ) } + /** + * Removes users from a workspace. + * @param user The user to remove users from the workspace for + * @param workspaceSlug The slug of the workspace to remove users from + * @param userEmails The emails of the users to remove from the workspace + * @throws BadRequestException if the user is trying to remove themselves from the workspace, + * or if the user is not a member of the workspace + * @throws NotFoundException if the workspace or any of the users to remove do not exist + * @throws InternalServerErrorException if there is an error in the transaction + */ async removeUsersFromWorkspace( user: User, workspaceSlug: Workspace['slug'], @@ -386,6 +436,16 @@ export class WorkspaceService { ) } + /** + * Updates the roles of a user in a workspace. + * + * @throws NotFoundException if the user is not a member of the workspace + * @throws BadRequestException if the admin role is tried to be assigned to the user + * @param user The user to update the roles for + * @param workspaceSlug The slug of the workspace to update the roles in + * @param otherUserEmail The email of the user to update the roles for + * @param roleSlugs The slugs of the roles to assign to the user + */ async updateMemberRoles( user: User, workspaceSlug: Workspace['slug'], @@ -492,6 +552,17 @@ export class WorkspaceService { ) } + /** + * Gets all members of a workspace, paginated. + * @param user The user to get the members for + * @param workspaceSlug The slug of the workspace to get the members from + * @param page The page number to get + * @param limit The number of items per page to get + * @param sort The field to sort by + * @param order The order to sort in + * @param search The search string to filter by + * @returns The members of the workspace, paginated, with metadata + */ async getAllMembersOfWorkspace( user: User, workspaceSlug: Workspace['slug'], @@ -595,6 +666,14 @@ export class WorkspaceService { return { items, metadata } } + /** + * Accepts an invitation to a workspace. + * @param user The user to accept the invitation for + * @param workspaceSlug The slug of the workspace to accept the invitation for + * @throws BadRequestException if the user does not have a pending invitation to the workspace + * @throws NotFoundException if the workspace does not exist + * @throws InternalServerErrorException if there is an error in the transaction + */ async acceptInvitation( user: User, workspaceSlug: Workspace['slug'] @@ -641,6 +720,15 @@ export class WorkspaceService { ) } + /** + * Cancels an invitation to a workspace. + * @param user The user cancelling the invitation + * @param workspaceSlug The slug of the workspace to cancel the invitation for + * @param inviteeEmail The email of the user to cancel the invitation for + * @throws BadRequestException if the user does not have a pending invitation to the workspace + * @throws NotFoundException if the workspace or the user to cancel the invitation for do not exist + * @throws InternalServerErrorException if there is an error in the transaction + */ async cancelInvitation( user: User, workspaceSlug: Workspace['slug'], @@ -683,6 +771,14 @@ export class WorkspaceService { ) } + /** + * Declines an invitation to a workspace. + * @param user The user declining the invitation + * @param workspaceSlug The slug of the workspace to decline the invitation for + * @throws BadRequestException if the user does not have a pending invitation to the workspace + * @throws NotFoundException if the workspace does not exist + * @throws InternalServerErrorException if there is an error in the transaction + */ async declineInvitation( user: User, workspaceSlug: Workspace['slug'] @@ -719,6 +815,12 @@ export class WorkspaceService { ) } + /** + * Leaves a workspace. + * @throws BadRequestException if the user is the owner of the workspace + * @param user The user to leave the workspace for + * @param workspaceSlug The slug of the workspace to leave + */ async leaveWorkspace( user: User, workspaceSlug: Workspace['slug'] @@ -771,6 +873,13 @@ export class WorkspaceService { ) } + /** + * Checks if a user is a member of a workspace. + * @param user The user to check if the other user is a member of the workspace for + * @param workspaceSlug The slug of the workspace to check if the user is a member of + * @param otherUserEmail The email of the user to check if is a member of the workspace + * @returns True if the user is a member of the workspace, false otherwise + */ async isUserMemberOfWorkspace( user: User, workspaceSlug: Workspace['slug'], @@ -789,6 +898,13 @@ export class WorkspaceService { return await this.memberExistsInWorkspace(workspace.id, otherUser.id) } + /** + * Gets a workspace by its slug. + * @param user The user to get the workspace for + * @param workspaceSlug The slug of the workspace to get + * @returns The workspace + * @throws NotFoundException if the workspace does not exist or the user does not have the authority to read the workspace + */ async getWorkspaceBySlug( user: User, workspaceSlug: Workspace['slug'] @@ -801,6 +917,16 @@ export class WorkspaceService { }) } + /** + * Gets all workspaces of a user, paginated. + * @param user The user to get the workspaces for + * @param page The page number to get + * @param limit The number of items per page to get + * @param sort The field to sort by + * @param order The order to sort in + * @param search The search string to filter by + * @returns The workspaces of the user, paginated, with metadata + */ async getWorkspacesOfUser( user: User, page: number, @@ -872,6 +998,14 @@ export class WorkspaceService { return { items, metadata } } + /** + * Exports all data of a workspace, including its roles, projects, environments, variables and secrets. + * @param user The user to export the data for + * @param workspaceSlug The slug of the workspace to export + * @returns The exported data + * @throws NotFoundException if the workspace does not exist or the user does not have the authority to read the workspace + * @throws InternalServerErrorException if there is an error in the transaction + */ async exportData(user: User, workspaceSlug: Workspace['slug']) { const workspace = await this.authorityCheckerService.checkAuthorityOverWorkspace({ @@ -955,6 +1089,15 @@ export class WorkspaceService { return data } + /** + * Searches for projects, environments, secrets and variables + * based on a search term. The search is scoped to the workspace + * and the user's permissions. + * @param user The user to search for + * @param workspaceSlug The slug of the workspace to search in + * @param searchTerm The search term to search for + * @returns An object with the search results + */ async globalSearch( user: User, workspaceSlug: Workspace['slug'], @@ -1001,6 +1144,14 @@ export class WorkspaceService { return { projects, environments, secrets, variables } } + /** + * Gets a list of project IDs that the user has access to READ. + * The user has access to a project if the project is global or if the user has the READ_PROJECT authority. + * @param userId The ID of the user to get the accessible project IDs for + * @param workspaceId The ID of the workspace to get the accessible project IDs for + * @returns The list of project IDs that the user has access to READ + * @private + */ private async getAccessibleProjectIds( userId: string, workspaceId: string @@ -1030,6 +1181,13 @@ export class WorkspaceService { return accessibleProjectIds } + /** + * Queries projects by IDs and search term. + * @param projectIds The IDs of projects to query + * @param searchTerm The search term to query by + * @returns The projects that match the search term + * @private + */ private async queryProjects( projectIds: string[], searchTerm: string @@ -1047,6 +1205,13 @@ export class WorkspaceService { }) } + /** + * Queries environments by IDs and search term. + * @param projectIds The IDs of projects to query + * @param searchTerm The search term to query by + * @returns The environments that match the search term + * @private + */ private async queryEnvironments( projectIds: string[], searchTerm: string @@ -1065,6 +1230,13 @@ export class WorkspaceService { }) } + /** + * Queries secrets by IDs and search term. + * @param projectIds The IDs of projects to query + * @param searchTerm The search term to query by + * @returns The secrets that match the search term + * @private + */ private async querySecrets( projectIds: string[], searchTerm: string @@ -1084,6 +1256,13 @@ export class WorkspaceService { }) } + /** + * Queries variables by IDs and search term. + * @param projectIds The IDs of projects to query + * @param searchTerm The search term to query by + * @returns The variables that match the search term + * @private + */ private async queryVariables( projectIds: string[], searchTerm: string @@ -1102,6 +1281,13 @@ export class WorkspaceService { }) } + /** + * Checks if a workspace with the given name exists for the given user. + * @param name The name of the workspace to check for + * @param userId The ID of the user to check for + * @returns True if the workspace exists, false otherwise + * @private + */ private async existsByName( name: string, userId: User['id'] @@ -1135,6 +1321,16 @@ export class WorkspaceService { return adminRole } + /** + * Adds members to a workspace. + * @param workspace The workspace to add members to + * @param currentUser The user performing the action + * @param members The members to add to the workspace + * @throws BadRequestException if the admin role is tried to be assigned to the user + * @throws ConflictException if the user is already a member of the workspace + * @throws InternalServerErrorException if there is an error in the transaction + * @private + */ private async addMembersToWorkspace( workspace: Workspace, currentUser: User, @@ -1256,6 +1452,13 @@ export class WorkspaceService { } } + /** + * Checks if a user is a member of a workspace. + * @param workspaceId The ID of the workspace to check + * @param userId The ID of the user to check + * @returns True if the user is a member of the workspace, false otherwise + * @private + */ private async memberExistsInWorkspace( workspaceId: string, userId: string @@ -1270,6 +1473,13 @@ export class WorkspaceService { ) } + /** + * Gets the workspace membership of a user in a workspace. + * @param workspaceId The ID of the workspace to get the membership for + * @param userId The ID of the user to get the membership for + * @returns The workspace membership of the user in the workspace + * @private + */ private async getWorkspaceMembership( workspaceId: Workspace['id'], userId: User['id'] @@ -1284,6 +1494,13 @@ export class WorkspaceService { }) } + /** + * Deletes the membership of a user in a workspace. + * @param workspaceId The ID of the workspace to delete the membership from + * @param userId The ID of the user to delete the membership for + * @returns A promise that resolves when the membership is deleted + * @private + */ private async deleteMembership( workspaceId: Workspace['id'], userId: User['id'] @@ -1298,6 +1515,12 @@ export class WorkspaceService { }) } + /** + * Checks if a user has a pending invitation to a workspace. + * @throws BadRequestException if the user is not invited to the workspace + * @param workspaceSlug The slug of the workspace to check if the user is invited to + * @param user The user to check if the user is invited to the workspace + */ private async checkInvitationPending( workspaceSlug: Workspace['slug'], user: User