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
This commit is contained in:
Balu Babu
2023-03-09 19:37:40 +05:30
committed by GitHub
parent 9b76d62753
commit 2a715d5348
19 changed files with 3079 additions and 2694 deletions

View File

View File

View File

@@ -41,14 +41,17 @@ model TeamInvitation {
} }
model TeamCollection { model TeamCollection {
id String @id @default(cuid()) id String @id @default(cuid())
parentID String? parentID String?
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id]) parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
children TeamCollection[] @relation("TeamCollectionChildParent") children TeamCollection[] @relation("TeamCollectionChildParent")
requests TeamRequest[] requests TeamRequest[]
teamID String teamID String
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade) team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
title String title String
orderIndex Int
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
} }
model TeamRequest { model TeamRequest {
@@ -59,6 +62,9 @@ model TeamRequest {
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade) team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
title String title String
request Json request Json
orderIndex Int
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
} }
model Shortcode { model Shortcode {

View File

@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class MultiAuthGuard extends AuthGuard(['google1', 'google2']) {}

View File

@@ -54,7 +54,8 @@ export const USER_COLLECTION_NOT_FOUND = 'user_collection/not_found' as const;
* Tried to reorder user request but failed * Tried to reorder user request but failed
* (UserRequestService) * (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 * 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 * Tried to reorder user request but failed
* (UserRequestService) * (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 * 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'; 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 * Tried to update the team to a state it doesn't have any owners
* (TeamService) * (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'; 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 * Tried to perform action on a request that doesn't accept their member role level
* (GqlRequestTeamMemberGuard) * (GqlRequestTeamMemberGuard)

View File

@@ -5,7 +5,10 @@ import { UserEnvironment } from '../user-environment/user-environments.model';
import { UserHistory } from '../user-history/user-history.model'; import { UserHistory } from '../user-history/user-history.model';
import { TeamMember } from 'src/team/team.model'; import { TeamMember } from 'src/team/team.model';
import { TeamEnvironment } from 'src/team-environments/team-environments.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 { TeamRequest } from 'src/team-request/team-request.model';
import { TeamInvitation } from 'src/team-invitation/team-invitation.model'; import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
import { UserCollection } from '@prisma/client'; import { UserCollection } from '@prisma/client';
@@ -42,11 +45,11 @@ export type TopicDef = {
topic: `team_environment/${string}/${'created' | 'updated' | 'deleted'}` topic: `team_environment/${string}/${'created' | 'updated' | 'deleted'}`
]: TeamEnvironment; ]: TeamEnvironment;
[ [
topic: `team_coll/${string}/${ topic: `team_coll/${string}/${'coll_added' | 'coll_updated'}`
| 'coll_added'
| 'coll_updated'
| 'coll_removed'}`
]: TeamCollection; ]: 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: `user_history/${string}/deleted_many`]: number;
[topic: `team_req/${string}/${'req_created' | 'req_updated'}`]: TeamRequest; [topic: `team_req/${string}/${'req_created' | 'req_updated'}`]: TeamRequest;
[topic: `team_req/${string}/req_deleted`]: string; [topic: `team_req/${string}/req_deleted`]: string;

View File

@@ -12,6 +12,7 @@ import {
TEAM_INVALID_COLL_ID, TEAM_INVALID_COLL_ID,
TEAM_REQ_NOT_MEMBER, TEAM_REQ_NOT_MEMBER,
} from 'src/errors'; } from 'src/errors';
import * as E from 'fp-ts/Either';
@Injectable() @Injectable()
export class GqlCollectionTeamMemberGuard implements CanActivate { 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); if (!requireRoles) throw new Error(BUG_TEAM_NO_REQUIRE_TEAM_ROLE);
const gqlExecCtx = GqlExecutionContext.create(context); const gqlExecCtx = GqlExecutionContext.create(context);
const { user } = gqlExecCtx.getContext().req; const { user } = gqlExecCtx.getContext().req;
if (user == undefined) throw new Error(BUG_AUTH_NO_USER_CTX); 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( const collection = await this.teamCollectionService.getCollection(
collectionID, 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( const member = await this.teamService.getTeamMember(
collection.teamID, collection.right.teamID,
user.uid, user.uid,
); );
if (!member) throw new Error(TEAM_REQ_NOT_MEMBER); if (!member) throw new Error(TEAM_REQ_NOT_MEMBER);

View File

@@ -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;
}

View File

@@ -12,6 +12,25 @@ export class TeamCollection {
}) })
title: string; title: string;
parentID: string | null; @Field(() => ID, {
description: 'ID of the collection',
nullable: true,
})
parentID: string;
teamID: 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;
}

View File

@@ -5,17 +5,10 @@ import { TeamCollectionResolver } from './team-collection.resolver';
import { GqlCollectionTeamMemberGuard } from './guards/gql-collection-team-member.guard'; import { GqlCollectionTeamMemberGuard } from './guards/gql-collection-team-member.guard';
import { TeamModule } from '../team/team.module'; import { TeamModule } from '../team/team.module';
import { UserModule } from '../user/user.module'; import { UserModule } from '../user/user.module';
// import { FirebaseModule } from '../firebase/firebase.module';
import { PubSubModule } from '../pubsub/pubsub.module'; import { PubSubModule } from '../pubsub/pubsub.module';
@Module({ @Module({
imports: [ imports: [PrismaModule, TeamModule, UserModule, PubSubModule],
PrismaModule,
// FirebaseModule,
TeamModule,
UserModule,
PubSubModule,
],
providers: [ providers: [
TeamCollectionService, TeamCollectionService,
TeamCollectionResolver, TeamCollectionResolver,

View File

@@ -8,7 +8,7 @@ import {
Subscription, Subscription,
ID, ID,
} from '@nestjs/graphql'; } from '@nestjs/graphql';
import { TeamCollection } from './team-collection.model'; import { CollectionReorderData, TeamCollection } from './team-collection.model';
import { Team, TeamMemberRole } from '../team/team.model'; import { Team, TeamMemberRole } from '../team/team.model';
import { TeamCollectionService } from './team-collection.service'; import { TeamCollectionService } from './team-collection.service';
import { GqlAuthGuard } from '../guards/gql-auth.guard'; 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 { RequiresTeamRole } from '../team/decorators/requires-team-role.decorator';
import { GqlCollectionTeamMemberGuard } from './guards/gql-collection-team-member.guard'; import { GqlCollectionTeamMemberGuard } from './guards/gql-collection-team-member.guard';
import { PubSubService } from 'src/pubsub/pubsub.service'; 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) @Resolver(() => TeamCollection)
export class TeamCollectionResolver { export class TeamCollectionResolver {
@@ -30,58 +42,43 @@ export class TeamCollectionResolver {
description: 'Team the collection belongs to', description: 'Team the collection belongs to',
complexity: 5, complexity: 5,
}) })
team(@Parent() collection: TeamCollection): Promise<Team> { async team(@Parent() collection: TeamCollection) {
return this.teamCollectionService.getTeamOfCollection(collection.id); const team = await this.teamCollectionService.getTeamOfCollection(
collection.id,
);
if (E.isLeft(team)) throwErr(team.left);
return team.right;
} }
@ResolveField(() => TeamCollection, { @ResolveField(() => TeamCollection, {
description: description: 'Return the parent Team Collection (null if root )',
'The collection who is the parent of this collection (null if this is root collection)',
nullable: true, nullable: true,
complexity: 3, complexity: 3,
}) })
parent(@Parent() collection: TeamCollection): Promise<TeamCollection | null> { async parent(@Parent() collection: TeamCollection) {
return this.teamCollectionService.getParentOfCollection(collection.id); return this.teamCollectionService.getParentOfCollection(collection.id);
} }
@ResolveField(() => [TeamCollection], { @ResolveField(() => [TeamCollection], {
description: 'List of children collection', description: 'List of children Team Collections',
complexity: 3, complexity: 3,
}) })
children( async children(
@Parent() collection: TeamCollection, @Parent() collection: TeamCollection,
@Args({ @Args() args: PaginationArgs,
name: 'cursor', ) {
nullable: true,
description: 'ID of the last returned collection (for pagination)',
})
cursor?: string,
): Promise<TeamCollection[]> {
return this.teamCollectionService.getChildrenOfCollection( return this.teamCollectionService.getChildrenOfCollection(
collection.id, collection.id,
cursor ?? null, args.cursor,
args.take,
); );
} }
// Queries // 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<string> {
// return this.teamCollectionService.exportCollectionsToJSON(teamID);
// }
@Query(() => [TeamCollection], { @Query(() => String, {
description: 'Returns the collections of the team', description:
'Returns the JSON string giving the collections and their contents of the team',
}) })
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard) @UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
@RequiresTeamRole( @RequiresTeamRole(
@@ -89,27 +86,20 @@ export class TeamCollectionResolver {
TeamMemberRole.EDITOR, TeamMemberRole.EDITOR,
TeamMemberRole.OWNER, TeamMemberRole.OWNER,
) )
rootCollectionsOfTeam( async exportCollectionsToJSON(
@Args({ name: 'teamID', description: 'ID of the team', type: () => ID }) @Args({ name: 'teamID', description: 'ID of the team', type: () => ID })
teamID: string, teamID: string,
@Args({ ) {
name: 'cursor', const jsonString = await this.teamCollectionService.exportCollectionsToJSON(
nullable: true,
type: () => ID,
description: 'ID of the last returned collection (for pagination)',
})
cursor?: string,
): Promise<TeamCollection[]> {
return this.teamCollectionService.getTeamRootCollections(
teamID, teamID,
cursor ?? null,
); );
if (E.isLeft(jsonString)) throwErr(jsonString.left as string);
return jsonString.right;
} }
@Query(() => [TeamCollection], { @Query(() => [TeamCollection], {
description: 'Returns the collections of the team', description: 'Returns the collections of a team',
deprecationReason:
'Deprecated because of no practical use. Use `rootCollectionsOfTeam` instead.',
}) })
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard) @UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
@RequiresTeamRole( @RequiresTeamRole(
@@ -117,25 +107,16 @@ export class TeamCollectionResolver {
TeamMemberRole.EDITOR, TeamMemberRole.EDITOR,
TeamMemberRole.OWNER, TeamMemberRole.OWNER,
) )
collectionsOfTeam( async rootCollectionsOfTeam(@Args() args: GetRootTeamCollectionsArgs) {
@Args({ name: 'teamID', description: 'ID of the team', type: () => ID }) return this.teamCollectionService.getTeamRootCollections(
teamID: string, args.teamID,
@Args({ args.cursor,
name: 'cursor', args.take,
type: () => ID,
nullable: true,
description: 'ID of the last returned collection (for pagination)',
})
cursor?: string,
): Promise<TeamCollection[]> {
return this.teamCollectionService.getTeamCollections(
teamID,
cursor ?? null,
); );
} }
@Query(() => TeamCollection, { @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, nullable: true,
}) })
@UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard)
@@ -144,15 +125,20 @@ export class TeamCollectionResolver {
TeamMemberRole.EDITOR, TeamMemberRole.EDITOR,
TeamMemberRole.OWNER, TeamMemberRole.OWNER,
) )
collection( async collection(
@Args({ @Args({
name: 'collectionID', name: 'collectionID',
description: 'ID of the collection', description: 'ID of the collection',
type: () => ID, type: () => ID,
}) })
collectionID: string, collectionID: string,
): Promise<TeamCollection | null> { ) {
return this.teamCollectionService.getCollection(collectionID); const teamCollections = await this.teamCollectionService.getCollection(
collectionID,
);
if (E.isLeft(teamCollections)) throwErr(teamCollections.left);
return teamCollections.right;
} }
// Mutations // Mutations
@@ -162,13 +148,264 @@ export class TeamCollectionResolver {
}) })
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard) @UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
createRootCollection( async createRootCollection(@Args() args: CreateRootTeamCollectionArgs) {
@Args({ name: 'teamID', description: 'ID of the team', type: () => ID }) 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, teamID: string,
@Args({ name: 'title', description: 'Title of the new collection' }) @Args({
title: string, name: 'jsonString',
): Promise<TeamCollection> { description: 'JSON string to import',
return this.teamCollectionService.createCollection(teamID, title, null); })
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<boolean> {
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, { // @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<boolean> {
// 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<boolean> {
// 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<TeamCollection> {
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<TeamCollection> {
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<boolean> {
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<TeamCollection> {
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<TeamCollection> {
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<TeamCollection> {
return this.pubsub.asyncIterator(`team_coll/${teamID}/coll_removed`);
}
} }

View File

@@ -43,13 +43,13 @@ export class TeamRequestResolver {
return this.teamRequestService.getTeamOfRequest(req); return this.teamRequestService.getTeamOfRequest(req);
} }
@ResolveField(() => TeamCollection, { // @ResolveField(() => TeamCollection, {
description: 'Collection the request belongs to', // description: 'Collection the request belongs to',
complexity: 3, // complexity: 3,
}) // })
collection(@Parent() req: TeamRequest): Promise<TeamCollection> { // collection(@Parent() req: TeamRequest): Promise<TeamCollection> {
return this.teamRequestService.getCollectionOfRequest(req); // return this.teamRequestService.getCollectionOfRequest(req);
} // }
// Query // Query
@Query(() => [TeamRequest], { @Query(() => [TeamRequest], {
@@ -126,29 +126,29 @@ export class TeamRequestResolver {
); );
} }
// Mutation // // Mutation
@Mutation(() => TeamRequest, { // @Mutation(() => TeamRequest, {
description: 'Create a request in the given collection.', // description: 'Create a request in the given collection.',
}) // })
@UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) // @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard)
@RequiresTeamRole(TeamMemberRole.EDITOR, TeamMemberRole.OWNER) // @RequiresTeamRole(TeamMemberRole.EDITOR, TeamMemberRole.OWNER)
createRequestInCollection( // createRequestInCollection(
@Args({ // @Args({
name: 'collectionID', // name: 'collectionID',
description: 'ID of the collection', // description: 'ID of the collection',
type: () => ID, // type: () => ID,
}) // })
collectionID: string, // collectionID: string,
@Args({ // @Args({
name: 'data', // name: 'data',
type: () => CreateTeamRequestInput, // type: () => CreateTeamRequestInput,
description: // description:
'The request data (stringified JSON of Hoppscotch request object)', // 'The request data (stringified JSON of Hoppscotch request object)',
}) // })
data: CreateTeamRequestInput, // data: CreateTeamRequestInput,
): Promise<TeamRequest> { // ): Promise<TeamRequest> {
return this.teamRequestService.createTeamRequest(collectionID, data); // return this.teamRequestService.createTeamRequest(collectionID, data);
} // }
@Mutation(() => TeamRequest, { @Mutation(() => TeamRequest, {
description: 'Update a request with the given ID', description: 'Update a request with the given ID',
@@ -190,30 +190,30 @@ export class TeamRequestResolver {
return true; return true;
} }
@Mutation(() => TeamRequest, { // @Mutation(() => TeamRequest, {
description: 'Move a request to the given collection', // description: 'Move a request to the given collection',
}) // })
@UseGuards(GqlAuthGuard, GqlRequestTeamMemberGuard) // @UseGuards(GqlAuthGuard, GqlRequestTeamMemberGuard)
@RequiresTeamRole(TeamMemberRole.EDITOR, TeamMemberRole.OWNER) // @RequiresTeamRole(TeamMemberRole.EDITOR, TeamMemberRole.OWNER)
moveRequest( // moveRequest(
@Args({ // @Args({
name: 'requestID', // name: 'requestID',
description: 'ID of the request to move', // description: 'ID of the request to move',
type: () => ID, // type: () => ID,
}) // })
requestID: string, // requestID: string,
@Args({ // @Args({
name: 'destCollID', // name: 'destCollID',
description: 'ID of the collection to move the request to', // description: 'ID of the collection to move the request to',
type: () => ID, // type: () => ID,
}) // })
destCollID: string, // destCollID: string,
): Promise<TeamRequest> { // ): Promise<TeamRequest> {
return pipe( // return pipe(
this.teamRequestService.moveRequest(requestID, destCollID), // this.teamRequestService.moveRequest(requestID, destCollID),
TE.getOrElse((e) => throwErr(e)), // TE.getOrElse((e) => throwErr(e)),
)(); // )();
} // }
// Subscriptions // Subscriptions
@Subscription(() => TeamRequest, { @Subscription(() => TeamRequest, {

View File

@@ -19,7 +19,7 @@ import { PubSubService } from 'src/pubsub/pubsub.service';
import { throwErr } from 'src/utils'; import { throwErr } from 'src/utils';
import { pipe } from 'fp-ts/function'; import { pipe } from 'fp-ts/function';
import * as TO from 'fp-ts/TaskOption'; 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'; import { Prisma } from '@prisma/client';
@Injectable() @Injectable()
@@ -127,43 +127,41 @@ export class TeamRequestService {
this.pubsub.publish(`team_req/${req.teamID}/req_deleted`, requestID); this.pubsub.publish(`team_req/${req.teamID}/req_deleted`, requestID);
} }
async createTeamRequest( // async createTeamRequest(collectionID: string, input: CreateTeamRequestInput) {
collectionID: string, // const team = await this.teamCollectionService.getTeamOfCollection(
input: CreateTeamRequestInput, // collectionID,
): Promise<TeamRequest> { // );
const team = await this.teamCollectionService.getTeamOfCollection( // if (E.isLeft(team)) return [];
collectionID,
);
const data = await this.prisma.teamRequest.create({ // const data = await this.prisma.teamRequest.create({
data: { // data: {
team: { // team: {
connect: { // connect: {
id: team.id, // id: team.right.id,
}, // },
}, // },
request: JSON.parse(input.request), // request: JSON.parse(input.request),
title: input.title, // title: input.title,
collection: { // collection: {
connect: { // connect: {
id: collectionID, // id: collectionID,
}, // },
}, // },
}, // },
}); // });
const result = { // const result = {
id: data.id, // id: data.id,
collectionID: data.collectionID, // collectionID: data.collectionID,
title: data.title, // title: data.title,
request: JSON.stringify(data.request), // request: JSON.stringify(data.request),
teamID: data.teamID, // 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( async getRequestsInCollection(
collectionID: string, collectionID: string,
@@ -242,12 +240,12 @@ export class TeamRequestService {
); );
} }
async getCollectionOfRequest(req: TeamRequest): Promise<TeamCollection> { // async getCollectionOfRequest(req: TeamRequest): Promise<TeamCollection> {
return ( // return (
(await this.teamCollectionService.getCollection(req.collectionID)) ?? // (await this.teamCollectionService.getCollection(req.collectionID)) ??
throwErr(TEAM_INVALID_COLL_ID) // throwErr(TEAM_INVALID_COLL_ID)
); // );
} // }
async getTeamOfRequestFromID(reqID: string): Promise<Team> { async getTeamOfRequestFromID(reqID: string): Promise<Team> {
const req = const req =
@@ -263,69 +261,69 @@ export class TeamRequestService {
return req.team; return req.team;
} }
moveRequest(reqID: string, destinationCollID: string) { // moveRequest(reqID: string, destinationCollID: string) {
return pipe( // return pipe(
TE.Do, // TE.Do,
// Check if the request exists // // Check if the request exists
TE.bind('request', () => // TE.bind('request', () =>
pipe( // pipe(
this.getRequestTO(reqID), // this.getRequestTO(reqID),
TE.fromTaskOption(() => TEAM_REQ_NOT_FOUND), // TE.fromTaskOption(() => TEAM_REQ_NOT_FOUND),
), // ),
), // ),
// Check if the destination collection exists (or null) // // Check if the destination collection exists (or null)
TE.bindW('targetCollection', () => // TE.bindW('targetCollection', () =>
pipe( // pipe(
this.teamCollectionService.getCollectionTO(destinationCollID), // this.teamCollectionService.getCollectionTO(destinationCollID),
TE.fromTaskOption(() => TEAM_REQ_INVALID_TARGET_COLL_ID), // TE.fromTaskOption(() => TEAM_REQ_INVALID_TARGET_COLL_ID),
), // ),
), // ),
// Block operation if target collection is not part of the same team // // Block operation if target collection is not part of the same team
// as the request // // as the request
TE.chainW( // TE.chainW(
TE.fromPredicate( // TE.fromPredicate(
({ request, targetCollection }) => // ({ request, targetCollection }) =>
request.teamID === targetCollection.teamID, // request.teamID === targetCollection.teamID,
() => TEAM_REQ_INVALID_TARGET_COLL_ID, // () => TEAM_REQ_INVALID_TARGET_COLL_ID,
), // ),
), // ),
// Update the collection // // Update the collection
TE.chain(({ request, targetCollection }) => // TE.chain(({ request, targetCollection }) =>
TE.fromTask(() => // TE.fromTask(() =>
this.prisma.teamRequest.update({ // this.prisma.teamRequest.update({
where: { // where: {
id: request.id, // id: request.id,
}, // },
data: { // data: {
collectionID: targetCollection.id, // collectionID: targetCollection.id,
}, // },
}), // }),
), // ),
), // ),
// Generate TeamRequest model object // // Generate TeamRequest model object
TE.map( // TE.map(
(request) => // (request) =>
<TeamRequest>{ // <TeamRequest>{
id: request.id, // id: request.id,
collectionID: request.collectionID, // collectionID: request.collectionID,
request: JSON.stringify(request.request), // request: JSON.stringify(request.request),
teamID: request.teamID, // teamID: request.teamID,
title: request.title, // title: request.title,
}, // },
), // ),
// Update on PubSub // // Update on PubSub
TE.chainFirst((req) => { // TE.chainFirst((req) => {
this.pubsub.publish(`team_req/${req.teamID}/req_deleted`, req.id); // this.pubsub.publish(`team_req/${req.teamID}/req_deleted`, req.id);
this.pubsub.publish(`team_req/${req.teamID}/req_created`, req); // 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
}), // }),
); // );
} // }
} }

View File

@@ -0,0 +1,7 @@
import { Prisma } from '@prisma/client';
export interface CollectionFolder {
folders: CollectionFolder[];
requests: any[];
name: string;
}

View File

@@ -18,7 +18,6 @@ import {
UserCollection, UserCollection,
UserCollectionReorderData, UserCollectionReorderData,
} from './user-collections.model'; } from './user-collections.model';
import * as E from 'fp-ts/Either';
import { throwErr } from 'src/utils'; import { throwErr } from 'src/utils';
import { User } from 'src/user/user.model'; import { User } from 'src/user/user.model';
import { PaginationArgs } from 'src/types/input-types.args'; import { PaginationArgs } from 'src/types/input-types.args';
@@ -30,6 +29,7 @@ import {
UpdateUserCollectionArgs, UpdateUserCollectionArgs,
} from './input-type.args'; } from './input-type.args';
import { ReqType } from 'src/types/RequestTypes'; import { ReqType } from 'src/types/RequestTypes';
import * as E from 'fp-ts/Either';
@Resolver(() => UserCollection) @Resolver(() => UserCollection)
export class UserCollectionResolver { export class UserCollectionResolver {
@@ -230,7 +230,7 @@ export class UserCollectionResolver {
@Args() args: RenameUserCollectionsArgs, @Args() args: RenameUserCollectionsArgs,
) { ) {
const updatedUserCollection = const updatedUserCollection =
await this.userCollectionService.renameCollection( await this.userCollectionService.renameUserCollection(
args.newTitle, args.newTitle,
args.userCollectionID, args.userCollectionID,
user.uid, user.uid,

View File

@@ -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 () => { 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, rootRESTUserCollection.id,
user.uid, user.uid,
@@ -707,7 +707,7 @@ describe('renameCollection', () => {
'NotFoundError', 'NotFoundError',
); );
const result = await userCollectionService.renameCollection( const result = await userCollectionService.renameUserCollection(
'validTitle', 'validTitle',
rootRESTUserCollection.id, rootRESTUserCollection.id,
'op09', 'op09',
@@ -725,7 +725,7 @@ describe('renameCollection', () => {
title: 'NewTitle', title: 'NewTitle',
}); });
const result = await userCollectionService.renameCollection( const result = await userCollectionService.renameUserCollection(
'NewTitle', 'NewTitle',
rootRESTUserCollection.id, rootRESTUserCollection.id,
user.uid, user.uid,
@@ -743,7 +743,7 @@ describe('renameCollection', () => {
mockPrisma.userCollection.update.mockRejectedValueOnce('RecordNotFound'); mockPrisma.userCollection.update.mockRejectedValueOnce('RecordNotFound');
const result = await userCollectionService.renameCollection( const result = await userCollectionService.renameUserCollection(
'NewTitle', 'NewTitle',
'invalidID', 'invalidID',
user.uid, user.uid,
@@ -761,7 +761,7 @@ describe('renameCollection', () => {
title: 'NewTitle', title: 'NewTitle',
}); });
const result = await userCollectionService.renameCollection( const result = await userCollectionService.renameUserCollection(
'NewTitle', 'NewTitle',
rootRESTUserCollection.id, rootRESTUserCollection.id,
user.uid, user.uid,

View File

@@ -28,6 +28,11 @@ export class UserCollectionService {
private readonly pubsub: PubSubService, private readonly pubsub: PubSubService,
) {} ) {}
/**
* Typecast a database UserCollection to a UserCollection model
* @param userCollection database UserCollection
* @returns UserCollection model
*/
private cast(collection: UserCollection) { private cast(collection: UserCollection) {
return <UserCollectionModel>{ return <UserCollectionModel>{
...collection, ...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) { private async getChildCollectionsCount(collectionID: string) {
const childCollectionCount = await this.prisma.userCollection.findMany({ const childCollectionCount = await this.prisma.userCollection.findMany({
where: { parentID: collectionID }, where: { parentID: collectionID },
@@ -46,6 +58,13 @@ export class UserCollectionService {
return childCollectionCount[0].orderIndex; 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) { private async getRootCollectionsCount(userID: string) {
const rootCollectionCount = await this.prisma.userCollection.findMany({ const rootCollectionCount = await this.prisma.userCollection.findMany({
where: { userUid: userID, parentID: null }, where: { userUid: userID, parentID: null },
@@ -57,6 +76,13 @@ export class UserCollectionService {
return rootCollectionCount[0].orderIndex; 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) { private async isOwnerCheck(collectionID: string, userID: string) {
try { try {
await this.prisma.userCollection.findFirstOrThrow({ 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) { async getUserOfCollection(collectionID: string) {
try { try {
const userCollection = await this.prisma.userCollection.findUniqueOrThrow( 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) { async getParentOfUserCollection(collectionID: string) {
const { parent } = await this.prisma.userCollection.findUnique({ const { parent } = await this.prisma.userCollection.findUnique({
where: { where: {
@@ -103,6 +141,15 @@ export class UserCollectionService {
return parent; 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( async getChildrenOfUserCollection(
collectionID: string, collectionID: string,
cursor: string | null, 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) { async getUserCollection(collectionID: string) {
try { try {
const userCollection = await this.prisma.userCollection.findUniqueOrThrow( 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( async createUserCollection(
user: AuthUser, user: AuthUser,
title: string, title: string,
@@ -147,6 +209,7 @@ export class UserCollectionService {
const isTitleValid = isValidLength(title, 3); const isTitleValid = isValidLength(title, 3);
if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE); if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE);
// Check to see if parentUserCollectionID belongs to this User
if (parentUserCollectionID !== null) { if (parentUserCollectionID !== null) {
const isOwner = await this.isOwnerCheck(parentUserCollectionID, user.uid); const isOwner = await this.isOwnerCheck(parentUserCollectionID, user.uid);
if (O.isNone(isOwner)) return E.left(USER_NOT_OWNER); if (O.isNone(isOwner)) return E.left(USER_NOT_OWNER);
@@ -181,6 +244,14 @@ export class UserCollectionService {
return E.right(userCollection); 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( async getUserRootCollections(
user: AuthUser, user: AuthUser,
cursor: string | null, 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( async getUserChildCollections(
user: AuthUser, user: AuthUser,
userCollectionID: string, 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, newTitle: string,
userCollectionID: string, userCollectionID: string,
userID: 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) { private async removeUserCollection(collectionID: string) {
try { try {
const deletedUserCollection = await this.prisma.userCollection.delete({ 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) { private async deleteCollectionData(collection: UserCollection) {
// Get all child collections in collectionID // Get all child collections in collectionID
const childCollectionList = await this.prisma.userCollection.findMany({ const childCollectionList = await this.prisma.userCollection.findMany({
@@ -312,6 +412,13 @@ export class UserCollectionService {
return E.right(true); 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) { async deleteUserCollection(collectionID: string, userID: string) {
// Get collection details of collectionID // Get collection details of collectionID
const collection = await this.getUserCollection(collectionID); const collection = await this.getUserCollection(collectionID);
@@ -327,6 +434,13 @@ export class UserCollectionService {
return E.right(true); 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( private async changeParent(
collection: UserCollection, collection: UserCollection,
parentCollectionID: string | null, 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( private async isParent(
collection: UserCollection, collection: UserCollection,
destCollection: 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( private async updateOrderIndex(
parentID: string, parentID: string,
orderIndexCondition: Prisma.IntFilter, orderIndexCondition: Prisma.IntFilter,
@@ -401,6 +530,14 @@ export class UserCollectionService {
return updatedUserCollection; 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( async moveUserCollection(
userCollectionID: string, userCollectionID: string,
destCollectionID: string | null, destCollectionID: string | null,
@@ -490,12 +627,26 @@ export class UserCollectionService {
return E.right(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<number> { getCollectionCount(collectionID: string): Promise<number> {
return this.prisma.userCollection.count({ return this.prisma.userCollection.count({
where: { parentID: collectionID }, 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( async updateUserCollectionOrder(
collectionID: string, collectionID: string,
nextCollectionID: string | null, nextCollectionID: string | null,