feat: Introducing user-collections into self-host (HBE-98) (#18)

* feat: team module added

* feat: teamEnvironment module added

* feat: teamCollection module added

* feat: team request module added

* feat: team invitation module added

* feat: selfhost auth frontend (#15)

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>

* feat: bringing shortcodes from central to selfhost

* chore: added review changes in resolver

* chore: commented out subscriptions

* chore: bump backend prettier version

* feat: created new user-collections module with base files

* feat: added new models for user-collection and user-request tables in schema.prisma file

* feat: mutations to create user-collections complete

* feat: added user field resolver for userCollections

* feat: added parent field resolver for userCollections

* feat: added child field resolver with pagination for userCollections

* feat: added query to fetch root user-collections with pagination for userCollections

* feat: added query to fetch user-collections for userCollections

* feat: added mutation to rename user-collections

* feat: added mutation to delete user-collections

* feat: added mutation to delete user-collections

* refactor: changed the way we fetch root and child user-collection counts for other operations

* feat: added mutation to move user-collections between root and other child collections

* refactor: abstracted orderIndex update logic into helpert function

* chore: mutation to update order root user-collections complete

* feat: user-collections order can be updated when moving it to the end of list

* feat: user-collections order update feature complete

* feat: subscriptions for user-collection module complete

* chore: removed all console.logs from user-collection.service file

* test: added tests for all field resolvers for user-collection module

* test: test cases for getUserCollection is complete

* test: test cases for getUserRootCollections is complete

* test: test cases for createUserCollection is complete

* test: test cases for renameCollection is complete

* test: test cases for moveUserCollection is complete

* test: test cases for updateUserCollectionOrder is complete

* chore: added createdOn and updatedOn fields to userCollections and userRequests schema

* chore: created function to check if title are of valid size

* refactor: simplified user-collection creation code

* chore: made changed requested in initial PR review

* chore: added requestType enum to user-collections

* refactor: created two seperate queries to fetch root REST or GQL queries

* chore: created seperate mutations and queries for REST and GQL root/child collections

* chore: migrated all input args classess into a single file

* chore: modified createUserCollection service method to work with different creation inputs args type

* chore: rewrote all test cases for user-collections service methods with new CollType

* fix: added updated and deleted subscription changes

* fix: made all the changes requested in the initial PR review

* fix: made all the changes requested in the second PR review

* chore: removed migrations from prisma directory

* fix: made all the changes requested in the third PR review

* chore: added collection type checking to updateUserCollectionOrder service method

* chore: refactored all test cases to reflect new additions to service methods

* chore: fixed issues with pnpm-lock

* chore: removed migrations from prisma directory

* chore: hopefully fixed pnpm-lock issues

* chore: removed console logs in auth controller

---------

Co-authored-by: Mir Arif Hasan <arif.ishan05@gmail.com>
Co-authored-by: Akash K <57758277+amk-dev@users.noreply.github.com>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
Co-authored-by: ankitsridhar16 <ankit.sridhar16@gmail.com>
This commit is contained in:
Balu Babu
2023-03-03 15:03:05 +05:30
committed by GitHub
parent 31c6b0664f
commit a938be3712
14 changed files with 2642 additions and 3 deletions

View File

@@ -0,0 +1,75 @@
import { Field, ID, ArgsType } from '@nestjs/graphql';
import { PaginationArgs } from 'src/types/input-types.args';
@ArgsType()
export class CreateRootUserCollectionArgs {
@Field({ name: 'title', description: 'Title of the new user collection' })
title: string;
}
@ArgsType()
export class CreateChildUserCollectionArgs {
@Field({ name: 'title', description: 'Title of the new user collection' })
title: string;
@Field(() => ID, {
name: 'parentUserCollectionID',
description: 'ID of the parent to the new user collection',
})
parentUserCollectionID: string;
}
@ArgsType()
export class GetUserChildCollectionArgs extends PaginationArgs {
@Field(() => ID, {
name: 'userCollectionID',
description: 'ID of the parent to the user collection',
})
userCollectionID: string;
}
@ArgsType()
export class RenameUserCollectionsArgs {
@Field(() => ID, {
name: 'userCollectionID',
description: 'ID of the user collection',
})
userCollectionID: string;
@Field({
name: 'newTitle',
description: 'The updated title of the user collection',
})
newTitle: string;
}
@ArgsType()
export class UpdateUserCollectionArgs {
@Field(() => ID, {
name: 'collectionID',
description: 'ID of collection being moved',
})
collectionID: string;
@Field(() => ID, {
name: 'nextCollectionID',
nullable: true,
description: 'ID of collection being moved',
})
nextCollectionID: string;
}
@ArgsType()
export class MoveUserCollectionArgs {
@Field(() => ID, {
name: 'destCollectionID',
description: 'ID of the parent to the new collection',
nullable: true,
})
destCollectionID: string;
@Field(() => ID, {
name: 'userCollectionID',
description: 'ID of the collection',
})
userCollectionID: string;
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { UserCollectionService } from './user-collection.service';
import { UserCollectionResolver } from './user-collection.resolver';
import { PrismaModule } from 'src/prisma/prisma.module';
import { UserModule } from 'src/user/user.module';
import { PubSubModule } from 'src/pubsub/pubsub.module';
@Module({
imports: [PrismaModule, UserModule, PubSubModule],
providers: [UserCollectionService, UserCollectionResolver],
})
export class UserCollectionModule {}

View File

@@ -0,0 +1,348 @@
import { UseGuards } from '@nestjs/common';
import {
Resolver,
Mutation,
Args,
ID,
Query,
ResolveField,
Parent,
Subscription,
} from '@nestjs/graphql';
import { GqlUser } from 'src/decorators/gql-user.decorator';
import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
import { PubSubService } from 'src/pubsub/pubsub.service';
import { AuthUser } from 'src/types/AuthUser';
import { UserCollectionService } from './user-collection.service';
import {
UserCollection,
UserCollectionReorderData,
} from './user-collections.model';
import * as E from 'fp-ts/Either';
import { throwErr } from 'src/utils';
import { User } from 'src/user/user.model';
import { PaginationArgs } from 'src/types/input-types.args';
import {
CreateChildUserCollectionArgs,
CreateRootUserCollectionArgs,
MoveUserCollectionArgs,
RenameUserCollectionsArgs,
UpdateUserCollectionArgs,
} from './input-type.args';
import { ReqType } from 'src/types/RequestTypes';
@Resolver(() => UserCollection)
export class UserCollectionResolver {
constructor(
private readonly userCollectionService: UserCollectionService,
private readonly pubSub: PubSubService,
) {}
// Field Resolvers
@ResolveField(() => User, {
description: 'User the collection belongs to',
})
async user(@GqlUser() user: AuthUser) {
return user;
}
@ResolveField(() => UserCollection, {
description: 'Parent user collection (null if root)',
nullable: true,
})
async parent(@Parent() collection: UserCollection) {
return this.userCollectionService.getParentOfUserCollection(collection.id);
}
@ResolveField(() => [UserCollection], {
description: 'List of children REST user collection',
complexity: 3,
})
childrenREST(
@Parent() collection: UserCollection,
@Args() args: PaginationArgs,
) {
return this.userCollectionService.getChildrenOfUserCollection(
collection.id,
args.cursor,
args.take,
ReqType.REST,
);
}
@ResolveField(() => [UserCollection], {
description: 'List of children GraphQL user collection',
complexity: 3,
})
childrenGQL(
@Parent() collection: UserCollection,
@Args() args: PaginationArgs,
) {
return this.userCollectionService.getChildrenOfUserCollection(
collection.id,
args.cursor,
args.take,
ReqType.GQL,
);
}
// Queries
@Query(() => [UserCollection], {
description: 'Get the root REST user collections for a user',
})
@UseGuards(GqlAuthGuard)
rootRESTUserCollections(
@GqlUser() user: AuthUser,
@Args() args: PaginationArgs,
) {
return this.userCollectionService.getUserRootCollections(
user,
args.cursor,
args.take,
ReqType.REST,
);
}
@Query(() => [UserCollection], {
description: 'Get the root GraphQL user collections for a user',
})
@UseGuards(GqlAuthGuard)
rootGQLUserCollections(
@GqlUser() user: AuthUser,
@Args() args: PaginationArgs,
) {
return this.userCollectionService.getUserRootCollections(
user,
args.cursor,
args.take,
ReqType.GQL,
);
}
@Query(() => UserCollection, {
description: 'Get user collection with ID',
})
@UseGuards(GqlAuthGuard)
async userCollection(
@Args({
type: () => ID,
name: 'userCollectionID',
description: 'ID of the user collection',
})
userCollectionID: string,
) {
const userCollection = await this.userCollectionService.getUserCollection(
userCollectionID,
);
if (E.isLeft(userCollection)) throwErr(userCollection.left);
return userCollection.right;
}
// Mutations
@Mutation(() => UserCollection, {
description: 'Creates root REST user collection(no parent user collection)',
})
@UseGuards(GqlAuthGuard)
async createRESTRootUserCollection(
@GqlUser() user: AuthUser,
@Args() args: CreateRootUserCollectionArgs,
) {
const userCollection =
await this.userCollectionService.createUserCollection(
user,
args.title,
null,
ReqType.REST,
);
if (E.isLeft(userCollection)) throwErr(userCollection.left);
return userCollection.right;
}
@Mutation(() => UserCollection, {
description:
'Creates root GraphQL user collection(no parent user collection)',
})
@UseGuards(GqlAuthGuard)
async createGQLRootUserCollection(
@GqlUser() user: AuthUser,
@Args() args: CreateRootUserCollectionArgs,
) {
const userCollection =
await this.userCollectionService.createUserCollection(
user,
args.title,
null,
ReqType.GQL,
);
if (E.isLeft(userCollection)) throwErr(userCollection.left);
return userCollection.right;
}
@Mutation(() => UserCollection, {
description: 'Creates a new child REST user collection',
})
@UseGuards(GqlAuthGuard)
async createGQLChildUserCollection(
@GqlUser() user: AuthUser,
@Args() args: CreateChildUserCollectionArgs,
) {
const userCollection =
await this.userCollectionService.createUserCollection(
user,
args.title,
args.parentUserCollectionID,
ReqType.GQL,
);
if (E.isLeft(userCollection)) throwErr(userCollection.left);
return userCollection.right;
}
@Mutation(() => UserCollection, {
description: 'Creates a new child GraphQL user collection',
})
@UseGuards(GqlAuthGuard)
async createRESTChildUserCollection(
@GqlUser() user: AuthUser,
@Args() args: CreateChildUserCollectionArgs,
) {
const userCollection =
await this.userCollectionService.createUserCollection(
user,
args.title,
args.parentUserCollectionID,
ReqType.REST,
);
if (E.isLeft(userCollection)) throwErr(userCollection.left);
return userCollection.right;
}
@Mutation(() => UserCollection, {
description: 'Rename a user collection',
})
@UseGuards(GqlAuthGuard)
async renameUserCollection(
@GqlUser() user: AuthUser,
@Args() args: RenameUserCollectionsArgs,
) {
const updatedUserCollection =
await this.userCollectionService.renameCollection(
args.newTitle,
args.userCollectionID,
user.uid,
);
if (E.isLeft(updatedUserCollection)) throwErr(updatedUserCollection.left);
return updatedUserCollection.right;
}
@Mutation(() => Boolean, {
description: 'Delete a user collection',
})
@UseGuards(GqlAuthGuard)
async deleteUserCollection(
@Args({
name: 'userCollectionID',
description: 'ID of the user collection',
type: () => ID,
})
userCollectionID: string,
@GqlUser() user: AuthUser,
) {
const result = await this.userCollectionService.deleteUserCollection(
userCollectionID,
user.uid,
);
if (E.isLeft(result)) throwErr(result.left);
return result.right;
}
@Mutation(() => UserCollection, {
description: 'Move user collection into new parent or root',
})
@UseGuards(GqlAuthGuard)
async moveUserCollection(
@Args() args: MoveUserCollectionArgs,
@GqlUser() user: AuthUser,
) {
const res = await this.userCollectionService.moveUserCollection(
args.userCollectionID,
args.destCollectionID,
user.uid,
);
if (E.isLeft(res)) {
throwErr(res.left);
}
return res.right;
}
@Mutation(() => Boolean, {
description: 'Move user collection into new parent or root',
})
@UseGuards(GqlAuthGuard)
async updateUserCollectionOrder(
@Args() args: UpdateUserCollectionArgs,
@GqlUser() user: AuthUser,
) {
const res = await this.userCollectionService.updateUserCollectionOrder(
args.collectionID,
args.nextCollectionID,
user.uid,
);
if (E.isLeft(res)) {
throwErr(res.left);
}
return res.right;
}
// Subscriptions
@Subscription(() => UserCollection, {
description: 'Listen for User Collection Creation',
resolve: (value) => value,
})
@UseGuards(GqlAuthGuard)
userCollectionCreated(@GqlUser() user: AuthUser) {
return this.pubSub.asyncIterator(`user_coll/${user.uid}/created`);
}
@Subscription(() => UserCollection, {
description: 'Listen to when a User Collection has been updated.',
resolve: (value) => value,
})
@UseGuards(GqlAuthGuard)
userCollectionUpdated(@GqlUser() user: AuthUser) {
return this.pubSub.asyncIterator(`user_coll/${user.uid}/updated`);
}
@Subscription(() => ID, {
description: 'Listen to when a User Collection has been deleted',
resolve: (value) => value,
})
@UseGuards(GqlAuthGuard)
userCollectionRemoved(@GqlUser() user: AuthUser) {
return this.pubSub.asyncIterator(`user_coll/${user.uid}/deleted`);
}
@Subscription(() => UserCollection, {
description: 'Listen to when a User Collection has been moved',
resolve: (value) => value,
})
@UseGuards(GqlAuthGuard)
userCollectionMoved(@GqlUser() user: AuthUser) {
return this.pubSub.asyncIterator(`user_coll/${user.uid}/moved`);
}
@Subscription(() => UserCollectionReorderData, {
description: 'Listen to when a User Collections position has changed',
resolve: (value) => value,
})
@UseGuards(GqlAuthGuard)
userCollectionOrderUpdated(@GqlUser() user: AuthUser) {
return this.pubSub.asyncIterator(`user_coll/${user.uid}/order_updated`);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,616 @@
import { Injectable } from '@nestjs/common';
import {
USER_COLL_DEST_SAME,
USER_COLL_IS_PARENT_COLL,
USER_COLL_NOT_FOUND,
USER_COLL_NOT_SAME_TYPE,
USER_COLL_NOT_SAME_USER,
USER_COLL_REORDERING_FAILED,
USER_COLL_SAME_NEXT_COLL,
USER_COLL_SHORT_TITLE,
USER_COL_ALREADY_ROOT,
USER_NOT_FOUND,
USER_NOT_OWNER,
} from 'src/errors';
import { PrismaService } from 'src/prisma/prisma.service';
import { AuthUser } from 'src/types/AuthUser';
import * as E from 'fp-ts/Either';
import * as O from 'fp-ts/Option';
import { PubSubService } from 'src/pubsub/pubsub.service';
import { Prisma, User, UserCollection } from '@prisma/client';
import { UserCollection as UserCollectionModel } from './user-collections.model';
import { ReqType } from 'src/types/RequestTypes';
import { isValidLength } from 'src/utils';
@Injectable()
export class UserCollectionService {
constructor(
private readonly prisma: PrismaService,
private readonly pubsub: PubSubService,
) {}
private cast(collection: UserCollection) {
return <UserCollectionModel>{
...collection,
userID: collection.userUid,
};
}
private async getChildCollectionsCount(collectionID: string) {
const childCollectionCount = await this.prisma.userCollection.findMany({
where: { parentID: collectionID },
orderBy: {
orderIndex: 'desc',
},
});
if (!childCollectionCount.length) return 0;
return childCollectionCount[0].orderIndex;
}
private async getRootCollectionsCount(userID: string) {
const rootCollectionCount = await this.prisma.userCollection.findMany({
where: { userUid: userID, parentID: null },
orderBy: {
orderIndex: 'desc',
},
});
if (!rootCollectionCount.length) return 0;
return rootCollectionCount[0].orderIndex;
}
private async isOwnerCheck(collectionID: string, userID: string) {
try {
await this.prisma.userCollection.findFirstOrThrow({
where: {
id: collectionID,
userUid: userID,
},
});
return O.some(true);
} catch (error) {
return O.none;
}
}
async getUserOfCollection(collectionID: string) {
try {
const userCollection = await this.prisma.userCollection.findUniqueOrThrow(
{
where: {
id: collectionID,
},
include: {
user: true,
},
},
);
return E.right(userCollection.user);
} catch (error) {
return E.left(USER_NOT_FOUND);
}
}
async getParentOfUserCollection(collectionID: string) {
const { parent } = await this.prisma.userCollection.findUnique({
where: {
id: collectionID,
},
include: {
parent: true,
},
});
return parent;
}
async getChildrenOfUserCollection(
collectionID: string,
cursor: string | null,
take: number,
type: ReqType,
) {
return this.prisma.userCollection.findMany({
where: {
parentID: collectionID,
type: type,
},
orderBy: {
orderIndex: 'asc',
},
take: take, // default: 10
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
});
}
async getUserCollection(collectionID: string) {
try {
const userCollection = await this.prisma.userCollection.findUniqueOrThrow(
{
where: {
id: collectionID,
},
},
);
return E.right(userCollection);
} catch (error) {
return E.left(USER_COLL_NOT_FOUND);
}
}
async createUserCollection(
user: AuthUser,
title: string,
parentUserCollectionID: string | null,
type: ReqType,
) {
const isTitleValid = isValidLength(title, 3);
if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE);
if (parentUserCollectionID !== null) {
const isOwner = await this.isOwnerCheck(parentUserCollectionID, user.uid);
if (O.isNone(isOwner)) return E.left(USER_NOT_OWNER);
}
const isParent = parentUserCollectionID
? {
connect: {
id: parentUserCollectionID,
},
}
: undefined;
const userCollection = await this.prisma.userCollection.create({
data: {
title: title,
type: type,
user: {
connect: {
uid: user.uid,
},
},
parent: isParent,
orderIndex: !parentUserCollectionID
? (await this.getRootCollectionsCount(user.uid)) + 1
: (await this.getChildCollectionsCount(parentUserCollectionID)) + 1,
},
});
await this.pubsub.publish(`user_coll/${user.uid}/created`, userCollection);
return E.right(userCollection);
}
async getUserRootCollections(
user: AuthUser,
cursor: string | null,
take: number,
type: ReqType,
) {
return this.prisma.userCollection.findMany({
where: {
userUid: user.uid,
parentID: null,
type: type,
},
orderBy: {
orderIndex: 'asc',
},
take: take, // default: 10
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
});
}
async getUserChildCollections(
user: AuthUser,
userCollectionID: string,
cursor: string | null,
take: number,
type: ReqType,
) {
return this.prisma.userCollection.findMany({
where: {
userUid: user.uid,
parentID: userCollectionID,
type: type,
},
take: take, // default: 10
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
});
}
async renameCollection(
newTitle: string,
userCollectionID: string,
userID: string,
) {
const isTitleValid = isValidLength(newTitle, 3);
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: {
title: newTitle,
},
});
this.pubsub.publish(
`user_coll/${updatedUserCollection.userUid}/updated`,
updatedUserCollection,
);
return E.right(updatedUserCollection);
} catch (error) {
return E.left(USER_COLL_NOT_FOUND);
}
}
private async removeUserCollection(collectionID: string) {
try {
const deletedUserCollection = await this.prisma.userCollection.delete({
where: {
id: collectionID,
},
});
return E.right(deletedUserCollection);
} catch (error) {
return E.left(USER_COLL_NOT_FOUND);
}
}
private async deleteCollectionData(collection: UserCollection) {
// Get all child collections in collectionID
const childCollectionList = await this.prisma.userCollection.findMany({
where: {
parentID: collection.id,
},
});
// Delete child collections
await Promise.all(
childCollectionList.map((coll) =>
this.deleteUserCollection(coll.id, coll.userUid),
),
);
// Delete all requests in collectionID
await this.prisma.userRequest.deleteMany({
where: {
collectionID: collection.id,
},
});
// Update orderIndexes in userCollection table for user
await this.updateOrderIndex(
collection.parentID,
{ gt: collection.orderIndex },
{ decrement: 1 },
);
// Delete collection from UserCollection table
const deletedUserCollection = await this.removeUserCollection(
collection.id,
);
if (E.isLeft(deletedUserCollection))
return E.left(deletedUserCollection.left);
this.pubsub.publish(
`user_coll/${deletedUserCollection.right.userUid}/deleted`,
deletedUserCollection.right.id,
);
return E.right(true);
}
async deleteUserCollection(collectionID: string, userID: string) {
// Get collection details of collectionID
const collection = await this.getUserCollection(collectionID);
if (E.isLeft(collection)) return E.left(USER_COLL_NOT_FOUND);
// Check to see is the collection belongs to the user
if (collection.right.userUid !== userID) return E.left(USER_NOT_OWNER);
// Delete all child collections and requests in the collection
const collectionData = await this.deleteCollectionData(collection.right);
if (E.isLeft(collectionData)) return E.left(collectionData.left);
return E.right(true);
}
private async changeParent(
collection: UserCollection,
parentCollectionID: string | null,
) {
try {
let collectionCount: number;
if (!parentCollectionID)
collectionCount = await this.getRootCollectionsCount(
collection.userUid,
);
collectionCount = await this.getChildCollectionsCount(parentCollectionID);
const updatedCollection = await this.prisma.userCollection.update({
where: {
id: collection.id,
},
data: {
// if parentCollectionID == null, collection becomes root collection
// if parentCollectionID != null, collection becomes child collection
parentID: parentCollectionID,
orderIndex: collectionCount + 1,
},
});
return E.right(updatedCollection);
} catch (error) {
return E.left(USER_COLL_NOT_FOUND);
}
}
private async isParent(
collection: UserCollection,
destCollection: UserCollection,
): Promise<O.Option<boolean>> {
// Check if collection and destCollection are same
if (collection === destCollection) {
return O.none;
}
if (destCollection.parentID !== null) {
// Check if ID of collection is same as parent of destCollection
if (destCollection.parentID === collection.id) {
return O.none;
}
// Get collection details of collection one step above in the tree i.e the parent collection
const parentCollection = await this.getUserCollection(
destCollection.parentID,
);
if (E.isLeft(parentCollection)) {
return O.none;
}
// Call isParent again now with parent collection
return await this.isParent(collection, parentCollection.right);
} else {
return O.some(true);
}
}
private async updateOrderIndex(
parentID: string,
orderIndexCondition: Prisma.IntFilter,
dataCondition: Prisma.IntFieldUpdateOperationsInput,
) {
const updatedUserCollection = await this.prisma.userCollection.updateMany({
where: {
parentID: parentID,
orderIndex: orderIndexCondition,
},
data: { orderIndex: dataCondition },
});
return updatedUserCollection;
}
async moveUserCollection(
userCollectionID: string,
destCollectionID: string | null,
userID: string,
) {
// Get collection details of collectionID
const collection = await this.getUserCollection(userCollectionID);
if (E.isLeft(collection)) return E.left(USER_COLL_NOT_FOUND);
// Check to see is the collection belongs to the user
if (collection.right.userUid !== userID) return E.left(USER_NOT_OWNER);
// destCollectionID == null i.e move collection to root
if (!destCollectionID) {
if (!collection.right.parentID) {
// collection is a root collection
// Throw error if collection is already a root collection
return E.left(USER_COL_ALREADY_ROOT);
}
// Move child collection into root and update orderIndexes for root userCollections
await this.updateOrderIndex(
collection.right.parentID,
{ gt: collection.right.orderIndex },
{ decrement: 1 },
);
// Change parent from child to root i.e child collection becomes a root collection
const updatedCollection = await this.changeParent(collection.right, null);
if (E.isLeft(updatedCollection)) return E.left(updatedCollection.left);
this.pubsub.publish(
`user_coll/${collection.right.userUid}/moved`,
updatedCollection.right,
);
return E.right(updatedCollection.right);
}
// destCollectionID != null i.e move into another collection
if (userCollectionID === destCollectionID) {
// Throw error if collectionID and destCollectionID are the same
return E.left(USER_COLL_DEST_SAME);
}
// Get collection details of destCollectionID
const destCollection = await this.getUserCollection(destCollectionID);
if (E.isLeft(destCollection)) return E.left(USER_COLL_NOT_FOUND);
// Check if collection and destCollection belong to the same collection type
if (collection.right.type !== destCollection.right.type) {
return E.left(USER_COLL_NOT_SAME_TYPE);
}
// Check if collection and destCollection belong to the same user account
if (collection.right.userUid !== destCollection.right.userUid) {
return E.left(USER_COLL_NOT_SAME_USER);
}
// Check if collection is present on the parent tree for destCollection
const checkIfParent = await this.isParent(
collection.right,
destCollection.right,
);
if (O.isNone(checkIfParent)) {
return E.left(USER_COLL_IS_PARENT_COLL);
}
// Move root/child collection into another child collection and update orderIndexes of the previous parent
await this.updateOrderIndex(
collection.right.parentID,
{ gt: collection.right.orderIndex },
{ decrement: 1 },
);
// Change parent from null to teamCollection i.e collection becomes a child collection
const updatedCollection = await this.changeParent(
collection.right,
destCollection.right.id,
);
if (E.isLeft(updatedCollection)) return E.left(updatedCollection.left);
this.pubsub.publish(
`user_coll/${collection.right.userUid}/moved`,
updatedCollection.right,
);
return E.right(updatedCollection.right);
}
getCollectionCount(collectionID: string): Promise<number> {
return this.prisma.userCollection.count({
where: { parentID: collectionID },
});
}
async updateUserCollectionOrder(
collectionID: string,
nextCollectionID: string | null,
userID: string,
) {
// Throw error if collectionID and nextCollectionID are the same
if (collectionID === nextCollectionID)
return E.left(USER_COLL_SAME_NEXT_COLL);
// Get collection details of collectionID
const collection = await this.getUserCollection(collectionID);
if (E.isLeft(collection)) return E.left(USER_COLL_NOT_FOUND);
// Check to see is the collection belongs to the user
if (collection.right.userUid !== userID) return E.left(USER_NOT_OWNER);
if (!nextCollectionID) {
// nextCollectionID == null i.e move collection to the end of the list
try {
await this.prisma.$transaction(async (tx) => {
// Step 1: Decrement orderIndex of all items that come after collection.orderIndex till end of list of items
await tx.userCollection.updateMany({
where: {
parentID: collection.right.parentID,
orderIndex: {
gte: collection.right.orderIndex + 1,
},
},
data: {
orderIndex: { decrement: 1 },
},
});
// Step 2: Update orderIndex of collection to length of list
const updatedUserCollection = await tx.userCollection.update({
where: { id: collection.right.id },
data: {
orderIndex: await this.getCollectionCount(
collection.right.parentID,
),
},
});
});
this.pubsub.publish(
`user_coll/${collection.right.userUid}/order_updated`,
{
userCollection: this.cast(collection.right),
nextUserCollection: null,
},
);
return E.right(true);
} catch (error) {
return E.left(USER_COLL_REORDERING_FAILED);
}
}
// nextCollectionID != null i.e move to a certain position
// Get collection details of nextCollectionID
const subsequentCollection = await this.getUserCollection(nextCollectionID);
if (E.isLeft(subsequentCollection)) return E.left(USER_COLL_NOT_FOUND);
if (collection.right.userUid !== subsequentCollection.right.userUid)
return E.left(USER_COLL_NOT_SAME_USER);
// Check if collection and subsequentCollection belong to the same collection type
if (collection.right.type !== subsequentCollection.right.type) {
return E.left(USER_COLL_NOT_SAME_TYPE);
}
try {
await this.prisma.$transaction(async (tx) => {
// Step 1: Determine if we are moving collection up or down the list
const isMovingUp =
subsequentCollection.right.orderIndex < collection.right.orderIndex;
// Step 2: Update OrderIndex of items in list depending on moving up or down
const updateFrom = isMovingUp
? subsequentCollection.right.orderIndex
: collection.right.orderIndex + 1;
const updateTo = isMovingUp
? collection.right.orderIndex - 1
: subsequentCollection.right.orderIndex - 1;
await tx.userCollection.updateMany({
where: {
parentID: collection.right.parentID,
orderIndex: { gte: updateFrom, lte: updateTo },
},
data: {
orderIndex: isMovingUp ? { increment: 1 } : { decrement: 1 },
},
});
// Step 3: Update OrderIndex of collection
const updatedUserCollection = await tx.userCollection.update({
where: { id: collection.right.id },
data: {
orderIndex: isMovingUp
? subsequentCollection.right.orderIndex
: subsequentCollection.right.orderIndex - 1,
},
});
});
this.pubsub.publish(
`user_coll/${collection.right.userUid}/order_updated`,
{
userCollection: this.cast(collection.right),
nextUserCollection: this.cast(subsequentCollection.right),
},
);
return E.right(true);
} catch (error) {
return E.left(USER_COLL_REORDERING_FAILED);
}
}
}

View File

@@ -0,0 +1,43 @@
import { ObjectType, Field, ID, registerEnumType } from '@nestjs/graphql';
import { ReqType } from 'src/types/RequestTypes';
@ObjectType()
export class UserCollection {
@Field(() => ID, {
description: 'ID of the user collection',
})
id: string;
@Field({
description: 'Displayed title of the user collection',
})
title: string;
@Field(() => ReqType, {
description: 'Type of the user collection',
})
type: ReqType;
parentID: string | null;
userID: string;
}
@ObjectType()
export class UserCollectionReorderData {
@Field({
description: 'User Collection being moved',
})
userCollection: UserCollection;
@Field({
description:
'User Collection succeeding the collection being moved in its new position',
nullable: true,
})
nextUserCollection?: UserCollection;
}
registerEnumType(ReqType, {
name: 'CollType',
});