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:
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
});
|
||||
Reference in New Issue
Block a user