From 2a715d534805eee4a907d1e078d6c21e6dd25dbe Mon Sep 17 00:00:00 2001 From: Balu Babu Date: Thu, 9 Mar 2023 19:37:40 +0530 Subject: [PATCH] refactor: refactoring Team-Collections with reordering in self-host (HBE-150) (#34) * chore: removed firebase module as a dependency from team-collection module * chore: modified team-collection resolver file to use input-args types * chore: modified getTeamOfCollection service method and resolver * chore: modified getParentOfCollection service method in team-collection module * chore: modified getChildrenOfCollection service method in team-collection module * chore: added new fields to TeamCollection model in prisma schema file * chore: modified getCollection service method and resolver in team-collection module * chore: modified createCollection service method and resolver in team-collection module * chore: created cast helper function to resolve issue with creation mutation in team-collection * chore: modified teamCollectionRemoved subscription return types * chore: removed return types from subscriptions in team-collection module * chore: removed all instances of getTeamCollections service method in team-collection module * feat: added mutation to handle moving collections and supporting subscriptions * feat: added mutation to re-ordering team-collection order * chore: added teacher comments to both collection modules * test: added test cases for getTeamOfCollection service method * test: added test cases for getParentOfCollection service method * test: added test cases for getChildrenOfCollection service method * test: added test cases for getTeamRootCollections service method * test: added test cases for getCollection service method * test: added test cases for createCollection service method * chore: renamed renameCollection to renameUserCollection in UserCollection module * test: added test cases for renameCollection service method * test: added test cases for deleteCollection service method * test: added test cases for moveCollection service method * test: added test cases for updateCollectionOrder service method * chore: added import and export to JSON mutations to team-collection module * chore: created replaceCollectionsWithJSON mutation in team-collection module * chore: moved the mutation and service method of importCollectionFromFirestore to the end of file * chore: added helper comments to all import,export functions * chore: exportCollectionsToJSON service method orders collections and requests in ascending order * chore: added test cases for importCollectionsFromJSON service method * chore: added ToDo to write test cases for exportCollectionsToJSON * chore: removed prisma migration folder * chore: completed all changes requested in inital PR review * chore: completed all changes requested in second PR review * chore: completed all changes requested in third PR review --- packages/hoppscotch-backend/cross-env | 0 packages/hoppscotch-backend/eslint | 0 .../hoppscotch-backend/prisma/schema.prisma | 22 +- .../src/auth/guards/multi.guard.ts | 5 + packages/hoppscotch-backend/src/errors.ts | 64 +- .../src/pubsub/topicsDefs.ts | 13 +- .../gql-collection-team-member.guard.ts | 6 +- .../src/team-collection/input-type.args.ts | 100 + .../team-collection/team-collection.model.ts | 21 +- .../team-collection/team-collection.module.ts | 9 +- .../team-collection.resolver.ts | 579 +-- .../team-collection.service.spec.ts | 3107 +++++++---------- .../team-collection.service.ts | 1369 +++++--- .../src/team-request/team-request.resolver.ts | 108 +- .../src/team-request/team-request.service.ts | 194 +- .../src/types/CollectionFolder.ts | 7 + .../user-collection.resolver.ts | 4 +- .../user-collection.service.spec.ts | 12 +- .../user-collection.service.ts | 153 +- 19 files changed, 3079 insertions(+), 2694 deletions(-) create mode 100644 packages/hoppscotch-backend/cross-env create mode 100644 packages/hoppscotch-backend/eslint create mode 100644 packages/hoppscotch-backend/src/auth/guards/multi.guard.ts create mode 100644 packages/hoppscotch-backend/src/team-collection/input-type.args.ts create mode 100644 packages/hoppscotch-backend/src/types/CollectionFolder.ts diff --git a/packages/hoppscotch-backend/cross-env b/packages/hoppscotch-backend/cross-env new file mode 100644 index 000000000..e69de29bb diff --git a/packages/hoppscotch-backend/eslint b/packages/hoppscotch-backend/eslint new file mode 100644 index 000000000..e69de29bb diff --git a/packages/hoppscotch-backend/prisma/schema.prisma b/packages/hoppscotch-backend/prisma/schema.prisma index edbcc8512..c88c7f16f 100644 --- a/packages/hoppscotch-backend/prisma/schema.prisma +++ b/packages/hoppscotch-backend/prisma/schema.prisma @@ -41,14 +41,17 @@ model TeamInvitation { } model TeamCollection { - id String @id @default(cuid()) - parentID String? - parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id]) - children TeamCollection[] @relation("TeamCollectionChildParent") - requests TeamRequest[] - teamID String - team Team @relation(fields: [teamID], references: [id], onDelete: Cascade) - title String + id String @id @default(cuid()) + parentID String? + parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id]) + children TeamCollection[] @relation("TeamCollectionChildParent") + requests TeamRequest[] + teamID String + team Team @relation(fields: [teamID], references: [id], onDelete: Cascade) + title String + orderIndex Int + createdOn DateTime @default(now()) @db.Timestamp(3) + updatedOn DateTime @updatedAt @db.Timestamp(3) } model TeamRequest { @@ -59,6 +62,9 @@ model TeamRequest { team Team @relation(fields: [teamID], references: [id], onDelete: Cascade) title String request Json + orderIndex Int + createdOn DateTime @default(now()) @db.Timestamp(3) + updatedOn DateTime @updatedAt @db.Timestamp(3) } model Shortcode { diff --git a/packages/hoppscotch-backend/src/auth/guards/multi.guard.ts b/packages/hoppscotch-backend/src/auth/guards/multi.guard.ts new file mode 100644 index 000000000..42fe8c1d7 --- /dev/null +++ b/packages/hoppscotch-backend/src/auth/guards/multi.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class MultiAuthGuard extends AuthGuard(['google1', 'google2']) {} diff --git a/packages/hoppscotch-backend/src/errors.ts b/packages/hoppscotch-backend/src/errors.ts index e5802059e..014452ac1 100644 --- a/packages/hoppscotch-backend/src/errors.ts +++ b/packages/hoppscotch-backend/src/errors.ts @@ -54,7 +54,8 @@ export const USER_COLLECTION_NOT_FOUND = 'user_collection/not_found' as const; * Tried to reorder user request but failed * (UserRequestService) */ -export const USER_REQUEST_CREATION_FAILED = 'user_request/creation_failed' as const; +export const USER_REQUEST_CREATION_FAILED = + 'user_request/creation_failed' as const; /** * Tried to do an action on a user request but user request is not matched with user collection @@ -72,7 +73,8 @@ export const USER_REQUEST_NOT_FOUND = 'user_request/not_found' as const; * Tried to reorder user request but failed * (UserRequestService) */ -export const USER_REQUEST_REORDERING_FAILED = 'user_request/reordering_failed' as const; +export const USER_REQUEST_REORDERING_FAILED = + 'user_request/reordering_failed' as const; /** * Tried to perform action on a team which they are not a member of @@ -104,6 +106,58 @@ export const TEAM_USER_NO_FB_SYNCDATA = 'team/user_no_fb_syncdata'; */ export const TEAM_FB_COLL_PATH_RESOLVE_FAIL = 'team/fb_coll_path_resolve_fail'; +/** + * Could not find the team in the database + * (TeamCollectionService) + */ +export const TEAM_COLL_NOT_FOUND = 'team_coll/collection_not_found'; + +/** + * Cannot make parent collection a child of a collection that a child of itself + * (TeamCollectionService) + */ +export const TEAM_COLL_IS_PARENT_COLL = 'team_coll/collection_is_parent_coll'; + +/** + * Target and Parent collections are not from the same team + * (TeamCollectionService) + */ +export const TEAM_COLL_NOT_SAME_TEAM = 'team_coll/collections_not_same_team'; + +/** + * Target and Parent collections are the same + * (TeamCollectionService) + */ +export const TEAM_COLL_DEST_SAME = + 'team_coll/target_and_destination_collection_are_same'; + +/** + * Collection is already a root collection + * (TeamCollectionService) + */ +export const TEAM_COL_ALREADY_ROOT = + 'team_coll/target_collection_is_already_root_collection'; + +/** + * Collections have different parents + * (TeamCollectionService) + */ +export const TEAM_COL_NOT_SAME_PARENT = + 'team_coll/team_collections_have_different_parents'; + +/** + * Collection and next Collection are the same + * (TeamCollectionService) + */ +export const TEAM_COL_SAME_NEXT_COLL = + 'team_coll/collection_and_next_collection_are_same'; + +/** + * Team Collection Re-Ordering Failed + * (TeamCollectionService) + */ +export const TEAM_COL_REORDERING_FAILED = 'team_coll/reordering_failed'; + /** * Tried to update the team to a state it doesn't have any owners * (TeamService) @@ -140,6 +194,12 @@ export const TEAM_COLL_SHORT_TITLE = 'team_coll/short_title'; */ export const TEAM_COLL_INVALID_JSON = 'team_coll/invalid_json'; +/** + * The Team Collection does not belong to the team + * (TeamCollectionService) + */ +export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const; + /** * Tried to perform action on a request that doesn't accept their member role level * (GqlRequestTeamMemberGuard) diff --git a/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts b/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts index 44a38467a..957a2b831 100644 --- a/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts +++ b/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts @@ -5,7 +5,10 @@ import { UserEnvironment } from '../user-environment/user-environments.model'; import { UserHistory } from '../user-history/user-history.model'; import { TeamMember } from 'src/team/team.model'; import { TeamEnvironment } from 'src/team-environments/team-environments.model'; -import { TeamCollection } from 'src/team-collection/team-collection.model'; +import { + CollectionReorderData, + TeamCollection, +} from 'src/team-collection/team-collection.model'; import { TeamRequest } from 'src/team-request/team-request.model'; import { TeamInvitation } from 'src/team-invitation/team-invitation.model'; import { UserCollection } from '@prisma/client'; @@ -42,11 +45,11 @@ export type TopicDef = { topic: `team_environment/${string}/${'created' | 'updated' | 'deleted'}` ]: TeamEnvironment; [ - topic: `team_coll/${string}/${ - | 'coll_added' - | 'coll_updated' - | 'coll_removed'}` + topic: `team_coll/${string}/${'coll_added' | 'coll_updated'}` ]: TeamCollection; + [topic: `team_coll/${string}/${'coll_removed'}`]: string; + [topic: `team_coll/${string}/${'coll_moved'}`]: TeamCollection; + [topic: `team_coll/${string}/${'coll_order_updated'}`]: CollectionReorderData; [topic: `user_history/${string}/deleted_many`]: number; [topic: `team_req/${string}/${'req_created' | 'req_updated'}`]: TeamRequest; [topic: `team_req/${string}/req_deleted`]: string; diff --git a/packages/hoppscotch-backend/src/team-collection/guards/gql-collection-team-member.guard.ts b/packages/hoppscotch-backend/src/team-collection/guards/gql-collection-team-member.guard.ts index 1d8dded6d..e70e5892f 100644 --- a/packages/hoppscotch-backend/src/team-collection/guards/gql-collection-team-member.guard.ts +++ b/packages/hoppscotch-backend/src/team-collection/guards/gql-collection-team-member.guard.ts @@ -12,6 +12,7 @@ import { TEAM_INVALID_COLL_ID, TEAM_REQ_NOT_MEMBER, } from 'src/errors'; +import * as E from 'fp-ts/Either'; @Injectable() export class GqlCollectionTeamMemberGuard implements CanActivate { @@ -29,6 +30,7 @@ export class GqlCollectionTeamMemberGuard implements CanActivate { if (!requireRoles) throw new Error(BUG_TEAM_NO_REQUIRE_TEAM_ROLE); const gqlExecCtx = GqlExecutionContext.create(context); + const { user } = gqlExecCtx.getContext().req; if (user == undefined) throw new Error(BUG_AUTH_NO_USER_CTX); @@ -38,10 +40,10 @@ export class GqlCollectionTeamMemberGuard implements CanActivate { const collection = await this.teamCollectionService.getCollection( collectionID, ); - if (!collection) throw new Error(TEAM_INVALID_COLL_ID); + if (E.isLeft(collection)) throw new Error(TEAM_INVALID_COLL_ID); const member = await this.teamService.getTeamMember( - collection.teamID, + collection.right.teamID, user.uid, ); if (!member) throw new Error(TEAM_REQ_NOT_MEMBER); diff --git a/packages/hoppscotch-backend/src/team-collection/input-type.args.ts b/packages/hoppscotch-backend/src/team-collection/input-type.args.ts new file mode 100644 index 000000000..1dbd7c91f --- /dev/null +++ b/packages/hoppscotch-backend/src/team-collection/input-type.args.ts @@ -0,0 +1,100 @@ +import { ArgsType, Field, ID } from '@nestjs/graphql'; +import { PaginationArgs } from 'src/types/input-types.args'; + +@ArgsType() +export class GetRootTeamCollectionsArgs extends PaginationArgs { + @Field(() => ID, { name: 'teamID', description: 'ID of the team' }) + teamID: string; +} + +@ArgsType() +export class CreateRootTeamCollectionArgs { + @Field(() => ID, { name: 'teamID', description: 'ID of the team' }) + teamID: string; + + @Field({ name: 'title', description: 'Title of the new collection' }) + title: string; +} + +@ArgsType() +export class CreateChildTeamCollectionArgs { + @Field(() => ID, { + name: 'collectionID', + description: 'ID of the parent to the new collection', + }) + collectionID: string; + + @Field({ name: 'childTitle', description: 'Title of the new collection' }) + childTitle: string; +} + +@ArgsType() +export class RenameTeamCollectionArgs { + @Field(() => ID, { + name: 'collectionID', + description: 'ID of the collection', + }) + collectionID: string; + + @Field({ + name: 'newTitle', + description: 'The updated title of the collection', + }) + newTitle: string; +} + +@ArgsType() +export class MoveTeamCollectionArgs { + @Field(() => ID, { + name: 'parentCollectionID', + description: 'ID of the parent to the new collection', + nullable: true, + }) + parentCollectionID: string; + + @Field(() => ID, { + name: 'collectionID', + description: 'ID of the collection', + }) + collectionID: string; +} + +@ArgsType() +export class UpdateTeamCollectionOrderArgs { + @Field(() => ID, { + name: 'collectionID', + description: 'ID of the collection', + }) + collectionID: string; + + @Field(() => ID, { + name: 'destCollID', + description: + 'ID of the collection that comes after the updated collection in its new position', + nullable: true, + }) + destCollID: string; +} + +@ArgsType() +export class ReplaceTeamCollectionArgs { + @Field(() => ID, { + name: 'teamID', + description: 'Id of the team to add to', + }) + teamID: string; + + @Field({ + name: 'jsonString', + description: 'JSON string to replace with', + }) + jsonString: string; + + @Field(() => ID, { + name: 'parentCollectionID', + description: + 'ID to the collection to which to import to (null if to import to the root of team)', + nullable: true, + }) + parentCollectionID?: string; +} diff --git a/packages/hoppscotch-backend/src/team-collection/team-collection.model.ts b/packages/hoppscotch-backend/src/team-collection/team-collection.model.ts index 32b1b07cb..f45784327 100644 --- a/packages/hoppscotch-backend/src/team-collection/team-collection.model.ts +++ b/packages/hoppscotch-backend/src/team-collection/team-collection.model.ts @@ -12,6 +12,25 @@ export class TeamCollection { }) title: string; - parentID: string | null; + @Field(() => ID, { + description: 'ID of the collection', + nullable: true, + }) + parentID: string; teamID: string; } + +@ObjectType() +export class CollectionReorderData { + @Field({ + description: 'Team Collection being moved', + }) + collection: TeamCollection; + + @Field({ + description: + 'Team Collection succeeding the collection being moved in its new position', + nullable: true, + }) + nextCollection?: TeamCollection; +} diff --git a/packages/hoppscotch-backend/src/team-collection/team-collection.module.ts b/packages/hoppscotch-backend/src/team-collection/team-collection.module.ts index dda0236da..cafdd5448 100644 --- a/packages/hoppscotch-backend/src/team-collection/team-collection.module.ts +++ b/packages/hoppscotch-backend/src/team-collection/team-collection.module.ts @@ -5,17 +5,10 @@ import { TeamCollectionResolver } from './team-collection.resolver'; import { GqlCollectionTeamMemberGuard } from './guards/gql-collection-team-member.guard'; import { TeamModule } from '../team/team.module'; import { UserModule } from '../user/user.module'; -// import { FirebaseModule } from '../firebase/firebase.module'; import { PubSubModule } from '../pubsub/pubsub.module'; @Module({ - imports: [ - PrismaModule, - // FirebaseModule, - TeamModule, - UserModule, - PubSubModule, - ], + imports: [PrismaModule, TeamModule, UserModule, PubSubModule], providers: [ TeamCollectionService, TeamCollectionResolver, diff --git a/packages/hoppscotch-backend/src/team-collection/team-collection.resolver.ts b/packages/hoppscotch-backend/src/team-collection/team-collection.resolver.ts index 6475668e3..ca729110c 100644 --- a/packages/hoppscotch-backend/src/team-collection/team-collection.resolver.ts +++ b/packages/hoppscotch-backend/src/team-collection/team-collection.resolver.ts @@ -8,7 +8,7 @@ import { Subscription, ID, } from '@nestjs/graphql'; -import { TeamCollection } from './team-collection.model'; +import { CollectionReorderData, TeamCollection } from './team-collection.model'; import { Team, TeamMemberRole } from '../team/team.model'; import { TeamCollectionService } from './team-collection.service'; import { GqlAuthGuard } from '../guards/gql-auth.guard'; @@ -17,6 +17,18 @@ import { UseGuards } from '@nestjs/common'; import { RequiresTeamRole } from '../team/decorators/requires-team-role.decorator'; import { GqlCollectionTeamMemberGuard } from './guards/gql-collection-team-member.guard'; import { PubSubService } from 'src/pubsub/pubsub.service'; +import { PaginationArgs } from 'src/types/input-types.args'; +import { + CreateChildTeamCollectionArgs, + CreateRootTeamCollectionArgs, + GetRootTeamCollectionsArgs, + MoveTeamCollectionArgs, + RenameTeamCollectionArgs, + ReplaceTeamCollectionArgs, + UpdateTeamCollectionOrderArgs, +} from './input-type.args'; +import * as E from 'fp-ts/Either'; +import { throwErr } from 'src/utils'; @Resolver(() => TeamCollection) export class TeamCollectionResolver { @@ -30,58 +42,43 @@ export class TeamCollectionResolver { description: 'Team the collection belongs to', complexity: 5, }) - team(@Parent() collection: TeamCollection): Promise { - return this.teamCollectionService.getTeamOfCollection(collection.id); + async team(@Parent() collection: TeamCollection) { + const team = await this.teamCollectionService.getTeamOfCollection( + collection.id, + ); + if (E.isLeft(team)) throwErr(team.left); + return team.right; } @ResolveField(() => TeamCollection, { - description: - 'The collection who is the parent of this collection (null if this is root collection)', + description: 'Return the parent Team Collection (null if root )', nullable: true, complexity: 3, }) - parent(@Parent() collection: TeamCollection): Promise { + async parent(@Parent() collection: TeamCollection) { return this.teamCollectionService.getParentOfCollection(collection.id); } @ResolveField(() => [TeamCollection], { - description: 'List of children collection', + description: 'List of children Team Collections', complexity: 3, }) - children( + async children( @Parent() collection: TeamCollection, - @Args({ - name: 'cursor', - nullable: true, - description: 'ID of the last returned collection (for pagination)', - }) - cursor?: string, - ): Promise { + @Args() args: PaginationArgs, + ) { return this.teamCollectionService.getChildrenOfCollection( collection.id, - cursor ?? null, + args.cursor, + args.take, ); } // Queries - // @Query(() => String, { - // description: - // 'Returns the JSON string giving the collections and their contents of the team', - // }) - // @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) - // @RequiresTeamRole( - // TeamMemberRole.VIEWER, - // TeamMemberRole.EDITOR, - // TeamMemberRole.OWNER, - // ) - // exportCollectionsToJSON( - // @Args({ name: 'teamID', description: 'ID of the team', type: () => ID }) teamID: string, - // ): Promise { - // return this.teamCollectionService.exportCollectionsToJSON(teamID); - // } - @Query(() => [TeamCollection], { - description: 'Returns the collections of the team', + @Query(() => String, { + description: + 'Returns the JSON string giving the collections and their contents of the team', }) @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) @RequiresTeamRole( @@ -89,27 +86,20 @@ export class TeamCollectionResolver { TeamMemberRole.EDITOR, TeamMemberRole.OWNER, ) - rootCollectionsOfTeam( + async exportCollectionsToJSON( @Args({ name: 'teamID', description: 'ID of the team', type: () => ID }) teamID: string, - @Args({ - name: 'cursor', - nullable: true, - type: () => ID, - description: 'ID of the last returned collection (for pagination)', - }) - cursor?: string, - ): Promise { - return this.teamCollectionService.getTeamRootCollections( + ) { + const jsonString = await this.teamCollectionService.exportCollectionsToJSON( teamID, - cursor ?? null, ); + + if (E.isLeft(jsonString)) throwErr(jsonString.left as string); + return jsonString.right; } @Query(() => [TeamCollection], { - description: 'Returns the collections of the team', - deprecationReason: - 'Deprecated because of no practical use. Use `rootCollectionsOfTeam` instead.', + description: 'Returns the collections of a team', }) @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) @RequiresTeamRole( @@ -117,25 +107,16 @@ export class TeamCollectionResolver { TeamMemberRole.EDITOR, TeamMemberRole.OWNER, ) - collectionsOfTeam( - @Args({ name: 'teamID', description: 'ID of the team', type: () => ID }) - teamID: string, - @Args({ - name: 'cursor', - type: () => ID, - nullable: true, - description: 'ID of the last returned collection (for pagination)', - }) - cursor?: string, - ): Promise { - return this.teamCollectionService.getTeamCollections( - teamID, - cursor ?? null, + async rootCollectionsOfTeam(@Args() args: GetRootTeamCollectionsArgs) { + return this.teamCollectionService.getTeamRootCollections( + args.teamID, + args.cursor, + args.take, ); } @Query(() => TeamCollection, { - description: 'Get a collection with the given ID or null (if not exists)', + description: 'Get a Team Collection with ID or null (if not exists)', nullable: true, }) @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) @@ -144,15 +125,20 @@ export class TeamCollectionResolver { TeamMemberRole.EDITOR, TeamMemberRole.OWNER, ) - collection( + async collection( @Args({ name: 'collectionID', description: 'ID of the collection', type: () => ID, }) collectionID: string, - ): Promise { - return this.teamCollectionService.getCollection(collectionID); + ) { + const teamCollections = await this.teamCollectionService.getCollection( + collectionID, + ); + + if (E.isLeft(teamCollections)) throwErr(teamCollections.left); + return teamCollections.right; } // Mutations @@ -162,13 +148,264 @@ export class TeamCollectionResolver { }) @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) - createRootCollection( - @Args({ name: 'teamID', description: 'ID of the team', type: () => ID }) + async createRootCollection(@Args() args: CreateRootTeamCollectionArgs) { + const teamCollection = await this.teamCollectionService.createCollection( + args.teamID, + args.title, + null, + ); + + if (E.isLeft(teamCollection)) throwErr(teamCollection.left); + return teamCollection.right; + } + + @Mutation(() => Boolean, { + description: 'Import collections from JSON string to the specified Team', + }) + @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) + @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) + async importCollectionsFromJSON( + @Args({ + name: 'teamID', + type: () => ID, + description: 'Id of the team to add to', + }) teamID: string, - @Args({ name: 'title', description: 'Title of the new collection' }) - title: string, - ): Promise { - return this.teamCollectionService.createCollection(teamID, title, null); + @Args({ + name: 'jsonString', + description: 'JSON string to import', + }) + jsonString: string, + @Args({ + name: 'parentCollectionID', + type: () => ID, + description: + 'ID to the collection to which to import to (null if to import to the root of team)', + nullable: true, + }) + parentCollectionID?: string, + ): Promise { + const importedCollection = + await this.teamCollectionService.importCollectionsFromJSON( + jsonString, + teamID, + parentCollectionID ?? null, + ); + if (E.isLeft(importedCollection)) throwErr(importedCollection.left); + return importedCollection.right; + } + + @Mutation(() => Boolean, { + description: + 'Replace existing collections of a specific team with collections in JSON string', + }) + @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) + @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) + async replaceCollectionsWithJSON(@Args() args: ReplaceTeamCollectionArgs) { + const teamCollection = + await this.teamCollectionService.replaceCollectionsWithJSON( + args.jsonString, + args.teamID, + args.parentCollectionID ?? null, + ); + + if (E.isLeft(teamCollection)) throwErr(teamCollection.left); + return teamCollection.right; + } + + @Mutation(() => TeamCollection, { + description: 'Create a collection that has a parent collection', + }) + @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) + @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) + async createChildCollection(@Args() args: CreateChildTeamCollectionArgs) { + const team = await this.teamCollectionService.getTeamOfCollection( + args.collectionID, + ); + if (E.isLeft(team)) throwErr(team.left); + + const teamCollection = await this.teamCollectionService.createCollection( + team.right.id, + args.childTitle, + args.collectionID, + ); + + if (E.isLeft(teamCollection)) throwErr(teamCollection.left); + return teamCollection.right; + } + + @Mutation(() => TeamCollection, { + description: 'Rename a collection', + }) + @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) + @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) + async renameCollection(@Args() args: RenameTeamCollectionArgs) { + const updatedTeamCollection = + await this.teamCollectionService.renameCollection( + args.collectionID, + args.newTitle, + ); + + if (E.isLeft(updatedTeamCollection)) throwErr(updatedTeamCollection.left); + return updatedTeamCollection.right; + } + + @Mutation(() => Boolean, { + description: 'Delete a collection', + }) + @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) + @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) + async deleteCollection( + @Args({ + name: 'collectionID', + description: 'ID of the collection', + type: () => ID, + }) + collectionID: string, + ) { + const result = await this.teamCollectionService.deleteCollection( + collectionID, + ); + + if (E.isLeft(result)) throwErr(result.left); + return result.right; + } + + @Mutation(() => TeamCollection, { + description: + 'Move a collection into a new parent collection or the root of the team', + }) + @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) + @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) + async moveCollection(@Args() args: MoveTeamCollectionArgs) { + const res = await this.teamCollectionService.moveCollection( + args.collectionID, + args.parentCollectionID, + ); + if (E.isLeft(res)) throwErr(res.left); + return res.right; + } + + @Mutation(() => Boolean, { + description: 'Update the order of collections', + }) + @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) + @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) + async updateCollectionOrder(@Args() args: UpdateTeamCollectionOrderArgs) { + const request = await this.teamCollectionService.updateCollectionOrder( + args.collectionID, + args.destCollID, + ); + if (E.isLeft(request)) throwErr(request.left); + return request.right; + } + + // Subscriptions + + @Subscription(() => TeamCollection, { + description: + 'Listen to when a collection has been added to a team. The emitted value is the team added', + resolve: (value) => value, + }) + @RequiresTeamRole( + TeamMemberRole.OWNER, + TeamMemberRole.EDITOR, + TeamMemberRole.VIEWER, + ) + @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) + teamCollectionAdded( + @Args({ + name: 'teamID', + description: 'ID of the team to listen to', + type: () => ID, + }) + teamID: string, + ) { + return this.pubsub.asyncIterator(`team_coll/${teamID}/coll_added`); + } + + @Subscription(() => TeamCollection, { + description: 'Listen to when a collection has been updated.', + resolve: (value) => value, + }) + @RequiresTeamRole( + TeamMemberRole.OWNER, + TeamMemberRole.EDITOR, + TeamMemberRole.VIEWER, + ) + @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) + teamCollectionUpdated( + @Args({ + name: 'teamID', + description: 'ID of the team to listen to', + type: () => ID, + }) + teamID: string, + ) { + return this.pubsub.asyncIterator(`team_coll/${teamID}/coll_updated`); + } + + @Subscription(() => ID, { + description: 'Listen to when a collection has been removed', + resolve: (value) => value, + }) + @RequiresTeamRole( + TeamMemberRole.OWNER, + TeamMemberRole.EDITOR, + TeamMemberRole.VIEWER, + ) + @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) + teamCollectionRemoved( + @Args({ + name: 'teamID', + description: 'ID of the team to listen to', + type: () => ID, + }) + teamID: string, + ) { + return this.pubsub.asyncIterator(`team_coll/${teamID}/coll_removed`); + } + + @Subscription(() => TeamCollection, { + description: 'Listen to when a collection has been moved', + resolve: (value) => value, + }) + @RequiresTeamRole( + TeamMemberRole.OWNER, + TeamMemberRole.EDITOR, + TeamMemberRole.VIEWER, + ) + @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) + teamCollectionMoved( + @Args({ + name: 'teamID', + description: 'ID of the team to listen to', + type: () => ID, + }) + teamID: string, + ) { + return this.pubsub.asyncIterator(`team_coll/${teamID}/coll_moved`); + } + + @Subscription(() => CollectionReorderData, { + description: 'Listen to when a collections position has changed', + resolve: (value) => value, + }) + @RequiresTeamRole( + TeamMemberRole.OWNER, + TeamMemberRole.EDITOR, + TeamMemberRole.VIEWER, + ) + @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) + collectionOrderUpdated( + @Args({ + name: 'teamID', + description: 'ID of the team to listen to', + type: () => ID, + }) + teamID: string, + ) { + return this.pubsub.asyncIterator(`team_coll/${teamID}/coll_order_updated`); } // @Mutation(() => TeamCollection, { @@ -214,204 +451,4 @@ export class TeamCollectionResolver { // ); // } // } - - // @Mutation(() => Boolean, { - // description: 'Import collections from JSON string to the specified Team', - // }) - // @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) - // @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) - // async importCollectionsFromJSON( - // @Args({ - // name: 'teamID', - // type: () => ID, - // description: 'Id of the team to add to', - // }) - // teamID: string, - // @Args({ - // name: 'jsonString', - // description: 'JSON string to import', - // }) - // jsonString: string, - // @Args({ - // name: 'parentCollectionID', - // type: () => ID, - // description: - // 'ID to the collection to which to import to (null if to import to the root of team)', - // nullable: true, - // }) - // parentCollectionID?: string, - // ): Promise { - // await this.teamCollectionService.importCollectionsFromJSON( - // jsonString, - // teamID, - // parentCollectionID ?? null, - // ); - - // return true; - // } - - // @Mutation(() => Boolean, { - // description: - // 'Replace existing collections of a specific team with collections in JSON string', - // }) - // @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) - // @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) - // async replaceCollectionsWithJSON( - // @Args({ - // name: 'teamID', - // type: () => ID, - // description: 'Id of the team to add to', - // }) - // teamID: string, - // @Args({ - // name: 'jsonString', - // description: 'JSON string to replace with', - // }) - // jsonString: string, - // @Args({ - // name: 'parentCollectionID', - // type: () => ID, - // description: - // 'ID to the collection to which to import to (null if to import to the root of team)', - // nullable: true, - // }) - // parentCollectionID?: string, - // ): Promise { - // await this.teamCollectionService.replaceCollectionsWithJSON( - // jsonString, - // teamID, - // parentCollectionID ?? null, - // ); - - // return true; - // } - - @Mutation(() => TeamCollection, { - description: 'Create a collection that has a parent collection', - }) - @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) - @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) - async createChildCollection( - @Args({ - name: 'collectionID', - type: () => ID, - description: 'ID of the parent to the new collection', - }) - collectionID: string, - @Args({ name: 'childTitle', description: 'Title of the new collection' }) - childTitle: string, - ): Promise { - const team = await this.teamCollectionService.getTeamOfCollection( - collectionID, - ); - return await this.teamCollectionService.createCollection( - team.id, - childTitle, - collectionID, - ); - } - - @Mutation(() => TeamCollection, { - description: 'Rename a collection', - }) - @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) - @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) - renameCollection( - @Args({ - name: 'collectionID', - description: 'ID of the collection', - type: () => ID, - }) - collectionID: string, - @Args({ - name: 'newTitle', - description: 'The updated title of the collection', - }) - newTitle: string, - ): Promise { - return this.teamCollectionService.renameCollection(collectionID, newTitle); - } - - @Mutation(() => Boolean, { - description: 'Delete a collection', - }) - @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) - @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) - async deleteCollection( - @Args({ - name: 'collectionID', - description: 'ID of the collection', - type: () => ID, - }) - collectionID: string, - ): Promise { - this.teamCollectionService.deleteCollection(collectionID); - - return true; - } - - // Subscriptions - @Subscription(() => TeamCollection, { - description: - 'Listen to when a collection has been added to a team. The emitted value is the team added', - resolve: (value) => value, - }) - @RequiresTeamRole( - TeamMemberRole.OWNER, - TeamMemberRole.EDITOR, - TeamMemberRole.VIEWER, - ) - @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) - teamCollectionAdded( - @Args({ - name: 'teamID', - description: 'ID of the team to listen to', - type: () => ID, - }) - teamID: string, - ): AsyncIterator { - return this.pubsub.asyncIterator(`team_coll/${teamID}/coll_added`); - } - - @Subscription(() => TeamCollection, { - description: 'Listen to when a collection has been updated.', - resolve: (value) => value, - }) - @RequiresTeamRole( - TeamMemberRole.OWNER, - TeamMemberRole.EDITOR, - TeamMemberRole.VIEWER, - ) - @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) - teamCollectionUpdated( - @Args({ - name: 'teamID', - description: 'ID of the team to listen to', - type: () => ID, - }) - teamID: string, - ): AsyncIterator { - return this.pubsub.asyncIterator(`team_coll/${teamID}/coll_updated`); - } - - @Subscription(() => ID, { - description: 'Listen to when a collection has been removed', - resolve: (value) => value, - }) - @RequiresTeamRole( - TeamMemberRole.OWNER, - TeamMemberRole.EDITOR, - TeamMemberRole.VIEWER, - ) - @UseGuards(GqlAuthGuard, GqlTeamMemberGuard) - teamCollectionRemoved( - @Args({ - name: 'teamID', - description: 'ID of the team to listen to', - type: () => ID, - }) - teamID: string, - ): AsyncIterator { - return this.pubsub.asyncIterator(`team_coll/${teamID}/coll_removed`); - } } diff --git a/packages/hoppscotch-backend/src/team-collection/team-collection.service.spec.ts b/packages/hoppscotch-backend/src/team-collection/team-collection.service.spec.ts index ad0592aed..07d53665a 100644 --- a/packages/hoppscotch-backend/src/team-collection/team-collection.service.spec.ts +++ b/packages/hoppscotch-backend/src/team-collection/team-collection.service.spec.ts @@ -1,1934 +1,1431 @@ -import { TeamCollectionService } from './team-collection.service'; -import { PrismaService } from '../prisma/prisma.service'; -import { TeamCollection } from './team-collection.model'; +import { Team, TeamCollection as DBTeamCollection } from '@prisma/client'; +import { mock, mockDeep, mockReset } from 'jest-mock-extended'; import { - TEAM_USER_NO_FB_SYNCDATA, - TEAM_FB_COLL_PATH_RESOLVE_FAIL, + TEAM_COLL_DEST_SAME, TEAM_COLL_INVALID_JSON, + TEAM_COLL_IS_PARENT_COLL, + TEAM_COLL_NOT_FOUND, + TEAM_COLL_NOT_SAME_TEAM, + TEAM_COLL_SHORT_TITLE, + TEAM_COL_ALREADY_ROOT, + TEAM_COL_REORDERING_FAILED, + TEAM_COL_SAME_NEXT_COLL, TEAM_INVALID_COLL_ID, -} from '../errors'; -import { mockDeep, mockReset } from 'jest-mock-extended'; + TEAM_NOT_OWNER, +} from 'src/errors'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { PubSubService } from 'src/pubsub/pubsub.service'; +import { AuthUser } from 'src/types/AuthUser'; +import { TeamCollectionService } from './team-collection.service'; +import { TeamCollection } from './team-collection.model'; +import { TeamCollectionModule } from './team-collection.module'; +import * as E from 'fp-ts/Either'; const mockPrisma = mockDeep(); +const mockPubSub = mockDeep(); -const mockDocFunc = jest.fn(); - -const mockFB = { - firestore: { - doc: mockDocFunc, - }, -}; - -const mockPubSub = { - publish: jest.fn().mockResolvedValue(null), -}; - -// eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const teamCollectionService = new TeamCollectionService( mockPrisma, mockPubSub as any, ); +const currentTime = new Date(); + +const user: AuthUser = { + uid: '123344', + email: 'dwight@dundermifflin.com', + displayName: 'Dwight Schrute', + photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute', + isAdmin: false, + refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb', + createdOn: currentTime, + currentGQLSession: {}, + currentRESTSession: {}, +}; + +const team: Team = { + id: 'team_1', + name: 'Team 1', +}; + +const rootTeamCollection: DBTeamCollection = { + id: '123', + orderIndex: 1, + parentID: null, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, +}; + +const rootTeamCollection_2: DBTeamCollection = { + id: 'erv', + orderIndex: 2, + parentID: null, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, +}; + +const childTeamCollection: DBTeamCollection = { + id: 'rfe', + orderIndex: 1, + parentID: rootTeamCollection.id, + title: 'Child Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, +}; + +const childTeamCollection_2: DBTeamCollection = { + id: 'bgdz', + orderIndex: 1, + parentID: rootTeamCollection_2.id, + title: 'Child Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, +}; + +const rootTeamCollectionList: DBTeamCollection[] = [ + { + id: 'fdv', + orderIndex: 1, + parentID: null, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, + { + id: 'fbbg', + orderIndex: 2, + parentID: null, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, + { + id: 'fgbfg', + orderIndex: 3, + parentID: null, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, + { + id: 'bre3', + orderIndex: 4, + parentID: null, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, + { + id: 'hghgf', + orderIndex: 5, + parentID: null, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, + { + id: '123', + orderIndex: 6, + parentID: null, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, + { + id: '54tyh', + orderIndex: 7, + parentID: null, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, + { + id: '234re', + orderIndex: 8, + parentID: null, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, + { + id: '34rtg', + orderIndex: 9, + parentID: null, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, + { + id: '45tgh', + orderIndex: 10, + parentID: null, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, +]; + +const childTeamCollectionList: DBTeamCollection[] = [ + { + id: '123', + orderIndex: 1, + parentID: rootTeamCollection.id, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, + { + id: '345', + orderIndex: 2, + parentID: rootTeamCollection.id, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, + { + id: '456', + orderIndex: 3, + parentID: rootTeamCollection.id, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, + { + id: '567', + orderIndex: 4, + parentID: rootTeamCollection.id, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, + { + id: '123', + orderIndex: 5, + parentID: rootTeamCollection.id, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, + { + id: '678', + orderIndex: 6, + parentID: rootTeamCollection.id, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, + { + id: '789', + orderIndex: 7, + parentID: rootTeamCollection.id, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, + { + id: '890', + orderIndex: 8, + parentID: rootTeamCollection.id, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, + { + id: '012', + orderIndex: 9, + parentID: rootTeamCollection.id, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, + { + id: '0bhu', + orderIndex: 10, + parentID: rootTeamCollection.id, + title: 'Root Collection 1', + teamID: team.id, + createdOn: currentTime, + updatedOn: currentTime, + }, +]; + beforeEach(() => { - mockPubSub.publish.mockClear(); mockReset(mockPrisma); + mockPubSub.publish.mockClear(); }); -// TODO: rewrite tests for importCollectionsFromJSON and replaceCollectionsWithJSON -describe('exportCollectionsToJSON', () => { - test('resolves with the correct collection info', async () => { - /** - * Imagine a team with collections of this format - * - * (id) - * Collection1 { - * Request1 { "name": "dev.to homepage", "url": "https://dev.to"} - * Collection2 { - * Request2 { "name": "dev.to homepage", "url": "https://dev.to"} - * } - * } - */ - const testCollectionsJSON = ` - [{ - "folders": [{ - "folders": [], - "name": "Collection2", - "requests": [{ - "name": "dev.to homepage", - "url": "https://dev.to" - }] - }], - "name": "Collection1", - "requests": [{ - "name": "dev.to homepage", - "url": "https://dev.to" - }] - }] - `; - mockPrisma.teamCollection.findMany.mockImplementation((query) => { - if (query?.where?.parentID === null) { - return Promise.resolve([ - { - prisma: true, - id: 'Collection1', - title: 'Collection1', - parentID: null, - teamID: '3170', - }, - ]) as any; - } else if (query?.where?.parentID === 'Collection1') { - return Promise.resolve([ - { - prisma: true, - id: 'Collection2', - title: 'Collection2', - parentID: 'Collection1', - teamID: '3170', - }, - ]); - } else { - return Promise.resolve([]); - } - }); +describe('getTeamOfCollection', () => { + test('should return the team of a collection successfully with valid collectionID', async () => { + mockPrisma.teamCollection.findUnique.mockResolvedValueOnce({ + ...rootTeamCollection, + team: team, + } as any); - mockPrisma.teamCollection.findUnique.mockImplementation((query) => { - if (query?.where?.id === 'Collection1') { - return Promise.resolve({ - prisma: true, - id: 'Collection1', - title: 'Collection1', - parentID: null, - teamID: '3170', - }); - } else if (query?.where?.id === 'Collection2') { - return Promise.resolve({ - prisma: true, - id: 'Collection2', - title: 'Collection2', - parentID: 'Collection1', - teamID: '3170', - }); - } else { - return Promise.reject('RecordNotFound') as any; - } - }); + const result = await teamCollectionService.getTeamOfCollection( + rootTeamCollection.id, + ); + expect(result).toEqualRight(team); + }); - mockPrisma.teamRequest.findMany.mockImplementation((query) => { - if (query?.where?.collectionID === 'Collection1') { - return Promise.resolve([ - { - prisma: true, - id: 'Request1', - collectionID: 'Collection1', - title: 'Request1', - request: { - url: 'https://dev.to', - name: 'dev.to homepage', - }, - teamID: '3170', - }, - ]) as any; - } else if (query?.where?.collectionID === 'Collection2') { - return Promise.resolve([ - { - prisma: true, - id: 'Request2', - collectionID: 'Collection2', - title: 'Request2', - request: { - url: 'https://dev.to', - name: 'dev.to homepage', - }, - teamID: '3170', - }, - ]); - } else { - return Promise.resolve([]); - } - }); + test('should throw TEAM_INVALID_COLL_ID when collectionID is invalid', async () => { + mockPrisma.teamCollection.findUnique.mockResolvedValueOnce(null); - expect( - JSON.parse(await teamCollectionService.exportCollectionsToJSON('3170')), - ).toEqual(JSON.parse(testCollectionsJSON)); + const result = await teamCollectionService.getTeamOfCollection( + rootTeamCollection.id, + ); + expect(result).toEqualLeft(TEAM_INVALID_COLL_ID); }); }); -describe('importCollectionFromFirestore', () => { - test('rejects if called when the user has no synced collection data with TEAM_USER_NO_FB_SYNCDATA', async () => { - mockDocFunc.mockReturnValueOnce({ - get: jest.fn().mockResolvedValue({ - exists: false, - }), - }); - - mockPrisma.teamCollection.create.mockResolvedValue(null as any); - - await expect( - teamCollectionService.importCollectionFromFirestore( - 'testuid1', - '0/0/0', - '3170', - ), - ).rejects.toThrowError(TEAM_USER_NO_FB_SYNCDATA); - expect(mockPrisma.teamCollection.create).not.toHaveBeenCalled(); - }); - - test('rejects if called with the collection path is invalid with TEAM_FB_COLL_PATH_RESOLVE_FAIL', async () => { - mockDocFunc.mockReturnValueOnce({ - get: jest.fn().mockResolvedValue({ - exists: true, - data: () => { - return { - collection: [], - }; - }, - }), - }); - - mockPrisma.teamCollection.create.mockResolvedValue(null as any); - - await expect( - teamCollectionService.importCollectionFromFirestore( - 'testuid1', - '0/0/0', - '3170', - ), - ).rejects.toThrowError(TEAM_FB_COLL_PATH_RESOLVE_FAIL); - expect(mockPrisma.teamCollection.create).not.toHaveBeenCalled(); - }); - - test('resolves with proper collection path for single level nesting and updates the database', async () => { - mockDocFunc.mockReturnValueOnce({ - get: jest.fn().mockResolvedValue({ - exists: true, - data: () => { - return { - collection: [ - { - folders: [], - name: 'Test', - requests: [{ name: 'Test 1' }, { name: 'Test 2' }], - }, - ], - }; - }, - }), - }); - - mockPrisma.teamCollection.create.mockResolvedValue({ - id: 'testuid1', - title: 'Test', - parentID: null, - teamID: '3170', - requests: [ - { - title: 'Test 1', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test 1"}', - }, - { - title: 'Test 2', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test 2"}', - }, - ], +describe('getParentOfCollection', () => { + test('should return the parent collection successfully for a child collection with valid collectionID', async () => { + mockPrisma.teamCollection.findUnique.mockResolvedValueOnce({ + ...childTeamCollection, + parent: rootTeamCollection, } as any); - await expect( - teamCollectionService.importCollectionFromFirestore( - 'testuid1', - '0', - '3170', - ), - ).resolves.toEqual( - expect.objectContaining({ - id: expect.any(String), - title: 'Test', - parentID: null, - teamID: '3170', - }), + const result = await teamCollectionService.getParentOfCollection( + childTeamCollection.id, ); + expect(result).toEqual(rootTeamCollection); }); - test('resolves with proper collection path for single level nesting and fires the pubsub "team_coll//coll_added" subject', async () => { - mockDocFunc.mockReturnValueOnce({ - get: jest.fn().mockResolvedValue({ - exists: true, - data: () => { - return { - collection: [ - { - folders: [], - name: 'Test', - requests: [{ name: 'Test 1' }, { name: 'Test 2' }], - }, - ], - }; - }, - }), - }); - - mockPrisma.teamCollection.create.mockResolvedValue({ - id: 'testuid1', - title: 'Test', - parentID: null, - teamID: '3170', - requests: [ - { - title: 'Test 1', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test 1"}', - }, - { - title: 'Test 2', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test 2"}', - }, - ], + test('should return null successfully for a root collection with valid collectionID', async () => { + mockPrisma.teamCollection.findUnique.mockResolvedValueOnce({ + ...rootTeamCollection, + parent: null, } as any); - const result = await teamCollectionService.importCollectionFromFirestore( - 'testuid1', - '0', - '3170', - ); - - expect(mockPubSub.publish).toBeCalledWith( - 'team_coll/3170/coll_added', - result, + const result = await teamCollectionService.getParentOfCollection( + rootTeamCollection.id, ); + expect(result).toEqual(null); }); - test('resolves with proper collection path for multi level nesting and updates the database with hierarachy', async () => { - mockDocFunc.mockReturnValueOnce({ - get: jest.fn().mockResolvedValue({ - exists: true, - data: () => { - return { - collection: [ - { - folders: [ - { - folders: [], - name: 'Test Subfolder', - requests: [ - { name: 'Test Sub 1 1' }, - { name: 'Test Sub 2 2' }, - ], - }, - ], - name: 'Test', - requests: [{ name: 'Test 1' }, { name: 'Test 2' }], - }, - ], - }; - }, - }), - }); + test('should return null with invalid collectionID', async () => { + mockPrisma.teamCollection.findUnique.mockResolvedValueOnce(null); - mockPrisma.teamCollection.create.mockResolvedValue({ - id: 'testuid1', - title: 'Test', - parentID: null, - teamID: '3170', - requests: [ - { - title: 'Test 1', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test 1"}', - }, - { - title: 'Test 2', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test 2"}', - }, - ], - children: [ - { - id: 'testchilduid1', - title: 'Test Subfolder', - parentID: 'testuid1', - teamID: '3170', - requests: [ - { - title: 'Test Sub 1 1', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test Sub 1 1"}', - }, - { - title: 'Test Sub 2 2', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test Sub 2 2"}', - }, - ], - }, - ], - } as any); - - await expect( - teamCollectionService.importCollectionFromFirestore( - 'testuid1', - '0/0', - '3170', - ), - ).resolves.toEqual( - expect.objectContaining({ - id: expect.any(String), - title: 'Test', - parentID: null, - teamID: '3170', - }), + const result = await teamCollectionService.getParentOfCollection( + 'invalidID', ); + expect(result).toEqual(null); }); +}); - test('resolves with proper collection path for multi level nesting and fires the pubsub "team_coll/3170/coll_added" topic', async () => { - mockDocFunc.mockReturnValueOnce({ - get: jest.fn().mockResolvedValue({ - exists: true, - data: () => { - return { - collection: [ - { - folders: [ - { - folders: [], - name: 'Test Subfolder', - requests: [ - { name: 'Test Sub 1 1' }, - { name: 'Test Sub 2 2' }, - ], - }, - ], - name: 'Test', - requests: [{ name: 'Test 1' }, { name: 'Test 2' }], - }, - ], - }; - }, - }), - }); - - mockPrisma.teamCollection.create.mockResolvedValue({ - id: 'testuid1', - title: 'Test', - parentID: null, - teamID: '3170', - requests: [ - { - title: 'Test 1', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test 1"}', - }, - { - title: 'Test 2', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test 2"}', - }, - ], - children: [ - { - id: 'testchilduid1', - title: 'Test Subfolder', - parentID: 'testuid1', - teamID: '3170', - requests: [ - { - title: 'Test Sub 1 1', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test Sub 1 1"}', - }, - { - title: 'Test Sub 2 2', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test Sub 2 2"}', - }, - ], - }, - ], - } as any); - - const result = await teamCollectionService.importCollectionFromFirestore( - 'testuid1', - '0/0', - '3170', +describe('getChildrenOfCollection', () => { + test('should return a list of child collections successfully with valid inputs', async () => { + mockPrisma.teamCollection.findMany.mockResolvedValueOnce( + childTeamCollectionList, ); - expect(mockPubSub.publish).toHaveBeenCalledWith( - 'team_coll/3170/coll_added', - result, - ); - }); - - test('when no parentCollectionID is specified (or null) the collection is created at root of team collection hierarachy', async () => { - mockDocFunc.mockReturnValueOnce({ - get: jest.fn().mockResolvedValue({ - exists: true, - data: () => { - return { - collection: [ - { - folders: [ - { - folders: [], - name: 'Test Subfolder', - requests: [ - { name: 'Test Sub 1 1' }, - { name: 'Test Sub 2 2' }, - ], - }, - ], - name: 'Test', - requests: [{ name: 'Test 1' }, { name: 'Test 2' }], - }, - ], - }; - }, - }), - }); - - mockPrisma.teamCollection.create.mockResolvedValue({ - id: 'testuid1', - title: 'Test', - parentID: null, - teamID: '3170', - requests: [ - { - title: 'Test 1', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test 1"}', - }, - { - title: 'Test 2', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test 2"}', - }, - ], - children: [ - { - id: 'testchilduid1', - title: 'Test Subfolder', - parentID: 'testuid1', - teamID: '3170', - requests: [ - { - title: 'Test Sub 1 1', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test Sub 1 1"}', - }, - { - title: 'Test Sub 2 2', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test Sub 2 2"}', - }, - ], - }, - ], - } as any); - - const result = await teamCollectionService.importCollectionFromFirestore( - 'testuid1', - '0/0', - '3170', - ); - - expect(result).toEqual( - expect.objectContaining({ - id: expect.any(String), - title: 'Test', - parentID: null, - teamID: '3170', - }), - ); - }); - - test('when parentCollectionID is specified the collection is created at the corresponding location', async () => { - mockDocFunc.mockReturnValueOnce({ - get: jest.fn().mockResolvedValue({ - exists: true, - data: () => { - return { - collection: [ - { - folders: [ - { - folders: [], - name: 'Test Subfolder', - requests: [ - { name: 'Test Sub 1 1' }, - { name: 'Test Sub 2 2' }, - ], - }, - ], - name: 'Test', - requests: [{ name: 'Test 1' }, { name: 'Test 2' }], - }, - ], - }; - }, - }), - }); - - mockPrisma.teamCollection.create.mockResolvedValue({ - id: 'testcoll1', - title: 'Test Coll 1', - parentID: null, - teamID: '3170', - team: { - id: '3170', - name: 'Team 1', - }, - } as any); - - const parentCollection = await teamCollectionService.createCollection( - '3170', - 'Test Coll 1', + const result = await teamCollectionService.getChildrenOfCollection( + rootTeamCollection.id, null, + 10, + ); + expect(result).toEqual(childTeamCollectionList); + }); + + test('should return a list of 3 child collections successfully with cursor being equal to the 7th item in the list', async () => { + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + { ...childTeamCollectionList[7] }, + { ...childTeamCollectionList[8] }, + { ...childTeamCollectionList[9] }, + ]); + + const result = await teamCollectionService.getChildrenOfCollection( + rootTeamCollection.id, + childTeamCollectionList[6].id, + 10, + ); + expect(result).toEqual([ + { ...childTeamCollectionList[7] }, + { ...childTeamCollectionList[8] }, + { ...childTeamCollectionList[9] }, + ]); + }); + + test('should return a empty array with invalid inputs', async () => { + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + + const result = await teamCollectionService.getChildrenOfCollection( + 'invalidID', + null, + 10, + ); + expect(result).toEqual([]); + }); +}); + +describe('getTeamRootCollections', () => { + test('should return a list of root collections successfully with valid inputs', async () => { + mockPrisma.teamCollection.findMany.mockResolvedValueOnce( + rootTeamCollectionList, ); - mockPrisma.teamCollection.create.mockResolvedValue({ - id: 'testuid1', - title: 'Test', - parentID: 'testcoll1', - teamID: '3170', - requests: [ - { - title: 'Test 1', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test 1"}', - }, - { - title: 'Test 2', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test 2"}', - }, - ], - children: [ - { - id: 'testchilduid1', - title: 'Test Subfolder', - parentID: 'testuid1', - teamID: '3170', - requests: [ - { - title: 'Test Sub 1 1', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test Sub 1 1"}', - }, - { - title: 'Test Sub 2 2', - team: { - id: '3170', - name: 'Team 1', - }, - request: '{ name: "Test Sub 2 2"}', - }, - ], - }, - ], - parent: { - prisma: true, - id: 'testcoll1', - title: 'Test Coll 1', - parentID: null, - teamID: '3170', - team: { - id: '3170', - name: 'Team 1', - }, - }, - } as any); - - await expect( - teamCollectionService.importCollectionFromFirestore( - 'testuid1', - '0/0', - '3170', - parentCollection.id, - ), - ).resolves.toEqual( - expect.objectContaining({ - id: expect.any(String), - title: 'Test', - parentID: parentCollection.id, - teamID: '3170', - }), + const result = await teamCollectionService.getTeamRootCollections( + rootTeamCollection.teamID, + null, + 10, ); + expect(result).toEqual(rootTeamCollectionList); + }); + + test('should return a list of 3 root collections successfully with cursor being equal to the 7th item in the list', async () => { + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + { ...rootTeamCollectionList[7] }, + { ...rootTeamCollectionList[8] }, + { ...rootTeamCollectionList[9] }, + ]); + + const result = await teamCollectionService.getTeamRootCollections( + rootTeamCollection.teamID, + rootTeamCollectionList[6].id, + 10, + ); + expect(result).toEqual([ + { ...rootTeamCollectionList[7] }, + { ...rootTeamCollectionList[8] }, + { ...rootTeamCollectionList[9] }, + ]); + }); + + test('should return a empty array with invalid inputs', async () => { + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + + const result = await teamCollectionService.getTeamRootCollections( + 'invalidTeamID', + null, + 10, + ); + expect(result).toEqual([]); + }); +}); + +describe('getCollection', () => { + test('should return a root TeamCollection with valid collectionID', async () => { + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + + const result = await teamCollectionService.getCollection( + rootTeamCollection.id, + ); + expect(result).toEqualRight(rootTeamCollection); + }); + + test('should return a child TeamCollection with valid collectionID', async () => { + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + childTeamCollection, + ); + + const result = await teamCollectionService.getCollection( + childTeamCollection.id, + ); + expect(result).toEqualRight(childTeamCollection); + }); + + test('should throw TEAM_COLL_NOT_FOUND when collectionID is invalid', async () => { + mockPrisma.teamCollection.findUniqueOrThrow.mockRejectedValue( + 'NotFoundError', + ); + + const result = await teamCollectionService.getCollection('123'); + expect(result).toEqualLeft(TEAM_COLL_NOT_FOUND); }); }); describe('createCollection', () => { - test('rejects if null team id', async () => { - mockPrisma.teamCollection.create.mockRejectedValue(null as any); - - await expect( - teamCollectionService.createCollection(null as any, 'Test Coll 2', null), - ).rejects.toBeDefined(); - }); - - test('rejects if invalid teamID', async () => { - mockPrisma.teamCollection.create.mockRejectedValue(null as any); - - await expect( - teamCollectionService.createCollection( - 'invalidteamid', - 'Test Coll 2', - null, - ), - ).rejects.toBeDefined(); - }); - - test('rejects if short title', async () => { - mockPrisma.teamCollection.create.mockRejectedValue(null as any); - - await expect( - teamCollectionService.createCollection('3170', 'Te', null), - ).rejects.toBeDefined(); - expect(mockPrisma.teamCollection.create).not.toHaveBeenCalled(); - }); - - test('resolves if valid team id and title with null parentID', async () => { - mockPrisma.teamCollection.create.mockResolvedValue({ - id: 'testcoll', - title: 'Test Collection 2', - parentID: null, - teamID: '3170', - team: { - id: '3170', - name: 'Test Team 1', - }, - } as any); - - await expect( - teamCollectionService.createCollection('3170', 'Test Collection 2', null), - ).resolves.toBeDefined(); - }); - - test('rejects if valid team id and title with invalid parentID', async () => { - mockPrisma.teamCollection.create.mockRejectedValue(null as any); - - await expect( - teamCollectionService.createCollection( - '3170', - 'Test Child Collection 1', - 'invalidtestcoll', - ), - ).rejects.toBeDefined(); - }); - - test('resolves if valid team id and title with valid parentID', async () => { - mockPrisma.teamCollection.create.mockResolvedValue({ - id: 'testchildcoll', - title: 'Test Collection 2', - parentID: 'testcoll', - teamID: '3170', - team: { - id: '3170', - name: 'Test Team 1', - }, - parent: { - id: 'testcoll', - title: 'Test Collection 3', - parentID: null, - teamID: '3170', - }, - } as any); - - await expect( - teamCollectionService.createCollection( - '3170', - 'Test Child Collection 1', - 'testcoll', - ), - ).resolves.toBeDefined(); - }); - - test('publishes creation to pubsub topic "team_coll//coll_added"', async () => { + test('should throw TEAM_COLL_SHORT_TITLE when title is less than 3 characters', async () => { const result = await teamCollectionService.createCollection( - '3170', - 'Test Child Collection 1', - 'testcoll', + rootTeamCollection.teamID, + 'ab', + rootTeamCollection.id, + ); + expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE); + }); + + test('should throw TEAM_NOT_OWNER when parent TeamCollection does not belong to the team', async () => { + // isOwnerCheck + mockPrisma.teamCollection.findFirstOrThrow.mockRejectedValueOnce( + 'NotFoundError', ); - mockPrisma.teamCollection.create.mockResolvedValue({ - id: 'testchildcoll', - title: 'Test Collection 1', - parentID: 'testcoll', - teamID: '3170', - team: { - id: '3170', - name: 'Test Team 1', - }, - parent: { - id: 'testcoll', - title: 'Test Collection 1', - parentID: null, - teamID: '3170', - }, - } as any); + const result = await teamCollectionService.createCollection( + rootTeamCollection.teamID, + 'abcd', + rootTeamCollection.id, + ); + expect(result).toEqualLeft(TEAM_NOT_OWNER); + }); + test('should successfully create a new root TeamCollection with valid inputs', async () => { + // isOwnerCheck + mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + + //getRootCollectionsCount + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + mockPrisma.teamCollection.create.mockResolvedValueOnce(rootTeamCollection); + + const result = await teamCollectionService.createCollection( + rootTeamCollection.teamID, + 'abcdefg', + rootTeamCollection.id, + ); + expect(result).toEqualRight(rootTeamCollection); + }); + + test('should successfully create a new child TeamCollection with valid inputs', async () => { + // isOwnerCheck + mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + + //getChildCollectionsCount + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + mockPrisma.teamCollection.create.mockResolvedValueOnce(childTeamCollection); + + const result = await teamCollectionService.createCollection( + childTeamCollection.teamID, + childTeamCollection.title, + rootTeamCollection.id, + ); + expect(result).toEqualRight(childTeamCollection); + }); + + test('should send pubsub message to "team_coll//coll_added" if child TeamCollection is created successfully', async () => { + // isOwnerCheck + mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + + //getChildCollectionsCount + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + mockPrisma.teamCollection.create.mockResolvedValueOnce(childTeamCollection); + + const result = await teamCollectionService.createCollection( + childTeamCollection.teamID, + childTeamCollection.title, + rootTeamCollection.id, + ); expect(mockPubSub.publish).toHaveBeenCalledWith( - 'team_coll/3170/coll_added', - result, + `team_coll/${childTeamCollection.teamID}/coll_added`, + childTeamCollection, + ); + }); + + test('should send pubsub message to "team_coll//coll_added" if root TeamCollection is created successfully', async () => { + // isOwnerCheck + mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + + //getRootCollectionsCount + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + mockPrisma.teamCollection.create.mockResolvedValueOnce(rootTeamCollection); + + const result = await teamCollectionService.createCollection( + rootTeamCollection.teamID, + 'abcdefg', + rootTeamCollection.id, + ); + expect(mockPubSub.publish).toHaveBeenCalledWith( + `team_coll/${rootTeamCollection.teamID}/coll_added`, + rootTeamCollection, + ); + }); +}); + +describe('renameCollection', () => { + test('should throw TEAM_COLL_SHORT_TITLE when title is less than 3 characters', async () => { + const result = await teamCollectionService.renameCollection( + rootTeamCollection.id, + 'ab', + ); + expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE); + }); + + test('should successfully update a TeamCollection with valid inputs', async () => { + // isOwnerCheck + mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + + mockPrisma.teamCollection.update.mockResolvedValueOnce({ + ...rootTeamCollection, + title: 'NewTitle', + }); + + const result = await teamCollectionService.renameCollection( + rootTeamCollection.id, + 'NewTitle', + ); + expect(result).toEqualRight({ + ...rootTeamCollection, + title: 'NewTitle', + }); + }); + + test('should throw TEAM_COLL_NOT_FOUND when collectionID is invalid', async () => { + // isOwnerCheck + mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + + mockPrisma.teamCollection.update.mockRejectedValueOnce('RecordNotFound'); + + const result = await teamCollectionService.renameCollection( + 'invalidID', + 'NewTitle', + ); + expect(result).toEqualLeft(TEAM_COLL_NOT_FOUND); + }); + + test('should send pubsub message to "team_coll//coll_updated" if TeamCollection title is updated successfully', async () => { + // isOwnerCheck + mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + + mockPrisma.teamCollection.update.mockResolvedValueOnce({ + ...rootTeamCollection, + title: 'NewTitle', + }); + + const result = await teamCollectionService.renameCollection( + rootTeamCollection.id, + 'NewTitle', + ); + expect(mockPubSub.publish).toHaveBeenCalledWith( + `team_coll/${rootTeamCollection.teamID}/coll_updated`, + { + ...rootTeamCollection, + title: 'NewTitle', + }, ); }); }); describe('deleteCollection', () => { - test('resolves if the collection id is valid', async () => { - mockPrisma.teamCollection.findUnique.mockResolvedValue({ - id: 'testcoll', - title: 'Test Collection 1', - parentID: null, - teamID: '3170', - }); - - mockPrisma.teamCollection.findMany.mockResolvedValue([]); - - mockPrisma.teamRequest.deleteMany.mockResolvedValue({ - count: 0, - }); - - mockPrisma.teamCollection.delete.mockResolvedValue({ - id: 'testcoll', - title: 'Test Collection 1', - parentID: null, - teamID: '3170', - }); - - await expect( - teamCollectionService.deleteCollection('testcoll'), - ).resolves.toBeUndefined(); - }); - - test('rejects if the collection id is not valid', async () => { - mockPrisma.teamCollection.findUnique.mockRejectedValue( - TEAM_INVALID_COLL_ID, + test('should successfully delete a TeamCollection with valid inputs', async () => { + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + rootTeamCollection, ); + // deleteCollectionData + // deleteCollectionData --> FindMany query 1st time + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + // deleteCollectionData --> FindMany query 2st time + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + // deleteCollectionData --> DeleteMany query + mockPrisma.teamRequest.deleteMany.mockResolvedValueOnce({ count: 0 }); + // deleteCollectionData --> updateOrderIndex + mockPrisma.teamCollection.updateMany.mockResolvedValueOnce({ count: 0 }); + // deleteCollectionData --> removeUserCollection + mockPrisma.teamCollection.delete.mockResolvedValueOnce(rootTeamCollection); - mockPrisma.teamCollection.findMany.mockResolvedValue([]); - - mockPrisma.teamRequest.deleteMany.mockResolvedValue({ - count: 0, - }); - - mockPrisma.teamCollection.delete.mockResolvedValue(null as any); - - await expect( - teamCollectionService.deleteCollection('invalidtestcoll'), - ).rejects.toBeDefined(); - - expect(mockPrisma.teamCollection.findMany).not.toHaveBeenCalled(); - - expect(mockPrisma.teamCollection.deleteMany).not.toHaveBeenCalled(); - - expect(mockPrisma.teamCollection.delete).not.toHaveBeenCalled(); + const result = await teamCollectionService.deleteCollection( + rootTeamCollection.id, + ); + expect(result).toEqualRight(true); }); - test('performs the deletion on the database', async () => { - mockPrisma.teamCollection.findUnique.mockResolvedValue({ - id: 'testcoll', - title: 'Test Collection 1', - parentID: null, - teamID: '3170', - }); - - mockPrisma.teamCollection.findMany.mockResolvedValue([]); - - mockPrisma.teamRequest.deleteMany.mockResolvedValue({ - count: 0, - }); - - mockPrisma.teamCollection.delete.mockResolvedValue({ - id: 'testcoll', - title: 'Test Collection 1', - parentID: null, - teamID: '3170', - }); - - await teamCollectionService.deleteCollection('testcoll'); - - expect(mockPrisma.teamCollection.delete).toHaveBeenCalledWith({ - where: { - id: 'testcoll', - }, - }); + test('should throw TEAM_COLL_NOT_FOUND when collectionID is invalid ', async () => { + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockRejectedValueOnce( + 'NotFoundError', + ); + const result = await teamCollectionService.deleteCollection( + rootTeamCollection.id, + ); + expect(result).toEqualLeft(TEAM_COLL_NOT_FOUND); }); - test('publishes to pubsub topic "team_coll//coll_removed"', async () => { - mockPrisma.teamCollection.findUnique.mockResolvedValue({ - id: 'testcoll', - title: 'Test Collection 1', - parentID: null, - teamID: '3170', - }); + test('should throw TEAM_COLL_NOT_FOUND when collectionID is invalid when deleting TeamCollection from UserCollectionTable ', async () => { + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + // deleteCollectionData + // deleteCollectionData --> FindMany query 1st time + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + // deleteCollectionData --> FindMany query 2st time + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + // deleteCollectionData --> DeleteMany query + mockPrisma.userRequest.deleteMany.mockResolvedValueOnce({ count: 0 }); + // deleteCollectionData --> updateOrderIndex + mockPrisma.teamCollection.updateMany.mockResolvedValueOnce({ count: 0 }); + // deleteCollectionData --> removeUserCollection + mockPrisma.teamCollection.delete.mockRejectedValueOnce('RecordNotFound'); - mockPrisma.teamCollection.findMany.mockResolvedValue([]); + const result = await teamCollectionService.deleteCollection( + rootTeamCollection.id, + ); + expect(result).toEqualLeft(TEAM_COLL_NOT_FOUND); + }); - mockPrisma.teamRequest.deleteMany.mockResolvedValue({ - count: 0, - }); - - mockPrisma.teamCollection.delete.mockResolvedValue({ - id: 'testcoll', - title: 'Test Collection 1', - parentID: null, - teamID: '3170', - }); - - await teamCollectionService.deleteCollection('testcoll'); + test('should send pubsub message to "team_coll//coll_removed" if TeamCollection is deleted successfully', async () => { + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + // deleteCollectionData + // deleteCollectionData --> FindMany query 1st time + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + // deleteCollectionData --> FindMany query 2st time + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + // deleteCollectionData --> DeleteMany query + mockPrisma.userRequest.deleteMany.mockResolvedValueOnce({ count: 0 }); + // deleteCollectionData --> updateOrderIndex + mockPrisma.teamCollection.updateMany.mockResolvedValueOnce({ count: 0 }); + // deleteCollectionData --> removeUserCollection + mockPrisma.teamCollection.delete.mockResolvedValueOnce(rootTeamCollection); + const result = await teamCollectionService.deleteCollection( + rootTeamCollection.id, + ); expect(mockPubSub.publish).toHaveBeenCalledWith( - 'team_coll/3170/coll_removed', - 'testcoll', + `team_coll/${rootTeamCollection.teamID}/coll_removed`, + rootTeamCollection.id, ); }); }); -describe('getCollection', () => { - test('resolves with the valid collection info for valid collection id', async () => { - mockPrisma.teamCollection.findUnique.mockResolvedValue({ - id: 'testcoll', - title: 'Test Collection 1', - parentID: null, - teamID: '3170', +describe('moveCollection', () => { + test('should throw TEAM_COLL_NOT_FOUND if collectionID is invalid', async () => { + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockRejectedValueOnce( + 'NotFoundError', + ); + + const result = await teamCollectionService.moveCollection('234', '009'); + expect(result).toEqualLeft(TEAM_COLL_NOT_FOUND); + }); + + test('should throw TEAM_COLL_DEST_SAME if collectionID and destCollectionID is the same', async () => { + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + + const result = await teamCollectionService.moveCollection( + rootTeamCollection.id, + rootTeamCollection.id, + ); + expect(result).toEqualLeft(TEAM_COLL_DEST_SAME); + }); + + test('should throw TEAM_COLL_NOT_FOUND if destCollectionID is invalid', async () => { + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + // getCollection for destCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockRejectedValueOnce( + 'NotFoundError', + ); + + const result = await teamCollectionService.moveCollection( + 'invalidID', + rootTeamCollection.id, + ); + expect(result).toEqualLeft(TEAM_COLL_NOT_FOUND); + }); + + test('should throw TEAM_COLL_NOT_SAME_TEAM if collectionID and destCollectionID are not from the same team', async () => { + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + // getCollection for destCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce({ + ...childTeamCollection_2, + teamID: 'differentTeamID', }); - await expect( - teamCollectionService.getCollection('testcoll'), - ).resolves.toEqual( - expect.objectContaining({ - id: 'testcoll', - title: 'Test Collection 1', - parentID: null, - teamID: '3170', - }), + const result = await teamCollectionService.moveCollection( + rootTeamCollection.id, + childTeamCollection_2.id, ); + expect(result).toEqualLeft(TEAM_COLL_NOT_SAME_TEAM); }); - test("resolves with null if the collection id doesn't exist", async () => { - mockPrisma.teamCollection.findUnique.mockResolvedValue(null as any); + test('should throw TEAM_COLL_IS_PARENT_COLL if collectionID is parent of destCollectionID ', async () => { + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + // getCollection for destCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + childTeamCollection, + ); - await expect( - teamCollectionService.getCollection('invalidtestcoll'), - ).resolves.toBeNull(); - }); -}); - -describe('renameCollection', () => { - test('rejects if the new title has length less than 3', async () => { - mockPrisma.teamCollection.update.mockRejectedValue(null as any); - - await expect( - teamCollectionService.renameCollection('testcoll', 'Te'), - ).rejects.toBeDefined(); - expect(mockPrisma.teamCollection.update).not.toHaveBeenCalled(); + const result = await teamCollectionService.moveCollection( + rootTeamCollection.id, + childTeamCollection.id, + ); + expect(result).toEqualLeft(TEAM_COLL_IS_PARENT_COLL); }); - test("rejects if the collection id doesn't exist", async () => { - mockPrisma.teamCollection.update.mockRejectedValue('RecordNotFound'); + test('should throw TEAM_COL_ALREADY_ROOT when moving root TeamCollection to root', async () => { + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); - await expect( - teamCollectionService.renameCollection( - 'invalidtestcoll', - 'Test Coll rename', - ), - ).rejects.toBeDefined(); + const result = await teamCollectionService.moveCollection( + rootTeamCollection.id, + null, + ); + expect(result).toEqualLeft(TEAM_COL_ALREADY_ROOT); }); - test('valid queries resolves with the updated team collection', async () => { + test('should successfully move a child TeamCollection into root', async () => { + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + childTeamCollection, + ); + // updateOrderIndex + mockPrisma.teamCollection.updateMany.mockResolvedValueOnce({ count: 0 }); + // changeParent + // changeParent --> getRootCollectionsCount + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + rootTeamCollection, + ]); + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + rootTeamCollection, + ]); mockPrisma.teamCollection.update.mockResolvedValue({ - id: 'testcoll', - title: 'Test Rename 1', + ...childTeamCollection, parentID: null, - teamID: '3170', + orderIndex: 2, }); - await expect( - teamCollectionService.renameCollection('testcoll', 'Test Rename 1'), - ).resolves.toEqual( - expect.objectContaining({ - id: 'testcoll', - title: 'Test Rename 1', - parentID: null, - teamID: '3170', - }), + const result = await teamCollectionService.moveCollection( + childTeamCollection.id, + null, ); + expect(result).toEqualRight({ + ...childTeamCollection, + parentID: null, + orderIndex: 2, + }); }); - test('publish pubsub topic "team_coll//coll_updated"', async () => { + test('should throw TEAM_COLL_NOT_FOUND when trying to change parent of collection with invalid collectionID', async () => { + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + childTeamCollection, + ); + // updateOrderIndex + mockPrisma.teamCollection.updateMany.mockResolvedValueOnce({ count: 0 }); + // changeParent + // changeParent --> getRootCollectionsCount + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + rootTeamCollection, + ]); + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + rootTeamCollection, + ]); + mockPrisma.teamCollection.update.mockRejectedValueOnce('RecordNotFound'); + + const result = await teamCollectionService.moveCollection( + childTeamCollection.id, + null, + ); + expect(result).toEqualLeft(TEAM_COLL_NOT_FOUND); + }); + + test('should send pubsub message to "team_coll//coll_moved" when a child TeamCollection is moved to root successfully', async () => { + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + childTeamCollection, + ); + // updateOrderIndex + mockPrisma.teamCollection.updateMany.mockResolvedValueOnce({ count: 0 }); + // changeParent + // changeParent --> getRootCollectionsCount + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + rootTeamCollection, + ]); + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + rootTeamCollection, + ]); mockPrisma.teamCollection.update.mockResolvedValue({ - id: 'testcoll', - title: 'Test Rename 1', + ...childTeamCollection, parentID: null, - teamID: '3170', + orderIndex: 2, }); - const member = await teamCollectionService.renameCollection( - 'testcoll', - 'Test Rename 1', + const result = await teamCollectionService.moveCollection( + childTeamCollection.id, + null, ); - expect(mockPubSub.publish).toHaveBeenCalledWith( - 'team_coll/3170/coll_updated', - member, + `team_coll/${childTeamCollection.teamID}/coll_moved`, + { + ...childTeamCollection, + parentID: null, + orderIndex: 2, + }, + ); + }); + + test('should successfully move a root TeamCollection into a child TeamCollection', async () => { + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + // getCollection for destCollection + mockPrisma.teamCollection.findUniqueOrThrow + .mockResolvedValueOnce(rootTeamCollection_2) + .mockResolvedValueOnce(null); + // isParent --> getCollection + mockPrisma.teamCollection.findUnique.mockResolvedValueOnce( + childTeamCollection_2, + ); + // updateOrderIndex + mockPrisma.teamCollection.updateMany.mockResolvedValueOnce({ count: 0 }); + // changeParent + // changeParent --> getRootCollectionsCount + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + rootTeamCollection, + ]); + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + rootTeamCollection, + ]); + mockPrisma.teamCollection.update.mockResolvedValue({ + ...rootTeamCollection, + parentID: childTeamCollection_2.id, + orderIndex: 1, + }); + + const result = await teamCollectionService.moveCollection( + rootTeamCollection.id, + childTeamCollection_2.id, + ); + expect(result).toEqualRight({ + ...rootTeamCollection, + parentID: childTeamCollection_2.id, + orderIndex: 1, + }); + }); + + test('should send pubsub message to "team_coll//coll_moved" when root TeamCollection is moved into another child TeamCollection successfully', async () => { + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + // getCollection for destCollection + mockPrisma.teamCollection.findUniqueOrThrow + .mockResolvedValueOnce(rootTeamCollection_2) + .mockResolvedValueOnce(null); + // isParent --> getCollection + mockPrisma.teamCollection.findUnique.mockResolvedValueOnce( + childTeamCollection_2, + ); + // updateOrderIndex + mockPrisma.teamCollection.updateMany.mockResolvedValueOnce({ count: 0 }); + // changeParent + // changeParent --> getRootCollectionsCount + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + rootTeamCollection, + ]); + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + rootTeamCollection, + ]); + mockPrisma.teamCollection.update.mockResolvedValue({ + ...rootTeamCollection, + parentID: childTeamCollection_2.id, + orderIndex: 1, + }); + + const result = await teamCollectionService.moveCollection( + rootTeamCollection.id, + childTeamCollection_2.id, + ); + expect(mockPubSub.publish).toHaveBeenCalledWith( + `team_coll/${childTeamCollection_2.teamID}/coll_moved`, + { + ...rootTeamCollection, + parentID: childTeamCollection_2.id, + orderIndex: 1, + }, + ); + }); + + test('should successfully move a child TeamCollection into another child TeamCollection', async () => { + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + childTeamCollection, + ); + // getCollection for destCollection + mockPrisma.teamCollection.findUniqueOrThrow + .mockResolvedValueOnce(rootTeamCollection_2) + .mockResolvedValueOnce(null); + // isParent --> getCollection + mockPrisma.teamCollection.findUnique.mockResolvedValueOnce( + childTeamCollection_2, + ); + // updateOrderIndex + mockPrisma.teamCollection.updateMany.mockResolvedValueOnce({ count: 0 }); + // changeParent + // changeParent --> getRootCollectionsCount + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + childTeamCollection, + ]); + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + childTeamCollection_2, + ]); + mockPrisma.teamCollection.update.mockResolvedValue({ + ...childTeamCollection, + parentID: childTeamCollection_2.id, + orderIndex: 1, + }); + + const result = await teamCollectionService.moveCollection( + childTeamCollection.id, + childTeamCollection_2.id, + ); + expect(result).toEqualRight({ + ...childTeamCollection, + parentID: childTeamCollection_2.id, + orderIndex: 1, + }); + }); + + test('should send pubsub message to "team_coll//coll_moved" when child TeamCollection is moved into another child TeamCollection successfully', async () => { + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + childTeamCollection, + ); + // getCollection for destCollection + mockPrisma.teamCollection.findUniqueOrThrow + .mockResolvedValueOnce(rootTeamCollection_2) + .mockResolvedValueOnce(null); + // isParent --> getCollection + mockPrisma.teamCollection.findUnique.mockResolvedValueOnce( + childTeamCollection_2, + ); + // updateOrderIndex + mockPrisma.teamCollection.updateMany.mockResolvedValueOnce({ count: 0 }); + // changeParent + // changeParent --> getRootCollectionsCount + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + childTeamCollection, + ]); + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + childTeamCollection_2, + ]); + mockPrisma.teamCollection.update.mockResolvedValue({ + ...childTeamCollection, + parentID: childTeamCollection_2.id, + orderIndex: 1, + }); + + const result = await teamCollectionService.moveCollection( + childTeamCollection.id, + childTeamCollection_2.id, + ); + expect(mockPubSub.publish).toHaveBeenCalledWith( + `team_coll/${childTeamCollection.teamID}/coll_moved`, + { + ...childTeamCollection, + parentID: childTeamCollection_2.id, + orderIndex: 1, + }, ); }); }); -describe('getTeamOfCollection', () => { - test('resolves with the correct team info if valid info is given', async () => { - mockPrisma.teamCollection.findUnique.mockResolvedValue({ - id: '3170', - title: 'Test Collection', - parentID: null, - teamID: '3170', - team: { - id: '3170', - name: 'Test Team', - }, - } as any); +describe('updateCollectionOrder', () => { + test('should throw TEAM_COL_SAME_NEXT_COLL if collectionID and nextCollectionID are the same', async () => { + const result = await teamCollectionService.updateCollectionOrder( + childTeamCollection.id, + childTeamCollection.id, + ); + expect(result).toEqualLeft(TEAM_COL_SAME_NEXT_COLL); + }); - await expect( - teamCollectionService.getTeamOfCollection('testcoll'), - ).resolves.toEqual( - expect.objectContaining({ - id: '3170', - name: 'Test Team', - }), + test('should throw TEAM_COLL_NOT_FOUND if collectionID is invalid', async () => { + // getCollection; + mockPrisma.teamCollection.findUniqueOrThrow.mockRejectedValueOnce( + 'NotFoundError', + ); + + const result = await teamCollectionService.updateCollectionOrder( + childTeamCollectionList[4].id, + null, + ); + expect(result).toEqualLeft(TEAM_COLL_NOT_FOUND); + }); + + test('should successfully move the child TeamCollection to the end of the list', async () => { + // getCollection; + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + childTeamCollectionList[4], + ); + mockPrisma.teamCollection.updateMany.mockResolvedValueOnce({ count: 10 }); + mockPrisma.teamCollection.update.mockResolvedValueOnce({ + ...childTeamCollectionList[4], + orderIndex: childTeamCollectionList.length, + }); + + const result = await teamCollectionService.updateCollectionOrder( + childTeamCollectionList[4].id, + null, + ); + expect(result).toEqualRight(true); + }); + + test('should successfully move the root TeamCollection to the end of the list', async () => { + // getCollection; + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + rootTeamCollectionList[4], + ); + mockPrisma.teamCollection.updateMany.mockResolvedValueOnce({ count: 10 }); + mockPrisma.teamCollection.update.mockResolvedValueOnce({ + ...rootTeamCollectionList[4], + orderIndex: rootTeamCollectionList.length, + }); + + const result = await teamCollectionService.updateCollectionOrder( + rootTeamCollectionList[4].id, + null, + ); + expect(result).toEqualRight(true); + }); + + test('should throw TEAM_COL_REORDERING_FAILED when re-ordering operation failed for child TeamCollection list', async () => { + // getCollection; + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + childTeamCollectionList[4], + ); + mockPrisma.$transaction.mockRejectedValueOnce('RecordNotFound'); + + const result = await teamCollectionService.updateCollectionOrder( + childTeamCollectionList[4].id, + null, + ); + expect(result).toEqualLeft(TEAM_COL_REORDERING_FAILED); + }); + + test('should send pubsub message to "team_coll//coll_order_updated" when TeamCollection order is updated successfully for a root TeamCollection', async () => { + // getCollection; + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + rootTeamCollectionList[4], + ); + mockPrisma.teamCollection.updateMany.mockResolvedValueOnce({ count: 10 }); + mockPrisma.teamCollection.update.mockResolvedValueOnce({ + ...rootTeamCollectionList[4], + orderIndex: rootTeamCollectionList.length, + }); + + const result = await teamCollectionService.updateCollectionOrder( + rootTeamCollectionList[4].id, + null, + ); + expect(mockPubSub.publish).toHaveBeenCalledWith( + `team_coll/${childTeamCollectionList[4].teamID}/coll_order_updated`, + { + collection: rootTeamCollectionList[4], + nextCollection: null, + }, ); }); - test("rejects if the collection id doesn't exist", async () => { - mockPrisma.teamCollection.findUnique.mockRejectedValue('RecordNotFound'); + test('should throw TEAM_COLL_NOT_SAME_TEAM when collectionID and nextCollectionID do not belong to the same team', async () => { + // getCollection; + mockPrisma.teamCollection.findUniqueOrThrow + .mockResolvedValueOnce(childTeamCollectionList[4]) + .mockResolvedValueOnce({ + ...childTeamCollection_2, + teamID: 'differendTeamID', + }); - await expect( - teamCollectionService.getTeamOfCollection('invalidtestcoll'), - ).rejects.toBeDefined(); - }); -}); - -describe('getParentOfCollection', () => { - test('resolves to null if the collection exists but no parent', async () => { - mockPrisma.teamCollection.findUnique.mockResolvedValue({ - id: 'testcoll', - title: 'Test Collection 1', - parentID: null, - teamID: '3170', - parent: null, - } as any); - - await expect( - teamCollectionService.getParentOfCollection('testcoll'), - ).resolves.toBeNull(); + const result = await teamCollectionService.updateCollectionOrder( + childTeamCollectionList[4].id, + childTeamCollection_2.id, + ); + expect(result).toEqualLeft(TEAM_COLL_NOT_SAME_TEAM); }); - test('resolves with the parent info if the collection exists and has parent', async () => { - mockPrisma.teamCollection.findUnique.mockResolvedValue({ - id: 'testcoll1', - title: 'Test Collection 1', - parentID: 'testparent', - teamID: '3170', - parent: { - id: 'testcoll2', - title: 'Test Collection 2', - parentID: null, - teamID: '3170', + test('should successfully update the order of the child TeamCollection list', async () => { + // getCollection; + mockPrisma.teamCollection.findUniqueOrThrow + .mockResolvedValueOnce(childTeamCollectionList[4]) + .mockResolvedValueOnce(childTeamCollectionList[2]); + mockPrisma.teamCollection.updateMany.mockResolvedValueOnce({ count: 3 }); + mockPrisma.teamCollection.update.mockResolvedValueOnce({ + ...childTeamCollectionList[4], + orderIndex: 2, + }); + + const result = await teamCollectionService.updateCollectionOrder( + childTeamCollectionList[4].id, + childTeamCollectionList[2].id, + ); + expect(result).toEqualRight(true); + }); + + test('should successfully update the order of the root TeamCollection list', async () => { + // getCollection; + mockPrisma.teamCollection.findUniqueOrThrow + .mockResolvedValueOnce(rootTeamCollectionList[4]) + .mockResolvedValueOnce(rootTeamCollectionList[2]); + + const result = await teamCollectionService.updateCollectionOrder( + rootTeamCollectionList[4].id, + rootTeamCollectionList[2].id, + ); + expect(result).toEqualRight(true); + }); + + test('should throw TEAM_COL_REORDERING_FAILED when re-ordering operation failed for child TeamCollection list', async () => { + // getCollection; + mockPrisma.teamCollection.findUniqueOrThrow + .mockResolvedValueOnce(childTeamCollectionList[4]) + .mockResolvedValueOnce(childTeamCollectionList[2]); + + mockPrisma.$transaction.mockRejectedValueOnce('RecordNotFound'); + + const result = await teamCollectionService.updateCollectionOrder( + childTeamCollectionList[4].id, + childTeamCollectionList[2].id, + ); + expect(result).toEqualLeft(TEAM_COL_REORDERING_FAILED); + }); + + test('should send pubsub message to "team_coll//coll_order_updated" when TeamCollection order is updated successfully', async () => { + // getCollection; + mockPrisma.teamCollection.findUniqueOrThrow + .mockResolvedValueOnce(childTeamCollectionList[4]) + .mockResolvedValueOnce(childTeamCollectionList[2]); + + const result = await teamCollectionService.updateCollectionOrder( + childTeamCollectionList[4].id, + childTeamCollectionList[2].id, + ); + expect(mockPubSub.publish).toHaveBeenCalledWith( + `team_coll/${childTeamCollectionList[2].teamID}/coll_order_updated`, + { + collection: childTeamCollectionList[4], + nextCollection: childTeamCollectionList[2], }, - } as any); - - await expect( - teamCollectionService.getParentOfCollection('testcoll2'), - ).resolves.toEqual( - expect.objectContaining({ - id: 'testcoll2', - title: 'Test Collection 2', - parentID: null, - teamID: '3170', - }), ); }); +}); - test("rejects if the collection id doesn't exist", async () => { - mockPrisma.teamCollection.findUnique.mockRejectedValue('RecordNotFound'); +const jsonString = + '[{"requests":[],"name":"Root Collection 1","folders":[],"v":1}]'; - await expect( - teamCollectionService.getParentOfCollection('invalidtestcoll'), - ).rejects.toBeDefined(); +describe('importCollectionsFromJSON', () => { + test('should throw TEAM_COLL_INVALID_JSON when the jsonString is invalid', async () => { + const result = await teamCollectionService.importCollectionsFromJSON( + 'invalidString', + rootTeamCollection.teamID, + null, + ); + expect(result).toEqualLeft(TEAM_COLL_INVALID_JSON); + }); + + test('should throw TEAM_COLL_INVALID_JSON when the parsed jsonString is not an array', async () => { + const result = await teamCollectionService.importCollectionsFromJSON( + '{}', + rootTeamCollection.teamID, + null, + ); + expect(result).toEqualLeft(TEAM_COLL_INVALID_JSON); + }); + + test('should successfully create new TeamCollections in root and TeamRequests with valid inputs', async () => { + //getRootCollectionsCount + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + mockPrisma.$transaction.mockResolvedValueOnce([rootTeamCollection]); + + const result = await teamCollectionService.importCollectionsFromJSON( + jsonString, + rootTeamCollection.teamID, + null, + ); + expect(result).toEqualRight(true); + }); + + test('should successfully create new TeamCollections in a child collection and TeamRequests with valid inputs', async () => { + //getChildCollectionsCount + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + mockPrisma.$transaction.mockResolvedValueOnce([rootTeamCollection]); + + const result = await teamCollectionService.importCollectionsFromJSON( + jsonString, + rootTeamCollection.teamID, + rootTeamCollection.id, + ); + expect(result).toEqualRight(true); + }); + + test('should send pubsub message to "team_coll//coll_added" on successful creation from jsonString', async () => { + //getRootCollectionsCount + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + mockPrisma.$transaction.mockResolvedValueOnce([rootTeamCollection]); + + const result = await teamCollectionService.importCollectionsFromJSON( + jsonString, + rootTeamCollection.teamID, + null, + ); + expect(mockPubSub.publish).toHaveBeenCalledWith( + `team_coll/${rootTeamCollection.teamID}/coll_added`, + rootTeamCollection, + ); }); }); -describe('getChildrenOfCollection', () => { - test('resolves for valid collection id and null cursor', async () => { - mockPrisma.teamCollection.findMany.mockResolvedValue([ - { - id: 'someid1', - title: 'testcoll1', - parentID: 'testcoll', - teamID: '3170', - }, - { - id: 'someid2', - title: 'testcoll2', - parentID: 'testcoll', - teamID: '3170', - }, - { - id: 'someid3', - title: 'testcoll3', - parentID: 'testcoll', - teamID: '3170', - }, - { - id: 'someid4', - title: 'testcoll4', - parentID: 'testcoll', - teamID: '3170', - }, - { - id: 'someid5', - title: 'testcoll5', - parentID: 'testcoll', - teamID: '3170', - }, - { - id: 'someid6', - title: 'testcoll6', - parentID: 'testcoll', - teamID: '3170', - }, - { - id: 'someid7', - title: 'testcoll7', - parentID: 'testcoll', - teamID: '3170', - }, - { - id: 'someid8', - title: 'testcoll8', - parentID: 'testcoll', - teamID: '3170', - }, - { - id: 'someid9', - title: 'testcoll9', - parentID: 'testcoll', - teamID: '3170', - }, - { - id: 'someid10', - title: 'testcoll10', - parentID: 'testcoll', - teamID: '3170', - }, - ]); - - await expect( - teamCollectionService.getChildrenOfCollection('testcoll', null), - ).resolves.toBeDefined(); +describe('replaceCollectionsWithJSON', () => { + test('should throw TEAM_COLL_INVALID_JSON when the jsonString is invalid', async () => { + const result = await teamCollectionService.replaceCollectionsWithJSON( + 'invalidString', + rootTeamCollection.teamID, + null, + ); + expect(result).toEqualLeft(TEAM_COLL_INVALID_JSON); }); - test('resolves for invalid collection id and null cursor with an empty array', async () => { - mockPrisma.teamCollection.findMany.mockResolvedValue([]); - - await expect( - teamCollectionService.getChildrenOfCollection('invalidcollid', null), - ).resolves.toHaveLength(0); + test('should throw TEAM_COLL_INVALID_JSON when the parsed jsonString is not an array', async () => { + const result = await teamCollectionService.replaceCollectionsWithJSON( + '{}', + rootTeamCollection.teamID, + null, + ); + expect(result).toEqualLeft(TEAM_COLL_INVALID_JSON); }); - test('resolves for valid collection id and non-null invalid cursor', async () => { - mockPrisma.teamCollection.findMany.mockResolvedValue([]); + test('should successfully replace TeamCollections in root with new TeamCollections and TeamRequests with valid inputs', async () => { + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + rootTeamCollection, + ]); + // deleteCollection + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + // deleteCollectionData + // deleteCollectionData --> FindMany query 1st time + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + // deleteCollectionData --> FindMany query 2st time + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + // deleteCollectionData --> DeleteMany query + mockPrisma.teamRequest.deleteMany.mockResolvedValueOnce({ count: 0 }); + // deleteCollectionData --> updateOrderIndex + mockPrisma.teamCollection.updateMany.mockResolvedValueOnce({ count: 0 }); + // deleteCollectionData --> removeUserCollection + mockPrisma.teamCollection.delete.mockResolvedValueOnce(rootTeamCollection); + //getRootCollectionsCount + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + mockPrisma.$transaction.mockResolvedValueOnce([rootTeamCollection]); - await expect( - teamCollectionService.getChildrenOfCollection( - 'testcoll', - 'invalidcursor', - ), - ).resolves.toBeDefined(); + const result = await teamCollectionService.replaceCollectionsWithJSON( + jsonString, + rootTeamCollection.teamID, + null, + ); + expect(result).toEqualRight(true); }); - test('returns the first 10 elements only for valid collection id and null cursor', async () => { - mockPrisma.teamCollection.findMany.mockResolvedValue([ - { - id: 'someid1', - title: 'testcoll1', - parentID: 'testcoll', - teamID: '3170', - }, - { - id: 'someid2', - title: 'testcoll2', - parentID: 'testcoll', - teamID: '3170', - }, - { - id: 'someid3', - title: 'testcoll3', - parentID: 'testcoll', - teamID: '3170', - }, - { - id: 'someid4', - title: 'testcoll4', - parentID: 'testcoll', - teamID: '3170', - }, - { - id: 'someid5', - title: 'testcoll5', - parentID: 'testcoll', - teamID: '3170', - }, - { - id: 'someid6', - title: 'testcoll6', - parentID: 'testcoll', - teamID: '3170', - }, - { - id: 'someid7', - title: 'testcoll7', - parentID: 'testcoll', - teamID: '3170', - }, - { - id: 'someid8', - title: 'testcoll8', - parentID: 'testcoll', - teamID: '3170', - }, - { - id: 'someid9', - title: 'testcoll9', - parentID: 'testcoll', - teamID: '3170', - }, - { - id: 'someid10', - title: 'testcoll10', - parentID: 'testcoll', - teamID: '3170', - }, + test('should successfully create new TeamCollections in a child collection and TeamRequests with valid inputs', async () => { + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + childTeamCollection, ]); + // deleteCollection + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + childTeamCollection, + ); + // deleteCollectionData + // deleteCollectionData --> FindMany query 1st time + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + // deleteCollectionData --> FindMany query 2st time + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + // deleteCollectionData --> DeleteMany query + mockPrisma.teamRequest.deleteMany.mockResolvedValueOnce({ count: 0 }); + // deleteCollectionData --> updateOrderIndex + mockPrisma.teamCollection.updateMany.mockResolvedValueOnce({ count: 0 }); + // deleteCollectionData --> removeUserCollection + mockPrisma.teamCollection.delete.mockResolvedValueOnce(childTeamCollection); + //getRootCollectionsCount + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + mockPrisma.$transaction.mockResolvedValueOnce([rootTeamCollection]); - await expect( - teamCollectionService.getChildrenOfCollection('testcoll', null), - ).resolves.toHaveLength(10); + const result = await teamCollectionService.replaceCollectionsWithJSON( + jsonString, + rootTeamCollection.teamID, + rootTeamCollection.id, + ); + expect(result).toEqualRight(true); }); - test('returns the 10 elements only for valid collection id and valid cursor', async () => { - mockPrisma.teamCollection.findMany.mockResolvedValue([ - { - id: 'someid1', - title: 'testcoll1', - parentID: 'someid', - teamID: '3170', - }, - { - id: 'someid2', - title: 'testcoll2', - parentID: 'someid', - teamID: '3170', - }, - { - id: 'someid3', - title: 'testcoll3', - parentID: 'someid', - teamID: '3170', - }, - { - id: 'someid4', - title: 'testcoll4', - parentID: 'someid', - teamID: '3170', - }, - { - id: 'someid5', - title: 'testcoll5', - parentID: 'someid', - teamID: '3170', - }, - { - id: 'someid6', - title: 'testcoll6', - parentID: 'someid', - teamID: '3170', - }, - { - id: 'someid7', - title: 'testcoll7', - parentID: 'someid', - teamID: '3170', - }, - { - id: 'someid8', - title: 'testcoll8', - parentID: 'someid', - teamID: '3170', - }, - { - id: 'someid9', - title: 'testcoll9', - parentID: 'someid', - teamID: '3170', - }, - { - id: 'someid10', - title: 'testcoll10', - parentID: 'someid', - teamID: '3170', - }, + test('should send pubsub message to "team_coll//coll_added" on successful creation from jsonString', async () => { + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([ + rootTeamCollection, ]); + // deleteCollection + // getCollection + mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + // deleteCollectionData + // deleteCollectionData --> FindMany query 1st time + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + // deleteCollectionData --> FindMany query 2st time + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + // deleteCollectionData --> DeleteMany query + mockPrisma.teamRequest.deleteMany.mockResolvedValueOnce({ count: 0 }); + // deleteCollectionData --> updateOrderIndex + mockPrisma.teamCollection.updateMany.mockResolvedValueOnce({ count: 0 }); + // deleteCollectionData --> removeUserCollection + mockPrisma.teamCollection.delete.mockResolvedValueOnce(rootTeamCollection); + //getRootCollectionsCount + mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]); + mockPrisma.$transaction.mockResolvedValueOnce([rootTeamCollection]); - const secondChildColl = ( - await teamCollectionService.getChildrenOfCollection('someid', null) - )[1]; - - mockReset(mockPrisma); - mockPrisma.teamCollection.findMany.mockResolvedValue([ - { - id: 'someid11', - title: 'testcoll11', - parentID: 'someid', - teamID: '3170', - }, - { - id: 'someid12', - title: 'testcoll12', - parentID: 'someid', - teamID: '3170', - }, - { - id: 'someid13', - title: 'testcoll13', - parentID: 'someid', - teamID: '3170', - }, - { - id: 'someid14', - title: 'testcoll14', - parentID: 'someid', - teamID: '3170', - }, - { - id: 'someid15', - title: 'testcoll15', - parentID: 'someid', - teamID: '3170', - }, - { - id: 'someid16', - title: 'testcoll16', - parentID: 'someid', - teamID: '3170', - }, - { - id: 'someid17', - title: 'testcoll17', - parentID: 'someid', - teamID: '3170', - }, - { - id: 'someid18', - title: 'testcoll18', - parentID: 'someid', - teamID: '3170', - }, - { - id: 'someid19', - title: 'testcoll19', - parentID: 'someid', - teamID: '3170', - }, - { - id: 'someid20', - title: 'testcoll20', - parentID: 'someid', - teamID: '3170', - }, - ]); - - await expect( - teamCollectionService.getChildrenOfCollection( - 'someid', - secondChildColl.id, - ), - ).resolves.toHaveLength(10); + const result = await teamCollectionService.replaceCollectionsWithJSON( + jsonString, + rootTeamCollection.teamID, + null, + ); + expect(mockPubSub.publish).toHaveBeenCalledWith( + `team_coll/${rootTeamCollection.teamID}/coll_added`, + rootTeamCollection, + ); }); }); -describe('getTeamRootCollections', () => { - test('resolves for valid team id and null cursor', async () => { - mockPrisma.teamCollection.findMany.mockResolvedValue([ - { - id: 'someid1', - title: 'testcoll1', - parentID: null, - teamID: '3170', - }, - ]); - - await expect( - teamCollectionService.getTeamRootCollections('3170', null), - ).resolves.toBeDefined(); - }); - - test('resolves for invalid team id and null cursor with an empty array', async () => { - mockPrisma.teamCollection.findMany.mockResolvedValue([]); - - await expect( - teamCollectionService.getTeamRootCollections('invalidteamid', null), - ).resolves.toHaveLength(0); - }); - - test('resolves for valid team id and null cursor with the first 10 elements', async () => { - mockPrisma.teamCollection.findMany.mockResolvedValue([ - { - id: 'someid1', - title: 'testcoll1', - parentID: null, - teamID: '3170', - }, - { - id: 'someid2', - title: 'testcoll2', - parentID: null, - teamID: '3170', - }, - { - id: 'someid3', - title: 'testcoll3', - parentID: null, - teamID: '3170', - }, - { - id: 'someid4', - title: 'testcoll4', - parentID: null, - teamID: '3170', - }, - { - id: 'someid5', - title: 'testcoll5', - parentID: null, - teamID: '3170', - }, - { - id: 'someid6', - title: 'testcoll6', - parentID: null, - teamID: '3170', - }, - { - id: 'someid7', - title: 'testcoll7', - parentID: null, - teamID: '3170', - }, - { - id: 'someid8', - title: 'testcoll8', - parentID: null, - teamID: '3170', - }, - { - id: 'someid9', - title: 'testcoll9', - parentID: null, - teamID: '3170', - }, - { - id: 'someid10', - title: 'testcoll10', - parentID: null, - teamID: '3170', - }, - ]); - - return expect( - teamCollectionService.getTeamRootCollections('3170', null), - ).resolves.toHaveLength(10); - }); - - test('resolves for valid team id and invalid cursor with empty array', async () => { - mockPrisma.teamCollection.findMany.mockResolvedValue([]); - - await expect( - teamCollectionService.getTeamRootCollections('3170', 'invalidcursor'), - ).resolves.toHaveLength(0); - }); - - test('resolves for valid team id and valid cursor with the next 10 elements', async () => { - mockPrisma.teamCollection.findMany.mockResolvedValue([ - { - id: 'someid1', - title: 'testcoll1', - parentID: null, - teamID: '3170', - }, - { - id: 'someid2', - title: 'testcoll2', - parentID: null, - teamID: '3170', - }, - { - id: 'someid3', - title: 'testcoll3', - parentID: null, - teamID: '3170', - }, - { - id: 'someid4', - title: 'testcoll4', - parentID: null, - teamID: '3170', - }, - { - id: 'someid5', - title: 'testcoll5', - parentID: null, - teamID: '3170', - }, - { - id: 'someid6', - title: 'testcoll6', - parentID: null, - teamID: '3170', - }, - { - id: 'someid7', - title: 'testcoll7', - parentID: null, - teamID: '3170', - }, - { - id: 'someid8', - title: 'testcoll8', - parentID: null, - teamID: '3170', - }, - { - id: 'someid9', - title: 'testcoll9', - parentID: null, - teamID: '3170', - }, - { - id: 'someid10', - title: 'testcoll10', - parentID: null, - teamID: '3170', - }, - ]); - - const secondColl = ( - await teamCollectionService.getTeamRootCollections('3170', null) - )[1]; - - mockReset(mockPrisma); - mockPrisma.teamCollection.findMany.mockResolvedValue([ - { - id: 'someid11', - title: 'testcoll11', - parentID: null, - teamID: '3170', - }, - { - id: 'someid12', - title: 'testcoll12', - parentID: null, - teamID: '3170', - }, - { - id: 'someid13', - title: 'testcoll13', - parentID: null, - teamID: '3170', - }, - { - id: 'someid14', - title: 'testcoll14', - parentID: null, - teamID: '3170', - }, - { - id: 'someid15', - title: 'testcoll15', - parentID: null, - teamID: '3170', - }, - { - id: 'someid6', - title: 'testcoll6', - parentID: null, - teamID: '3170', - }, - { - id: 'someid17', - title: 'testcoll17', - parentID: null, - teamID: '3170', - }, - { - id: 'someid18', - title: 'testcoll18', - parentID: null, - teamID: '3170', - }, - { - id: 'someid19', - title: 'testcoll19', - parentID: null, - teamID: '3170', - }, - { - id: 'someid20', - title: 'testcoll20', - parentID: null, - teamID: '3170', - }, - ]); - - await expect( - teamCollectionService.getTeamRootCollections('3170', secondColl.id), - ).resolves.toHaveLength(10); - }); -}); - -describe('getTeamCollections', () => { - test('resolves for valid team id and null cursor', async () => { - mockPrisma.teamCollection.findMany.mockResolvedValue([ - { - id: 'someid1', - title: 'testcoll1', - parentID: null, - teamID: '3170', - }, - ]); - - await expect( - teamCollectionService.getTeamCollections('3170', null), - ).resolves.toBeDefined(); - }); - - test('resolves for invalid team id and null cursor with an empty array', async () => { - mockPrisma.teamCollection.findMany.mockResolvedValue([]); - - await expect( - teamCollectionService.getTeamCollections('invalidteamid', null), - ).resolves.toHaveLength(0); - }); - - test('resolves for valid team id and null cursor with the first 10 elements', async () => { - mockPrisma.teamCollection.findMany.mockResolvedValue([ - { - id: 'someid1', - title: 'testcoll1', - parentID: null, - teamID: '3170', - }, - { - id: 'someid2', - title: 'testcoll2', - parentID: 'someid1', - teamID: '3170', - }, - { - id: 'someid3', - title: 'testcoll3', - parentID: null, - teamID: '3170', - }, - { - id: 'someid4', - title: 'testcoll4', - parentID: 'someid3', - teamID: '3170', - }, - { - id: 'someid5', - title: 'testcoll5', - parentID: null, - teamID: '3170', - }, - { - id: 'someid6', - title: 'testcoll6', - parentID: 'someid5', - teamID: '3170', - }, - { - id: 'someid7', - title: 'testcoll7', - parentID: null, - teamID: '3170', - }, - { - id: 'someid8', - title: 'testcoll8', - parentID: 'someid7', - teamID: '3170', - }, - { - id: 'someid9', - title: 'testcoll9', - parentID: null, - teamID: '3170', - }, - { - id: 'someid10', - title: 'testcoll10', - parentID: 'someid9', - teamID: '3170', - }, - ]); - - await expect( - teamCollectionService.getTeamCollections('3170', null), - ).resolves.toHaveLength(10); - }); - - test('resolves for valid team id and invalid cursor with empty array', async () => { - mockPrisma.teamCollection.findMany.mockResolvedValue([]); - - await expect( - teamCollectionService.getTeamCollections('3170', 'invalidcursor'), - ).resolves.toHaveLength(0); - }); - - test('resolves for valid team id and valid cursor with the next 10 elements', async () => { - mockPrisma.teamCollection.findMany.mockResolvedValue([ - { - id: 'someid1', - title: 'testcoll1', - parentID: null, - teamID: '3170', - }, - { - id: 'someid2', - title: 'testcoll2', - parentID: 'someid1', - teamID: '3170', - }, - { - id: 'someid3', - title: 'testcoll3', - parentID: null, - teamID: '3170', - }, - { - id: 'someid4', - title: 'testcoll4', - parentID: 'someid3', - teamID: '3170', - }, - { - id: 'someid5', - title: 'testcoll5', - parentID: null, - teamID: '3170', - }, - { - id: 'someid6', - title: 'testcoll6', - parentID: 'someid5', - teamID: '3170', - }, - { - id: 'someid7', - title: 'testcoll7', - parentID: null, - teamID: '3170', - }, - { - id: 'someid8', - title: 'testcoll8', - parentID: 'someid7', - teamID: '3170', - }, - { - id: 'someid9', - title: 'testcoll9', - parentID: null, - teamID: '3170', - }, - { - id: 'someid10', - title: 'testcoll10', - parentID: 'someid9', - teamID: '3170', - }, - ]); - - const secondColl = ( - await teamCollectionService.getTeamCollections('3170', null) - )[1]; - - mockReset(mockPrisma); - mockPrisma.teamCollection.findMany.mockResolvedValue([ - { - id: 'someid1', - title: 'testcoll1', - parentID: null, - teamID: '3170', - }, - { - id: 'someid2', - title: 'testcoll2', - parentID: 'someid1', - teamID: '3170', - }, - { - id: 'someid3', - title: 'testcoll3', - parentID: null, - teamID: '3170', - }, - { - id: 'someid4', - title: 'testcoll4', - parentID: 'someid3', - teamID: '3170', - }, - { - id: 'someid5', - title: 'testcoll5', - parentID: null, - teamID: '3170', - }, - { - id: 'someid6', - title: 'testcoll6', - parentID: 'someid5', - teamID: '3170', - }, - { - id: 'someid7', - title: 'testcoll7', - parentID: null, - teamID: '3170', - }, - { - id: 'someid8', - title: 'testcoll8', - parentID: 'someid7', - teamID: '3170', - }, - { - id: 'someid9', - title: 'testcoll9', - parentID: null, - teamID: '3170', - }, - { - id: 'someid10', - title: 'testcoll10', - parentID: 'someid9', - teamID: '3170', - }, - ]); - - await expect( - teamCollectionService.getTeamCollections('3170', secondColl.id), - ).resolves.toHaveLength(10); - }); - - describe('getCollectionTO', () => { - test('should resolve to Some for valid collection ID', async () => { - mockPrisma.teamCollection.findUnique.mockResolvedValueOnce({ - id: 'testcoll', - parentID: 'testparentcoll', - teamID: '3170', - title: 'Test Collection', - }); - - expect( - await teamCollectionService.getCollectionTO('testcoll')(), - ).toBeSome(); - }); - - test('should resolve to the correct Some value for a valid collection ID', async () => { - mockPrisma.teamCollection.findUnique.mockResolvedValueOnce({ - id: 'testcoll', - parentID: 'testparentcoll', - teamID: '3170', - title: 'Test Collection', - }); - - expect( - await teamCollectionService.getCollectionTO('testcoll')(), - ).toEqualSome({ - id: 'testcoll', - parentID: 'testparentcoll', - teamID: '3170', - title: 'Test Collection', - }); - }); - - test('should resolve a None value if the the collection ID does not exist', async () => { - mockPrisma.teamCollection.findUnique.mockResolvedValueOnce(null); - - expect( - await teamCollectionService.getCollectionTO('testcoll')(), - ).toBeNone(); - }); - }); -}); +//ToDo: write test cases for exportCollectionsToJSON diff --git a/packages/hoppscotch-backend/src/team-collection/team-collection.service.ts b/packages/hoppscotch-backend/src/team-collection/team-collection.service.ts index 5e8f71567..2f5780aa1 100644 --- a/packages/hoppscotch-backend/src/team-collection/team-collection.service.ts +++ b/packages/hoppscotch-backend/src/team-collection/team-collection.service.ts @@ -4,55 +4,957 @@ import { Team } from '../team/team.model'; import { TeamCollection } from './team-collection.model'; // import { FirebaseService } from '../firebase/firebase.service'; import { - TEAM_USER_NO_FB_SYNCDATA, - TEAM_FB_COLL_PATH_RESOLVE_FAIL, TEAM_COLL_SHORT_TITLE, TEAM_COLL_INVALID_JSON, TEAM_INVALID_COLL_ID, + TEAM_NOT_OWNER, + TEAM_COLL_NOT_FOUND, + TEAM_COL_ALREADY_ROOT, + TEAM_COLL_DEST_SAME, + TEAM_COLL_NOT_SAME_TEAM, + TEAM_COLL_IS_PARENT_COLL, + TEAM_COL_SAME_NEXT_COLL, + TEAM_COL_REORDERING_FAILED, } from '../errors'; import { PubSubService } from '../pubsub/pubsub.service'; -import { throwErr } from 'src/utils'; -import { pipe } from 'fp-ts/function'; -import * as TO from 'fp-ts/TaskOption'; +import { isValidLength } from 'src/utils'; +import * as E from 'fp-ts/Either'; +import * as O from 'fp-ts/Option'; +import { Prisma, TeamCollection as DBTeamCollection } from '@prisma/client'; +import { CollectionFolder } from 'src/types/CollectionFolder'; +import { stringToJson } from 'src/utils'; @Injectable() export class TeamCollectionService { constructor( private readonly prisma: PrismaService, - // private readonly fb: FirebaseService, private readonly pubsub: PubSubService, ) {} - // TODO: Change return type - // private generatePrismaQueryObjForFBCollFolder( - // folder: FBCollectionFolder, - // teamID: string, - // ): any { - // return { - // title: folder.name, - // team: { - // connect: { - // id: teamID, - // }, - // }, - // requests: { - // create: folder.requests.map((r) => ({ - // title: r.name, - // team: { - // connect: { - // id: teamID, - // }, - // }, - // request: r, - // })), - // }, - // children: { - // create: folder.folders.map((f) => - // this.generatePrismaQueryObjForFBCollFolder(f, teamID), - // ), - // }, - // }; - // } + TITLE_LENGTH = 3; + + /** + * Generate a Prisma query object representation of a collection and its child collections and requests + * + * @param folder CollectionFolder from client + * @param teamID The Team ID + * @param orderIndex Initial OrderIndex of + * @returns A Prisma query object to create a collection, its child collections and requests + */ + private generatePrismaQueryObjForFBCollFolder( + folder: CollectionFolder, + teamID: string, + orderIndex: number, + ): Prisma.TeamCollectionCreateInput { + return { + title: folder.name, + team: { + connect: { + id: teamID, + }, + }, + requests: { + create: folder.requests.map((r, index) => ({ + title: r.name, + team: { + connect: { + id: teamID, + }, + }, + request: r, + orderIndex: index + 1, + })), + }, + orderIndex: orderIndex, + children: { + create: folder.folders.map((f, index) => + this.generatePrismaQueryObjForFBCollFolder(f, teamID, index + 1), + ), + }, + }; + } + + /** + * Generate a JSON containing all the contents of a collection + * + * @param teamID The Team ID + * @param collectionID The Collection ID + * @returns A JSON string containing all the contents of a collection + */ + private async exportCollectionToJSONObject( + teamID: string, + collectionID: string, + ) { + const collection = await this.getCollection(collectionID); + if (E.isLeft(collection)) return E.left(TEAM_INVALID_COLL_ID); + + const childrenCollection = await this.prisma.teamCollection.findMany({ + where: { + teamID, + parentID: collectionID, + }, + orderBy: { + orderIndex: 'asc', + }, + }); + + const childrenCollectionObjects = []; + for (const coll of childrenCollection) { + const result = await this.exportCollectionToJSONObject(teamID, coll.id); + if (E.isLeft(result)) return E.left(result.left); + + childrenCollectionObjects.push(result.right); + } + + const requests = await this.prisma.teamRequest.findMany({ + where: { + teamID, + collectionID, + }, + orderBy: { + orderIndex: 'asc', + }, + }); + + const result: CollectionFolder = { + name: collection.right.title, + folders: childrenCollectionObjects, + requests: requests.map((x) => x.request), + }; + + return E.right(result); + } + + /** + * Generate a JSON containing all the contents of collections and requests of a team + * + * @param teamID The Team ID + * @returns A JSON string containing all the contents of collections and requests of a team + */ + async exportCollectionsToJSON(teamID: string) { + const rootCollections = await this.prisma.teamCollection.findMany({ + where: { + teamID, + parentID: null, + }, + }); + + const rootCollectionObjects = []; + for (const coll of rootCollections) { + const result = await this.exportCollectionToJSONObject(teamID, coll.id); + if (E.isLeft(result)) return E.left(result.left); + + rootCollectionObjects.push(result.right); + } + + return E.right(JSON.stringify(rootCollectionObjects)); + } + + /** + * Create new TeamCollections and TeamRequests from JSON string + * + * @param jsonString The JSON string of the content + * @param destTeamID The Team ID + * @param destCollectionID The Collection ID + * @returns An Either of a Boolean if the creation operation was successful + */ + async importCollectionsFromJSON( + jsonString: string, + destTeamID: string, + destCollectionID: string | null, + ) { + // Check to see if jsonString is valid + const collectionsList = stringToJson(jsonString); + if (E.isLeft(collectionsList)) return E.left(TEAM_COLL_INVALID_JSON); + + // Check to see if parsed jsonString is an array + if (!Array.isArray(collectionsList.right)) + return E.left(TEAM_COLL_INVALID_JSON); + + // Get number of root or child collections for destCollectionID(if destcollectionID != null) or destTeamID(if destcollectionID == null) + const count = !destCollectionID + ? await this.getRootCollectionsCount(destTeamID) + : await this.getChildCollectionsCount(destCollectionID); + + // Generate Prisma Query Object for all child collections in collectionsList + const queryList = collectionsList.right.map((x) => + this.generatePrismaQueryObjForFBCollFolder(x, destTeamID, count + 1), + ); + + const parent = destCollectionID + ? { + connect: { + id: destCollectionID, + }, + } + : undefined; + + const teamCollections = await this.prisma.$transaction( + queryList.map((x) => + this.prisma.teamCollection.create({ + data: { + ...x, + parent, + }, + }), + ), + ); + + teamCollections.forEach((x) => + this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x), + ); + + return E.right(true); + } + + /** + * Replace all the existing contents of a collection (or root collections) with data from JSON String + * + * @param jsonString The JSON string of the content + * @param destTeamID The Team ID + * @param destCollectionID The Collection ID + * @returns An Either of a Boolean if the operation was successful + */ + async replaceCollectionsWithJSON( + jsonString: string, + destTeamID: string, + destCollectionID: string | null, + ) { + // Check to see if jsonString is valid + const collectionsList = stringToJson(jsonString); + if (E.isLeft(collectionsList)) return E.left(TEAM_COLL_INVALID_JSON); + + // Check to see if parsed jsonString is an array + if (!Array.isArray(collectionsList.right)) + return E.left(TEAM_COLL_INVALID_JSON); + + // Fetch all child collections of destCollectionID + const childrenCollection = await this.prisma.teamCollection.findMany({ + where: { + teamID: destTeamID, + parentID: destCollectionID, + }, + }); + + for (const coll of childrenCollection) { + const deletedTeamCollection = await this.deleteCollection(coll.id); + if (E.isLeft(deletedTeamCollection)) + return E.left(deletedTeamCollection.left); + } + + // Get number of root or child collections for destCollectionID(if destcollectionID != null) or destTeamID(if destcollectionID == null) + const count = !destCollectionID + ? await this.getRootCollectionsCount(destTeamID) + : await this.getChildCollectionsCount(destCollectionID); + + const queryList = collectionsList.right.map((x) => + this.generatePrismaQueryObjForFBCollFolder(x, destTeamID, count + 1), + ); + + const parent = destCollectionID + ? { + connect: { + id: destCollectionID, + }, + } + : undefined; + + const teamCollections = await this.prisma.$transaction( + queryList.map((x) => + this.prisma.teamCollection.create({ + data: { + ...x, + parent, + }, + }), + ), + ); + + teamCollections.forEach((x) => + this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x), + ); + + return E.right(true); + } + + /** + * Typecast a database TeamCollection to a TeamCollection model + * @param teamCollection database TeamCollection + * @returns TeamCollection model + */ + private cast(teamCollection: DBTeamCollection): TeamCollection { + return { ...teamCollection }; + } + + /** + * Get Team of given Collection ID + * + * @param collectionID The collection ID + * @returns Team of given Collection ID + */ + async getTeamOfCollection(collectionID: string) { + try { + const teamCollection = await this.prisma.teamCollection.findUnique({ + where: { + id: collectionID, + }, + include: { + team: true, + }, + }); + + return E.right(teamCollection.team); + } catch (error) { + return E.left(TEAM_INVALID_COLL_ID); + } + } + + /** + * Get parent of given Collection ID + * + * @param collectionID The collection ID + * @returns Parent TeamCollection of given Collection ID + */ + async getParentOfCollection(collectionID: string) { + const teamCollection = await this.prisma.teamCollection.findUnique({ + where: { + id: collectionID, + }, + include: { + parent: true, + }, + }); + if (!teamCollection) return null; + + return teamCollection.parent; + } + + /** + * Get child collections of given Collection ID + * + * @param collectionID The collection ID + * @param cursor collectionID for pagination + * @param take Number of items we want returned + * @returns A list of child collections + */ + getChildrenOfCollection( + collectionID: string, + cursor: string | null, + take: number, + ) { + return this.prisma.teamCollection.findMany({ + where: { + parentID: collectionID, + }, + orderBy: { + orderIndex: 'asc', + }, + take: take, // default: 10 + skip: cursor ? 1 : 0, + cursor: cursor ? { id: cursor } : undefined, + }); + } + + /** + * Get root collections of given Collection ID + * + * @param teamID The Team ID + * @param cursor collectionID for pagination + * @param take Number of items we want returned + * @returns A list of root TeamCollections + */ + async getTeamRootCollections( + teamID: string, + cursor: string | null, + take: number, + ) { + return this.prisma.teamCollection.findMany({ + where: { + teamID, + parentID: null, + }, + orderBy: { + orderIndex: 'asc', + }, + take: take, // default: 10 + skip: cursor ? 1 : 0, + cursor: cursor ? { id: cursor } : undefined, + }); + } + + /** + * Get collection details + * + * @param collectionID The collection ID + * @returns An Either of the Collection details + */ + async getCollection(collectionID: string) { + try { + const teamCollection = await this.prisma.teamCollection.findUniqueOrThrow( + { + where: { + id: collectionID, + }, + }, + ); + return E.right(teamCollection); + } catch (error) { + return E.left(TEAM_COLL_NOT_FOUND); + } + } + + /** + * Check to see if Collection belongs to Team + * + * @param collectionID getChildCollectionsCount + * @param teamID The Team ID + * @returns An Option of a Boolean + */ + private async isOwnerCheck(collectionID: string, teamID: string) { + try { + await this.prisma.teamCollection.findFirstOrThrow({ + where: { + id: collectionID, + teamID, + }, + }); + + return O.some(true); + } catch (error) { + return O.none; + } + } + + /** + * Returns the count of child collections present for a given collectionID + * * The count returned is highest OrderIndex + 1 + * + * @param collectionID The Collection ID + * @returns Number of Child Collections + */ + private async getChildCollectionsCount(collectionID: string) { + const childCollectionCount = await this.prisma.teamCollection.findMany({ + where: { parentID: collectionID }, + orderBy: { + orderIndex: 'desc', + }, + }); + if (!childCollectionCount.length) return 0; + return childCollectionCount[0].orderIndex; + } + + /** + * Returns the count of root collections present for a given teamID + * * The count returned is highest OrderIndex + 1 + * + * @param teamID The Team ID + * @returns Number of Root Collections + */ + private async getRootCollectionsCount(teamID: string) { + const rootCollectionCount = await this.prisma.teamCollection.findMany({ + where: { teamID, parentID: null }, + orderBy: { + orderIndex: 'desc', + }, + }); + if (!rootCollectionCount.length) return 0; + return rootCollectionCount[0].orderIndex; + } + + /** + * Create a new TeamCollection + * + * @param teamID The Team ID + * @param title The title of new TeamCollection + * @param parentTeamCollectionID The parent collectionID (null if root collection) + * @returns An Either of TeamCollection + */ + async createCollection( + teamID: string, + title: string, + parentTeamCollectionID: string | null, + ) { + const isTitleValid = isValidLength(title, this.TITLE_LENGTH); + if (!isTitleValid) return E.left(TEAM_COLL_SHORT_TITLE); + + // Check to see if parentTeamCollectionID belongs to this Team + if (parentTeamCollectionID !== null) { + const isOwner = await this.isOwnerCheck(parentTeamCollectionID, teamID); + if (O.isNone(isOwner)) return E.left(TEAM_NOT_OWNER); + } + + const isParent = parentTeamCollectionID + ? { + connect: { + id: parentTeamCollectionID, + }, + } + : undefined; + + const teamCollection = await this.prisma.teamCollection.create({ + data: { + title: title, + team: { + connect: { + id: teamID, + }, + }, + parent: isParent, + orderIndex: !parentTeamCollectionID + ? (await this.getRootCollectionsCount(teamID)) + 1 + : (await this.getChildCollectionsCount(parentTeamCollectionID)) + 1, + }, + }); + + this.pubsub.publish(`team_coll/${teamID}/coll_added`, teamCollection); + + return E.right(this.cast(teamCollection)); + } + + /** + * Update the title of a TeamCollection + * + * @param collectionID The Collection ID + * @param newTitle The new title of collection + * @returns An Either of the updated TeamCollection + */ + async renameCollection(collectionID: string, newTitle: string) { + const isTitleValid = isValidLength(newTitle, this.TITLE_LENGTH); + if (!isTitleValid) return E.left(TEAM_COLL_SHORT_TITLE); + + try { + const updatedTeamCollection = await this.prisma.teamCollection.update({ + where: { + id: collectionID, + }, + data: { + title: newTitle, + }, + }); + + this.pubsub.publish( + `team_coll/${updatedTeamCollection.teamID}/coll_updated`, + updatedTeamCollection, + ); + + return E.right(updatedTeamCollection); + } catch (error) { + return E.left(TEAM_COLL_NOT_FOUND); + } + } + + /** + * Update the OrderIndex of all collections in given parentID + * + * @param parentID The Parent collectionID + * @param orderIndexCondition Condition to decide what collections will be updated + * @param dataCondition Increment/Decrement OrderIndex condition + * @returns A Collection with updated OrderIndexes + */ + private async updateOrderIndex( + parentID: string, + orderIndexCondition: Prisma.IntFilter, + dataCondition: Prisma.IntFieldUpdateOperationsInput, + ) { + const updatedTeamCollection = await this.prisma.teamCollection.updateMany({ + where: { + parentID: parentID, + orderIndex: orderIndexCondition, + }, + data: { orderIndex: dataCondition }, + }); + + return updatedTeamCollection; + } + + /** + * Delete a TeamCollection from the DB + * + * @param collectionID The Collection Id + * @returns The deleted TeamCollection + */ + private async removeTeamCollection(collectionID: string) { + try { + const deletedTeamCollection = await this.prisma.teamCollection.delete({ + where: { + id: collectionID, + }, + }); + + return E.right(deletedTeamCollection); + } catch (error) { + return E.left(TEAM_COLL_NOT_FOUND); + } + } + + /** + * Delete child collection and requests of a TeamCollection + * + * @param collectionID The Collection Id + * @returns A Boolean of deletion status + */ + private async deleteCollectionData(collection: DBTeamCollection) { + // Get all child collections in collectionID + const childCollectionList = await this.prisma.teamCollection.findMany({ + where: { + parentID: collection.id, + }, + }); + + // Delete child collections + await Promise.all( + childCollectionList.map((coll) => this.deleteCollection(coll.id)), + ); + + // Delete all requests in collectionID + await this.prisma.teamRequest.deleteMany({ + where: { + collectionID: collection.id, + }, + }); + + // // Update orderIndexes in TeamCollection table for user + // await this.updateOrderIndex( + // collection.parentID, + // { gt: collection.orderIndex }, + // { decrement: 1 }, + // ); + + // Delete collection from TeamCollection table + const deletedTeamCollection = await this.removeTeamCollection( + collection.id, + ); + if (E.isLeft(deletedTeamCollection)) + return E.left(deletedTeamCollection.left); + + this.pubsub.publish( + `team_coll/${deletedTeamCollection.right.teamID}/coll_removed`, + deletedTeamCollection.right.id, + ); + + return E.right(deletedTeamCollection.right); + } + + /** + * Delete a TeamCollection + * + * @param collectionID The Collection Id + * @returns An Either of Boolean of deletion status + */ + async deleteCollection(collectionID: string) { + const collection = await this.getCollection(collectionID); + if (E.isLeft(collection)) return E.left(collection.left); + + // Delete all child collections and requests in the collection + const collectionData = await this.deleteCollectionData(collection.right); + if (E.isLeft(collectionData)) return E.left(collectionData.left); + + // Update orderIndexes in TeamCollection table for user + await this.updateOrderIndex( + collectionData.right.parentID, + { gt: collectionData.right.orderIndex }, + { decrement: 1 }, + ); + + return E.right(true); + } + + /** + * Change parentID of TeamCollection's + * + * @param collectionID The collection ID + * @param parentCollectionID The new parent's collection ID or change to root collection + * @returns If successful return an Either of true + */ + private async changeParent( + collection: DBTeamCollection, + parentCollectionID: string | null, + ) { + try { + let collectionCount: number; + + if (!parentCollectionID) + collectionCount = await this.getRootCollectionsCount(collection.teamID); + collectionCount = await this.getChildCollectionsCount(parentCollectionID); + + const updatedCollection = await this.prisma.teamCollection.update({ + where: { + id: collection.id, + }, + data: { + // if parentCollectionID == null, collection becomes root collection + // if parentCollectionID != null, collection becomes child collection + parentID: parentCollectionID, + orderIndex: collectionCount + 1, + }, + }); + + return E.right(this.cast(updatedCollection)); + } catch (error) { + return E.left(TEAM_COLL_NOT_FOUND); + } + } + + /** + * Check if collection is parent of destCollection + * + * @param collection The ID of collection being moved + * @param destCollection The ID of collection into which we are moving target collection into + * @returns An Option of boolean, is parent or not + */ + private async isParent( + collection: TeamCollection, + destCollection: TeamCollection, + ): Promise> { + //* Recursively check if collection is a parent by going up the tree of child-parent collections until we reach a root collection i.e parentID === null + //* Valid condition, isParent returns false + //* Consider us moving Collection_E into Collection_D + //* Collection_A [parent:null !== Collection_E] return false, exit + //* |--> Collection_B [parent:Collection_A !== Collection_E] call isParent(Collection_E,Collection_A) + //* |--> Collection_C [parent:Collection_B !== Collection_E] call isParent(Collection_E,Collection_B) + //* |--> Collection_D [parent:Collection_C !== Collection_E] call isParent(Collection_E,Collection_C) + //* Invalid condition, isParent returns true + //* Consider us moving Collection_B into Collection_D + //* Collection_A + //* |--> Collection_B + //* |--> Collection_C [parent:Collection_B === Collection_B] return true, exit + //* |--> Collection_D [parent:Collection_C !== Collection_B] call isParent(Collection_B,Collection_C) + + // Check if collection and destCollection are same + if (collection === destCollection) { + return O.none; + } + if (destCollection.parentID !== null) { + // Check if ID of collection is same as parent of destCollection + if (destCollection.parentID === collection.id) { + return O.none; + } + // Get collection details of collection one step above in the tree i.e the parent collection + const parentCollection = await this.getCollection( + destCollection.parentID, + ); + if (E.isLeft(parentCollection)) { + return O.none; + } + // Call isParent again now with parent collection + return await this.isParent(collection, parentCollection.right); + } else { + return O.some(true); + } + } + + /** + * Move TeamCollection into root or another collection + * + * @param collectionID The ID of collection being moved + * @param destCollectionID The ID of collection the target collection is being moved into or move target collection to root + * @returns An Either of the moved TeamCollection + */ + async moveCollection(collectionID: string, destCollectionID: string | null) { + // Get collection details of collectionID + const collection = await this.getCollection(collectionID); + if (E.isLeft(collection)) return E.left(collection.left); + + // destCollectionID == null i.e move collection to root + if (!destCollectionID) { + if (!collection.right.parentID) { + // collection is a root collection + // Throw error if collection is already a root collection + return E.left(TEAM_COL_ALREADY_ROOT); + } + // Move child collection into root and update orderIndexes for root teamCollections + await this.updateOrderIndex( + collection.right.parentID, + { gt: collection.right.orderIndex }, + { decrement: 1 }, + ); + + // Change parent from child to root i.e child collection becomes a root collection + const updatedCollection = await this.changeParent(collection.right, null); + if (E.isLeft(updatedCollection)) return E.left(updatedCollection.left); + + this.pubsub.publish( + `team_coll/${collection.right.teamID}/coll_moved`, + updatedCollection.right, + ); + + return E.right(updatedCollection.right); + } + + // destCollectionID != null i.e move into another collection + if (collectionID === destCollectionID) { + // Throw error if collectionID and destCollectionID are the same + return E.left(TEAM_COLL_DEST_SAME); + } + + // Get collection details of destCollectionID + const destCollection = await this.getCollection(destCollectionID); + if (E.isLeft(destCollection)) return E.left(TEAM_COLL_NOT_FOUND); + + // Check if collection and destCollection belong to the same user account + if (collection.right.teamID !== destCollection.right.teamID) { + return E.left(TEAM_COLL_NOT_SAME_TEAM); + } + + // Check if collection is present on the parent tree for destCollection + const checkIfParent = await this.isParent( + collection.right, + destCollection.right, + ); + if (O.isNone(checkIfParent)) { + return E.left(TEAM_COLL_IS_PARENT_COLL); + } + + // Move root/child collection into another child collection and update orderIndexes of the previous parent + await this.updateOrderIndex( + collection.right.parentID, + { gt: collection.right.orderIndex }, + { decrement: 1 }, + ); + + // Change parent from null to teamCollection i.e collection becomes a child collection + const updatedCollection = await this.changeParent( + collection.right, + destCollection.right.id, + ); + if (E.isLeft(updatedCollection)) return E.left(updatedCollection.left); + + this.pubsub.publish( + `team_coll/${collection.right.teamID}/coll_moved`, + updatedCollection.right, + ); + + return E.right(updatedCollection.right); + } + + /** + * Find the number of child collections present in collectionID + * + * @param collectionID The Collection ID + * @returns Number of collections + */ + getCollectionCount(collectionID: string): Promise { + return this.prisma.teamCollection.count({ + where: { parentID: collectionID }, + }); + } + + /** + * Update order of root or child collectionID's + * + * @param collectionID The ID of collection being re-ordered + * @param nextCollectionID The ID of collection that is after the moved collection in its new position + * @returns If successful return an Either of true + */ + async updateCollectionOrder( + collectionID: string, + nextCollectionID: string | null, + ) { + // Throw error if collectionID and nextCollectionID are the same + if (collectionID === nextCollectionID) + return E.left(TEAM_COL_SAME_NEXT_COLL); + + // Get collection details of collectionID + const collection = await this.getCollection(collectionID); + if (E.isLeft(collection)) return E.left(collection.left); + + if (!nextCollectionID) { + // nextCollectionID == null i.e move collection to the end of the list + try { + await this.prisma.$transaction(async (tx) => { + // Step 1: Decrement orderIndex of all items that come after collection.orderIndex till end of list of items + await tx.teamCollection.updateMany({ + where: { + parentID: collection.right.parentID, + orderIndex: { + gte: collection.right.orderIndex + 1, + }, + }, + data: { + orderIndex: { decrement: 1 }, + }, + }); + // Step 2: Update orderIndex of collection to length of list + const updatedTeamCollection = await tx.teamCollection.update({ + where: { id: collection.right.id }, + data: { + orderIndex: await this.getCollectionCount( + collection.right.parentID, + ), + }, + }); + }); + + this.pubsub.publish( + `team_coll/${collection.right.teamID}/coll_order_updated`, + { + collection: this.cast(collection.right), + nextCollection: null, + }, + ); + + return E.right(true); + } catch (error) { + return E.left(TEAM_COL_REORDERING_FAILED); + } + } + + // nextCollectionID != null i.e move to a certain position + // Get collection details of nextCollectionID + const subsequentCollection = await this.getCollection(nextCollectionID); + if (E.isLeft(subsequentCollection)) return E.left(TEAM_COLL_NOT_FOUND); + + // Check if collection and subsequentCollection belong to the same collection team + if (collection.right.teamID !== subsequentCollection.right.teamID) + return E.left(TEAM_COLL_NOT_SAME_TEAM); + + try { + await this.prisma.$transaction(async (tx) => { + // Step 1: Determine if we are moving collection up or down the list + const isMovingUp = + subsequentCollection.right.orderIndex < collection.right.orderIndex; + // Step 2: Update OrderIndex of items in list depending on moving up or down + const updateFrom = isMovingUp + ? subsequentCollection.right.orderIndex + : collection.right.orderIndex + 1; + + const updateTo = isMovingUp + ? collection.right.orderIndex - 1 + : subsequentCollection.right.orderIndex - 1; + + await tx.teamCollection.updateMany({ + where: { + parentID: collection.right.parentID, + orderIndex: { gte: updateFrom, lte: updateTo }, + }, + data: { + orderIndex: isMovingUp ? { increment: 1 } : { decrement: 1 }, + }, + }); + // Step 3: Update OrderIndex of collection + const updatedTeamCollection = await tx.teamCollection.update({ + where: { id: collection.right.id }, + data: { + orderIndex: isMovingUp + ? subsequentCollection.right.orderIndex + : subsequentCollection.right.orderIndex - 1, + }, + }); + }); + + this.pubsub.publish( + `team_coll/${collection.right.teamID}/coll_order_updated`, + { + collection: this.cast(collection.right), + nextCollection: this.cast(subsequentCollection.right), + }, + ); + + return E.right(true); + } catch (error) { + return E.left(TEAM_COL_REORDERING_FAILED); + } + } // async importCollectionFromFirestore( // userUid: string, @@ -111,399 +1013,4 @@ export class TeamCollectionService { // return result; // } - - // private async exportCollectionToJSONObject( - // teamID: string, - // collectionID: string, - // ): Promise { - // const collection = await this.getCollection(collectionID); - - // if (!collection) throw new Error(TEAM_INVALID_COLL_ID) - - // const childrenCollection = await this.prisma.teamCollection.findMany({ - // where: { - // teamID, - // parentID: collectionID, - // }, - // }); - - // const childrenCollectionObjects = await Promise.all( - // childrenCollection.map((coll) => - // this.exportCollectionToJSONObject(teamID, coll.id), - // ), - // ); - - // const requests = await this.prisma.teamRequest.findMany({ - // where: { - // teamID, - // collectionID, - // }, - // }); - - // return { - // name: collection.title, - // folders: childrenCollectionObjects, - // requests: requests.map((x) => x.request), - // }; - // } - - // async exportCollectionsToJSON(teamID: string): Promise { - // const rootCollections = await this.prisma.teamCollection.findMany({ - // where: { - // teamID, - // parentID: null, - // }, - // }); - - // const rootCollectionObjects = await Promise.all( - // rootCollections.map((coll) => - // this.exportCollectionToJSONObject(teamID, coll.id), - // ), - // ); - - // return JSON.stringify(rootCollectionObjects); - // } - - // async importCollectionsFromJSON( - // jsonString: string, - // destTeamID: string, - // destCollectionID: string | null, - // ): Promise { - // let collectionsList: FBCollectionFolder[]; - - // try { - // collectionsList = JSON.parse(jsonString); - - // if (!Array.isArray(collectionsList)) - // throw new Error(TEAM_COLL_INVALID_JSON); - // } catch (e) { - // throw new Error(TEAM_COLL_INVALID_JSON); - // } - - // const queryList = collectionsList.map((x) => - // this.generatePrismaQueryObjForFBCollFolder(x, destTeamID), - // ); - - // let requests: TeamCollection[]; - - // if (destCollectionID) { - // requests = await this.prisma.$transaction( - // queryList.map((x) => - // this.prisma.teamCollection.create({ - // data: { - // ...x, - // parent: { - // connect: { - // id: destCollectionID, - // }, - // }, - // }, - // }), - // ), - // ); - // } else { - // requests = await this.prisma.$transaction( - // queryList.map((x) => - // this.prisma.teamCollection.create({ - // data: { - // ...x, - // }, - // }), - // ), - // ); - // } - - // requests.forEach((x) => - // this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x), - // ); - // } - - // async replaceCollectionsWithJSON( - // jsonString: string, - // destTeamID: string, - // destCollectionID: string | null, - // ): Promise { - // let collectionsList: FBCollectionFolder[]; - - // try { - // collectionsList = JSON.parse(jsonString); - - // if (!Array.isArray(collectionsList)) - // throw new Error(TEAM_COLL_INVALID_JSON); - // } catch (e) { - // throw new Error(TEAM_COLL_INVALID_JSON); - // } - // const childrenCollection = await this.prisma.teamCollection.findMany({ - // where: { - // teamID: destTeamID, - // parentID: destCollectionID, - // }, - // }); - - // await Promise.all( - // childrenCollection.map(async (coll) => { - // await this.deleteCollection(coll.id); - // }), - // ); - - // const queryList = collectionsList.map((x) => - // this.generatePrismaQueryObjForFBCollFolder(x, destTeamID), - // ); - - // let requests: TeamCollection[]; - - // if (destCollectionID) { - // requests = await this.prisma.$transaction( - // queryList.map((x) => - // this.prisma.teamCollection.create({ - // data: { - // ...x, - // parent: { - // connect: { - // id: destCollectionID, - // }, - // }, - // }, - // }), - // ), - // ); - // } else { - // requests = await this.prisma.$transaction( - // queryList.map((x) => - // this.prisma.teamCollection.create({ - // data: { - // ...x, - // }, - // }), - // ), - // ); - // } - - // requests.forEach((x) => - // this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x), - // ); - // } - - async getTeamOfCollection(collectionID: string): Promise { - const { team } = - (await this.prisma.teamCollection.findUnique({ - where: { - id: collectionID, - }, - include: { - team: true, - }, - })) ?? throwErr(TEAM_INVALID_COLL_ID); - - return team; - } - - async getParentOfCollection( - collectionID: string, - ): Promise { - const { parent } = - (await this.prisma.teamCollection.findUnique({ - where: { - id: collectionID, - }, - include: { - parent: true, - }, - })) ?? throwErr(TEAM_INVALID_COLL_ID); - - return parent; - } - - getChildrenOfCollection( - collectionID: string, - cursor: string | null, - ): Promise { - if (!cursor) { - return this.prisma.teamCollection.findMany({ - take: 10, - where: { - parent: { - id: collectionID, - }, - }, - }); - } else { - return this.prisma.teamCollection.findMany({ - take: 10, - skip: 1, - cursor: { - id: cursor, - }, - where: { - parent: { - id: collectionID, - }, - }, - }); - } - } - - async getTeamRootCollections( - teamID: string, - cursor: string | null, - ): Promise { - if (!cursor) { - return await this.prisma.teamCollection.findMany({ - take: 10, - where: { - teamID, - parentID: null, - }, - }); - } else { - return this.prisma.teamCollection.findMany({ - take: 10, - skip: 1, - cursor: { - id: cursor, - }, - where: { - teamID, - parentID: null, - }, - }); - } - } - - getTeamCollections( - teamID: string, - cursor: string | null, - ): Promise { - if (!cursor) { - return this.prisma.teamCollection.findMany({ - take: 10, - where: { - teamID, - }, - }); - } else { - return this.prisma.teamCollection.findMany({ - take: 10, - skip: 1, - cursor: { - id: cursor, - }, - where: { - teamID, - }, - }); - } - } - - getCollection(collectionID: string): Promise { - return this.prisma.teamCollection.findUnique({ - where: { - id: collectionID, - }, - }); - } - - getCollectionTO(collectionID: string): TO.TaskOption { - return pipe( - TO.fromTask(() => this.getCollection(collectionID)), - TO.chain(TO.fromNullable), - ); - } - - async createCollection( - teamID: string, - title: string, - parentID: string | null, - ): Promise { - if (title.length < 3) { - throw new Error(TEAM_COLL_SHORT_TITLE); - } - - let result: TeamCollection; - - if (!parentID) { - result = await this.prisma.teamCollection.create({ - data: { - title: title, - team: { - connect: { - id: teamID, - }, - }, - }, - }); - } else { - result = await this.prisma.teamCollection.create({ - data: { - title: title, - team: { - connect: { - id: teamID, - }, - }, - parent: { - connect: { - id: parentID, - }, - }, - }, - }); - } - - this.pubsub.publish(`team_coll/${teamID}/coll_added`, result); - - return result; - } - - async renameCollection( - collectionID: string, - newTitle: string, - ): Promise { - if (newTitle.length < 3) { - throw new Error(TEAM_COLL_SHORT_TITLE); - } - - const res = await this.prisma.teamCollection.update({ - where: { - id: collectionID, - }, - data: { - title: newTitle, - }, - }); - - this.pubsub.publish(`team_coll/${res.teamID}/coll_updated`, res); - - return res; - } - - async deleteCollection(collectionID: string): Promise { - const coll = - (await this.getCollection(collectionID)) ?? - throwErr(TEAM_INVALID_COLL_ID); - - const childrenCollection = await this.prisma.teamCollection.findMany({ - where: { - parentID: coll.id, - }, - }); - - await Promise.all( - childrenCollection.map((coll) => this.deleteCollection(coll.id)), - ); - - await this.prisma.teamRequest.deleteMany({ - where: { - collectionID: coll.id, - }, - }); - - await this.prisma.teamCollection.delete({ - where: { - id: collectionID, - }, - }); - - this.pubsub.publish(`team_coll/${coll.teamID}/coll_removed`, coll); - } } diff --git a/packages/hoppscotch-backend/src/team-request/team-request.resolver.ts b/packages/hoppscotch-backend/src/team-request/team-request.resolver.ts index f78ca4626..b0beae42b 100644 --- a/packages/hoppscotch-backend/src/team-request/team-request.resolver.ts +++ b/packages/hoppscotch-backend/src/team-request/team-request.resolver.ts @@ -43,13 +43,13 @@ export class TeamRequestResolver { return this.teamRequestService.getTeamOfRequest(req); } - @ResolveField(() => TeamCollection, { - description: 'Collection the request belongs to', - complexity: 3, - }) - collection(@Parent() req: TeamRequest): Promise { - return this.teamRequestService.getCollectionOfRequest(req); - } + // @ResolveField(() => TeamCollection, { + // description: 'Collection the request belongs to', + // complexity: 3, + // }) + // collection(@Parent() req: TeamRequest): Promise { + // return this.teamRequestService.getCollectionOfRequest(req); + // } // Query @Query(() => [TeamRequest], { @@ -126,29 +126,29 @@ export class TeamRequestResolver { ); } - // Mutation - @Mutation(() => TeamRequest, { - description: 'Create a request in the given collection.', - }) - @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) - @RequiresTeamRole(TeamMemberRole.EDITOR, TeamMemberRole.OWNER) - createRequestInCollection( - @Args({ - name: 'collectionID', - description: 'ID of the collection', - type: () => ID, - }) - collectionID: string, - @Args({ - name: 'data', - type: () => CreateTeamRequestInput, - description: - 'The request data (stringified JSON of Hoppscotch request object)', - }) - data: CreateTeamRequestInput, - ): Promise { - return this.teamRequestService.createTeamRequest(collectionID, data); - } + // // Mutation + // @Mutation(() => TeamRequest, { + // description: 'Create a request in the given collection.', + // }) + // @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) + // @RequiresTeamRole(TeamMemberRole.EDITOR, TeamMemberRole.OWNER) + // createRequestInCollection( + // @Args({ + // name: 'collectionID', + // description: 'ID of the collection', + // type: () => ID, + // }) + // collectionID: string, + // @Args({ + // name: 'data', + // type: () => CreateTeamRequestInput, + // description: + // 'The request data (stringified JSON of Hoppscotch request object)', + // }) + // data: CreateTeamRequestInput, + // ): Promise { + // return this.teamRequestService.createTeamRequest(collectionID, data); + // } @Mutation(() => TeamRequest, { description: 'Update a request with the given ID', @@ -190,30 +190,30 @@ export class TeamRequestResolver { return true; } - @Mutation(() => TeamRequest, { - description: 'Move a request to the given collection', - }) - @UseGuards(GqlAuthGuard, GqlRequestTeamMemberGuard) - @RequiresTeamRole(TeamMemberRole.EDITOR, TeamMemberRole.OWNER) - moveRequest( - @Args({ - name: 'requestID', - description: 'ID of the request to move', - type: () => ID, - }) - requestID: string, - @Args({ - name: 'destCollID', - description: 'ID of the collection to move the request to', - type: () => ID, - }) - destCollID: string, - ): Promise { - return pipe( - this.teamRequestService.moveRequest(requestID, destCollID), - TE.getOrElse((e) => throwErr(e)), - )(); - } + // @Mutation(() => TeamRequest, { + // description: 'Move a request to the given collection', + // }) + // @UseGuards(GqlAuthGuard, GqlRequestTeamMemberGuard) + // @RequiresTeamRole(TeamMemberRole.EDITOR, TeamMemberRole.OWNER) + // moveRequest( + // @Args({ + // name: 'requestID', + // description: 'ID of the request to move', + // type: () => ID, + // }) + // requestID: string, + // @Args({ + // name: 'destCollID', + // description: 'ID of the collection to move the request to', + // type: () => ID, + // }) + // destCollID: string, + // ): Promise { + // return pipe( + // this.teamRequestService.moveRequest(requestID, destCollID), + // TE.getOrElse((e) => throwErr(e)), + // )(); + // } // Subscriptions @Subscription(() => TeamRequest, { diff --git a/packages/hoppscotch-backend/src/team-request/team-request.service.ts b/packages/hoppscotch-backend/src/team-request/team-request.service.ts index b59f62b84..e1345c96c 100644 --- a/packages/hoppscotch-backend/src/team-request/team-request.service.ts +++ b/packages/hoppscotch-backend/src/team-request/team-request.service.ts @@ -19,7 +19,7 @@ import { PubSubService } from 'src/pubsub/pubsub.service'; import { throwErr } from 'src/utils'; import { pipe } from 'fp-ts/function'; import * as TO from 'fp-ts/TaskOption'; -import * as TE from 'fp-ts/TaskEither'; +import * as E from 'fp-ts/Either'; import { Prisma } from '@prisma/client'; @Injectable() @@ -127,43 +127,41 @@ export class TeamRequestService { this.pubsub.publish(`team_req/${req.teamID}/req_deleted`, requestID); } - async createTeamRequest( - collectionID: string, - input: CreateTeamRequestInput, - ): Promise { - const team = await this.teamCollectionService.getTeamOfCollection( - collectionID, - ); + // async createTeamRequest(collectionID: string, input: CreateTeamRequestInput) { + // const team = await this.teamCollectionService.getTeamOfCollection( + // collectionID, + // ); + // if (E.isLeft(team)) return []; - const data = await this.prisma.teamRequest.create({ - data: { - team: { - connect: { - id: team.id, - }, - }, - request: JSON.parse(input.request), - title: input.title, - collection: { - connect: { - id: collectionID, - }, - }, - }, - }); + // const data = await this.prisma.teamRequest.create({ + // data: { + // team: { + // connect: { + // id: team.right.id, + // }, + // }, + // request: JSON.parse(input.request), + // title: input.title, + // collection: { + // connect: { + // id: collectionID, + // }, + // }, + // }, + // }); - const result = { - id: data.id, - collectionID: data.collectionID, - title: data.title, - request: JSON.stringify(data.request), - teamID: data.teamID, - }; + // const result = { + // id: data.id, + // collectionID: data.collectionID, + // title: data.title, + // request: JSON.stringify(data.request), + // teamID: data.teamID, + // }; - this.pubsub.publish(`team_req/${result.teamID}/req_created`, result); + // this.pubsub.publish(`team_req/${result.teamID}/req_created`, result); - return result; - } + // return result; + // } async getRequestsInCollection( collectionID: string, @@ -242,12 +240,12 @@ export class TeamRequestService { ); } - async getCollectionOfRequest(req: TeamRequest): Promise { - return ( - (await this.teamCollectionService.getCollection(req.collectionID)) ?? - throwErr(TEAM_INVALID_COLL_ID) - ); - } + // async getCollectionOfRequest(req: TeamRequest): Promise { + // return ( + // (await this.teamCollectionService.getCollection(req.collectionID)) ?? + // throwErr(TEAM_INVALID_COLL_ID) + // ); + // } async getTeamOfRequestFromID(reqID: string): Promise { const req = @@ -263,69 +261,69 @@ export class TeamRequestService { return req.team; } - moveRequest(reqID: string, destinationCollID: string) { - return pipe( - TE.Do, + // moveRequest(reqID: string, destinationCollID: string) { + // return pipe( + // TE.Do, - // Check if the request exists - TE.bind('request', () => - pipe( - this.getRequestTO(reqID), - TE.fromTaskOption(() => TEAM_REQ_NOT_FOUND), - ), - ), + // // Check if the request exists + // TE.bind('request', () => + // pipe( + // this.getRequestTO(reqID), + // TE.fromTaskOption(() => TEAM_REQ_NOT_FOUND), + // ), + // ), - // Check if the destination collection exists (or null) - TE.bindW('targetCollection', () => - pipe( - this.teamCollectionService.getCollectionTO(destinationCollID), - TE.fromTaskOption(() => TEAM_REQ_INVALID_TARGET_COLL_ID), - ), - ), + // // Check if the destination collection exists (or null) + // TE.bindW('targetCollection', () => + // pipe( + // this.teamCollectionService.getCollectionTO(destinationCollID), + // TE.fromTaskOption(() => TEAM_REQ_INVALID_TARGET_COLL_ID), + // ), + // ), - // Block operation if target collection is not part of the same team - // as the request - TE.chainW( - TE.fromPredicate( - ({ request, targetCollection }) => - request.teamID === targetCollection.teamID, - () => TEAM_REQ_INVALID_TARGET_COLL_ID, - ), - ), + // // Block operation if target collection is not part of the same team + // // as the request + // TE.chainW( + // TE.fromPredicate( + // ({ request, targetCollection }) => + // request.teamID === targetCollection.teamID, + // () => TEAM_REQ_INVALID_TARGET_COLL_ID, + // ), + // ), - // Update the collection - TE.chain(({ request, targetCollection }) => - TE.fromTask(() => - this.prisma.teamRequest.update({ - where: { - id: request.id, - }, - data: { - collectionID: targetCollection.id, - }, - }), - ), - ), + // // Update the collection + // TE.chain(({ request, targetCollection }) => + // TE.fromTask(() => + // this.prisma.teamRequest.update({ + // where: { + // id: request.id, + // }, + // data: { + // collectionID: targetCollection.id, + // }, + // }), + // ), + // ), - // Generate TeamRequest model object - TE.map( - (request) => - { - id: request.id, - collectionID: request.collectionID, - request: JSON.stringify(request.request), - teamID: request.teamID, - title: request.title, - }, - ), + // // Generate TeamRequest model object + // TE.map( + // (request) => + // { + // id: request.id, + // collectionID: request.collectionID, + // request: JSON.stringify(request.request), + // teamID: request.teamID, + // title: request.title, + // }, + // ), - // Update on PubSub - TE.chainFirst((req) => { - this.pubsub.publish(`team_req/${req.teamID}/req_deleted`, req.id); - this.pubsub.publish(`team_req/${req.teamID}/req_created`, req); + // // Update on PubSub + // TE.chainFirst((req) => { + // this.pubsub.publish(`team_req/${req.teamID}/req_deleted`, req.id); + // this.pubsub.publish(`team_req/${req.teamID}/req_created`, req); - return TE.of({}); // We don't care about the return type - }), - ); - } + // return TE.of({}); // We don't care about the return type + // }), + // ); + // } } diff --git a/packages/hoppscotch-backend/src/types/CollectionFolder.ts b/packages/hoppscotch-backend/src/types/CollectionFolder.ts new file mode 100644 index 000000000..ca2d24e72 --- /dev/null +++ b/packages/hoppscotch-backend/src/types/CollectionFolder.ts @@ -0,0 +1,7 @@ +import { Prisma } from '@prisma/client'; + +export interface CollectionFolder { + folders: CollectionFolder[]; + requests: any[]; + name: string; +} diff --git a/packages/hoppscotch-backend/src/user-collection/user-collection.resolver.ts b/packages/hoppscotch-backend/src/user-collection/user-collection.resolver.ts index ed882ca8d..a5a91841e 100644 --- a/packages/hoppscotch-backend/src/user-collection/user-collection.resolver.ts +++ b/packages/hoppscotch-backend/src/user-collection/user-collection.resolver.ts @@ -18,7 +18,6 @@ import { UserCollection, UserCollectionReorderData, } from './user-collections.model'; -import * as E from 'fp-ts/Either'; import { throwErr } from 'src/utils'; import { User } from 'src/user/user.model'; import { PaginationArgs } from 'src/types/input-types.args'; @@ -30,6 +29,7 @@ import { UpdateUserCollectionArgs, } from './input-type.args'; import { ReqType } from 'src/types/RequestTypes'; +import * as E from 'fp-ts/Either'; @Resolver(() => UserCollection) export class UserCollectionResolver { @@ -230,7 +230,7 @@ export class UserCollectionResolver { @Args() args: RenameUserCollectionsArgs, ) { const updatedUserCollection = - await this.userCollectionService.renameCollection( + await this.userCollectionService.renameUserCollection( args.newTitle, args.userCollectionID, user.uid, diff --git a/packages/hoppscotch-backend/src/user-collection/user-collection.service.spec.ts b/packages/hoppscotch-backend/src/user-collection/user-collection.service.spec.ts index ee5e3e6e2..934daad0e 100644 --- a/packages/hoppscotch-backend/src/user-collection/user-collection.service.spec.ts +++ b/packages/hoppscotch-backend/src/user-collection/user-collection.service.spec.ts @@ -692,9 +692,9 @@ describe('createUserCollection', () => { }); }); -describe('renameCollection', () => { +describe('renameUserCollection', () => { test('should throw USER_COLL_SHORT_TITLE when title is less than 3 characters', async () => { - const result = await userCollectionService.renameCollection( + const result = await userCollectionService.renameUserCollection( '', rootRESTUserCollection.id, user.uid, @@ -707,7 +707,7 @@ describe('renameCollection', () => { 'NotFoundError', ); - const result = await userCollectionService.renameCollection( + const result = await userCollectionService.renameUserCollection( 'validTitle', rootRESTUserCollection.id, 'op09', @@ -725,7 +725,7 @@ describe('renameCollection', () => { title: 'NewTitle', }); - const result = await userCollectionService.renameCollection( + const result = await userCollectionService.renameUserCollection( 'NewTitle', rootRESTUserCollection.id, user.uid, @@ -743,7 +743,7 @@ describe('renameCollection', () => { mockPrisma.userCollection.update.mockRejectedValueOnce('RecordNotFound'); - const result = await userCollectionService.renameCollection( + const result = await userCollectionService.renameUserCollection( 'NewTitle', 'invalidID', user.uid, @@ -761,7 +761,7 @@ describe('renameCollection', () => { title: 'NewTitle', }); - const result = await userCollectionService.renameCollection( + const result = await userCollectionService.renameUserCollection( 'NewTitle', rootRESTUserCollection.id, user.uid, diff --git a/packages/hoppscotch-backend/src/user-collection/user-collection.service.ts b/packages/hoppscotch-backend/src/user-collection/user-collection.service.ts index 572500076..9e31fec13 100644 --- a/packages/hoppscotch-backend/src/user-collection/user-collection.service.ts +++ b/packages/hoppscotch-backend/src/user-collection/user-collection.service.ts @@ -28,6 +28,11 @@ export class UserCollectionService { private readonly pubsub: PubSubService, ) {} + /** + * Typecast a database UserCollection to a UserCollection model + * @param userCollection database UserCollection + * @returns UserCollection model + */ private cast(collection: UserCollection) { return { ...collection, @@ -35,6 +40,13 @@ export class UserCollectionService { }; } + /** + * Returns the count of child collections present for a given collectionID + * * The count returned is highest OrderIndex + 1 + * + * @param collectionID The Collection ID + * @returns Number of Child Collections + */ private async getChildCollectionsCount(collectionID: string) { const childCollectionCount = await this.prisma.userCollection.findMany({ where: { parentID: collectionID }, @@ -46,6 +58,13 @@ export class UserCollectionService { return childCollectionCount[0].orderIndex; } + /** + * Returns the count of root collections present for a given userUID + * * The count returned is highest OrderIndex + 1 + * + * @param userID The User UID + * @returns Number of Root Collections + */ private async getRootCollectionsCount(userID: string) { const rootCollectionCount = await this.prisma.userCollection.findMany({ where: { userUid: userID, parentID: null }, @@ -57,6 +76,13 @@ export class UserCollectionService { return rootCollectionCount[0].orderIndex; } + /** + * Check to see if Collection belongs to User + * + * @param collectionID The collection ID + * @param userID The User ID + * @returns An Option of a Boolean + */ private async isOwnerCheck(collectionID: string, userID: string) { try { await this.prisma.userCollection.findFirstOrThrow({ @@ -72,6 +98,12 @@ export class UserCollectionService { } } + /** + * Get User of given Collection ID + * + * @param collectionID The collection ID + * @returns User of given Collection ID + */ async getUserOfCollection(collectionID: string) { try { const userCollection = await this.prisma.userCollection.findUniqueOrThrow( @@ -90,6 +122,12 @@ export class UserCollectionService { } } + /** + * Get parent of given Collection ID + * + * @param collectionID The collection ID + * @returns Parent UserCollection of given Collection ID + */ async getParentOfUserCollection(collectionID: string) { const { parent } = await this.prisma.userCollection.findUnique({ where: { @@ -103,6 +141,15 @@ export class UserCollectionService { return parent; } + /** + * Get child collections of given Collection ID + * + * @param collectionID The collection ID + * @param cursor collectionID for pagination + * @param take Number of items we want returned + * @param type Type of UserCollection + * @returns A list of child collections + */ async getChildrenOfUserCollection( collectionID: string, cursor: string | null, @@ -123,6 +170,12 @@ export class UserCollectionService { }); } + /** + * Get collection details + * + * @param collectionID The collection ID + * @returns An Either of the Collection details + */ async getUserCollection(collectionID: string) { try { const userCollection = await this.prisma.userCollection.findUniqueOrThrow( @@ -138,6 +191,15 @@ export class UserCollectionService { } } + /** + * Create a new UserCollection + * + * @param user The User object + * @param title The title of new UserCollection + * @param parentUserCollectionID The parent collectionID (null if root collection) + * @param type Type of Collection we want to create (REST/GQL) + * @returns + */ async createUserCollection( user: AuthUser, title: string, @@ -147,6 +209,7 @@ export class UserCollectionService { const isTitleValid = isValidLength(title, 3); if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE); + // Check to see if parentUserCollectionID belongs to this User if (parentUserCollectionID !== null) { const isOwner = await this.isOwnerCheck(parentUserCollectionID, user.uid); if (O.isNone(isOwner)) return E.left(USER_NOT_OWNER); @@ -181,6 +244,14 @@ export class UserCollectionService { return E.right(userCollection); } + /** + * + * @param user The User Object + * @param cursor collectionID for pagination + * @param take Number of items we want returned + * @param type Type of UserCollection + * @returns A list of root UserCollections + */ async getUserRootCollections( user: AuthUser, cursor: string | null, @@ -202,6 +273,15 @@ export class UserCollectionService { }); } + /** + * + * @param user The User Object + * @param userCollectionID The User UID + * @param cursor collectionID for pagination + * @param take Number of items we want returned + * @param type Type of UserCollection + * @returns A list of child UserCollections + */ async getUserChildCollections( user: AuthUser, userCollectionID: string, @@ -221,7 +301,15 @@ export class UserCollectionService { }); } - async renameCollection( + /** + * Update the title of a UserCollection + * + * @param newTitle The new title of collection + * @param userCollectionID The Collection Id + * @param userID The User UID + * @returns An Either of the updated UserCollection + */ + async renameUserCollection( newTitle: string, userCollectionID: string, userID: string, @@ -254,6 +342,12 @@ export class UserCollectionService { } } + /** + * Delete a UserCollection from the DB + * + * @param collectionID The Collection Id + * @returns The deleted UserCollection + */ private async removeUserCollection(collectionID: string) { try { const deletedUserCollection = await this.prisma.userCollection.delete({ @@ -268,6 +362,12 @@ export class UserCollectionService { } } + /** + * Delete child collection and requests of a UserCollection + * + * @param collectionID The Collection Id + * @returns A Boolean of deletion status + */ private async deleteCollectionData(collection: UserCollection) { // Get all child collections in collectionID const childCollectionList = await this.prisma.userCollection.findMany({ @@ -312,6 +412,13 @@ export class UserCollectionService { return E.right(true); } + /** + * Delete a UserCollection + * + * @param collectionID The Collection Id + * @param userID The User UID + * @returns An Either of Boolean of deletion status + */ async deleteUserCollection(collectionID: string, userID: string) { // Get collection details of collectionID const collection = await this.getUserCollection(collectionID); @@ -327,6 +434,13 @@ export class UserCollectionService { return E.right(true); } + /** + * Change parentID of UserCollection's + * + * @param collectionID The collection ID + * @param parentCollectionID The new parent's collection ID or change to root collection + * @returns If successful return an Either of true + */ private async changeParent( collection: UserCollection, parentCollectionID: string | null, @@ -358,6 +472,13 @@ export class UserCollectionService { } } + /** + * Check if collection is parent of destCollection + * + * @param collection The ID of collection being moved + * @param destCollection The ID of collection into which we are moving target collection into + * @returns An Option of boolean, is parent or not + */ private async isParent( collection: UserCollection, destCollection: UserCollection, @@ -385,6 +506,14 @@ export class UserCollectionService { } } + /** + * Update the OrderIndex of all collections in given parentID + * + * @param parentID The Parent collectionID + * @param orderIndexCondition Condition to decide what collections will be updated + * @param dataCondition Increment/Decrement OrderIndex condition + * @returns A Collection with updated OrderIndexes + */ private async updateOrderIndex( parentID: string, orderIndexCondition: Prisma.IntFilter, @@ -401,6 +530,14 @@ export class UserCollectionService { return updatedUserCollection; } + /** + * Move UserCollection into root or another collection + * + * @param userCollectionID The ID of collection being moved + * @param destCollectionID The ID of collection the target collection is being moved into or move target collection to root + * @param userID The User UID + * @returns An Either of the moved UserCollection + */ async moveUserCollection( userCollectionID: string, destCollectionID: string | null, @@ -490,12 +627,26 @@ export class UserCollectionService { return E.right(updatedCollection.right); } + /** + * Find the number of child collections present in collectionID + * + * @param collectionID The Collection ID + * @returns Number of collections + */ getCollectionCount(collectionID: string): Promise { return this.prisma.userCollection.count({ where: { parentID: collectionID }, }); } + /** + * Update order of root or child collectionID's + * + * @param collectionID The ID of collection being re-ordered + * @param nextCollectionID The ID of collection that is after the moved collection in its new position + * @param userID The User UID + * @returns If successful return an Either of true + */ async updateUserCollectionOrder( collectionID: string, nextCollectionID: string | null,