diff --git a/packages/hoppscotch-backend/prisma/migrations/20231130082054_collection_headers/migration.sql b/packages/hoppscotch-backend/prisma/migrations/20231130082054_collection_headers/migration.sql new file mode 100644 index 000000000..0276e9099 --- /dev/null +++ b/packages/hoppscotch-backend/prisma/migrations/20231130082054_collection_headers/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "TeamCollection" ADD COLUMN "data" JSONB; + +-- AlterTable +ALTER TABLE "UserCollection" ADD COLUMN "data" JSONB; diff --git a/packages/hoppscotch-backend/prisma/schema.prisma b/packages/hoppscotch-backend/prisma/schema.prisma index 68dfed992..1c945fa2d 100644 --- a/packages/hoppscotch-backend/prisma/schema.prisma +++ b/packages/hoppscotch-backend/prisma/schema.prisma @@ -43,6 +43,7 @@ model TeamInvitation { model TeamCollection { id String @id @default(cuid()) parentID String? + data Json? parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id]) children TeamCollection[] @relation("TeamCollectionChildParent") requests TeamRequest[] @@ -74,7 +75,8 @@ model Shortcode { creatorUid String? User User? @relation(fields: [creatorUid], references: [uid]) createdOn DateTime @default(now()) - updatedOn DateTime @updatedAt @default(now()) + updatedOn DateTime @default(now()) @updatedAt + @@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique") } @@ -195,6 +197,7 @@ model UserCollection { userUid String user User @relation(fields: [userUid], references: [uid], onDelete: Cascade) title String + data Json? orderIndex Int type ReqType createdOn DateTime @default(now()) @db.Timestamp(3) diff --git a/packages/hoppscotch-backend/src/errors.ts b/packages/hoppscotch-backend/src/errors.ts index 2a98aef38..df81a5b85 100644 --- a/packages/hoppscotch-backend/src/errors.ts +++ b/packages/hoppscotch-backend/src/errors.ts @@ -254,6 +254,13 @@ export const TEAM_COLL_INVALID_JSON = 'team_coll/invalid_json'; */ export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const; +/** + * The Team Collection data is not valid + * (TeamCollectionService) + */ +export const TEAM_COLL_DATA_INVALID = + 'team_coll/team_coll_data_invalid' as const; + /** * Tried to perform an action on a request that doesn't accept their member role level * (GqlRequestTeamMemberGuard) @@ -585,6 +592,13 @@ export const USER_COLL_REORDERING_FAILED = export const USER_COLL_SAME_NEXT_COLL = 'user_coll/user_collection_and_next_user_collection_are_same' as const; +/** + * The User Collection data is not valid + * (UserCollectionService) + */ +export const USER_COLL_DATA_INVALID = + 'user_coll/user_coll_data_invalid' as const; + /** * The User Collection does not belong to the logged-in user * (UserCollectionService) diff --git a/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts b/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts index b0cc8a854..49506c67e 100644 --- a/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts +++ b/packages/hoppscotch-backend/src/pubsub/topicsDefs.ts @@ -21,8 +21,8 @@ import { } from 'src/team-request/team-request.model'; import { TeamInvitation } from 'src/team-invitation/team-invitation.model'; import { InvitedUser } from '../admin/invited-user.model'; -import { UserCollection } from '@prisma/client'; import { + UserCollection, UserCollectionRemovedData, UserCollectionReorderData, } from 'src/user-collection/user-collections.model'; diff --git a/packages/hoppscotch-backend/src/team-collection/input-type.args.ts b/packages/hoppscotch-backend/src/team-collection/input-type.args.ts index 1dbd7c91f..2b4dd20a8 100644 --- a/packages/hoppscotch-backend/src/team-collection/input-type.args.ts +++ b/packages/hoppscotch-backend/src/team-collection/input-type.args.ts @@ -14,6 +14,13 @@ export class CreateRootTeamCollectionArgs { @Field({ name: 'title', description: 'Title of the new collection' }) title: string; + + @Field({ + name: 'data', + description: 'JSON string representing the collection data', + nullable: true, + }) + data: string; } @ArgsType() @@ -26,6 +33,13 @@ export class CreateChildTeamCollectionArgs { @Field({ name: 'childTitle', description: 'Title of the new collection' }) childTitle: string; + + @Field({ + name: 'data', + description: 'JSON string representing the collection data', + nullable: true, + }) + data: string; } @ArgsType() @@ -33,12 +47,14 @@ export class RenameTeamCollectionArgs { @Field(() => ID, { name: 'collectionID', description: 'ID of the collection', + deprecationReason: 'Switch to updateTeamCollection mutation instead', }) collectionID: string; @Field({ name: 'newTitle', description: 'The updated title of the collection', + deprecationReason: 'Switch to updateTeamCollection mutation instead', }) newTitle: string; } @@ -98,3 +114,26 @@ export class ReplaceTeamCollectionArgs { }) parentCollectionID?: string; } + +@ArgsType() +export class UpdateTeamCollectionArgs { + @Field(() => ID, { + name: 'collectionID', + description: 'ID of the collection', + }) + collectionID: string; + + @Field({ + name: 'newTitle', + description: 'The updated title of the collection', + nullable: true, + }) + newTitle: string; + + @Field({ + name: 'data', + description: 'JSON string representing the collection data', + nullable: true, + }) + data: 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 f45784327..d34a290f8 100644 --- a/packages/hoppscotch-backend/src/team-collection/team-collection.model.ts +++ b/packages/hoppscotch-backend/src/team-collection/team-collection.model.ts @@ -12,12 +12,17 @@ export class TeamCollection { }) title: string; + @Field({ + description: 'JSON string representing the collection data', + nullable: true, + }) + data: string; + @Field(() => ID, { description: 'ID of the collection', nullable: true, }) parentID: string; - teamID: string; } @ObjectType() 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 7ba886884..f5a2ccfb8 100644 --- a/packages/hoppscotch-backend/src/team-collection/team-collection.resolver.ts +++ b/packages/hoppscotch-backend/src/team-collection/team-collection.resolver.ts @@ -25,6 +25,7 @@ import { MoveTeamCollectionArgs, RenameTeamCollectionArgs, ReplaceTeamCollectionArgs, + UpdateTeamCollectionArgs, UpdateTeamCollectionOrderArgs, } from './input-type.args'; import * as E from 'fp-ts/Either'; @@ -141,7 +142,14 @@ export class TeamCollectionResolver { ); if (E.isLeft(teamCollections)) throwErr(teamCollections.left); - return teamCollections.right; + return { + id: teamCollections.right.id, + title: teamCollections.right.title, + parentID: teamCollections.right.parentID, + data: !teamCollections.right.data + ? null + : JSON.stringify(teamCollections.right.data), + }; } // Mutations @@ -155,6 +163,7 @@ export class TeamCollectionResolver { const teamCollection = await this.teamCollectionService.createCollection( args.teamID, args.title, + args.data, null, ); @@ -230,6 +239,7 @@ export class TeamCollectionResolver { const teamCollection = await this.teamCollectionService.createCollection( team.right.id, args.childTitle, + args.data, args.collectionID, ); @@ -239,6 +249,7 @@ export class TeamCollectionResolver { @Mutation(() => TeamCollection, { description: 'Rename a collection', + deprecationReason: 'Switch to updateTeamCollection mutation instead', }) @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) @@ -303,6 +314,23 @@ export class TeamCollectionResolver { return request.right; } + @Mutation(() => TeamCollection, { + description: 'Update Team Collection details', + }) + @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) + @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) + async updateTeamCollection(@Args() args: UpdateTeamCollectionArgs) { + const updatedTeamCollection = + await this.teamCollectionService.updateTeamCollection( + args.collectionID, + args.data, + args.newTitle, + ); + + if (E.isLeft(updatedTeamCollection)) throwErr(updatedTeamCollection.left); + return updatedTeamCollection.right; + } + // Subscriptions @Subscription(() => TeamCollection, { 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 076c30408..041e3858c 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,6 +1,7 @@ import { Team, TeamCollection as DBTeamCollection } from '@prisma/client'; import { mockDeep, mockReset } from 'jest-mock-extended'; import { + TEAM_COLL_DATA_INVALID, TEAM_COLL_DEST_SAME, TEAM_COLL_INVALID_JSON, TEAM_COLL_IS_PARENT_COLL, @@ -17,6 +18,7 @@ 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'; const mockPrisma = mockDeep(); const mockPubSub = mockDeep(); @@ -51,35 +53,60 @@ const rootTeamCollection: DBTeamCollection = { id: '123', orderIndex: 1, parentID: null, + data: {}, title: 'Root Collection 1', teamID: team.id, createdOn: currentTime, updatedOn: currentTime, }; +const rootTeamCollectionsCasted: TeamCollection = { + id: rootTeamCollection.id, + title: rootTeamCollection.title, + parentID: rootTeamCollection.parentID, + data: JSON.stringify(rootTeamCollection.data), +}; + const rootTeamCollection_2: DBTeamCollection = { id: 'erv', orderIndex: 2, parentID: null, + data: {}, title: 'Root Collection 1', teamID: team.id, createdOn: currentTime, updatedOn: currentTime, }; +const rootTeamCollection_2Casted: TeamCollection = { + id: 'erv', + parentID: null, + data: JSON.stringify(rootTeamCollection_2.data), + title: 'Root Collection 1', +}; + const childTeamCollection: DBTeamCollection = { id: 'rfe', orderIndex: 1, parentID: rootTeamCollection.id, + data: {}, title: 'Child Collection 1', teamID: team.id, createdOn: currentTime, updatedOn: currentTime, }; +const childTeamCollectionCasted: TeamCollection = { + id: 'rfe', + parentID: rootTeamCollection.id, + data: JSON.stringify(childTeamCollection.data), + title: 'Child Collection 1', +}; + const childTeamCollection_2: DBTeamCollection = { id: 'bgdz', orderIndex: 1, + data: {}, parentID: rootTeamCollection_2.id, title: 'Child Collection 1', teamID: team.id, @@ -87,11 +114,20 @@ const childTeamCollection_2: DBTeamCollection = { updatedOn: currentTime, }; +const childTeamCollection_2Casted: TeamCollection = { + id: 'bgdz', + data: JSON.stringify(childTeamCollection_2.data), + parentID: rootTeamCollection_2.id, + title: 'Child Collection 1', +}; + const rootTeamCollectionList: DBTeamCollection[] = [ { id: 'fdv', orderIndex: 1, parentID: null, + data: {}, + title: 'Root Collection 1', teamID: team.id, createdOn: currentTime, @@ -102,6 +138,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [ orderIndex: 2, parentID: null, title: 'Root Collection 1', + data: {}, + teamID: team.id, createdOn: currentTime, updatedOn: currentTime, @@ -111,6 +149,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [ orderIndex: 3, parentID: null, title: 'Root Collection 1', + data: {}, + teamID: team.id, createdOn: currentTime, updatedOn: currentTime, @@ -119,6 +159,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [ id: 'bre3', orderIndex: 4, parentID: null, + data: {}, + title: 'Root Collection 1', teamID: team.id, createdOn: currentTime, @@ -129,6 +171,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [ orderIndex: 5, parentID: null, title: 'Root Collection 1', + data: {}, + teamID: team.id, createdOn: currentTime, updatedOn: currentTime, @@ -139,6 +183,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [ parentID: null, title: 'Root Collection 1', teamID: team.id, + data: {}, + createdOn: currentTime, updatedOn: currentTime, }, @@ -148,6 +194,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [ parentID: null, title: 'Root Collection 1', teamID: team.id, + data: {}, + createdOn: currentTime, updatedOn: currentTime, }, @@ -156,6 +204,7 @@ const rootTeamCollectionList: DBTeamCollection[] = [ orderIndex: 8, parentID: null, title: 'Root Collection 1', + data: {}, teamID: team.id, createdOn: currentTime, updatedOn: currentTime, @@ -165,6 +214,7 @@ const rootTeamCollectionList: DBTeamCollection[] = [ orderIndex: 9, parentID: null, title: 'Root Collection 1', + data: {}, teamID: team.id, createdOn: currentTime, updatedOn: currentTime, @@ -175,17 +225,83 @@ const rootTeamCollectionList: DBTeamCollection[] = [ parentID: null, title: 'Root Collection 1', teamID: team.id, + data: {}, createdOn: currentTime, updatedOn: currentTime, }, ]; +const rootTeamCollectionListCasted: TeamCollection[] = [ + { + id: 'fdv', + parentID: null, + title: 'Root Collection 1', + data: JSON.stringify(rootTeamCollection.data), + }, + { + id: 'fbbg', + parentID: null, + title: 'Root Collection 1', + data: JSON.stringify(rootTeamCollection.data), + }, + { + id: 'fgbfg', + parentID: null, + title: 'Root Collection 1', + data: JSON.stringify(rootTeamCollection.data), + }, + { + id: 'bre3', + parentID: null, + data: JSON.stringify(rootTeamCollection.data), + title: 'Root Collection 1', + }, + { + id: 'hghgf', + parentID: null, + title: 'Root Collection 1', + data: JSON.stringify(rootTeamCollection.data), + }, + { + id: '123', + parentID: null, + title: 'Root Collection 1', + data: JSON.stringify(rootTeamCollection.data), + }, + { + id: '54tyh', + parentID: null, + title: 'Root Collection 1', + data: JSON.stringify(rootTeamCollection.data), + }, + { + id: '234re', + parentID: null, + title: 'Root Collection 1', + data: JSON.stringify(rootTeamCollection.data), + }, + { + id: '34rtg', + parentID: null, + title: 'Root Collection 1', + data: JSON.stringify(rootTeamCollection.data), + }, + { + id: '45tgh', + parentID: null, + title: 'Root Collection 1', + data: JSON.stringify(rootTeamCollection.data), + }, +]; + const childTeamCollectionList: DBTeamCollection[] = [ { id: '123', orderIndex: 1, parentID: rootTeamCollection.id, title: 'Root Collection 1', + data: {}, + teamID: team.id, createdOn: currentTime, updatedOn: currentTime, @@ -195,6 +311,8 @@ const childTeamCollectionList: DBTeamCollection[] = [ orderIndex: 2, parentID: rootTeamCollection.id, title: 'Root Collection 1', + data: {}, + teamID: team.id, createdOn: currentTime, updatedOn: currentTime, @@ -204,6 +322,8 @@ const childTeamCollectionList: DBTeamCollection[] = [ orderIndex: 3, parentID: rootTeamCollection.id, title: 'Root Collection 1', + data: {}, + teamID: team.id, createdOn: currentTime, updatedOn: currentTime, @@ -212,6 +332,8 @@ const childTeamCollectionList: DBTeamCollection[] = [ id: '567', orderIndex: 4, parentID: rootTeamCollection.id, + data: {}, + title: 'Root Collection 1', teamID: team.id, createdOn: currentTime, @@ -221,6 +343,8 @@ const childTeamCollectionList: DBTeamCollection[] = [ id: '123', orderIndex: 5, parentID: rootTeamCollection.id, + data: {}, + title: 'Root Collection 1', teamID: team.id, createdOn: currentTime, @@ -230,6 +354,8 @@ const childTeamCollectionList: DBTeamCollection[] = [ id: '678', orderIndex: 6, parentID: rootTeamCollection.id, + data: {}, + title: 'Root Collection 1', teamID: team.id, createdOn: currentTime, @@ -239,6 +365,8 @@ const childTeamCollectionList: DBTeamCollection[] = [ id: '789', orderIndex: 7, parentID: rootTeamCollection.id, + data: {}, + title: 'Root Collection 1', teamID: team.id, createdOn: currentTime, @@ -248,6 +376,8 @@ const childTeamCollectionList: DBTeamCollection[] = [ id: '890', orderIndex: 8, parentID: rootTeamCollection.id, + data: {}, + title: 'Root Collection 1', teamID: team.id, createdOn: currentTime, @@ -257,6 +387,7 @@ const childTeamCollectionList: DBTeamCollection[] = [ id: '012', orderIndex: 9, parentID: rootTeamCollection.id, + data: {}, title: 'Root Collection 1', teamID: team.id, createdOn: currentTime, @@ -266,6 +397,8 @@ const childTeamCollectionList: DBTeamCollection[] = [ id: '0bhu', orderIndex: 10, parentID: rootTeamCollection.id, + data: {}, + title: 'Root Collection 1', teamID: team.id, createdOn: currentTime, @@ -273,6 +406,75 @@ const childTeamCollectionList: DBTeamCollection[] = [ }, ]; +const childTeamCollectionListCasted: TeamCollection[] = [ + { + id: '123', + parentID: rootTeamCollection.id, + title: 'Root Collection 1', + data: JSON.stringify({}), + }, + { + id: '345', + parentID: rootTeamCollection.id, + title: 'Root Collection 1', + data: JSON.stringify({}), + }, + { + id: '456', + parentID: rootTeamCollection.id, + title: 'Root Collection 1', + data: JSON.stringify({}), + }, + { + id: '567', + parentID: rootTeamCollection.id, + data: JSON.stringify({}), + + title: 'Root Collection 1', + }, + { + id: '123', + parentID: rootTeamCollection.id, + data: JSON.stringify({}), + + title: 'Root Collection 1', + }, + { + id: '678', + parentID: rootTeamCollection.id, + data: JSON.stringify({}), + + title: 'Root Collection 1', + }, + { + id: '789', + parentID: rootTeamCollection.id, + data: JSON.stringify({}), + + title: 'Root Collection 1', + }, + { + id: '890', + parentID: rootTeamCollection.id, + data: JSON.stringify({}), + + title: 'Root Collection 1', + }, + { + id: '012', + parentID: rootTeamCollection.id, + data: JSON.stringify({}), + title: 'Root Collection 1', + }, + { + id: '0bhu', + parentID: rootTeamCollection.id, + data: JSON.stringify({}), + + title: 'Root Collection 1', + }, +]; + beforeEach(() => { mockReset(mockPrisma); mockPubSub.publish.mockClear(); @@ -311,7 +513,7 @@ describe('getParentOfCollection', () => { const result = await teamCollectionService.getParentOfCollection( childTeamCollection.id, ); - expect(result).toEqual(rootTeamCollection); + expect(result).toEqual(rootTeamCollectionsCasted); }); test('should return null successfully for a root collection with valid collectionID', async () => { @@ -347,7 +549,7 @@ describe('getChildrenOfCollection', () => { null, 10, ); - expect(result).toEqual(childTeamCollectionList); + expect(result).toEqual(childTeamCollectionListCasted); }); test('should return a list of 3 child collections successfully with cursor being equal to the 7th item in the list', async () => { @@ -363,9 +565,9 @@ describe('getChildrenOfCollection', () => { 10, ); expect(result).toEqual([ - { ...childTeamCollectionList[7] }, - { ...childTeamCollectionList[8] }, - { ...childTeamCollectionList[9] }, + { ...childTeamCollectionListCasted[7] }, + { ...childTeamCollectionListCasted[8] }, + { ...childTeamCollectionListCasted[9] }, ]); }); @@ -392,7 +594,7 @@ describe('getTeamRootCollections', () => { null, 10, ); - expect(result).toEqual(rootTeamCollectionList); + expect(result).toEqual(rootTeamCollectionListCasted); }); test('should return a list of 3 root collections successfully with cursor being equal to the 7th item in the list', async () => { @@ -408,9 +610,9 @@ describe('getTeamRootCollections', () => { 10, ); expect(result).toEqual([ - { ...rootTeamCollectionList[7] }, - { ...rootTeamCollectionList[8] }, - { ...rootTeamCollectionList[9] }, + { ...rootTeamCollectionListCasted[7] }, + { ...rootTeamCollectionListCasted[8] }, + { ...rootTeamCollectionListCasted[9] }, ]); }); @@ -464,6 +666,7 @@ describe('createCollection', () => { const result = await teamCollectionService.createCollection( rootTeamCollection.teamID, 'ab', + JSON.stringify(rootTeamCollection.data), rootTeamCollection.id, ); expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE); @@ -478,11 +681,27 @@ describe('createCollection', () => { const result = await teamCollectionService.createCollection( rootTeamCollection.teamID, 'abcd', + JSON.stringify(rootTeamCollection.data), rootTeamCollection.id, ); expect(result).toEqualLeft(TEAM_NOT_OWNER); }); + test('should throw TEAM_COLL_DATA_INVALID when parent TeamCollection does not belong to the team', async () => { + // isOwnerCheck + mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce( + rootTeamCollection, + ); + + const result = await teamCollectionService.createCollection( + rootTeamCollection.teamID, + 'abcd', + '{', + rootTeamCollection.id, + ); + expect(result).toEqualLeft(TEAM_COLL_DATA_INVALID); + }); + test('should successfully create a new root TeamCollection with valid inputs', async () => { // isOwnerCheck mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce( @@ -496,9 +715,10 @@ describe('createCollection', () => { const result = await teamCollectionService.createCollection( rootTeamCollection.teamID, 'abcdefg', + JSON.stringify(rootTeamCollection.data), rootTeamCollection.id, ); - expect(result).toEqualRight(rootTeamCollection); + expect(result).toEqualRight(rootTeamCollectionsCasted); }); test('should successfully create a new child TeamCollection with valid inputs', async () => { @@ -514,9 +734,10 @@ describe('createCollection', () => { const result = await teamCollectionService.createCollection( childTeamCollection.teamID, childTeamCollection.title, + JSON.stringify(rootTeamCollection.data), rootTeamCollection.id, ); - expect(result).toEqualRight(childTeamCollection); + expect(result).toEqualRight(childTeamCollectionCasted); }); test('should send pubsub message to "team_coll//coll_added" if child TeamCollection is created successfully', async () => { @@ -532,11 +753,13 @@ describe('createCollection', () => { const result = await teamCollectionService.createCollection( childTeamCollection.teamID, childTeamCollection.title, + JSON.stringify(rootTeamCollection.data), + rootTeamCollection.id, ); expect(mockPubSub.publish).toHaveBeenCalledWith( `team_coll/${childTeamCollection.teamID}/coll_added`, - childTeamCollection, + childTeamCollectionCasted, ); }); @@ -553,11 +776,13 @@ describe('createCollection', () => { const result = await teamCollectionService.createCollection( rootTeamCollection.teamID, 'abcdefg', + JSON.stringify(rootTeamCollection.data), + rootTeamCollection.id, ); expect(mockPubSub.publish).toHaveBeenCalledWith( `team_coll/${rootTeamCollection.teamID}/coll_added`, - rootTeamCollection, + rootTeamCollectionsCasted, ); }); }); @@ -587,7 +812,7 @@ describe('renameCollection', () => { 'NewTitle', ); expect(result).toEqualRight({ - ...rootTeamCollection, + ...rootTeamCollectionsCasted, title: 'NewTitle', }); }); @@ -625,7 +850,7 @@ describe('renameCollection', () => { expect(mockPubSub.publish).toHaveBeenCalledWith( `team_coll/${rootTeamCollection.teamID}/coll_updated`, { - ...rootTeamCollection, + ...rootTeamCollectionsCasted, title: 'NewTitle', }, ); @@ -832,9 +1057,8 @@ describe('moveCollection', () => { null, ); expect(result).toEqualRight({ - ...childTeamCollection, + ...childTeamCollectionCasted, parentID: null, - orderIndex: 2, }); }); @@ -890,9 +1114,8 @@ describe('moveCollection', () => { expect(mockPubSub.publish).toHaveBeenCalledWith( `team_coll/${childTeamCollection.teamID}/coll_moved`, { - ...childTeamCollection, + ...childTeamCollectionCasted, parentID: null, - orderIndex: 2, }, ); }); @@ -931,9 +1154,8 @@ describe('moveCollection', () => { childTeamCollection_2.id, ); expect(result).toEqualRight({ - ...rootTeamCollection, - parentID: childTeamCollection_2.id, - orderIndex: 1, + ...rootTeamCollectionsCasted, + parentID: childTeamCollection_2Casted.id, }); }); @@ -973,9 +1195,8 @@ describe('moveCollection', () => { expect(mockPubSub.publish).toHaveBeenCalledWith( `team_coll/${childTeamCollection_2.teamID}/coll_moved`, { - ...rootTeamCollection, - parentID: childTeamCollection_2.id, - orderIndex: 1, + ...rootTeamCollectionsCasted, + parentID: childTeamCollection_2Casted.id, }, ); }); @@ -1014,9 +1235,8 @@ describe('moveCollection', () => { childTeamCollection_2.id, ); expect(result).toEqualRight({ - ...childTeamCollection, - parentID: childTeamCollection_2.id, - orderIndex: 1, + ...childTeamCollectionCasted, + parentID: childTeamCollection_2Casted.id, }); }); @@ -1056,9 +1276,8 @@ describe('moveCollection', () => { expect(mockPubSub.publish).toHaveBeenCalledWith( `team_coll/${childTeamCollection.teamID}/coll_moved`, { - ...childTeamCollection, - parentID: childTeamCollection_2.id, - orderIndex: 1, + ...childTeamCollectionCasted, + parentID: childTeamCollection_2Casted.id, }, ); }); @@ -1154,7 +1373,7 @@ describe('updateCollectionOrder', () => { expect(mockPubSub.publish).toHaveBeenCalledWith( `team_coll/${childTeamCollectionList[4].teamID}/coll_order_updated`, { - collection: rootTeamCollectionList[4], + collection: rootTeamCollectionListCasted[4], nextCollection: null, }, ); @@ -1235,8 +1454,8 @@ describe('updateCollectionOrder', () => { expect(mockPubSub.publish).toHaveBeenCalledWith( `team_coll/${childTeamCollectionList[2].teamID}/coll_order_updated`, { - collection: childTeamCollectionList[4], - nextCollection: childTeamCollectionList[2], + collection: childTeamCollectionListCasted[4], + nextCollection: childTeamCollectionListCasted[2], }, ); }); @@ -1302,7 +1521,7 @@ describe('importCollectionsFromJSON', () => { ); expect(mockPubSub.publish).toHaveBeenCalledWith( `team_coll/${rootTeamCollection.teamID}/coll_added`, - rootTeamCollection, + rootTeamCollectionsCasted, ); }); }); @@ -1421,7 +1640,7 @@ describe('replaceCollectionsWithJSON', () => { ); expect(mockPubSub.publish).toHaveBeenCalledWith( `team_coll/${rootTeamCollection.teamID}/coll_added`, - rootTeamCollection, + rootTeamCollectionsCasted, ); }); }); @@ -1458,4 +1677,64 @@ describe('totalCollectionsInTeam', () => { }); }); +describe('updateTeamCollection', () => { + test('should throw TEAM_COLL_SHORT_TITLE if title is invalid', async () => { + const result = await teamCollectionService.updateTeamCollection( + rootTeamCollection.id, + JSON.stringify(rootTeamCollection.data), + 'de', + ); + expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE); + }); + + test('should throw TEAM_COLL_DATA_INVALID is collection data is invalid', async () => { + const result = await teamCollectionService.updateTeamCollection( + rootTeamCollection.id, + '{', + rootTeamCollection.title, + ); + expect(result).toEqualLeft(TEAM_COLL_DATA_INVALID); + }); + + test('should throw TEAM_COLL_NOT_FOUND is collectionID is invalid', async () => { + mockPrisma.teamCollection.update.mockRejectedValueOnce('RecordNotFound'); + + const result = await teamCollectionService.updateTeamCollection( + 'invalid_id', + JSON.stringify(rootTeamCollection.data), + rootTeamCollection.title, + ); + expect(result).toEqualLeft(TEAM_COLL_NOT_FOUND); + }); + + test('should successfully update a collection', async () => { + mockPrisma.teamCollection.update.mockResolvedValueOnce(rootTeamCollection); + + const result = await teamCollectionService.updateTeamCollection( + rootTeamCollection.id, + JSON.stringify({ foo: 'bar' }), + 'new_title', + ); + expect(result).toEqualRight({ + data: JSON.stringify({ foo: 'bar' }), + title: 'new_title', + ...rootTeamCollectionsCasted, + }); + }); + + test('should send pubsub message to "team_coll//coll_updated" if TeamCollection is updated successfully', async () => { + mockPrisma.teamCollection.update.mockResolvedValueOnce(rootTeamCollection); + + const result = await teamCollectionService.updateTeamCollection( + rootTeamCollection.id, + JSON.stringify(rootTeamCollection.data), + rootTeamCollection.title, + ); + expect(mockPubSub.publish).toHaveBeenCalledWith( + `team_coll/${rootTeamCollection.teamID}/coll_updated`, + rootTeamCollectionsCasted, + ); + }); +}); + //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 01a2ab78a..734e9e679 100644 --- a/packages/hoppscotch-backend/src/team-collection/team-collection.service.ts +++ b/packages/hoppscotch-backend/src/team-collection/team-collection.service.ts @@ -13,6 +13,7 @@ import { TEAM_COLL_IS_PARENT_COLL, TEAM_COL_SAME_NEXT_COLL, TEAM_COL_REORDERING_FAILED, + TEAM_COLL_DATA_INVALID, } from '../errors'; import { PubSubService } from '../pubsub/pubsub.service'; import { isValidLength } from 'src/utils'; @@ -69,6 +70,7 @@ export class TeamCollectionService { this.generatePrismaQueryObjForFBCollFolder(f, teamID, index + 1), ), }, + data: folder.data ?? undefined, }; } @@ -118,6 +120,7 @@ export class TeamCollectionService { name: collection.right.title, folders: childrenCollectionObjects, requests: requests.map((x) => x.request), + data: JSON.stringify(collection.right.data), }; return E.right(result); @@ -198,8 +201,11 @@ export class TeamCollectionService { ), ); - teamCollections.forEach((x) => - this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x), + teamCollections.forEach((collection) => + this.pubsub.publish( + `team_coll/${destTeamID}/coll_added`, + this.cast(collection), + ), ); return E.right(true); @@ -268,8 +274,11 @@ export class TeamCollectionService { ), ); - teamCollections.forEach((x) => - this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x), + teamCollections.forEach((collections) => + this.pubsub.publish( + `team_coll/${destTeamID}/coll_added`, + this.cast(collections), + ), ); return E.right(true); @@ -277,11 +286,17 @@ export class TeamCollectionService { /** * Typecast a database TeamCollection to a TeamCollection model + * * @param teamCollection database TeamCollection * @returns TeamCollection model */ private cast(teamCollection: DBTeamCollection): TeamCollection { - return { ...teamCollection }; + return { + id: teamCollection.id, + title: teamCollection.title, + parentID: teamCollection.parentID, + data: !teamCollection.data ? null : JSON.stringify(teamCollection.data), + }; } /** @@ -324,7 +339,7 @@ export class TeamCollectionService { }); if (!teamCollection) return null; - return teamCollection.parent; + return !teamCollection.parent ? null : this.cast(teamCollection.parent); } /** @@ -335,12 +350,12 @@ export class TeamCollectionService { * @param take Number of items we want returned * @returns A list of child collections */ - getChildrenOfCollection( + async getChildrenOfCollection( collectionID: string, cursor: string | null, take: number, ) { - return this.prisma.teamCollection.findMany({ + const res = await this.prisma.teamCollection.findMany({ where: { parentID: collectionID, }, @@ -351,6 +366,12 @@ export class TeamCollectionService { skip: cursor ? 1 : 0, cursor: cursor ? { id: cursor } : undefined, }); + + const childCollections = res.map((teamCollection) => + this.cast(teamCollection), + ); + + return childCollections; } /** @@ -366,7 +387,7 @@ export class TeamCollectionService { cursor: string | null, take: number, ) { - return this.prisma.teamCollection.findMany({ + const res = await this.prisma.teamCollection.findMany({ where: { teamID, parentID: null, @@ -378,6 +399,12 @@ export class TeamCollectionService { skip: cursor ? 1 : 0, cursor: cursor ? { id: cursor } : undefined, }); + + const teamCollections = res.map((teamCollection) => + this.cast(teamCollection), + ); + + return teamCollections; } /** @@ -470,6 +497,7 @@ export class TeamCollectionService { async createCollection( teamID: string, title: string, + data: string | null = null, parentTeamCollectionID: string | null, ) { const isTitleValid = isValidLength(title, this.TITLE_LENGTH); @@ -481,6 +509,13 @@ export class TeamCollectionService { if (O.isNone(isOwner)) return E.left(TEAM_NOT_OWNER); } + if (data === '') return E.left(TEAM_COLL_DATA_INVALID); + if (data) { + const jsonReq = stringToJson(data); + if (E.isLeft(jsonReq)) return E.left(TEAM_COLL_DATA_INVALID); + data = jsonReq.right; + } + const isParent = parentTeamCollectionID ? { connect: { @@ -498,18 +533,23 @@ export class TeamCollectionService { }, }, parent: isParent, + data: data ?? undefined, orderIndex: !parentTeamCollectionID ? (await this.getRootCollectionsCount(teamID)) + 1 : (await this.getChildCollectionsCount(parentTeamCollectionID)) + 1, }, }); - this.pubsub.publish(`team_coll/${teamID}/coll_added`, teamCollection); + this.pubsub.publish( + `team_coll/${teamID}/coll_added`, + this.cast(teamCollection), + ); return E.right(this.cast(teamCollection)); } /** + * @deprecated Use updateTeamCollection method instead * Update the title of a TeamCollection * * @param collectionID The Collection ID @@ -532,10 +572,10 @@ export class TeamCollectionService { this.pubsub.publish( `team_coll/${updatedTeamCollection.teamID}/coll_updated`, - updatedTeamCollection, + this.cast(updatedTeamCollection), ); - return E.right(updatedTeamCollection); + return E.right(this.cast(updatedTeamCollection)); } catch (error) { return E.left(TEAM_COLL_NOT_FOUND); } @@ -694,8 +734,8 @@ export class TeamCollectionService { * @returns An Option of boolean, is parent or not */ private async isParent( - collection: TeamCollection, - destCollection: TeamCollection, + collection: DBTeamCollection, + destCollection: DBTeamCollection, ): 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 @@ -971,4 +1011,49 @@ export class TeamCollectionService { const teamCollectionsCount = this.prisma.teamCollection.count(); return teamCollectionsCount; } + + /** + * Update Team Collection details + * + * @param collectionID Collection ID + * @param collectionData new header data in a JSONified string form + * @param newTitle New title of the collection + * @returns Updated TeamCollection + */ + async updateTeamCollection( + collectionID: string, + collectionData: string = null, + newTitle: string = null, + ) { + try { + if (newTitle != null) { + const isTitleValid = isValidLength(newTitle, this.TITLE_LENGTH); + if (!isTitleValid) return E.left(TEAM_COLL_SHORT_TITLE); + } + + if (collectionData === '') return E.left(TEAM_COLL_DATA_INVALID); + if (collectionData) { + const jsonReq = stringToJson(collectionData); + if (E.isLeft(jsonReq)) return E.left(TEAM_COLL_DATA_INVALID); + collectionData = jsonReq.right; + } + + const updatedTeamCollection = await this.prisma.teamCollection.update({ + where: { id: collectionID }, + data: { + data: collectionData ?? undefined, + title: newTitle ?? undefined, + }, + }); + + this.pubsub.publish( + `team_coll/${updatedTeamCollection.teamID}/coll_updated`, + this.cast(updatedTeamCollection), + ); + + return E.right(this.cast(updatedTeamCollection)); + } catch (e) { + return E.left(TEAM_COLL_NOT_FOUND); + } + } } diff --git a/packages/hoppscotch-backend/src/team-request/team-request.service.spec.ts b/packages/hoppscotch-backend/src/team-request/team-request.service.spec.ts index 4786742a6..bc9dc64fa 100644 --- a/packages/hoppscotch-backend/src/team-request/team-request.service.spec.ts +++ b/packages/hoppscotch-backend/src/team-request/team-request.service.spec.ts @@ -42,6 +42,7 @@ const teamCollection: DbTeamCollection = { id: 'team-coll-1', parentID: null, teamID: team.id, + data: {}, title: 'Team Collection 1', orderIndex: 1, createdOn: new Date(), diff --git a/packages/hoppscotch-backend/src/types/CollectionFolder.ts b/packages/hoppscotch-backend/src/types/CollectionFolder.ts index e7dd7e194..990a22b55 100644 --- a/packages/hoppscotch-backend/src/types/CollectionFolder.ts +++ b/packages/hoppscotch-backend/src/types/CollectionFolder.ts @@ -1,6 +1,8 @@ +// This interface defines how data will be received from the app when we are importing Hoppscotch collections export interface CollectionFolder { id?: string; folders: CollectionFolder[]; requests: any[]; name: string; + data?: string; } diff --git a/packages/hoppscotch-backend/src/user-collection/input-type.args.ts b/packages/hoppscotch-backend/src/user-collection/input-type.args.ts index 07e612bf0..a3d118d19 100644 --- a/packages/hoppscotch-backend/src/user-collection/input-type.args.ts +++ b/packages/hoppscotch-backend/src/user-collection/input-type.args.ts @@ -6,6 +6,13 @@ import { PaginationArgs } from 'src/types/input-types.args'; export class CreateRootUserCollectionArgs { @Field({ name: 'title', description: 'Title of the new user collection' }) title: string; + + @Field({ + name: 'data', + description: 'JSON string representing the collection data', + nullable: true, + }) + data: string; } @ArgsType() export class CreateChildUserCollectionArgs { @@ -17,6 +24,13 @@ export class CreateChildUserCollectionArgs { description: 'ID of the parent to the new user collection', }) parentUserCollectionID: string; + + @Field({ + name: 'data', + description: 'JSON string representing the collection data', + nullable: true, + }) + data: string; } @ArgsType() @@ -95,3 +109,26 @@ export class ImportUserCollectionsFromJSONArgs { }) parentCollectionID?: string; } + +@ArgsType() +export class UpdateUserCollectionsArgs { + @Field(() => ID, { + name: 'userCollectionID', + description: 'ID of the user collection', + }) + userCollectionID: string; + + @Field({ + name: 'newTitle', + description: 'The updated title of the user collection', + nullable: true, + }) + newTitle: string; + + @Field({ + name: 'data', + description: 'JSON string representing the collection data', + nullable: true, + }) + data: 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 48196e48e..be0c3530e 100644 --- a/packages/hoppscotch-backend/src/user-collection/user-collection.resolver.ts +++ b/packages/hoppscotch-backend/src/user-collection/user-collection.resolver.ts @@ -30,6 +30,7 @@ import { MoveUserCollectionArgs, RenameUserCollectionsArgs, UpdateUserCollectionArgs, + UpdateUserCollectionsArgs, } from './input-type.args'; import { ReqType } from 'src/types/RequestTypes'; import * as E from 'fp-ts/Either'; @@ -142,7 +143,13 @@ export class UserCollectionResolver { ); if (E.isLeft(userCollection)) throwErr(userCollection.left); - return userCollection.right; + return { + ...userCollection.right, + userID: userCollection.right.userUid, + data: !userCollection.right.data + ? null + : JSON.stringify(userCollection.right.data), + }; } @Query(() => UserCollectionExportJSONData, { @@ -191,6 +198,7 @@ export class UserCollectionResolver { await this.userCollectionService.createUserCollection( user, args.title, + args.data, null, ReqType.REST, ); @@ -212,6 +220,7 @@ export class UserCollectionResolver { await this.userCollectionService.createUserCollection( user, args.title, + args.data, null, ReqType.GQL, ); @@ -232,6 +241,7 @@ export class UserCollectionResolver { await this.userCollectionService.createUserCollection( user, args.title, + args.data, args.parentUserCollectionID, ReqType.GQL, ); @@ -252,6 +262,7 @@ export class UserCollectionResolver { await this.userCollectionService.createUserCollection( user, args.title, + args.data, args.parentUserCollectionID, ReqType.REST, ); @@ -359,6 +370,26 @@ export class UserCollectionResolver { return importedCollection.right; } + @Mutation(() => UserCollection, { + description: 'Update a UserCollection', + }) + @UseGuards(GqlAuthGuard) + async updateUserCollection( + @GqlUser() user: AuthUser, + @Args() args: UpdateUserCollectionsArgs, + ) { + const updatedUserCollection = + await this.userCollectionService.updateUserCollection( + args.newTitle, + args.data, + args.userCollectionID, + user.uid, + ); + + if (E.isLeft(updatedUserCollection)) throwErr(updatedUserCollection.left); + return updatedUserCollection.right; + } + // Subscriptions @Subscription(() => UserCollection, { description: 'Listen for User Collection Creation', 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 38cc066ca..ceb8f3b45 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 @@ -1,4 +1,4 @@ -import { UserCollection } from '@prisma/client'; +import { UserCollection as DBUserCollection } from '@prisma/client'; import { mockDeep, mockReset } from 'jest-mock-extended'; import { USER_COLL_DEST_SAME, @@ -11,12 +11,14 @@ import { USER_COLL_SHORT_TITLE, USER_COLL_ALREADY_ROOT, USER_NOT_OWNER, + USER_COLL_DATA_INVALID, } from 'src/errors'; import { PrismaService } from 'src/prisma/prisma.service'; import { PubSubService } from 'src/pubsub/pubsub.service'; import { AuthUser } from 'src/types/AuthUser'; import { ReqType } from 'src/types/RequestTypes'; import { UserCollectionService } from './user-collection.service'; +import { UserCollection } from './user-collections.model'; const mockPrisma = mockDeep(); const mockPubSub = mockDeep(); @@ -41,7 +43,7 @@ const user: AuthUser = { currentRESTSession: {}, }; -const rootRESTUserCollection: UserCollection = { +const rootRESTUserCollection: DBUserCollection = { id: '123', orderIndex: 1, parentID: null, @@ -50,9 +52,19 @@ const rootRESTUserCollection: UserCollection = { type: ReqType.REST, createdOn: currentTime, updatedOn: currentTime, + data: {}, }; -const rootGQLUserCollection: UserCollection = { +const rootRESTUserCollectionCasted: UserCollection = { + id: '123', + parentID: null, + userID: user.uid, + title: 'Root Collection 1', + type: ReqType.REST, + data: JSON.stringify(rootRESTUserCollection.data), +}; + +const rootGQLUserCollection: DBUserCollection = { id: '123', orderIndex: 1, parentID: null, @@ -61,9 +73,19 @@ const rootGQLUserCollection: UserCollection = { type: ReqType.GQL, createdOn: currentTime, updatedOn: currentTime, + data: {}, }; -const rootRESTUserCollection_2: UserCollection = { +const rootGQLUserCollectionCasted: UserCollection = { + id: '123', + parentID: null, + title: 'Root Collection 1', + userID: user.uid, + type: ReqType.GQL, + data: JSON.stringify(rootGQLUserCollection.data), +}; + +const rootRESTUserCollection_2: DBUserCollection = { id: '4gf', orderIndex: 2, parentID: null, @@ -72,9 +94,19 @@ const rootRESTUserCollection_2: UserCollection = { type: ReqType.REST, createdOn: currentTime, updatedOn: currentTime, + data: {}, }; -const rootGQLUserCollection_2: UserCollection = { +const rootRESTUserCollection_2Casted: UserCollection = { + id: '4gf', + parentID: null, + title: 'Root Collection 2', + userID: user.uid, + type: ReqType.REST, + data: JSON.stringify(rootRESTUserCollection_2.data), +}; + +const rootGQLUserCollection_2: DBUserCollection = { id: '4gf', orderIndex: 2, parentID: null, @@ -83,9 +115,19 @@ const rootGQLUserCollection_2: UserCollection = { type: ReqType.GQL, createdOn: currentTime, updatedOn: currentTime, + data: {}, }; -const childRESTUserCollection: UserCollection = { +const rootGQLUserCollection_2Casted: UserCollection = { + id: '4gf', + parentID: null, + title: 'Root Collection 2', + userID: user.uid, + type: ReqType.GQL, + data: JSON.stringify(rootGQLUserCollection_2.data), +}; + +const childRESTUserCollection: DBUserCollection = { id: '234', orderIndex: 1, parentID: rootRESTUserCollection.id, @@ -94,9 +136,19 @@ const childRESTUserCollection: UserCollection = { type: ReqType.REST, createdOn: currentTime, updatedOn: currentTime, + data: {}, }; -const childGQLUserCollection: UserCollection = { +const childRESTUserCollectionCasted: UserCollection = { + id: '234', + parentID: rootRESTUserCollection.id, + title: 'Child Collection 1', + userID: user.uid, + type: ReqType.REST, + data: JSON.stringify({}), +}; + +const childGQLUserCollection: DBUserCollection = { id: '234', orderIndex: 1, parentID: rootRESTUserCollection.id, @@ -105,9 +157,19 @@ const childGQLUserCollection: UserCollection = { type: ReqType.GQL, createdOn: currentTime, updatedOn: currentTime, + data: {}, }; -const childRESTUserCollection_2: UserCollection = { +const childGQLUserCollectionCasted: UserCollection = { + id: '234', + parentID: rootRESTUserCollection.id, + title: 'Child Collection 1', + userID: user.uid, + type: ReqType.GQL, + data: JSON.stringify({}), +}; + +const childRESTUserCollection_2: DBUserCollection = { id: '0kn', orderIndex: 2, parentID: rootRESTUserCollection_2.id, @@ -116,9 +178,19 @@ const childRESTUserCollection_2: UserCollection = { type: ReqType.REST, createdOn: currentTime, updatedOn: currentTime, + data: {}, }; -const childGQLUserCollection_2: UserCollection = { +const childRESTUserCollection_2Casted: UserCollection = { + id: '0kn', + parentID: rootRESTUserCollection_2.id, + title: 'Child Collection 2', + userID: user.uid, + type: ReqType.REST, + data: JSON.stringify({}), +}; + +const childGQLUserCollection_2: DBUserCollection = { id: '0kn', orderIndex: 2, parentID: rootRESTUserCollection_2.id, @@ -127,9 +199,19 @@ const childGQLUserCollection_2: UserCollection = { type: ReqType.GQL, createdOn: currentTime, updatedOn: currentTime, + data: {}, }; -const childRESTUserCollectionList: UserCollection[] = [ +const childGQLUserCollection_2Casted: UserCollection = { + id: '0kn', + parentID: rootRESTUserCollection_2.id, + title: 'Child Collection 2', + userID: user.uid, + type: ReqType.GQL, + data: JSON.stringify({}), +}; + +const childRESTUserCollectionList: DBUserCollection[] = [ { id: '234', orderIndex: 1, @@ -139,6 +221,7 @@ const childRESTUserCollectionList: UserCollection[] = [ type: ReqType.REST, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, { id: '345', @@ -149,6 +232,7 @@ const childRESTUserCollectionList: UserCollection[] = [ type: ReqType.REST, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, { id: '456', @@ -159,6 +243,7 @@ const childRESTUserCollectionList: UserCollection[] = [ type: ReqType.REST, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, { id: '567', @@ -169,6 +254,7 @@ const childRESTUserCollectionList: UserCollection[] = [ type: ReqType.REST, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, { id: '678', @@ -179,10 +265,54 @@ const childRESTUserCollectionList: UserCollection[] = [ type: ReqType.REST, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, ]; -const childGQLUserCollectionList: UserCollection[] = [ +const childRESTUserCollectionListCasted: UserCollection[] = [ + { + id: '234', + parentID: rootRESTUserCollection.id, + title: 'Child Collection 1', + userID: user.uid, + type: ReqType.REST, + data: JSON.stringify({}), + }, + { + id: '345', + parentID: rootRESTUserCollection.id, + title: 'Child Collection 2', + userID: user.uid, + type: ReqType.REST, + data: JSON.stringify({}), + }, + { + id: '456', + parentID: rootRESTUserCollection.id, + title: 'Child Collection 3', + userID: user.uid, + type: ReqType.REST, + data: JSON.stringify({}), + }, + { + id: '567', + parentID: rootRESTUserCollection.id, + title: 'Child Collection 4', + userID: user.uid, + type: ReqType.REST, + data: JSON.stringify({}), + }, + { + id: '678', + parentID: rootRESTUserCollection.id, + title: 'Child Collection 5', + userID: user.uid, + type: ReqType.REST, + data: JSON.stringify({}), + }, +]; + +const childGQLUserCollectionList: DBUserCollection[] = [ { id: '234', orderIndex: 1, @@ -192,6 +322,7 @@ const childGQLUserCollectionList: UserCollection[] = [ type: ReqType.GQL, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, { id: '345', @@ -202,6 +333,7 @@ const childGQLUserCollectionList: UserCollection[] = [ type: ReqType.GQL, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, { id: '456', @@ -212,6 +344,7 @@ const childGQLUserCollectionList: UserCollection[] = [ type: ReqType.GQL, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, { id: '567', @@ -222,6 +355,7 @@ const childGQLUserCollectionList: UserCollection[] = [ type: ReqType.GQL, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, { id: '678', @@ -232,10 +366,54 @@ const childGQLUserCollectionList: UserCollection[] = [ type: ReqType.GQL, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, ]; -const rootRESTUserCollectionList: UserCollection[] = [ +const childGQLUserCollectionListCasted: UserCollection[] = [ + { + id: '234', + parentID: rootRESTUserCollection.id, + title: 'Child Collection 1', + userID: user.uid, + type: ReqType.GQL, + data: JSON.stringify({}), + }, + { + id: '345', + parentID: rootRESTUserCollection.id, + title: 'Child Collection 2', + userID: user.uid, + type: ReqType.GQL, + data: JSON.stringify({}), + }, + { + id: '456', + parentID: rootRESTUserCollection.id, + title: 'Child Collection 3', + userID: user.uid, + type: ReqType.GQL, + data: JSON.stringify({}), + }, + { + id: '567', + parentID: rootRESTUserCollection.id, + title: 'Child Collection 4', + userID: user.uid, + type: ReqType.GQL, + data: JSON.stringify({}), + }, + { + id: '678', + parentID: rootRESTUserCollection.id, + title: 'Child Collection 5', + userID: user.uid, + type: ReqType.GQL, + data: JSON.stringify({}), + }, +]; + +const rootRESTUserCollectionList: DBUserCollection[] = [ { id: '123', orderIndex: 1, @@ -245,6 +423,7 @@ const rootRESTUserCollectionList: UserCollection[] = [ type: ReqType.REST, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, { id: '234', @@ -255,6 +434,7 @@ const rootRESTUserCollectionList: UserCollection[] = [ type: ReqType.REST, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, { id: '345', @@ -265,6 +445,7 @@ const rootRESTUserCollectionList: UserCollection[] = [ type: ReqType.REST, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, { id: '456', @@ -275,6 +456,7 @@ const rootRESTUserCollectionList: UserCollection[] = [ type: ReqType.REST, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, { id: '567', @@ -285,10 +467,54 @@ const rootRESTUserCollectionList: UserCollection[] = [ type: ReqType.REST, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, ]; -const rootGQLGQLUserCollectionList: UserCollection[] = [ +const rootRESTUserCollectionListCasted: UserCollection[] = [ + { + id: '123', + parentID: null, + title: 'Root Collection 1', + userID: user.uid, + type: ReqType.REST, + data: JSON.stringify({}), + }, + { + id: '234', + parentID: null, + title: 'Root Collection 2', + userID: user.uid, + type: ReqType.REST, + data: JSON.stringify({}), + }, + { + id: '345', + parentID: null, + title: 'Root Collection 3', + userID: user.uid, + type: ReqType.REST, + data: JSON.stringify({}), + }, + { + id: '456', + parentID: null, + title: 'Root Collection 4', + userID: user.uid, + type: ReqType.REST, + data: JSON.stringify({}), + }, + { + id: '567', + parentID: null, + title: 'Root Collection 5', + userID: user.uid, + type: ReqType.REST, + data: JSON.stringify({}), + }, +]; + +const rootGQLUserCollectionList: DBUserCollection[] = [ { id: '123', orderIndex: 1, @@ -298,6 +524,7 @@ const rootGQLGQLUserCollectionList: UserCollection[] = [ type: ReqType.GQL, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, { id: '234', @@ -308,6 +535,7 @@ const rootGQLGQLUserCollectionList: UserCollection[] = [ type: ReqType.GQL, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, { id: '345', @@ -318,6 +546,7 @@ const rootGQLGQLUserCollectionList: UserCollection[] = [ type: ReqType.GQL, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, { id: '456', @@ -328,6 +557,7 @@ const rootGQLGQLUserCollectionList: UserCollection[] = [ type: ReqType.GQL, createdOn: currentTime, updatedOn: currentTime, + data: {}, }, { id: '567', @@ -338,6 +568,50 @@ const rootGQLGQLUserCollectionList: UserCollection[] = [ type: ReqType.GQL, createdOn: currentTime, updatedOn: currentTime, + data: {}, + }, +]; + +const rootGQLUserCollectionListCasted: UserCollection[] = [ + { + id: '123', + parentID: null, + title: 'Root Collection 1', + userID: user.uid, + type: ReqType.GQL, + data: JSON.stringify({}), + }, + { + id: '234', + parentID: null, + title: 'Root Collection 2', + userID: user.uid, + type: ReqType.GQL, + data: JSON.stringify({}), + }, + { + id: '345', + parentID: null, + title: 'Root Collection 3', + userID: user.uid, + type: ReqType.GQL, + data: JSON.stringify({}), + }, + { + id: '456', + parentID: null, + title: 'Root Collection 4', + userID: user.uid, + type: ReqType.GQL, + data: JSON.stringify({}), + }, + { + id: '567', + parentID: null, + title: 'Root Collection 5', + userID: user.uid, + type: ReqType.GQL, + data: JSON.stringify({}), }, ]; @@ -356,7 +630,7 @@ describe('getParentOfUserCollection', () => { const result = await userCollectionService.getParentOfUserCollection( childRESTUserCollection.id, ); - expect(result).toEqual(rootRESTUserCollection); + expect(result).toEqual(rootRESTUserCollectionCasted); }); test('should return null with invalid collectionID', async () => { mockPrisma.userCollection.findUnique.mockResolvedValueOnce( @@ -367,7 +641,7 @@ describe('getParentOfUserCollection', () => { 'invalidId', ); //TODO: check it not null - expect(result).toEqual(undefined); + expect(result).toEqual(null); }); }); @@ -383,7 +657,7 @@ describe('getChildrenOfUserCollection', () => { 10, ReqType.REST, ); - expect(result).toEqual(childRESTUserCollectionList); + expect(result).toEqual(childRESTUserCollectionListCasted); }); test('should return a list of paginated child GQL user-collections with valid collectionID', async () => { mockPrisma.userCollection.findMany.mockResolvedValueOnce( @@ -396,7 +670,7 @@ describe('getChildrenOfUserCollection', () => { 10, ReqType.REST, ); - expect(result).toEqual(childGQLUserCollectionList); + expect(result).toEqual(childGQLUserCollectionListCasted); }); test('should return a empty array with a invalid REST collectionID', async () => { mockPrisma.userCollection.findMany.mockResolvedValueOnce([]); @@ -455,11 +729,11 @@ describe('getUserRootCollections', () => { 10, ReqType.REST, ); - expect(result).toEqual(rootRESTUserCollectionList); + expect(result).toEqual(rootRESTUserCollectionListCasted); }); test('should return a list of paginated root GQL user-collections with valid collectionID', async () => { mockPrisma.userCollection.findMany.mockResolvedValueOnce( - rootGQLGQLUserCollectionList, + rootGQLUserCollectionList, ); const result = await userCollectionService.getUserRootCollections( @@ -468,7 +742,7 @@ describe('getUserRootCollections', () => { 10, ReqType.GQL, ); - expect(result).toEqual(rootGQLGQLUserCollectionList); + expect(result).toEqual(rootGQLUserCollectionListCasted); }); test('should return a empty array with a invalid REST collectionID', async () => { mockPrisma.userCollection.findMany.mockResolvedValueOnce([]); @@ -499,11 +773,13 @@ describe('createUserCollection', () => { const result = await userCollectionService.createUserCollection( user, '', + JSON.stringify(rootRESTUserCollection.data), rootRESTUserCollection.id, ReqType.REST, ); expect(result).toEqualLeft(USER_COLL_SHORT_TITLE); }); + test('should throw USER_NOT_OWNER when user is not the owner of the collection', async () => { // isOwnerCheck mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce({ @@ -514,11 +790,13 @@ describe('createUserCollection', () => { const result = await userCollectionService.createUserCollection( user, rootRESTUserCollection.title, + JSON.stringify(rootRESTUserCollection.data), rootRESTUserCollection.id, ReqType.REST, ); expect(result).toEqualLeft(USER_NOT_OWNER); }); + test('should successfully create a new root REST user-collection with valid inputs', async () => { // isOwnerCheck mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -534,11 +812,13 @@ describe('createUserCollection', () => { const result = await userCollectionService.createUserCollection( user, rootRESTUserCollection.title, + JSON.stringify(rootRESTUserCollection.data), rootRESTUserCollection.id, ReqType.REST, ); - expect(result).toEqualRight(rootRESTUserCollection); + expect(result).toEqualRight(rootRESTUserCollectionCasted); }); + test('should successfully create a new root GQL user-collection with valid inputs', async () => { // isOwnerCheck mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -554,11 +834,13 @@ describe('createUserCollection', () => { const result = await userCollectionService.createUserCollection( user, rootGQLUserCollection.title, + JSON.stringify(rootGQLUserCollection.data), rootGQLUserCollection.id, ReqType.GQL, ); - expect(result).toEqualRight(rootGQLUserCollection); + expect(result).toEqualRight(rootGQLUserCollectionCasted); }); + test('should successfully create a new child REST user-collection with valid inputs', async () => { // isOwnerCheck mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -574,11 +856,13 @@ describe('createUserCollection', () => { const result = await userCollectionService.createUserCollection( user, childRESTUserCollection.title, + JSON.stringify(childRESTUserCollection.data), childRESTUserCollection.id, ReqType.REST, ); - expect(result).toEqualRight(childRESTUserCollection); + expect(result).toEqualRight(childRESTUserCollectionCasted); }); + test('should successfully create a new child GQL user-collection with valid inputs', async () => { // isOwnerCheck mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -594,11 +878,13 @@ describe('createUserCollection', () => { const result = await userCollectionService.createUserCollection( user, childGQLUserCollection.title, + JSON.stringify(childGQLUserCollection.data), childGQLUserCollection.id, ReqType.GQL, ); - expect(result).toEqualRight(childGQLUserCollection); + expect(result).toEqualRight(childGQLUserCollectionCasted); }); + test('should send pubsub message to "user_coll//created" if child REST user-collection is created successfully', async () => { // isOwnerCheck mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -614,14 +900,16 @@ describe('createUserCollection', () => { const result = await userCollectionService.createUserCollection( user, childRESTUserCollection.title, + JSON.stringify(childRESTUserCollection.data), childRESTUserCollection.id, ReqType.REST, ); expect(mockPubSub.publish).toHaveBeenCalledWith( `user_coll/${user.uid}/created`, - childRESTUserCollection, + childRESTUserCollectionCasted, ); }); + test('should send pubsub message to "user_coll//created" if child GQL user-collection is created successfully', async () => { // isOwnerCheck mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -637,14 +925,16 @@ describe('createUserCollection', () => { const result = await userCollectionService.createUserCollection( user, childGQLUserCollection.title, + JSON.stringify(childGQLUserCollection.data), childGQLUserCollection.id, ReqType.GQL, ); expect(mockPubSub.publish).toHaveBeenCalledWith( `user_coll/${user.uid}/created`, - childGQLUserCollection, + childGQLUserCollectionCasted, ); }); + test('should send pubsub message to "user_coll//created" if REST root user-collection is created successfully', async () => { // isOwnerCheck mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -660,14 +950,16 @@ describe('createUserCollection', () => { const result = await userCollectionService.createUserCollection( user, rootRESTUserCollection.title, + JSON.stringify(rootRESTUserCollection.data), rootRESTUserCollection.id, ReqType.REST, ); expect(mockPubSub.publish).toHaveBeenCalledWith( `user_coll/${user.uid}/created`, - rootRESTUserCollection, + rootRESTUserCollectionCasted, ); }); + test('should send pubsub message to "user_coll//created" if GQL root user-collection is created successfully', async () => { // isOwnerCheck mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -683,12 +975,13 @@ describe('createUserCollection', () => { const result = await userCollectionService.createUserCollection( user, rootGQLUserCollection.title, + JSON.stringify(rootGQLUserCollection.data), rootGQLUserCollection.id, ReqType.GQL, ); expect(mockPubSub.publish).toHaveBeenCalledWith( `user_coll/${user.uid}/created`, - rootGQLUserCollection, + rootGQLUserCollectionCasted, ); }); }); @@ -732,7 +1025,7 @@ describe('renameUserCollection', () => { user.uid, ); expect(result).toEqualRight({ - ...rootRESTUserCollection, + ...rootRESTUserCollectionCasted, title: 'NewTitle', }); }); @@ -770,7 +1063,7 @@ describe('renameUserCollection', () => { expect(mockPubSub.publish).toHaveBeenCalledWith( `user_coll/${user.uid}/updated`, { - ...rootRESTUserCollection, + ...rootRESTUserCollectionCasted, title: 'NewTitle', }, ); @@ -895,6 +1188,7 @@ describe('moveUserCollection', () => { ); expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); + test('should throw USER_NOT_OWNER if user is not owner of collection', async () => { // getUserCollection mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -908,6 +1202,7 @@ describe('moveUserCollection', () => { ); expect(result).toEqualLeft(USER_NOT_OWNER); }); + test('should throw USER_COLL_DEST_SAME if userCollectionID and destCollectionID is the same', async () => { // getUserCollection mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -921,6 +1216,7 @@ describe('moveUserCollection', () => { ); expect(result).toEqualLeft(USER_COLL_DEST_SAME); }); + test('should throw USER_COLL_NOT_FOUND if destCollectionID is invalid', async () => { // getUserCollection mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -938,6 +1234,7 @@ describe('moveUserCollection', () => { ); expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); + test('should throw USER_COLL_NOT_SAME_TYPE if userCollectionID and destCollectionID are not the same collection type', async () => { // getUserCollection mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -955,6 +1252,7 @@ describe('moveUserCollection', () => { ); expect(result).toEqualLeft(USER_COLL_NOT_SAME_TYPE); }); + test('should throw USER_COLL_NOT_SAME_USER if userCollectionID and destCollectionID are not from the same user', async () => { // getUserCollection mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -973,6 +1271,7 @@ describe('moveUserCollection', () => { ); expect(result).toEqualLeft(USER_COLL_NOT_SAME_USER); }); + test('should throw USER_COLL_IS_PARENT_COLL if userCollectionID is parent of destCollectionID ', async () => { // getUserCollection mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -990,6 +1289,7 @@ describe('moveUserCollection', () => { ); expect(result).toEqualLeft(USER_COLL_IS_PARENT_COLL); }); + test('should throw USER_COL_ALREADY_ROOT when moving root user-collection to root', async () => { // getUserCollection mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -1003,6 +1303,7 @@ describe('moveUserCollection', () => { ); expect(result).toEqualLeft(USER_COLL_ALREADY_ROOT); }); + test('should successfully move a child user-collection into root', async () => { // getUserCollection mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -1030,11 +1331,11 @@ describe('moveUserCollection', () => { user.uid, ); expect(result).toEqualRight({ - ...childRESTUserCollection, + ...childRESTUserCollectionCasted, parentID: null, - orderIndex: 2, }); }); + test('should throw USER_COLL_NOT_FOUND when trying to change parent of collection with invalid collectionID', async () => { // getUserCollection mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -1059,6 +1360,7 @@ describe('moveUserCollection', () => { ); expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); + test('should send pubsub message to "user_coll//moved" when user-collection is moved to root successfully', async () => { // getUserCollection mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -1088,12 +1390,12 @@ describe('moveUserCollection', () => { expect(mockPubSub.publish).toHaveBeenCalledWith( `user_coll/${user.uid}/moved`, { - ...childRESTUserCollection, + ...childRESTUserCollectionCasted, parentID: null, - orderIndex: 2, }, ); }); + test('should successfully move a root user-collection into a child user-collection', async () => { // getUserCollection mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -1129,11 +1431,11 @@ describe('moveUserCollection', () => { user.uid, ); expect(result).toEqualRight({ - ...rootRESTUserCollection, - parentID: childRESTUserCollection_2.id, - orderIndex: 1, + ...rootRESTUserCollectionCasted, + parentID: childRESTUserCollection_2Casted.id, }); }); + test('should successfully move a child user-collection into another child user-collection', async () => { // getUserCollection mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -1169,11 +1471,11 @@ describe('moveUserCollection', () => { user.uid, ); expect(result).toEqualRight({ - ...rootRESTUserCollection, - parentID: childRESTUserCollection.id, - orderIndex: 1, + ...rootRESTUserCollectionCasted, + parentID: childRESTUserCollectionCasted.id, }); }); + test('should send pubsub message to "user_coll//moved" when user-collection is moved into another child user-collection successfully', async () => { // getUserCollection mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -1211,9 +1513,8 @@ describe('moveUserCollection', () => { expect(mockPubSub.publish).toHaveBeenCalledWith( `user_coll/${user.uid}/moved`, { - ...rootRESTUserCollection, - parentID: childRESTUserCollection.id, - orderIndex: 1, + ...rootRESTUserCollectionCasted, + parentID: childRESTUserCollectionCasted.id, }, ); }); @@ -1228,6 +1529,7 @@ describe('updateUserCollectionOrder', () => { ); expect(result).toEqualLeft(USER_COLL_SAME_NEXT_COLL); }); + test('should throw USER_COLL_NOT_FOUND if collectionID is invalid', async () => { // getUserCollection; mockPrisma.userCollection.findUniqueOrThrow.mockRejectedValueOnce( @@ -1241,6 +1543,7 @@ describe('updateUserCollectionOrder', () => { ); expect(result).toEqualLeft(USER_COLL_NOT_FOUND); }); + test('should throw USER_NOT_OWNER if userUID is of a different user', async () => { // getUserCollection; mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -1254,6 +1557,7 @@ describe('updateUserCollectionOrder', () => { ); expect(result).toEqualLeft(USER_NOT_OWNER); }); + test('should successfully move the child user-collection to the end of the list', async () => { // getUserCollection; mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -1272,6 +1576,7 @@ describe('updateUserCollectionOrder', () => { ); expect(result).toEqualRight(true); }); + test('should successfully move the root user-collection to the end of the list', async () => { // getUserCollection; mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -1290,6 +1595,7 @@ describe('updateUserCollectionOrder', () => { ); expect(result).toEqualRight(true); }); + test('should throw USER_COLL_REORDERING_FAILED when re-ordering operation failed for child user-collection list', async () => { // getUserCollection; mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -1304,6 +1610,7 @@ describe('updateUserCollectionOrder', () => { ); expect(result).toEqualLeft(USER_COLL_REORDERING_FAILED); }); + test('should send pubsub message to "user_coll//order_updated" when user-collection order is updated successfully', async () => { // getUserCollection; mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce( @@ -1324,13 +1631,14 @@ describe('updateUserCollectionOrder', () => { `user_coll/${user.uid}/order_updated`, { userCollection: { - ...childRESTUserCollectionList[4], - userID: childRESTUserCollectionList[4].userUid, + ...childRESTUserCollectionListCasted[4], + userID: childRESTUserCollectionListCasted[4].userID, }, nextUserCollection: null, }, ); }); + test('should throw USER_COLL_NOT_SAME_USER when collectionID and nextCollectionID do not belong to the same user account', async () => { // getUserCollection; mockPrisma.userCollection.findUniqueOrThrow @@ -1347,6 +1655,7 @@ describe('updateUserCollectionOrder', () => { ); expect(result).toEqualLeft(USER_COLL_NOT_SAME_USER); }); + test('should throw USER_COLL_NOT_SAME_TYPE when collectionID and nextCollectionID do not belong to the same collection type', async () => { // getUserCollection; mockPrisma.userCollection.findUniqueOrThrow @@ -1363,6 +1672,7 @@ describe('updateUserCollectionOrder', () => { ); expect(result).toEqualLeft(USER_COLL_NOT_SAME_TYPE); }); + test('should successfully update the order of the child user-collection list', async () => { // getUserCollection; mockPrisma.userCollection.findUniqueOrThrow @@ -1376,6 +1686,7 @@ describe('updateUserCollectionOrder', () => { ); expect(result).toEqualRight(true); }); + test('should throw USER_COLL_REORDERING_FAILED when re-ordering operation failed for child user-collection list', async () => { // getUserCollection; mockPrisma.userCollection.findUniqueOrThrow @@ -1391,6 +1702,7 @@ describe('updateUserCollectionOrder', () => { ); expect(result).toEqualLeft(USER_COLL_REORDERING_FAILED); }); + test('should send pubsub message to "user_coll//order_updated" when user-collection order is updated successfully', async () => { // getUserCollection; mockPrisma.userCollection.findUniqueOrThrow @@ -1406,14 +1718,118 @@ describe('updateUserCollectionOrder', () => { `user_coll/${user.uid}/order_updated`, { userCollection: { - ...childRESTUserCollectionList[4], - userID: childRESTUserCollectionList[4].userUid, + ...childRESTUserCollectionListCasted[4], + userID: childRESTUserCollectionListCasted[4].userID, }, nextUserCollection: { - ...childRESTUserCollectionList[2], - userID: childRESTUserCollectionList[2].userUid, + ...childRESTUserCollectionListCasted[2], + userID: childRESTUserCollectionListCasted[2].userID, }, }, ); }); }); + +describe('updateUserCollection', () => { + test('should throw USER_COLL_DATA_INVALID is collection data is invalid', async () => { + const result = await userCollectionService.updateUserCollection( + rootRESTUserCollection.title, + '{', + rootRESTUserCollection.id, + rootRESTUserCollection.userUid, + ); + expect(result).toEqualLeft(USER_COLL_DATA_INVALID); + }); + + test('should throw USER_COLL_SHORT_TITLE if title is invalid', async () => { + const result = await userCollectionService.updateUserCollection( + '', + JSON.stringify(rootRESTUserCollection.data), + rootRESTUserCollection.id, + rootRESTUserCollection.userUid, + ); + + expect(result).toEqualLeft(USER_COLL_SHORT_TITLE); + }); + + test('should throw USER_NOT_OWNER is user is not owner of collection', async () => { + // isOwnerCheck + mockPrisma.userCollection.findFirstOrThrow.mockRejectedValueOnce( + 'NotFoundError', + ); + + const result = await userCollectionService.updateUserCollection( + rootRESTUserCollection.title, + JSON.stringify(rootRESTUserCollection.data), + rootRESTUserCollection.id, + rootRESTUserCollection.userUid, + ); + + expect(result).toEqualLeft(USER_NOT_OWNER); + }); + + test('should throw USER_COLL_NOT_FOUND is collectionID is invalid', async () => { + // isOwnerCheck + mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce({ + ...rootRESTUserCollection, + }); + mockPrisma.userCollection.update.mockRejectedValueOnce('RecordNotFound'); + + const result = await userCollectionService.updateUserCollection( + rootRESTUserCollection.title, + JSON.stringify(rootRESTUserCollection.data), + 'invalid_id', + rootRESTUserCollection.userUid, + ); + expect(result).toEqualLeft(USER_COLL_NOT_FOUND); + }); + + test('should successfully update a collection', async () => { + // isOwnerCheck + mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce({ + ...rootRESTUserCollection, + }); + mockPrisma.userCollection.update.mockResolvedValueOnce( + rootRESTUserCollection, + ); + + const result = await userCollectionService.updateUserCollection( + 'new_title', + JSON.stringify({ foo: 'bar' }), + rootRESTUserCollection.id, + rootRESTUserCollection.userUid, + ); + + expect(result).toEqualRight({ + data: JSON.stringify({ foo: 'bar' }), + title: 'new_title', + ...rootRESTUserCollectionCasted, + }); + }); + + test('should send pubsub message to "user_coll//updated" when UserCollection is updated successfully', async () => { + // isOwnerCheck + mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce({ + ...rootRESTUserCollection, + }); + mockPrisma.userCollection.update.mockResolvedValueOnce( + rootRESTUserCollection, + ); + + const result = await userCollectionService.updateUserCollection( + 'new_title', + JSON.stringify({ foo: 'bar' }), + rootRESTUserCollection.id, + rootRESTUserCollection.userUid, + ); + + expect(mockPubSub.publish).toHaveBeenCalledWith( + `user_coll/${rootRESTUserCollectionCasted.userID}/updated`, + { + data: JSON.stringify({ foo: 'bar' }), + title: 'new_title', + ...rootRESTUserCollectionCasted, + }, + ); + }); +}); 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 169f47aa6..06128e0fd 100644 --- a/packages/hoppscotch-backend/src/user-collection/user-collection.service.ts +++ b/packages/hoppscotch-backend/src/user-collection/user-collection.service.ts @@ -12,6 +12,7 @@ import { USER_NOT_FOUND, USER_NOT_OWNER, USER_COLL_INVALID_JSON, + USER_COLL_DATA_INVALID, } from 'src/errors'; import { PrismaService } from 'src/prisma/prisma.service'; import { AuthUser } from 'src/types/AuthUser'; @@ -43,8 +44,12 @@ export class UserCollectionService { */ private cast(collection: UserCollection) { return { - ...collection, + id: collection.id, + title: collection.title, + type: collection.type, + parentID: collection.parentID, userID: collection.userUid, + data: !collection.data ? null : JSON.stringify(collection.data), }; } @@ -146,7 +151,7 @@ export class UserCollectionService { }, }); - return parent; + return !parent ? null : this.cast(parent); } /** @@ -164,7 +169,7 @@ export class UserCollectionService { take: number, type: ReqType, ) { - return this.prisma.userCollection.findMany({ + const res = await this.prisma.userCollection.findMany({ where: { parentID: collectionID, type: type, @@ -176,6 +181,12 @@ export class UserCollectionService { skip: cursor ? 1 : 0, cursor: cursor ? { id: cursor } : undefined, }); + + const childCollections = res.map((childCollection) => + this.cast(childCollection), + ); + + return childCollections; } /** @@ -211,12 +222,20 @@ export class UserCollectionService { async createUserCollection( user: AuthUser, title: string, + data: string | null = null, parentUserCollectionID: string | null, type: ReqType, ) { const isTitleValid = isValidLength(title, this.TITLE_LENGTH); if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE); + if (data === '') return E.left(USER_COLL_DATA_INVALID); + if (data) { + const jsonReq = stringToJson(data); + if (E.isLeft(jsonReq)) return E.left(USER_COLL_DATA_INVALID); + data = jsonReq.right; + } + // If creating a child collection if (parentUserCollectionID !== null) { const parentCollection = await this.getUserCollection( @@ -251,15 +270,19 @@ export class UserCollectionService { }, }, parent: isParent, + data: data ?? undefined, orderIndex: !parentUserCollectionID ? (await this.getRootCollectionsCount(user.uid)) + 1 : (await this.getChildCollectionsCount(parentUserCollectionID)) + 1, }, }); - await this.pubsub.publish(`user_coll/${user.uid}/created`, userCollection); + await this.pubsub.publish( + `user_coll/${user.uid}/created`, + this.cast(userCollection), + ); - return E.right(userCollection); + return E.right(this.cast(userCollection)); } /** @@ -276,7 +299,7 @@ export class UserCollectionService { take: number, type: ReqType, ) { - return this.prisma.userCollection.findMany({ + const res = await this.prisma.userCollection.findMany({ where: { userUid: user.uid, parentID: null, @@ -289,6 +312,12 @@ export class UserCollectionService { skip: cursor ? 1 : 0, cursor: cursor ? { id: cursor } : undefined, }); + + const userCollections = res.map((childCollection) => + this.cast(childCollection), + ); + + return userCollections; } /** @@ -307,7 +336,7 @@ export class UserCollectionService { take: number, type: ReqType, ) { - return this.prisma.userCollection.findMany({ + const res = await this.prisma.userCollection.findMany({ where: { userUid: user.uid, parentID: userCollectionID, @@ -317,9 +346,16 @@ export class UserCollectionService { skip: cursor ? 1 : 0, cursor: cursor ? { id: cursor } : undefined, }); + + const childCollections = res.map((childCollection) => + this.cast(childCollection), + ); + + return childCollections; } /** + * @deprecated Use updateUserCollection method instead * Update the title of a UserCollection * * @param newTitle The new title of collection @@ -351,10 +387,10 @@ export class UserCollectionService { this.pubsub.publish( `user_coll/${updatedUserCollection.userUid}/updated`, - updatedUserCollection, + this.cast(updatedUserCollection), ); - return E.right(updatedUserCollection); + return E.right(this.cast(updatedUserCollection)); } catch (error) { return E.left(USER_COLL_NOT_FOUND); } @@ -591,10 +627,10 @@ export class UserCollectionService { this.pubsub.publish( `user_coll/${collection.right.userUid}/moved`, - updatedCollection.right, + this.cast(updatedCollection.right), ); - return E.right(updatedCollection.right); + return E.right(this.cast(updatedCollection.right)); } // destCollectionID != null i.e move into another collection @@ -642,10 +678,10 @@ export class UserCollectionService { this.pubsub.publish( `user_coll/${collection.right.userUid}/moved`, - updatedCollection.right, + this.cast(updatedCollection.right), ); - return E.right(updatedCollection.right); + return E.right(this.cast(updatedCollection.right)); } /** @@ -846,6 +882,7 @@ export class UserCollectionService { ...(x.request as Record), // type casting x.request of type Prisma.JSONValue to an object to enable spread }; }), + data: JSON.stringify(collection.right.data), }; return E.right(result); @@ -918,6 +955,7 @@ export class UserCollectionService { ...(x.request as Record), // type casting x.request of type Prisma.JSONValue to an object to enable spread }; }), + data: JSON.stringify(parentCollection.right.data), }), collectionType: parentCollection.right.type, }); @@ -971,6 +1009,7 @@ export class UserCollectionService { this.generatePrismaQueryObj(f, userID, index + 1, reqType), ), }, + data: folder.data ?? undefined, }; } @@ -1040,10 +1079,63 @@ export class UserCollectionService { ), ); - userCollections.forEach((x) => - this.pubsub.publish(`user_coll/${userID}/created`, x), + userCollections.forEach((collection) => + this.pubsub.publish(`user_coll/${userID}/created`, this.cast(collection)), ); return E.right(true); } + + /** + * Update 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 updateUserCollection( + newTitle: string = null, + collectionData: string | null = null, + userCollectionID: string, + userID: string, + ) { + if (collectionData === '') return E.left(USER_COLL_DATA_INVALID); + + if (collectionData) { + const jsonReq = stringToJson(collectionData); + if (E.isLeft(jsonReq)) return E.left(USER_COLL_DATA_INVALID); + collectionData = jsonReq.right; + } + + if (newTitle != null) { + const isTitleValid = isValidLength(newTitle, this.TITLE_LENGTH); + if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE); + } + + // Check to see is the collection belongs to the user + const isOwner = await this.isOwnerCheck(userCollectionID, userID); + if (O.isNone(isOwner)) return E.left(USER_NOT_OWNER); + + try { + const updatedUserCollection = await this.prisma.userCollection.update({ + where: { + id: userCollectionID, + }, + data: { + data: collectionData ?? undefined, + title: newTitle ?? undefined, + }, + }); + + this.pubsub.publish( + `user_coll/${updatedUserCollection.userUid}/updated`, + this.cast(updatedUserCollection), + ); + + return E.right(this.cast(updatedUserCollection)); + } catch (error) { + return E.left(USER_COLL_NOT_FOUND); + } + } } diff --git a/packages/hoppscotch-backend/src/user-collection/user-collections.model.ts b/packages/hoppscotch-backend/src/user-collection/user-collections.model.ts index 21a6b6242..89c26861c 100644 --- a/packages/hoppscotch-backend/src/user-collection/user-collections.model.ts +++ b/packages/hoppscotch-backend/src/user-collection/user-collections.model.ts @@ -13,6 +13,12 @@ export class UserCollection { }) title: string; + @Field({ + description: 'JSON string representing the collection data', + nullable: true, + }) + data: string; + @Field(() => ReqType, { description: 'Type of the user collection', })