refactor: adding JSON import/export functions to UserCollections module (HBE-169) (#37)

* feat: created exportUserCollectionsToJSON mutation for UserCollection module

* chore: added comments to export functions

* chore: added type and user ownership checking to creation methods

* chore: replaced request property with spread request object instead

* chore: completed all changes requested in inital review of PR

* chore: explicitly exporting request title in export function

* chore: explicitly exporting request title in export function

* chore: added codegen folder to gitignore

* chore: removed gql-code gen file from repo
This commit is contained in:
Balu Babu
2023-03-14 18:31:47 +05:30
committed by GitHub
parent e5002b4ef3
commit be46ed2686
6 changed files with 352 additions and 14 deletions

View File

@@ -223,8 +223,7 @@ export const TEAM_REQ_INVALID_TARGET_COLL_ID =
* Tried to reorder team request but failed
* (TeamRequestService)
*/
export const TEAM_REQ_REORDERING_FAILED =
'team_req/reordering_failed' as const;
export const TEAM_REQ_REORDERING_FAILED = 'team_req/reordering_failed' as const;
/**
* No Postmark Sender Email defined
@@ -487,7 +486,7 @@ 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 =
export const USER_COLL_ALREADY_ROOT =
'user_coll/target_user_collection_is_already_root_user_collection' as const;
/**
@@ -535,3 +534,9 @@ export const USER_COLL_SAME_NEXT_COLL =
* (UserCollectionService)
*/
export const USER_NOT_OWNER = 'user_coll/user_not_owner' as const;
/**
* The JSON used is not valid
* (UserCollectionService)
*/
export const USER_COLL_INVALID_JSON = 'user_coll/invalid_json';

View File

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

View File

@@ -1,4 +1,5 @@
import { Field, ID, ArgsType } from '@nestjs/graphql';
import { ReqType } from '@prisma/client';
import { PaginationArgs } from 'src/types/input-types.args';
@ArgsType()
@@ -73,3 +74,24 @@ export class MoveUserCollectionArgs {
})
userCollectionID: string;
}
@ArgsType()
export class ImportUserCollectionsFromJSONArgs {
@Field({
name: 'jsonString',
description: 'JSON string to import',
})
jsonString: string;
@Field({
name: 'reqType',
description: 'Type of UserCollection',
})
reqType: ReqType;
@Field(() => ID, {
name: 'parentCollectionID',
description:
'ID to the collection to which to import into (null if to import into the root of the user)',
nullable: true,
})
parentCollectionID?: string;
}

View File

@@ -24,6 +24,7 @@ import { PaginationArgs } from 'src/types/input-types.args';
import {
CreateChildUserCollectionArgs,
CreateRootUserCollectionArgs,
ImportUserCollectionsFromJSONArgs,
MoveUserCollectionArgs,
RenameUserCollectionsArgs,
UpdateUserCollectionArgs,
@@ -139,6 +140,32 @@ export class UserCollectionResolver {
return userCollection.right;
}
@Query(() => String, {
description:
'Returns the JSON string giving the collections and their contents of a user',
})
@UseGuards(GqlAuthGuard)
async exportUserCollectionsToJSON(
@GqlUser() user: AuthUser,
@Args({
type: () => ID,
name: 'collectionID',
description: 'ID of the user collection',
nullable: true,
defaultValue: null,
})
collectionID: string,
) {
const jsonString =
await this.userCollectionService.exportUserCollectionsToJSON(
user.uid,
collectionID,
);
if (E.isLeft(jsonString)) throwErr(jsonString.left as string);
return jsonString.right;
}
// Mutations
@Mutation(() => UserCollection, {
description: 'Creates root REST user collection(no parent user collection)',
@@ -300,6 +327,25 @@ export class UserCollectionResolver {
return res.right;
}
@Mutation(() => Boolean, {
description: 'Import collections from JSON string to the specified Team',
})
@UseGuards(GqlAuthGuard)
async importUserCollectionsFromJSON(
@Args() args: ImportUserCollectionsFromJSONArgs,
@GqlUser() user: AuthUser,
) {
const importedCollection =
await this.userCollectionService.importCollectionsFromJSON(
args.jsonString,
user.uid,
args.parentCollectionID,
args.reqType,
);
if (E.isLeft(importedCollection)) throwErr(importedCollection.left);
return importedCollection.right;
}
// Subscriptions
@Subscription(() => UserCollection, {
description: 'Listen for User Collection Creation',

View File

@@ -9,7 +9,7 @@ import {
USER_COLL_REORDERING_FAILED,
USER_COLL_SAME_NEXT_COLL,
USER_COLL_SHORT_TITLE,
USER_COL_ALREADY_ROOT,
USER_COLL_ALREADY_ROOT,
USER_NOT_OWNER,
} from 'src/errors';
import { PrismaService } from 'src/prisma/prisma.service';
@@ -997,7 +997,7 @@ describe('moveUserCollection', () => {
null,
user.uid,
);
expect(result).toEqualLeft(USER_COL_ALREADY_ROOT);
expect(result).toEqualLeft(USER_COLL_ALREADY_ROOT);
});
test('should successfully move a child user-collection into root', async () => {
// getUserCollection

View File

@@ -8,19 +8,26 @@ import {
USER_COLL_REORDERING_FAILED,
USER_COLL_SAME_NEXT_COLL,
USER_COLL_SHORT_TITLE,
USER_COL_ALREADY_ROOT,
USER_COLL_ALREADY_ROOT,
USER_NOT_FOUND,
USER_NOT_OWNER,
USER_COLL_INVALID_JSON,
} 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 {
Prisma,
User,
UserCollection,
ReqType as DBReqType,
} from '@prisma/client';
import { UserCollection as UserCollectionModel } from './user-collections.model';
import { ReqType } from 'src/types/RequestTypes';
import { isValidLength } from 'src/utils';
import { isValidLength, stringToJson } from 'src/utils';
import { CollectionFolder } from 'src/types/CollectionFolder';
@Injectable()
export class UserCollectionService {
constructor(
@@ -209,10 +216,20 @@ export class UserCollectionService {
const isTitleValid = isValidLength(title, 3);
if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE);
// Check to see if parentUserCollectionID belongs to this User
// If creating a child collection
if (parentUserCollectionID !== null) {
const isOwner = await this.isOwnerCheck(parentUserCollectionID, user.uid);
if (O.isNone(isOwner)) return E.left(USER_NOT_OWNER);
const parentCollection = await this.getUserCollection(
parentUserCollectionID,
);
if (E.isLeft(parentCollection)) return E.left(parentCollection.left);
// Check to see if parentUserCollectionID belongs to this User
if (parentCollection.right.userUid !== user.uid)
return E.left(USER_NOT_OWNER);
// Check to see if parent collection is of the same type of new collection being created
if (parentCollection.right.type !== type)
return E.left(USER_COLL_NOT_SAME_TYPE);
}
const isParent = parentUserCollectionID
@@ -555,7 +572,7 @@ export class UserCollectionService {
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);
return E.left(USER_COLL_ALREADY_ROOT);
}
// Move child collection into root and update orderIndexes for root userCollections
await this.updateOrderIndex(
@@ -764,4 +781,253 @@ export class UserCollectionService {
return E.left(USER_COLL_REORDERING_FAILED);
}
}
/**
* Generate a JSON containing all the contents of a collection
*
* @param userUID The User UID
* @param collectionID The Collection ID
* @returns A JSON string containing all the contents of a collection
*/
private async exportUserCollectionToJSONObject(
userUID: string,
collectionID: string,
): Promise<E.Left<string> | E.Right<CollectionFolder>> {
// Get Collection details
const collection = await this.getUserCollection(collectionID);
if (E.isLeft(collection)) return E.left(collection.left);
// Get all child collections whose parentID === collectionID
const childCollectionList = await this.prisma.userCollection.findMany({
where: {
parentID: collectionID,
userUid: userUID,
},
orderBy: {
orderIndex: 'asc',
},
});
// Create a list of child collection and request data ready for export
const childrenCollectionObjects: CollectionFolder[] = [];
for (const coll of childCollectionList) {
const result = await this.exportUserCollectionToJSONObject(
userUID,
coll.id,
);
if (E.isLeft(result)) return E.left(result.left);
childrenCollectionObjects.push(result.right);
}
// Fetch all child requests that belong to collectionID
const requests = await this.prisma.userRequest.findMany({
where: {
userUid: userUID,
collectionID,
},
orderBy: {
orderIndex: 'asc',
},
});
const result: CollectionFolder = {
id: collection.right.id,
name: collection.right.title,
folders: childrenCollectionObjects,
requests: requests.map((x) => {
return {
id: x.id,
name: x.title,
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
};
}),
};
return E.right(result);
}
/**
* Generate a JSON containing all the contents of collections and requests of a team
*
* @param userUID The User UID
* @returns A JSON string containing all the contents of collections and requests of a team
*/
async exportUserCollectionsToJSON(
userUID: string,
collectionID: string | null,
) {
// Get all child collections details
const childCollectionList = await this.prisma.userCollection.findMany({
where: {
userUid: userUID,
parentID: collectionID,
},
});
// Create a list of child collection and request data ready for export
const collectionListObjects: CollectionFolder[] = [];
for (const coll of childCollectionList) {
const result = await this.exportUserCollectionToJSONObject(
userUID,
coll.id,
);
if (E.isLeft(result)) return E.left(result.left);
collectionListObjects.push(result.right);
}
// If collectionID is not null, return JSONified data for specific collection
if (collectionID) {
// Get Details of collection
const parentCollection = await this.getUserCollection(collectionID);
if (E.isLeft(parentCollection)) return E.left(parentCollection.left);
// Fetch all child requests that belong to collectionID
const requests = await this.prisma.userRequest.findMany({
where: {
userUid: userUID,
collectionID: parentCollection.right.id,
},
orderBy: {
orderIndex: 'asc',
},
});
return E.right(
JSON.stringify({
id: parentCollection.right.id,
name: parentCollection.right.title,
folders: collectionListObjects,
requests: requests.map((x) => {
return {
id: x.id,
name: x.title,
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
};
}),
}),
);
}
return E.right(JSON.stringify(collectionListObjects));
}
/**
* Generate a Prisma query object representation of a collection and its child collections and requests
*
* @param folder CollectionFolder from client
* @param userID The User ID
* @param orderIndex Initial OrderIndex of
* @param reqType The Type of Collection
* @returns A Prisma query object to create a collection, its child collections and requests
*/
private generatePrismaQueryObj(
folder: CollectionFolder,
userID: string,
orderIndex: number,
reqType: DBReqType,
): Prisma.UserCollectionCreateInput {
return {
title: folder.name,
user: {
connect: {
uid: userID,
},
},
requests: {
create: folder.requests.map((r, index) => ({
title: r.name,
user: {
connect: {
uid: userID,
},
},
type: reqType,
request: r,
orderIndex: index + 1,
})),
},
orderIndex: orderIndex,
type: reqType,
children: {
create: folder.folders.map((f, index) =>
this.generatePrismaQueryObj(f, userID, index + 1, reqType),
),
},
};
}
/**
* Create new UserCollections and UserRequests from JSON string
*
* @param jsonString The JSON string of the content
* @param userID The User ID
* @param destCollectionID The Collection ID
* @param reqType The Type of Collection
* @returns An Either of a Boolean if the creation operation was successful
*/
async importCollectionsFromJSON(
jsonString: string,
userID: string,
destCollectionID: string | null,
reqType: DBReqType,
) {
// Check to see if jsonString is valid
const collectionsList = stringToJson<CollectionFolder[]>(jsonString);
if (E.isLeft(collectionsList)) return E.left(USER_COLL_INVALID_JSON);
// Check to see if parsed jsonString is an array
if (!Array.isArray(collectionsList.right))
return E.left(USER_COLL_INVALID_JSON);
// Check to see if destCollectionID belongs to this User
if (destCollectionID) {
const parentCollection = await this.getUserCollection(destCollectionID);
if (E.isLeft(parentCollection)) return E.left(parentCollection.left);
// Check to see if parentUserCollectionID belongs to this User
if (parentCollection.right.userUid !== userID)
return E.left(USER_NOT_OWNER);
// Check to see if parent collection is of the same type of new collection being created
if (parentCollection.right.type !== reqType)
return E.left(USER_COLL_NOT_SAME_TYPE);
}
// Get number of root or child collections for destCollectionID(if destcollectionID != null) or destTeamID(if destcollectionID == null)
const count = !destCollectionID
? await this.getRootCollectionsCount(userID)
: await this.getChildCollectionsCount(destCollectionID);
// Generate Prisma Query Object for all child collections in collectionsList
const queryList = collectionsList.right.map((x) =>
this.generatePrismaQueryObj(x, userID, count + 1, reqType),
);
const parent = destCollectionID
? {
connect: {
id: destCollectionID,
},
}
: undefined;
const userCollections = await this.prisma.$transaction(
queryList.map((x) =>
this.prisma.userCollection.create({
data: {
...x,
parent,
},
}),
),
);
userCollections.forEach((x) =>
this.pubsub.publish(`user_coll/${userID}/created`, x),
);
return E.right(true);
}
}