feat: adding support for hopp-cli in self-host Hoppscotch (#4079)

* feat: created a new table to store user PATs

* chore: renamed UserTokens table name to PersonalAccessToken

* chore: added unique property to token column

* chore: made expiresOn field optional

* chore: created access-token module

* feat: created access-token rest routes

* chore: created a new auth guard for PATs

* chore: scaffolded routes in team collection and environments modules for CLI

* chore: created method to update lastUsedOn property for accessTokens

* chore: created interceptor to update lastUsedOn property of PAT

* feat: interceptor to update lastUpdatedOn property complete

* chore: removed unused imports in access-token interceptor

* chore: moved routes to fetch collection and environment into access-token module

* feat: added routes to fetch collections and environments for CLI

* chore: modified access-token interceptor

* chore: removed log statement from interceptor

* chore: added team member checking logic to ForCLI methods in team collection and environments module

* chore: changed return error messages to those defined in spec

* chore: added comments to all service methods

* chore: removed unused imports

* chore: updated testcases for team-environments module service file

* chore: added and updated testcases

* chore: removed unneseccary SQL from auto-generated migration sql for PAT

* chore: remobed JWTAuthGuard from relevant routes in PAT controllers file

* chore: modified token for auth in PATAuthGuard

* chore: changed error codes in some certain service methods in access-token module

* feat: worked on feedback for PR review

* chore: renamed service method in access-token module

* chore: removed console log statements

* chore: modified cli error type

* test: fix broken test case

* chore: changed target of hopp-old-backend to prod

---------

Co-authored-by: mirarifhasan <arif.ishan05@gmail.com>
This commit is contained in:
Balu Babu
2024-05-28 16:39:50 +05:30
committed by GitHub
parent f4f3fdf2d5
commit 4bd23a8f4c
20 changed files with 983 additions and 24 deletions

View File

@@ -1,3 +1,5 @@
import { TeamRequest } from '@prisma/client';
// Type of data returned from the query to obtain all search results
export type SearchQueryReturnType = {
id: string;
@@ -12,3 +14,12 @@ export type ParentTreeQueryReturnType = {
parentID: string;
title: string;
};
// Type of data returned from the query to fetch collection details from CLI
export type GetCollectionResponse = {
id: string;
data: string | null;
title: string;
parentID: string | null;
folders: GetCollectionResponse[];
requests: TeamRequest[];
};

View File

@@ -12,6 +12,7 @@ import {
TEAM_COL_REORDERING_FAILED,
TEAM_COL_SAME_NEXT_COLL,
TEAM_INVALID_COLL_ID,
TEAM_MEMBER_NOT_FOUND,
TEAM_NOT_OWNER,
} from 'src/errors';
import { PrismaService } from 'src/prisma/prisma.service';
@@ -19,15 +20,18 @@ import { PubSubService } from 'src/pubsub/pubsub.service';
import { AuthUser } from 'src/types/AuthUser';
import { TeamCollectionService } from './team-collection.service';
import { TeamCollection } from './team-collection.model';
import { TeamService } from 'src/team/team.service';
const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>();
const mockTeamService = mockDeep<TeamService>();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const teamCollectionService = new TeamCollectionService(
mockPrisma,
mockPubSub as any,
mockTeamService,
);
const currentTime = new Date();
@@ -1739,3 +1743,63 @@ describe('updateTeamCollection', () => {
});
//ToDo: write test cases for exportCollectionsToJSON
describe('getCollectionForCLI', () => {
test('should throw TEAM_COLL_NOT_FOUND if collectionID is invalid', async () => {
mockPrisma.teamCollection.findUniqueOrThrow.mockRejectedValueOnce(
'NotFoundError',
);
const result = await teamCollectionService.getCollectionForCLI(
'invalidID',
user.uid,
);
expect(result).toEqualLeft(TEAM_COLL_NOT_FOUND);
});
test('should throw TEAM_MEMBER_NOT_FOUND if user not in same team', async () => {
mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce(
rootTeamCollection,
);
mockTeamService.getTeamMember.mockResolvedValue(null);
const result = await teamCollectionService.getCollectionForCLI(
rootTeamCollection.id,
user.uid,
);
expect(result).toEqualLeft(TEAM_MEMBER_NOT_FOUND);
});
// test('should return the TeamCollection data for CLI', async () => {
// mockPrisma.teamCollection.findUniqueOrThrow.mockResolvedValueOnce(
// rootTeamCollection,
// );
// mockTeamService.getTeamMember.mockResolvedValue({
// membershipID: 'sdc3sfdv',
// userUid: user.uid,
// role: TeamMemberRole.OWNER,
// });
// const result = await teamCollectionService.getCollectionForCLI(
// rootTeamCollection.id,
// user.uid,
// );
// expect(result).toEqualRight({
// id: rootTeamCollection.id,
// data: JSON.stringify(rootTeamCollection.data),
// title: rootTeamCollection.title,
// parentID: rootTeamCollection.parentID,
// folders: [
// {
// id: childTeamCollection.id,
// data: JSON.stringify(childTeamCollection.data),
// title: childTeamCollection.title,
// parentID: childTeamCollection.parentID,
// folders: [],
// requests: [],
// },
// ],
// requests: [],
// });
// });
});

View File

@@ -18,23 +18,34 @@ import {
TEAM_COL_SEARCH_FAILED,
TEAM_REQ_PARENT_TREE_GEN_FAILED,
TEAM_COLL_PARENT_TREE_GEN_FAILED,
TEAM_MEMBER_NOT_FOUND,
} from '../errors';
import { PubSubService } from '../pubsub/pubsub.service';
import { escapeSqlLikeString, isValidLength } from 'src/utils';
import * as E from 'fp-ts/Either';
import * as O from 'fp-ts/Option';
import { Prisma, TeamCollection as DBTeamCollection } from '@prisma/client';
import {
Prisma,
TeamCollection as DBTeamCollection,
TeamRequest,
} from '@prisma/client';
import { CollectionFolder } from 'src/types/CollectionFolder';
import { stringToJson } from 'src/utils';
import { CollectionSearchNode } from 'src/types/CollectionSearchNode';
import { ParentTreeQueryReturnType, SearchQueryReturnType } from './helper';
import {
GetCollectionResponse,
ParentTreeQueryReturnType,
SearchQueryReturnType,
} from './helper';
import { RESTError } from 'src/types/RESTError';
import { TeamService } from 'src/team/team.service';
@Injectable()
export class TeamCollectionService {
constructor(
private readonly prisma: PrismaService,
private readonly pubsub: PubSubService,
private readonly teamService: TeamService,
) {}
TITLE_LENGTH = 3;
@@ -1344,4 +1355,95 @@ export class TeamCollectionService {
return E.left(TEAM_REQ_PARENT_TREE_GEN_FAILED);
}
}
/**
* Get all requests in a collection
*
* @param collectionID The Collection ID
* @returns A list of all requests in the collection
*/
private async getAllRequestsInCollection(collectionID: string) {
const dbTeamRequests = await this.prisma.teamRequest.findMany({
where: {
collectionID: collectionID,
},
orderBy: {
orderIndex: 'asc',
},
});
const teamRequests = dbTeamRequests.map((tr) => {
return <TeamRequest>{
id: tr.id,
collectionID: tr.collectionID,
teamID: tr.teamID,
title: tr.title,
request: JSON.stringify(tr.request),
};
});
return teamRequests;
}
/**
* Get Collection Tree for CLI
*
* @param parentID The parent Collection ID
* @returns Collection tree for CLI
*/
private async getCollectionTreeForCLI(parentID: string | null) {
const childCollections = await this.prisma.teamCollection.findMany({
where: { parentID },
orderBy: { orderIndex: 'asc' },
});
const response: GetCollectionResponse[] = [];
for (const collection of childCollections) {
const folder: GetCollectionResponse = {
id: collection.id,
data: collection.data === null ? null : JSON.stringify(collection.data),
title: collection.title,
parentID: collection.parentID,
folders: await this.getCollectionTreeForCLI(collection.id),
requests: await this.getAllRequestsInCollection(collection.id),
};
response.push(folder);
}
return response;
}
/**
* Get Collection for CLI
*
* @param collectionID The Collection ID
* @param userUid The User UID
* @returns An Either of the Collection details
*/
async getCollectionForCLI(collectionID: string, userUid: string) {
try {
const collection = await this.prisma.teamCollection.findUniqueOrThrow({
where: { id: collectionID },
});
const teamMember = await this.teamService.getTeamMember(
collection.teamID,
userUid,
);
if (!teamMember) return E.left(TEAM_MEMBER_NOT_FOUND);
return E.right(<GetCollectionResponse>{
id: collection.id,
data: collection.data === null ? null : JSON.stringify(collection.data),
title: collection.title,
parentID: collection.parentID,
folders: await this.getCollectionTreeForCLI(collection.id),
requests: await this.getAllRequestsInCollection(collection.id),
});
} catch (error) {
return E.left(TEAM_COLL_NOT_FOUND);
}
}
}