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:
@@ -90,6 +90,8 @@ model User {
|
||||
settings UserSettings?
|
||||
UserHistory UserHistory[]
|
||||
UserEnvironments UserEnvironment[]
|
||||
userCollections UserCollection[]
|
||||
userRequests UserRequest[]
|
||||
currentRESTSession Json?
|
||||
currentGQLSession Json?
|
||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||
@@ -152,6 +154,35 @@ model UserEnvironment {
|
||||
isGlobal Boolean
|
||||
}
|
||||
|
||||
model UserCollection {
|
||||
id String @id @default(cuid())
|
||||
parentID String?
|
||||
parent UserCollection? @relation("ParentUserCollection", fields: [parentID], references: [id], onDelete: Cascade)
|
||||
children UserCollection[] @relation("ParentUserCollection")
|
||||
requests UserRequest[]
|
||||
userUid String
|
||||
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
|
||||
title String
|
||||
orderIndex Int
|
||||
type ReqType
|
||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
||||
}
|
||||
|
||||
model UserRequest {
|
||||
id String @id @default(cuid())
|
||||
collectionID String
|
||||
collection UserCollection @relation(fields: [collectionID], references: [id], onDelete: Cascade)
|
||||
userUid String
|
||||
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
|
||||
title String
|
||||
request Json
|
||||
type ReqType
|
||||
orderIndex Int
|
||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
||||
}
|
||||
|
||||
enum TeamMemberRole {
|
||||
OWNER
|
||||
VIEWER
|
||||
|
||||
@@ -13,6 +13,7 @@ import { TeamEnvironmentsModule } from './team-environments/team-environments.mo
|
||||
import { TeamCollectionModule } from './team-collection/team-collection.module';
|
||||
import { TeamRequestModule } from './team-request/team-request.module';
|
||||
import { TeamInvitationModule } from './team-invitation/team-invitation.module';
|
||||
import { UserCollectionModule } from './user-collection/user-collection.module';
|
||||
import { ShortcodeModule } from './shortcode/shortcode.module';
|
||||
import { COOKIES_NOT_FOUND } from './errors';
|
||||
|
||||
@@ -64,6 +65,7 @@ import { COOKIES_NOT_FOUND } from './errors';
|
||||
TeamCollectionModule,
|
||||
TeamRequestModule,
|
||||
TeamInvitationModule,
|
||||
UserCollectionModule,
|
||||
ShortcodeModule,
|
||||
],
|
||||
providers: [GQLComplexityPlugin],
|
||||
|
||||
@@ -17,10 +17,17 @@ export class GoogleStrategy extends PassportStrategy(Strategy) {
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
callbackURL: process.env.GOOGLE_CALLBACK_URL,
|
||||
scope: process.env.GOOGLE_SCOPE.split(','),
|
||||
passReqToCallback: true,
|
||||
});
|
||||
}
|
||||
|
||||
async validate(accessToken, refreshToken, profile, done: VerifyCallback) {
|
||||
async validate(
|
||||
req: Request,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
profile,
|
||||
done: VerifyCallback,
|
||||
) {
|
||||
const user = await this.usersService.findUserByEmail(
|
||||
profile.emails[0].value,
|
||||
);
|
||||
|
||||
@@ -373,3 +373,68 @@ export const INVALID_ACCESS_TOKEN = 'auth/invalid_access_token' as const;
|
||||
* (AuthService)
|
||||
*/
|
||||
export const INVALID_REFRESH_TOKEN = 'auth/invalid_refresh_token' as const;
|
||||
|
||||
/**
|
||||
* The provided title for the user collection is short (less than 3 characters)
|
||||
* (UserCollectionService)
|
||||
*/
|
||||
export const USER_COLL_SHORT_TITLE = 'user_coll/short_title' as const;
|
||||
|
||||
/**
|
||||
* User Collection could not be found
|
||||
* (UserCollectionService)
|
||||
*/
|
||||
export const USER_COLL_NOT_FOUND = 'user_coll/not_found' as const;
|
||||
|
||||
/**
|
||||
* UserCollection is already a root collection
|
||||
* (UserCollectionService)
|
||||
*/
|
||||
export const USER_COL_ALREADY_ROOT =
|
||||
'user_coll/target_user_collection_is_already_root_user_collection' as const;
|
||||
|
||||
/**
|
||||
* Target and Parent user collections are the same
|
||||
* (UserCollectionService)
|
||||
*/
|
||||
export const USER_COLL_DEST_SAME =
|
||||
'user_coll/target_and_destination_user_collection_are_same' as const;
|
||||
|
||||
/**
|
||||
* Target and Parent user collections are not from the same user
|
||||
* (UserCollectionService)
|
||||
*/
|
||||
export const USER_COLL_NOT_SAME_USER = 'user_coll/not_same_user' as const;
|
||||
|
||||
/**
|
||||
* Target and Parent user collections are not from the same type
|
||||
* (UserCollectionService)
|
||||
*/
|
||||
export const USER_COLL_NOT_SAME_TYPE = 'user_coll/type_mismatch' as const;
|
||||
|
||||
/**
|
||||
* Cannot make a parent user collection a child of itself
|
||||
* (UserCollectionService)
|
||||
*/
|
||||
export const USER_COLL_IS_PARENT_COLL =
|
||||
'user_coll/user_collection_is_parent_coll' as const;
|
||||
|
||||
/**
|
||||
* User Collection Re-Ordering Failed
|
||||
* (UserCollectionService)
|
||||
*/
|
||||
export const USER_COLL_REORDERING_FAILED =
|
||||
'user_coll/reordering_failed' as const;
|
||||
|
||||
/**
|
||||
* The Collection and Next User Collection are the same
|
||||
* (UserCollectionService)
|
||||
*/
|
||||
export const USER_COLL_SAME_NEXT_COLL =
|
||||
'user_coll/user_collection_and_next_user_collection_are_same' as const;
|
||||
|
||||
/**
|
||||
* The User Collection does not belong to the logged-in user
|
||||
* (UserCollectionService)
|
||||
*/
|
||||
export const USER_NOT_OWNER = 'user_coll/user_not_owner' as const;
|
||||
|
||||
@@ -7,6 +7,8 @@ import { TeamEnvironment } from 'src/team-environments/team-environments.model';
|
||||
import { TeamCollection } from 'src/team-collection/team-collection.model';
|
||||
import { TeamRequest } from 'src/team-request/team-request.model';
|
||||
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
||||
import { UserCollection } from '@prisma/client';
|
||||
import { UserCollectionReorderData } from 'src/user-collection/user-collections.model';
|
||||
import { Shortcode } from 'src/shortcode/shortcode.model';
|
||||
|
||||
// A custom message type that defines the topic and the corresponding payload.
|
||||
@@ -21,6 +23,11 @@ export type TopicDef = {
|
||||
[
|
||||
topic: `user_history/${string}/${'created' | 'updated' | 'deleted'}`
|
||||
]: UserHistory;
|
||||
[
|
||||
topic: `user_coll/${string}/${'created' | 'updated' | 'moved'}`
|
||||
]: UserCollection;
|
||||
[topic: `user_coll/${string}/${'deleted'}`]: string;
|
||||
[topic: `user_coll/${string}/${'order_updated'}`]: UserCollectionReorderData;
|
||||
[topic: `team/${string}/member_removed`]: string;
|
||||
[topic: `team/${string}/${'member_added' | 'member_updated'}`]: TeamMember;
|
||||
[
|
||||
|
||||
@@ -36,7 +36,7 @@ export class TeamCollectionResolver {
|
||||
|
||||
@ResolveField(() => TeamCollection, {
|
||||
description:
|
||||
'The collection whom is the parent of this collection (null if this is root collection)',
|
||||
'The collection who is the parent of this collection (null if this is root collection)',
|
||||
nullable: true,
|
||||
complexity: 3,
|
||||
})
|
||||
|
||||
4
packages/hoppscotch-backend/src/types/RequestTypes.ts
Normal file
4
packages/hoppscotch-backend/src/types/RequestTypes.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum ReqType {
|
||||
REST = 'REST',
|
||||
GQL = 'GQL',
|
||||
}
|
||||
@@ -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',
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import * as T from 'fp-ts/Task';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import * as A from 'fp-ts/Array';
|
||||
import { TeamMemberRole } from './team/team.model';
|
||||
import { User } from './user/user.model';
|
||||
import { JSON_INVALID } from './errors';
|
||||
|
||||
/**
|
||||
@@ -124,7 +125,7 @@ export const validateEmail = (email: string) => {
|
||||
).test(email);
|
||||
};
|
||||
|
||||
/*
|
||||
/**
|
||||
* String to JSON parser
|
||||
* @param {str} str The string to parse
|
||||
* @returns {E.Right<T> | E.Left<"json_invalid">} An Either of the parsed JSON
|
||||
@@ -138,3 +139,16 @@ export function stringToJson<T>(
|
||||
return E.left(JSON_INVALID);
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param title string whose length we need to check
|
||||
* @param length minimum length the title needs to be
|
||||
* @returns boolean if title is of valid length or not
|
||||
*/
|
||||
export function isValidLength(title: string, length: number) {
|
||||
if (title.length < length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user