Compare commits
3 Commits
test/backe
...
fix/duplic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
596ea7e364 | ||
|
|
2c7f5f6173 | ||
|
|
f19dc700ef |
@@ -31,7 +31,6 @@ MICROSOFT_CLIENT_ID="************************************************"
|
|||||||
MICROSOFT_CLIENT_SECRET="************************************************"
|
MICROSOFT_CLIENT_SECRET="************************************************"
|
||||||
MICROSOFT_CALLBACK_URL="http://localhost:3170/v1/auth/microsoft/callback"
|
MICROSOFT_CALLBACK_URL="http://localhost:3170/v1/auth/microsoft/callback"
|
||||||
MICROSOFT_SCOPE="user.read"
|
MICROSOFT_SCOPE="user.read"
|
||||||
MICROSOFT_TENANT="common"
|
|
||||||
|
|
||||||
# Mailer config
|
# Mailer config
|
||||||
MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com"
|
MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hoppscotch-backend",
|
"name": "hoppscotch-backend",
|
||||||
"version": "2023.4.7",
|
"version": "2023.4.6",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -10,23 +10,11 @@ import { TeamInvitationService } from '../team-invitation/team-invitation.servic
|
|||||||
import { TeamCollectionService } from '../team-collection/team-collection.service';
|
import { TeamCollectionService } from '../team-collection/team-collection.service';
|
||||||
import { MailerService } from '../mailer/mailer.service';
|
import { MailerService } from '../mailer/mailer.service';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { User as DbUser } from '@prisma/client';
|
|
||||||
import {
|
import {
|
||||||
DUPLICATE_EMAIL,
|
DUPLICATE_EMAIL,
|
||||||
INVALID_EMAIL,
|
INVALID_EMAIL,
|
||||||
ONLY_ONE_ADMIN_ACCOUNT,
|
|
||||||
TEAM_INVITE_ALREADY_MEMBER,
|
|
||||||
TEAM_MEMBER_NOT_FOUND,
|
|
||||||
USER_ALREADY_INVITED,
|
USER_ALREADY_INVITED,
|
||||||
USER_IS_ADMIN,
|
|
||||||
USER_NOT_FOUND,
|
|
||||||
} from '../errors';
|
} from '../errors';
|
||||||
import { Team, TeamMember, TeamMemberRole } from 'src/team/team.model';
|
|
||||||
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
|
||||||
import * as E from 'fp-ts/Either';
|
|
||||||
import * as O from 'fp-ts/Option';
|
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import * as utils from 'src/utils';
|
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockPubSub = mockDeep<PubSubService>();
|
const mockPubSub = mockDeep<PubSubService>();
|
||||||
@@ -64,582 +52,7 @@ const invitedUsers: InvitedUsers[] = [
|
|||||||
invitedOn: new Date(),
|
invitedOn: new Date(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const allUsers: DbUser[] = [
|
|
||||||
{
|
|
||||||
uid: 'uid1',
|
|
||||||
displayName: 'user1',
|
|
||||||
email: 'user1@hoppscotch.io',
|
|
||||||
photoURL: 'https://hoppscotch.io',
|
|
||||||
isAdmin: true,
|
|
||||||
refreshToken: 'refreshToken',
|
|
||||||
currentRESTSession: null,
|
|
||||||
currentGQLSession: null,
|
|
||||||
createdOn: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
uid: 'uid2',
|
|
||||||
displayName: 'user2',
|
|
||||||
email: 'user2@hoppscotch.io',
|
|
||||||
photoURL: 'https://hoppscotch.io',
|
|
||||||
isAdmin: false,
|
|
||||||
refreshToken: 'refreshToken',
|
|
||||||
currentRESTSession: null,
|
|
||||||
currentGQLSession: null,
|
|
||||||
createdOn: new Date(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const teamMembers: TeamMember[] = [
|
|
||||||
{
|
|
||||||
membershipID: 'teamMember1',
|
|
||||||
userUid: allUsers[0].uid,
|
|
||||||
role: TeamMemberRole.OWNER,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const teams: Team[] = [
|
|
||||||
{
|
|
||||||
id: 'team1',
|
|
||||||
name: 'team1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'team2',
|
|
||||||
name: 'team2',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const teamInvitations: TeamInvitation[] = [
|
|
||||||
{
|
|
||||||
id: 'teamInvitation1',
|
|
||||||
teamID: 'team1',
|
|
||||||
creatorUid: 'uid1',
|
|
||||||
inviteeEmail: '',
|
|
||||||
inviteeRole: TeamMemberRole.OWNER,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('AdminService', () => {
|
describe('AdminService', () => {
|
||||||
describe('fetchUsers', () => {
|
|
||||||
test('should resolve right and return an array of users if cursorID is null', async () => {
|
|
||||||
mockUserService.fetchAllUsers.mockResolvedValueOnce(allUsers);
|
|
||||||
|
|
||||||
const result = await adminService.fetchUsers(null, 10);
|
|
||||||
|
|
||||||
expect(result).toEqual(allUsers);
|
|
||||||
expect(mockUserService.fetchAllUsers).toHaveBeenCalledWith(null, 10);
|
|
||||||
});
|
|
||||||
test('should resolve right and return an array of users if cursorID is not null', async () => {
|
|
||||||
mockUserService.fetchAllUsers.mockResolvedValueOnce([allUsers[1]]);
|
|
||||||
|
|
||||||
const cursorID = allUsers[0].uid;
|
|
||||||
const result = await adminService.fetchUsers(cursorID, 10);
|
|
||||||
|
|
||||||
expect(result).toEqual([allUsers[1]]);
|
|
||||||
expect(mockUserService.fetchAllUsers).toHaveBeenCalledWith(cursorID, 10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('fetchAllTeams', () => {
|
|
||||||
test('should resolve right and return an array of teams if cursorID is null', async () => {
|
|
||||||
mockTeamService.fetchAllTeams.mockResolvedValueOnce(teams);
|
|
||||||
|
|
||||||
const result = await adminService.fetchAllTeams(null, 10);
|
|
||||||
|
|
||||||
expect(result).toEqual(teams);
|
|
||||||
expect(mockTeamService.fetchAllTeams).toHaveBeenCalledWith(null, 10);
|
|
||||||
});
|
|
||||||
test('should resolve right and return an array of teams if cursorID is not null', async () => {
|
|
||||||
mockTeamService.fetchAllTeams.mockResolvedValueOnce([teams[1]]);
|
|
||||||
|
|
||||||
const cursorID = teams[0].id;
|
|
||||||
const result = await adminService.fetchAllTeams(cursorID, 10);
|
|
||||||
|
|
||||||
expect(result).toEqual([teams[1]]);
|
|
||||||
expect(mockTeamService.fetchAllTeams).toHaveBeenCalledWith(cursorID, 10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('membersCountInTeam', () => {
|
|
||||||
test('should resolve right and return the count of members in a team', async () => {
|
|
||||||
mockTeamService.getCountOfMembersInTeam.mockResolvedValueOnce(10);
|
|
||||||
|
|
||||||
const result = await adminService.membersCountInTeam('team1');
|
|
||||||
|
|
||||||
expect(result).toEqual(10);
|
|
||||||
expect(mockTeamService.getCountOfMembersInTeam).toHaveBeenCalledWith(
|
|
||||||
'team1',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('collectionCountInTeam', () => {
|
|
||||||
test('should resolve right and return the count of collections in a team', async () => {
|
|
||||||
mockTeamCollectionService.totalCollectionsInTeam.mockResolvedValueOnce(
|
|
||||||
10,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await adminService.collectionCountInTeam('team1');
|
|
||||||
|
|
||||||
expect(result).toEqual(10);
|
|
||||||
expect(
|
|
||||||
mockTeamCollectionService.totalCollectionsInTeam,
|
|
||||||
).toHaveBeenCalledWith('team1');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('requestCountInTeam', () => {
|
|
||||||
test('should resolve right and return the count of requests in a team', async () => {
|
|
||||||
mockTeamRequestService.totalRequestsInATeam.mockResolvedValueOnce(10);
|
|
||||||
|
|
||||||
const result = await adminService.requestCountInTeam('team1');
|
|
||||||
|
|
||||||
expect(result).toEqual(10);
|
|
||||||
expect(mockTeamRequestService.totalRequestsInATeam).toHaveBeenCalledWith(
|
|
||||||
'team1',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('environmentCountInTeam', () => {
|
|
||||||
test('should resolve right and return the count of environments in a team', async () => {
|
|
||||||
mockTeamEnvironmentsService.totalEnvsInTeam.mockResolvedValueOnce(10);
|
|
||||||
|
|
||||||
const result = await adminService.environmentCountInTeam('team1');
|
|
||||||
|
|
||||||
expect(result).toEqual(10);
|
|
||||||
expect(mockTeamEnvironmentsService.totalEnvsInTeam).toHaveBeenCalledWith(
|
|
||||||
'team1',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('pendingInvitationCountInTeam', () => {
|
|
||||||
test('should resolve right and return the count of pending invitations in a team', async () => {
|
|
||||||
mockTeamInvitationService.getTeamInvitations.mockResolvedValueOnce(
|
|
||||||
teamInvitations,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await adminService.pendingInvitationCountInTeam('team1');
|
|
||||||
|
|
||||||
expect(result).toEqual(teamInvitations);
|
|
||||||
expect(
|
|
||||||
mockTeamInvitationService.getTeamInvitations,
|
|
||||||
).toHaveBeenCalledWith('team1');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('changeRoleOfUserTeam', () => {
|
|
||||||
test('should resolve right and return the count of pending invitations in a team', async () => {
|
|
||||||
const teamMember = teamMembers[0];
|
|
||||||
|
|
||||||
mockTeamService.updateTeamMemberRole.mockResolvedValueOnce(
|
|
||||||
E.right(teamMember),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await adminService.changeRoleOfUserTeam(
|
|
||||||
teamMember.userUid,
|
|
||||||
'team1',
|
|
||||||
teamMember.role,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqualRight(teamMember);
|
|
||||||
expect(mockTeamService.updateTeamMemberRole).toHaveBeenCalledWith(
|
|
||||||
'team1',
|
|
||||||
teamMember.userUid,
|
|
||||||
teamMember.role,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should resolve left and return the error if any error occurred', async () => {
|
|
||||||
const teamMember = teamMembers[0];
|
|
||||||
const errorMessage = 'Team member not found';
|
|
||||||
|
|
||||||
mockTeamService.updateTeamMemberRole.mockResolvedValueOnce(
|
|
||||||
E.left(errorMessage),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await adminService.changeRoleOfUserTeam(
|
|
||||||
teamMember.userUid,
|
|
||||||
'team1',
|
|
||||||
teamMember.role,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqualLeft(errorMessage);
|
|
||||||
expect(mockTeamService.updateTeamMemberRole).toHaveBeenCalledWith(
|
|
||||||
'team1',
|
|
||||||
teamMember.userUid,
|
|
||||||
teamMember.role,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('removeUserFromTeam', () => {
|
|
||||||
test('should resolve right and remove user from a team', async () => {
|
|
||||||
const teamMember = teamMembers[0];
|
|
||||||
|
|
||||||
mockTeamService.leaveTeam.mockResolvedValueOnce(E.right(true));
|
|
||||||
|
|
||||||
const result = await adminService.removeUserFromTeam(
|
|
||||||
teamMember.userUid,
|
|
||||||
'team1',
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqualRight(true);
|
|
||||||
expect(mockTeamService.leaveTeam).toHaveBeenCalledWith(
|
|
||||||
'team1',
|
|
||||||
teamMember.userUid,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should resolve left and return the error if any error occurred', async () => {
|
|
||||||
const teamMember = teamMembers[0];
|
|
||||||
const errorMessage = 'Team member not found';
|
|
||||||
|
|
||||||
mockTeamService.leaveTeam.mockResolvedValueOnce(E.left(errorMessage));
|
|
||||||
|
|
||||||
const result = await adminService.removeUserFromTeam(
|
|
||||||
teamMember.userUid,
|
|
||||||
'team1',
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqualLeft(errorMessage);
|
|
||||||
expect(mockTeamService.leaveTeam).toHaveBeenCalledWith(
|
|
||||||
'team1',
|
|
||||||
teamMember.userUid,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('addUserToTeam', () => {
|
|
||||||
test('should return INVALID_EMAIL when email is invalid', async () => {
|
|
||||||
const teamID = 'team1';
|
|
||||||
const userEmail = 'invalidEmail';
|
|
||||||
const role = TeamMemberRole.EDITOR;
|
|
||||||
|
|
||||||
const mockValidateEmail = jest.spyOn(utils, 'validateEmail');
|
|
||||||
mockValidateEmail.mockReturnValueOnce(false);
|
|
||||||
|
|
||||||
const result = await adminService.addUserToTeam(teamID, userEmail, role);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.left(INVALID_EMAIL));
|
|
||||||
expect(mockValidateEmail).toHaveBeenCalledWith(userEmail);
|
|
||||||
expect(mockUserService.findUserByEmail).not.toHaveBeenCalled();
|
|
||||||
expect(mockTeamService.getTeamMemberTE).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return USER_NOT_FOUND when user is not found', async () => {
|
|
||||||
const teamID = 'team1';
|
|
||||||
const userEmail = 'u@example.com';
|
|
||||||
const role = TeamMemberRole.EDITOR;
|
|
||||||
|
|
||||||
const mockValidateEmail = jest.spyOn(utils, 'validateEmail');
|
|
||||||
mockValidateEmail.mockReturnValueOnce(true);
|
|
||||||
mockUserService.findUserByEmail.mockResolvedValue(O.none);
|
|
||||||
|
|
||||||
const result = await adminService.addUserToTeam(teamID, userEmail, role);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.left(USER_NOT_FOUND));
|
|
||||||
expect(mockValidateEmail).toHaveBeenCalledWith(userEmail);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return TEAM_INVITE_ALREADY_MEMBER when user is already a member of the team', async () => {
|
|
||||||
const teamID = 'team1';
|
|
||||||
const userEmail = allUsers[0].email;
|
|
||||||
const role = TeamMemberRole.EDITOR;
|
|
||||||
|
|
||||||
const mockValidateEmail = jest.spyOn(utils, 'validateEmail');
|
|
||||||
mockValidateEmail.mockReturnValueOnce(true);
|
|
||||||
mockUserService.findUserByEmail.mockResolvedValueOnce(
|
|
||||||
O.some(allUsers[0]),
|
|
||||||
);
|
|
||||||
mockTeamService.getTeamMemberTE.mockReturnValueOnce(
|
|
||||||
TE.right(teamMembers[0]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await adminService.addUserToTeam(teamID, userEmail, role);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.left(TEAM_INVITE_ALREADY_MEMBER));
|
|
||||||
expect(mockValidateEmail).toHaveBeenCalledWith(userEmail);
|
|
||||||
expect(mockUserService.findUserByEmail).toHaveBeenCalledWith(userEmail);
|
|
||||||
expect(mockTeamService.getTeamMemberTE).toHaveBeenCalledWith(
|
|
||||||
teamID,
|
|
||||||
allUsers[0].uid,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should add user to the team and return the result when user is not a member of the team', async () => {
|
|
||||||
const teamID = 'team1';
|
|
||||||
const userEmail = allUsers[0].email;
|
|
||||||
const role = TeamMemberRole.EDITOR;
|
|
||||||
|
|
||||||
const mockValidateEmail = jest.spyOn(utils, 'validateEmail');
|
|
||||||
mockValidateEmail.mockReturnValueOnce(true);
|
|
||||||
mockUserService.findUserByEmail.mockResolvedValueOnce(
|
|
||||||
O.some(allUsers[0]),
|
|
||||||
);
|
|
||||||
mockTeamService.getTeamMemberTE.mockReturnValueOnce(
|
|
||||||
TE.left(TEAM_MEMBER_NOT_FOUND),
|
|
||||||
);
|
|
||||||
mockTeamService.addMemberToTeamWithEmail.mockResolvedValueOnce(
|
|
||||||
E.right(teamMembers[0]),
|
|
||||||
);
|
|
||||||
mockTeamInvitationService.getTeamInviteByEmailAndTeamID.mockResolvedValueOnce(
|
|
||||||
E.right(teamInvitations[0])
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await adminService.addUserToTeam(teamID, userEmail, role);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.right(teamMembers[0]));
|
|
||||||
expect(mockValidateEmail).toHaveBeenCalledWith(userEmail);
|
|
||||||
expect(mockUserService.findUserByEmail).toHaveBeenCalledWith(userEmail);
|
|
||||||
expect(mockTeamService.getTeamMemberTE).toHaveBeenCalledWith(
|
|
||||||
teamID,
|
|
||||||
allUsers[0].uid,
|
|
||||||
);
|
|
||||||
expect(mockTeamService.addMemberToTeamWithEmail).toHaveBeenCalledWith(
|
|
||||||
teamID,
|
|
||||||
allUsers[0].email,
|
|
||||||
role,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('createATeam', () => {
|
|
||||||
test('should return USER_NOT_FOUND when user is not found', async () => {
|
|
||||||
const userUid = allUsers[0].uid;
|
|
||||||
const teamName = 'team1';
|
|
||||||
|
|
||||||
mockUserService.findUserById.mockResolvedValue(O.none);
|
|
||||||
|
|
||||||
const result = await adminService.createATeam(userUid, teamName);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.left(USER_NOT_FOUND));
|
|
||||||
expect(mockUserService.findUserById).toHaveBeenCalledWith(userUid);
|
|
||||||
expect(mockTeamService.createTeam).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should create a team and return the result when the team is created successfully', async () => {
|
|
||||||
const user = allUsers[0];
|
|
||||||
const team = teams[0];
|
|
||||||
|
|
||||||
mockUserService.findUserById.mockResolvedValueOnce(O.some(user));
|
|
||||||
mockTeamService.createTeam.mockResolvedValueOnce(E.right(team));
|
|
||||||
|
|
||||||
const result = await adminService.createATeam(user.uid, team.name);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.right(team));
|
|
||||||
expect(mockUserService.findUserById).toHaveBeenCalledWith(user.uid);
|
|
||||||
expect(mockTeamService.createTeam).toHaveBeenCalledWith(
|
|
||||||
team.name,
|
|
||||||
user.uid,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return the error when the team creation fails', async () => {
|
|
||||||
const user = allUsers[0];
|
|
||||||
const team = teams[0];
|
|
||||||
const errorMessage = 'error';
|
|
||||||
|
|
||||||
mockUserService.findUserById.mockResolvedValueOnce(O.some(user));
|
|
||||||
mockTeamService.createTeam.mockResolvedValueOnce(E.left(errorMessage));
|
|
||||||
|
|
||||||
const result = await adminService.createATeam(user.uid, team.name);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.left(errorMessage));
|
|
||||||
expect(mockUserService.findUserById).toHaveBeenCalledWith(user.uid);
|
|
||||||
expect(mockTeamService.createTeam).toHaveBeenCalledWith(
|
|
||||||
team.name,
|
|
||||||
user.uid,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('renameATeam', () => {
|
|
||||||
test('should rename a team and return the result when the team is renamed successfully', async () => {
|
|
||||||
const team = teams[0];
|
|
||||||
const newName = 'new name';
|
|
||||||
|
|
||||||
mockTeamService.renameTeam.mockResolvedValueOnce(E.right(team));
|
|
||||||
|
|
||||||
const result = await adminService.renameATeam(team.id, newName);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.right(team));
|
|
||||||
expect(mockTeamService.renameTeam).toHaveBeenCalledWith(team.id, newName);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return the error when the team renaming fails', async () => {
|
|
||||||
const team = teams[0];
|
|
||||||
const newName = 'new name';
|
|
||||||
const errorMessage = 'error';
|
|
||||||
|
|
||||||
mockTeamService.renameTeam.mockResolvedValueOnce(E.left(errorMessage));
|
|
||||||
|
|
||||||
const result = await adminService.renameATeam(team.id, newName);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.left(errorMessage));
|
|
||||||
expect(mockTeamService.renameTeam).toHaveBeenCalledWith(team.id, newName);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('deleteATeam', () => {
|
|
||||||
test('should delete a team and return the result when the team is deleted successfully', async () => {
|
|
||||||
const team = teams[0];
|
|
||||||
|
|
||||||
mockTeamService.deleteTeam.mockResolvedValueOnce(E.right(true));
|
|
||||||
|
|
||||||
const result = await adminService.deleteATeam(team.id);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.right(true));
|
|
||||||
expect(mockTeamService.deleteTeam).toHaveBeenCalledWith(team.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return the error when the team deletion fails', async () => {
|
|
||||||
const team = teams[0];
|
|
||||||
const errorMessage = 'error';
|
|
||||||
|
|
||||||
mockTeamService.deleteTeam.mockResolvedValueOnce(E.left(errorMessage));
|
|
||||||
|
|
||||||
const result = await adminService.deleteATeam(team.id);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.left(errorMessage));
|
|
||||||
expect(mockTeamService.deleteTeam).toHaveBeenCalledWith(team.id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('fetchAdmins', () => {
|
|
||||||
test('should return the list of admin users', async () => {
|
|
||||||
const adminUsers = [];
|
|
||||||
mockUserService.fetchAdminUsers.mockResolvedValueOnce(adminUsers);
|
|
||||||
const result = await adminService.fetchAdmins();
|
|
||||||
|
|
||||||
expect(result).toEqual(adminUsers);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('fetchUserInfo', () => {
|
|
||||||
test('should return the user info when the user is found', async () => {
|
|
||||||
const user = allUsers[0];
|
|
||||||
mockUserService.findUserById.mockResolvedValueOnce(O.some(user));
|
|
||||||
const result = await adminService.fetchUserInfo(user.uid);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.right(user));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return USER_NOT_FOUND when the user is not found', async () => {
|
|
||||||
const user = allUsers[0];
|
|
||||||
mockUserService.findUserById.mockResolvedValueOnce(O.none);
|
|
||||||
const result = await adminService.fetchUserInfo(user.uid);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.left(USER_NOT_FOUND));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('removeUserAccount', () => {
|
|
||||||
test('should return USER_NOT_FOUND when the user is not found', async () => {
|
|
||||||
const user = allUsers[0];
|
|
||||||
mockUserService.findUserById.mockResolvedValueOnce(O.none);
|
|
||||||
const result = await adminService.removeUserAccount(user.uid);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.left(USER_NOT_FOUND));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return USER_IS_ADMIN when the user is an admin', async () => {
|
|
||||||
const user = allUsers[0];
|
|
||||||
|
|
||||||
mockUserService.findUserById.mockResolvedValueOnce(O.some(user));
|
|
||||||
const result = await adminService.removeUserAccount(user.uid);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.left(USER_IS_ADMIN));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should remove the user account and return the result when the user is not an admin', async () => {
|
|
||||||
const user = allUsers[1];
|
|
||||||
|
|
||||||
mockUserService.findUserById.mockResolvedValueOnce(O.some(user));
|
|
||||||
mockUserService.deleteUserByUID.mockReturnValueOnce(TE.right(true));
|
|
||||||
const result = await adminService.removeUserAccount(user.uid);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.right(true));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return the error when the user account deletion fails', async () => {
|
|
||||||
const user = allUsers[1];
|
|
||||||
const errorMessage = 'error';
|
|
||||||
|
|
||||||
mockUserService.findUserById.mockResolvedValueOnce(O.some(user));
|
|
||||||
mockUserService.deleteUserByUID.mockReturnValueOnce(
|
|
||||||
TE.left(errorMessage),
|
|
||||||
);
|
|
||||||
const result = await adminService.removeUserAccount(user.uid);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.left(errorMessage));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('makeUserAdmin', () => {
|
|
||||||
test('should make the user an admin and return true when the operation is successful', async () => {
|
|
||||||
const user = allUsers[0];
|
|
||||||
|
|
||||||
mockUserService.makeAdmin.mockResolvedValueOnce(E.right(user));
|
|
||||||
const result = await adminService.makeUserAdmin(user.uid);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.right(true));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return the error when making the user an admin fails', async () => {
|
|
||||||
const user = allUsers[0];
|
|
||||||
|
|
||||||
mockUserService.makeAdmin.mockResolvedValueOnce(E.left(USER_NOT_FOUND));
|
|
||||||
const result = await adminService.makeUserAdmin(user.uid);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.left(USER_NOT_FOUND));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('removeUserAsAdmin', () => {
|
|
||||||
test('should return ONLY_ONE_ADMIN_ACCOUNT when there is only one admin account', async () => {
|
|
||||||
const user = allUsers[0];
|
|
||||||
|
|
||||||
mockUserService.fetchAdminUsers.mockResolvedValueOnce([user]);
|
|
||||||
const result = await adminService.removeUserAsAdmin(user.uid);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.left(ONLY_ONE_ADMIN_ACCOUNT));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should remove the user as an admin and return true when the operation is successful', async () => {
|
|
||||||
const user = allUsers[0];
|
|
||||||
|
|
||||||
mockUserService.fetchAdminUsers.mockResolvedValueOnce(allUsers);
|
|
||||||
mockUserService.removeUserAsAdmin.mockResolvedValueOnce(E.right(user));
|
|
||||||
const result = await adminService.removeUserAsAdmin(user.uid);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.right(true));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return the error when removing the user as an admin fails', async () => {
|
|
||||||
const user = allUsers[0];
|
|
||||||
|
|
||||||
mockUserService.fetchAdminUsers.mockResolvedValueOnce(allUsers);
|
|
||||||
mockUserService.removeUserAsAdmin.mockResolvedValueOnce(
|
|
||||||
E.left(USER_NOT_FOUND),
|
|
||||||
);
|
|
||||||
const result = await adminService.removeUserAsAdmin(user.uid);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.left(USER_NOT_FOUND));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getTeamInfo', () => {
|
|
||||||
test('should return the team info when the team is found', async () => {
|
|
||||||
const team = teams[0];
|
|
||||||
mockTeamService.getTeamWithIDTE.mockReturnValue(TE.right(team));
|
|
||||||
const result = await adminService.getTeamInfo(team.id);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.right(team));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('fetchInvitedUsers', () => {
|
describe('fetchInvitedUsers', () => {
|
||||||
test('should resolve right and return an array of invited users', async () => {
|
test('should resolve right and return an array of invited users', async () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ export class AdminService {
|
|||||||
* @returns an array team invitations
|
* @returns an array team invitations
|
||||||
*/
|
*/
|
||||||
async pendingInvitationCountInTeam(teamID: string) {
|
async pendingInvitationCountInTeam(teamID: string) {
|
||||||
const invitations = await this.teamInvitationService.getTeamInvitations(
|
const invitations = await this.teamInvitationService.getAllTeamInvitations(
|
||||||
teamID,
|
teamID,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -240,7 +240,6 @@ export class AdminService {
|
|||||||
teamID,
|
teamID,
|
||||||
user.value.uid,
|
user.value.uid,
|
||||||
)();
|
)();
|
||||||
|
|
||||||
if (E.isLeft(teamMember)) {
|
if (E.isLeft(teamMember)) {
|
||||||
const addedUser = await this.teamService.addMemberToTeamWithEmail(
|
const addedUser = await this.teamService.addMemberToTeamWithEmail(
|
||||||
teamID,
|
teamID,
|
||||||
@@ -258,7 +257,7 @@ export class AdminService {
|
|||||||
if (E.isRight(userInvitation)) {
|
if (E.isRight(userInvitation)) {
|
||||||
await this.teamInvitationService.revokeInvitation(
|
await this.teamInvitationService.revokeInvitation(
|
||||||
userInvitation.right.id,
|
userInvitation.right.id,
|
||||||
);
|
)();
|
||||||
}
|
}
|
||||||
|
|
||||||
return E.right(addedUser.right);
|
return E.right(addedUser.right);
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ export class AuthService {
|
|||||||
url = process.env.VITE_BASE_URL;
|
url = process.env.VITE_BASE_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.mailerService.sendEmail(email, {
|
await this.mailerService.sendAuthEmail(email, {
|
||||||
template: 'code-your-own',
|
template: 'code-your-own',
|
||||||
variables: {
|
variables: {
|
||||||
inviteeEmail: email,
|
inviteeEmail: email,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
|||||||
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
|
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
|
||||||
callbackURL: process.env.MICROSOFT_CALLBACK_URL,
|
callbackURL: process.env.MICROSOFT_CALLBACK_URL,
|
||||||
scope: [process.env.MICROSOFT_SCOPE],
|
scope: [process.env.MICROSOFT_SCOPE],
|
||||||
tenant: process.env.MICROSOFT_TENANT,
|
passReqToCallback: true,
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,13 +312,6 @@ export const SHORTCODE_ALREADY_EXISTS = 'shortcode/already_exists' as const;
|
|||||||
*/
|
*/
|
||||||
export const TEAM_ENVIRONMENT_NOT_FOUND = 'team_environment/not_found' as const;
|
export const TEAM_ENVIRONMENT_NOT_FOUND = 'team_environment/not_found' as const;
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalid TEAM ENVIRONMENT name
|
|
||||||
* (TeamEnvironmentsService)
|
|
||||||
*/
|
|
||||||
export const TEAM_ENVIRONMENT_SHORT_NAME =
|
|
||||||
'team_environment/short_name' as const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The user is not a member of the team of the given environment
|
* The user is not a member of the team of the given environment
|
||||||
* (GqlTeamEnvTeamGuard)
|
* (GqlTeamEnvTeamGuard)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
UserMagicLinkMailDescription,
|
UserMagicLinkMailDescription,
|
||||||
} from './MailDescriptions';
|
} from './MailDescriptions';
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
|
import * as TE from 'fp-ts/TaskEither';
|
||||||
import { EMAIL_FAILED } from 'src/errors';
|
import { EMAIL_FAILED } from 'src/errors';
|
||||||
import { MailerService as NestMailerService } from '@nestjs-modules/mailer';
|
import { MailerService as NestMailerService } from '@nestjs-modules/mailer';
|
||||||
|
|
||||||
@@ -34,14 +35,33 @@ export class MailerService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends an email to the given email address given a mail description
|
* Sends an email to the given email address given a mail description
|
||||||
* @param to Receiver's email id
|
* @param to The email address to be sent to (NOTE: this is not validated)
|
||||||
* @param mailDesc Definition of what email to be sent
|
* @param mailDesc Definition of what email to be sent
|
||||||
* @returns Response if email was send successfully or not
|
|
||||||
*/
|
*/
|
||||||
async sendEmail(
|
sendMail(
|
||||||
to: string,
|
to: string,
|
||||||
mailDesc: MailDescription | UserMagicLinkMailDescription,
|
mailDesc: MailDescription | UserMagicLinkMailDescription,
|
||||||
) {
|
) {
|
||||||
|
return TE.tryCatch(
|
||||||
|
async () => {
|
||||||
|
await this.nestMailerService.sendMail({
|
||||||
|
to,
|
||||||
|
template: mailDesc.template,
|
||||||
|
subject: this.resolveSubjectForMailDesc(mailDesc),
|
||||||
|
context: mailDesc.variables,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
() => EMAIL_FAILED,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param to Receiver's email id
|
||||||
|
* @param mailDesc Details of email to be sent for Magic-Link auth
|
||||||
|
* @returns Response if email was send successfully or not
|
||||||
|
*/
|
||||||
|
async sendAuthEmail(to: string, mailDesc: UserMagicLinkMailDescription) {
|
||||||
try {
|
try {
|
||||||
await this.nestMailerService.sendMail({
|
await this.nestMailerService.sendMail({
|
||||||
to,
|
to,
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import {
|
import { Team, TeamCollection as DBTeamCollection } from '@prisma/client';
|
||||||
Team,
|
import { mock, mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
TeamCollection as DBTeamCollection,
|
|
||||||
TeamRequest as DBTeamRequest,
|
|
||||||
} from '@prisma/client';
|
|
||||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
|
||||||
import {
|
import {
|
||||||
TEAM_COLL_DEST_SAME,
|
TEAM_COLL_DEST_SAME,
|
||||||
TEAM_COLL_INVALID_JSON,
|
TEAM_COLL_INVALID_JSON,
|
||||||
@@ -21,8 +17,9 @@ import { PrismaService } from 'src/prisma/prisma.service';
|
|||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { TeamCollectionService } from './team-collection.service';
|
import { TeamCollectionService } from './team-collection.service';
|
||||||
|
import { TeamCollection } from './team-collection.model';
|
||||||
|
import { TeamCollectionModule } from './team-collection.module';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { CollectionFolder } from 'src/types/CollectionFolder';
|
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockPubSub = mockDeep<PubSubService>();
|
const mockPubSub = mockDeep<PubSubService>();
|
||||||
@@ -279,188 +276,11 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const teamRequestList: DBTeamRequest[] = [
|
|
||||||
{
|
|
||||||
id: 'req1',
|
|
||||||
collectionID: childTeamCollection.id,
|
|
||||||
teamID: team.id,
|
|
||||||
title: 'request 1',
|
|
||||||
request: {},
|
|
||||||
orderIndex: 1,
|
|
||||||
createdOn: new Date(),
|
|
||||||
updatedOn: new Date(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockReset(mockPrisma);
|
mockReset(mockPrisma);
|
||||||
mockPubSub.publish.mockClear();
|
mockPubSub.publish.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('exportCollectionsToJSON', () => {
|
|
||||||
test('should export collections to JSON string successfully for structure-1', async () => {
|
|
||||||
/*
|
|
||||||
Assuming collection and request structure is as follows:
|
|
||||||
|
|
||||||
rootTeamCollection
|
|
||||||
|-> childTeamCollection
|
|
||||||
| |-> <no request of child coll>
|
|
||||||
|-> <no request of root coll>
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
mockPrisma.teamCollection.findMany.mockResolvedValueOnce([
|
|
||||||
rootTeamCollection,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// RCV CALL 1: Inside exportCollectionsToJSON.exportCollectionToJSONObject for Root Collection
|
|
||||||
jest
|
|
||||||
.spyOn(teamCollectionService, 'getCollection')
|
|
||||||
.mockResolvedValueOnce(E.right(rootTeamCollection));
|
|
||||||
mockPrisma.teamCollection.findMany.mockResolvedValueOnce([
|
|
||||||
childTeamCollection,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// RCV CALL 2: Inside exportCollectionsToJSON.exportCollectionToJSONObject for Child Collection
|
|
||||||
jest
|
|
||||||
.spyOn(teamCollectionService, 'getCollection')
|
|
||||||
.mockResolvedValueOnce(E.right(childTeamCollection));
|
|
||||||
mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]);
|
|
||||||
mockPrisma.teamRequest.findMany.mockResolvedValueOnce([]);
|
|
||||||
// return { name: childTeamCollection.title, folders: [], requests: [], };
|
|
||||||
|
|
||||||
// Back to RCV CALL 1
|
|
||||||
mockPrisma.teamRequest.findMany.mockResolvedValueOnce([]);
|
|
||||||
|
|
||||||
const returnedValue: CollectionFolder = {
|
|
||||||
name: rootTeamCollection.title,
|
|
||||||
folders: [
|
|
||||||
{
|
|
||||||
name: childTeamCollection.title,
|
|
||||||
folders: [],
|
|
||||||
requests: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
requests: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await teamCollectionService.exportCollectionsToJSON(team.id);
|
|
||||||
expect(result).toEqualRight(JSON.stringify([returnedValue]));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should export collections to JSON string successfully for structure-2', async () => {
|
|
||||||
/*
|
|
||||||
Assuming collection and request structure is as follows:
|
|
||||||
|
|
||||||
rootTeamCollection
|
|
||||||
|-> childTeamCollection
|
|
||||||
| |-> request1
|
|
||||||
|-> <no request of root coll>
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
mockPrisma.teamCollection.findMany.mockResolvedValueOnce([
|
|
||||||
rootTeamCollection,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// RCV CALL 1: Inside exportCollectionsToJSON.exportCollectionToJSONObject for Root Collection
|
|
||||||
jest
|
|
||||||
.spyOn(teamCollectionService, 'getCollection')
|
|
||||||
.mockResolvedValueOnce(E.right(rootTeamCollection));
|
|
||||||
mockPrisma.teamCollection.findMany.mockResolvedValueOnce([
|
|
||||||
childTeamCollection,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// RCV CALL 2: Inside exportCollectionsToJSON.exportCollectionToJSONObject for Child Collection
|
|
||||||
jest
|
|
||||||
.spyOn(teamCollectionService, 'getCollection')
|
|
||||||
.mockResolvedValueOnce(E.right(childTeamCollection));
|
|
||||||
mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]);
|
|
||||||
mockPrisma.teamRequest.findMany.mockResolvedValueOnce(teamRequestList);
|
|
||||||
// return { name: childTeamCollection.title, folders: [], requests: teamRequestList, };
|
|
||||||
|
|
||||||
// Back to RCV CALL 1
|
|
||||||
mockPrisma.teamRequest.findMany.mockResolvedValueOnce([]);
|
|
||||||
|
|
||||||
const returnedValue: CollectionFolder = {
|
|
||||||
name: rootTeamCollection.title,
|
|
||||||
folders: [
|
|
||||||
{
|
|
||||||
name: childTeamCollection.title,
|
|
||||||
folders: [],
|
|
||||||
requests: teamRequestList.map((req) => req.request),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
requests: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await teamCollectionService.exportCollectionsToJSON(team.id);
|
|
||||||
expect(result).toEqualRight(JSON.stringify([returnedValue]));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should export collections to JSON string successfully for structure-3', async () => {
|
|
||||||
/*
|
|
||||||
Assuming collection and request structure is as follows:
|
|
||||||
|
|
||||||
rootTeamCollection
|
|
||||||
|-> childTeamCollection
|
|
||||||
| |-> child-request1
|
|
||||||
|-> root-request1
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
mockPrisma.teamCollection.findMany.mockResolvedValueOnce([
|
|
||||||
rootTeamCollection,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// RCV CALL 1: Inside exportCollectionsToJSON.exportCollectionToJSONObject for Root Collection
|
|
||||||
jest
|
|
||||||
.spyOn(teamCollectionService, 'getCollection')
|
|
||||||
.mockResolvedValueOnce(E.right(rootTeamCollection));
|
|
||||||
mockPrisma.teamCollection.findMany.mockResolvedValueOnce([
|
|
||||||
childTeamCollection,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// RCV CALL 2: Inside exportCollectionsToJSON.exportCollectionToJSONObject for Child Collection
|
|
||||||
jest
|
|
||||||
.spyOn(teamCollectionService, 'getCollection')
|
|
||||||
.mockResolvedValueOnce(E.right(childTeamCollection));
|
|
||||||
mockPrisma.teamCollection.findMany.mockResolvedValueOnce([]);
|
|
||||||
mockPrisma.teamRequest.findMany.mockResolvedValueOnce(teamRequestList);
|
|
||||||
// return { name: childTeamCollection.title, folders: [], requests: teamRequestList, };
|
|
||||||
|
|
||||||
// Back to RCV CALL 1
|
|
||||||
mockPrisma.teamRequest.findMany.mockResolvedValueOnce(teamRequestList);
|
|
||||||
|
|
||||||
const returnedValue: CollectionFolder = {
|
|
||||||
name: rootTeamCollection.title,
|
|
||||||
folders: [
|
|
||||||
{
|
|
||||||
name: childTeamCollection.title,
|
|
||||||
folders: [],
|
|
||||||
requests: teamRequestList.map((req) => req.request),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
requests: teamRequestList.map((req) => req.request),
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await teamCollectionService.exportCollectionsToJSON(team.id);
|
|
||||||
expect(result).toEqualRight(JSON.stringify([returnedValue]));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getCollectionCount', () => {
|
|
||||||
test('should return the count of collections successfully', async () => {
|
|
||||||
const count = 10;
|
|
||||||
|
|
||||||
mockPrisma.teamCollection.count.mockResolvedValueOnce(count);
|
|
||||||
const result = await teamCollectionService.getCollectionCount(
|
|
||||||
rootTeamCollection.id,
|
|
||||||
);
|
|
||||||
expect(result).toEqual(count);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getTeamOfCollection', () => {
|
describe('getTeamOfCollection', () => {
|
||||||
test('should return the team of a collection successfully with valid collectionID', async () => {
|
test('should return the team of a collection successfully with valid collectionID', async () => {
|
||||||
mockPrisma.teamCollection.findUnique.mockResolvedValueOnce({
|
mockPrisma.teamCollection.findUnique.mockResolvedValueOnce({
|
||||||
@@ -1640,3 +1460,5 @@ describe('totalCollectionsInTeam', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//ToDo: write test cases for exportCollectionsToJSON
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import * as TE from 'fp-ts/TaskEither';
|
||||||
|
import * as O from 'fp-ts/Option';
|
||||||
|
import * as S from 'fp-ts/string';
|
||||||
|
import { pipe } from 'fp-ts/function';
|
||||||
|
import {
|
||||||
|
getAnnotatedRequiredRoles,
|
||||||
|
getGqlArg,
|
||||||
|
getUserFromGQLContext,
|
||||||
|
throwErr,
|
||||||
|
} from 'src/utils';
|
||||||
import { TeamEnvironmentsService } from './team-environments.service';
|
import { TeamEnvironmentsService } from './team-environments.service';
|
||||||
import {
|
import {
|
||||||
BUG_AUTH_NO_USER_CTX,
|
BUG_AUTH_NO_USER_CTX,
|
||||||
@@ -9,10 +19,6 @@ import {
|
|||||||
TEAM_ENVIRONMENT_NOT_FOUND,
|
TEAM_ENVIRONMENT_NOT_FOUND,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { TeamService } from 'src/team/team.service';
|
import { TeamService } from 'src/team/team.service';
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
|
||||||
import * as E from 'fp-ts/Either';
|
|
||||||
import { TeamMemberRole } from '@prisma/client';
|
|
||||||
import { throwErr } from 'src/utils';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A guard which checks whether the caller of a GQL Operation
|
* A guard which checks whether the caller of a GQL Operation
|
||||||
@@ -27,31 +33,50 @@ export class GqlTeamEnvTeamGuard implements CanActivate {
|
|||||||
private readonly teamService: TeamService,
|
private readonly teamService: TeamService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const requireRoles = this.reflector.get<TeamMemberRole[]>(
|
return pipe(
|
||||||
'requiresTeamRole',
|
TE.Do,
|
||||||
context.getHandler(),
|
|
||||||
);
|
|
||||||
if (!requireRoles) throw new Error(BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES);
|
|
||||||
|
|
||||||
const gqlExecCtx = GqlExecutionContext.create(context);
|
TE.bindW('requiredRoles', () =>
|
||||||
|
pipe(
|
||||||
|
getAnnotatedRequiredRoles(this.reflector, context),
|
||||||
|
TE.fromOption(() => BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const { user } = gqlExecCtx.getContext().req;
|
TE.bindW('user', () =>
|
||||||
if (user == undefined) throw new Error(BUG_AUTH_NO_USER_CTX);
|
pipe(
|
||||||
|
getUserFromGQLContext(context),
|
||||||
|
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const { id } = gqlExecCtx.getArgs<{ id: string }>();
|
TE.bindW('envID', () =>
|
||||||
if (!id) throwErr(BUG_TEAM_ENV_GUARD_NO_ENV_ID);
|
pipe(
|
||||||
|
getGqlArg('id', context),
|
||||||
|
O.fromPredicate(S.isString),
|
||||||
|
TE.fromOption(() => BUG_TEAM_ENV_GUARD_NO_ENV_ID),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const teamEnvironment =
|
TE.bindW('membership', ({ envID, user }) =>
|
||||||
await this.teamEnvironmentService.getTeamEnvironment(id);
|
pipe(
|
||||||
if (E.isLeft(teamEnvironment)) throwErr(TEAM_ENVIRONMENT_NOT_FOUND);
|
this.teamEnvironmentService.getTeamEnvironment(envID),
|
||||||
|
TE.fromTaskOption(() => TEAM_ENVIRONMENT_NOT_FOUND),
|
||||||
|
TE.chainW((env) =>
|
||||||
|
pipe(
|
||||||
|
this.teamService.getTeamMemberTE(env.teamID, user.uid),
|
||||||
|
TE.mapLeft(() => TEAM_ENVIRONMENT_NOT_TEAM_MEMBER),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const member = await this.teamService.getTeamMember(
|
TE.map(({ membership, requiredRoles }) =>
|
||||||
teamEnvironment.right.teamID,
|
requiredRoles.includes(membership.role),
|
||||||
user.uid,
|
),
|
||||||
);
|
|
||||||
if (!member) throwErr(TEAM_ENVIRONMENT_NOT_TEAM_MEMBER);
|
|
||||||
|
|
||||||
return requireRoles.includes(member.role);
|
TE.getOrElse(throwErr),
|
||||||
|
)();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import { ArgsType, Field, ID } from '@nestjs/graphql';
|
|
||||||
|
|
||||||
@ArgsType()
|
|
||||||
export class CreateTeamEnvironmentArgs {
|
|
||||||
@Field({
|
|
||||||
name: 'name',
|
|
||||||
description: 'Name of the Team Environment',
|
|
||||||
})
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@Field(() => ID, {
|
|
||||||
name: 'teamID',
|
|
||||||
description: 'ID of the Team',
|
|
||||||
})
|
|
||||||
teamID: string;
|
|
||||||
|
|
||||||
@Field({
|
|
||||||
name: 'variables',
|
|
||||||
description: 'JSON string of the variables object',
|
|
||||||
})
|
|
||||||
variables: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ArgsType()
|
|
||||||
export class UpdateTeamEnvironmentArgs {
|
|
||||||
@Field(() => ID, {
|
|
||||||
name: 'id',
|
|
||||||
description: 'ID of the Team Environment',
|
|
||||||
})
|
|
||||||
id: string;
|
|
||||||
@Field({
|
|
||||||
name: 'name',
|
|
||||||
description: 'Name of the Team Environment',
|
|
||||||
})
|
|
||||||
name: string;
|
|
||||||
@Field({
|
|
||||||
name: 'variables',
|
|
||||||
description: 'JSON string of the variables object',
|
|
||||||
})
|
|
||||||
variables: string;
|
|
||||||
}
|
|
||||||
@@ -13,11 +13,6 @@ import { throwErr } from 'src/utils';
|
|||||||
import { GqlTeamEnvTeamGuard } from './gql-team-env-team.guard';
|
import { GqlTeamEnvTeamGuard } from './gql-team-env-team.guard';
|
||||||
import { TeamEnvironment } from './team-environments.model';
|
import { TeamEnvironment } from './team-environments.model';
|
||||||
import { TeamEnvironmentsService } from './team-environments.service';
|
import { TeamEnvironmentsService } from './team-environments.service';
|
||||||
import * as E from 'fp-ts/Either';
|
|
||||||
import {
|
|
||||||
CreateTeamEnvironmentArgs,
|
|
||||||
UpdateTeamEnvironmentArgs,
|
|
||||||
} from './input-type.args';
|
|
||||||
|
|
||||||
@UseGuards(GqlThrottlerGuard)
|
@UseGuards(GqlThrottlerGuard)
|
||||||
@Resolver(() => 'TeamEnvironment')
|
@Resolver(() => 'TeamEnvironment')
|
||||||
@@ -34,18 +29,29 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||||
async createTeamEnvironment(
|
createTeamEnvironment(
|
||||||
@Args() args: CreateTeamEnvironmentArgs,
|
@Args({
|
||||||
|
name: 'name',
|
||||||
|
description: 'Name of the Team Environment',
|
||||||
|
})
|
||||||
|
name: string,
|
||||||
|
@Args({
|
||||||
|
name: 'teamID',
|
||||||
|
description: 'ID of the Team',
|
||||||
|
type: () => ID,
|
||||||
|
})
|
||||||
|
teamID: string,
|
||||||
|
@Args({
|
||||||
|
name: 'variables',
|
||||||
|
description: 'JSON string of the variables object',
|
||||||
|
})
|
||||||
|
variables: string,
|
||||||
): Promise<TeamEnvironment> {
|
): Promise<TeamEnvironment> {
|
||||||
const teamEnvironment =
|
return this.teamEnvironmentsService.createTeamEnvironment(
|
||||||
await this.teamEnvironmentsService.createTeamEnvironment(
|
name,
|
||||||
args.name,
|
teamID,
|
||||||
args.teamID,
|
variables,
|
||||||
args.variables,
|
)();
|
||||||
);
|
|
||||||
|
|
||||||
if (E.isLeft(teamEnvironment)) throwErr(teamEnvironment.left);
|
|
||||||
return teamEnvironment.right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
@Mutation(() => Boolean, {
|
||||||
@@ -53,7 +59,7 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||||
async deleteTeamEnvironment(
|
deleteTeamEnvironment(
|
||||||
@Args({
|
@Args({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
description: 'ID of the Team Environment',
|
description: 'ID of the Team Environment',
|
||||||
@@ -61,12 +67,10 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
id: string,
|
id: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const isDeleted = await this.teamEnvironmentsService.deleteTeamEnvironment(
|
return pipe(
|
||||||
id,
|
this.teamEnvironmentsService.deleteTeamEnvironment(id),
|
||||||
);
|
TE.getOrElse(throwErr),
|
||||||
|
)();
|
||||||
if (E.isLeft(isDeleted)) throwErr(isDeleted.left);
|
|
||||||
return isDeleted.right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TeamEnvironment, {
|
@Mutation(() => TeamEnvironment, {
|
||||||
@@ -75,19 +79,28 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||||
async updateTeamEnvironment(
|
updateTeamEnvironment(
|
||||||
@Args()
|
@Args({
|
||||||
args: UpdateTeamEnvironmentArgs,
|
name: 'id',
|
||||||
|
description: 'ID of the Team Environment',
|
||||||
|
type: () => ID,
|
||||||
|
})
|
||||||
|
id: string,
|
||||||
|
@Args({
|
||||||
|
name: 'name',
|
||||||
|
description: 'Name of the Team Environment',
|
||||||
|
})
|
||||||
|
name: string,
|
||||||
|
@Args({
|
||||||
|
name: 'variables',
|
||||||
|
description: 'JSON string of the variables object',
|
||||||
|
})
|
||||||
|
variables: string,
|
||||||
): Promise<TeamEnvironment> {
|
): Promise<TeamEnvironment> {
|
||||||
const updatedTeamEnvironment =
|
return pipe(
|
||||||
await this.teamEnvironmentsService.updateTeamEnvironment(
|
this.teamEnvironmentsService.updateTeamEnvironment(id, name, variables),
|
||||||
args.id,
|
TE.getOrElse(throwErr),
|
||||||
args.name,
|
)();
|
||||||
args.variables,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (E.isLeft(updatedTeamEnvironment)) throwErr(updatedTeamEnvironment.left);
|
|
||||||
return updatedTeamEnvironment.right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TeamEnvironment, {
|
@Mutation(() => TeamEnvironment, {
|
||||||
@@ -95,7 +108,7 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||||
async deleteAllVariablesFromTeamEnvironment(
|
deleteAllVariablesFromTeamEnvironment(
|
||||||
@Args({
|
@Args({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
description: 'ID of the Team Environment',
|
description: 'ID of the Team Environment',
|
||||||
@@ -103,13 +116,10 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
id: string,
|
id: string,
|
||||||
): Promise<TeamEnvironment> {
|
): Promise<TeamEnvironment> {
|
||||||
const teamEnvironment =
|
return pipe(
|
||||||
await this.teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
this.teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(id),
|
||||||
id,
|
TE.getOrElse(throwErr),
|
||||||
);
|
)();
|
||||||
|
|
||||||
if (E.isLeft(teamEnvironment)) throwErr(teamEnvironment.left);
|
|
||||||
return teamEnvironment.right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TeamEnvironment, {
|
@Mutation(() => TeamEnvironment, {
|
||||||
@@ -117,7 +127,7 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||||
async createDuplicateEnvironment(
|
createDuplicateEnvironment(
|
||||||
@Args({
|
@Args({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
description: 'ID of the Team Environment',
|
description: 'ID of the Team Environment',
|
||||||
@@ -125,12 +135,10 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
id: string,
|
id: string,
|
||||||
): Promise<TeamEnvironment> {
|
): Promise<TeamEnvironment> {
|
||||||
const res = await this.teamEnvironmentsService.createDuplicateEnvironment(
|
return pipe(
|
||||||
id,
|
this.teamEnvironmentsService.createDuplicateEnvironment(id),
|
||||||
);
|
TE.getOrElse(throwErr),
|
||||||
|
)();
|
||||||
if (E.isLeft(res)) throwErr(res.left);
|
|
||||||
return res.right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Subscriptions */
|
/* Subscriptions */
|
||||||
|
|||||||
@@ -2,11 +2,7 @@ import { mockDeep, mockReset } from 'jest-mock-extended';
|
|||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { TeamEnvironment } from './team-environments.model';
|
import { TeamEnvironment } from './team-environments.model';
|
||||||
import { TeamEnvironmentsService } from './team-environments.service';
|
import { TeamEnvironmentsService } from './team-environments.service';
|
||||||
import {
|
import { TEAM_ENVIRONMENT_NOT_FOUND } from 'src/errors';
|
||||||
JSON_INVALID,
|
|
||||||
TEAM_ENVIRONMENT_NOT_FOUND,
|
|
||||||
TEAM_ENVIRONMENT_SHORT_NAME,
|
|
||||||
} from 'src/errors';
|
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
|
|
||||||
@@ -35,81 +31,125 @@ beforeEach(() => {
|
|||||||
|
|
||||||
describe('TeamEnvironmentsService', () => {
|
describe('TeamEnvironmentsService', () => {
|
||||||
describe('getTeamEnvironment', () => {
|
describe('getTeamEnvironment', () => {
|
||||||
test('should successfully return a TeamEnvironment with valid ID', async () => {
|
test('queries the db with the id', async () => {
|
||||||
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
|
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
|
||||||
teamEnvironment,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.getTeamEnvironment(
|
await teamEnvironmentsService.getTeamEnvironment('123')();
|
||||||
teamEnvironment.id,
|
|
||||||
|
expect(mockPrisma.teamEnvironment.findFirst).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
where: {
|
||||||
|
id: '123',
|
||||||
|
},
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
expect(result).toEqualRight(teamEnvironment);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw TEAM_ENVIRONMENT_NOT_FOUND with invalid ID', async () => {
|
test('requests prisma to reject the query promise if not found', async () => {
|
||||||
mockPrisma.teamEnvironment.findFirstOrThrow.mockRejectedValueOnce(
|
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
|
||||||
'RejectOnNotFound',
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.getTeamEnvironment(
|
await teamEnvironmentsService.getTeamEnvironment('123')();
|
||||||
teamEnvironment.id,
|
|
||||||
|
expect(mockPrisma.teamEnvironment.findFirst).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
rejectOnNotFound: true,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
});
|
||||||
|
|
||||||
|
test('should return a Some of the correct environment if exists', async () => {
|
||||||
|
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
|
||||||
|
|
||||||
|
const result = await teamEnvironmentsService.getTeamEnvironment('123')();
|
||||||
|
|
||||||
|
expect(result).toEqualSome(teamEnvironment);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return a None if the environment does not exist', async () => {
|
||||||
|
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
|
||||||
|
|
||||||
|
const result = await teamEnvironmentsService.getTeamEnvironment('123')();
|
||||||
|
|
||||||
|
expect(result).toBeNone();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createTeamEnvironment', () => {
|
describe('createTeamEnvironment', () => {
|
||||||
test('should successfully create and return a new team environment given valid inputs', async () => {
|
test('should create and return a new team environment given a valid name,variable and team ID', async () => {
|
||||||
mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment);
|
mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment);
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.createTeamEnvironment(
|
const result = await teamEnvironmentsService.createTeamEnvironment(
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
teamEnvironment.teamID,
|
teamEnvironment.teamID,
|
||||||
JSON.stringify(teamEnvironment.variables),
|
JSON.stringify(teamEnvironment.variables),
|
||||||
);
|
)();
|
||||||
|
|
||||||
expect(result).toEqualRight({
|
expect(result).toEqual(<TeamEnvironment>{
|
||||||
...teamEnvironment,
|
id: teamEnvironment.id,
|
||||||
|
name: teamEnvironment.name,
|
||||||
|
teamID: teamEnvironment.teamID,
|
||||||
variables: JSON.stringify(teamEnvironment.variables),
|
variables: JSON.stringify(teamEnvironment.variables),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw TEAM_ENVIRONMENT_SHORT_NAME if input TeamEnvironment name is invalid', async () => {
|
test('should reject if given team ID is invalid', async () => {
|
||||||
const result = await teamEnvironmentsService.createTeamEnvironment(
|
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
|
||||||
'12',
|
|
||||||
teamEnvironment.teamID,
|
|
||||||
JSON.stringify(teamEnvironment.variables),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_SHORT_NAME);
|
await expect(
|
||||||
|
teamEnvironmentsService.createTeamEnvironment(
|
||||||
|
teamEnvironment.name,
|
||||||
|
'invalidteamid',
|
||||||
|
JSON.stringify(teamEnvironment.variables),
|
||||||
|
),
|
||||||
|
).rejects.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should reject if provided team environment name is not a string', async () => {
|
||||||
|
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
teamEnvironmentsService.createTeamEnvironment(
|
||||||
|
null as any,
|
||||||
|
teamEnvironment.teamID,
|
||||||
|
JSON.stringify(teamEnvironment.variables),
|
||||||
|
),
|
||||||
|
).rejects.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should reject if provided variable is not a string', async () => {
|
||||||
|
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
teamEnvironmentsService.createTeamEnvironment(
|
||||||
|
teamEnvironment.name,
|
||||||
|
teamEnvironment.teamID,
|
||||||
|
null as any,
|
||||||
|
),
|
||||||
|
).rejects.toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should send pubsub message to "team_environment/<teamID>/created" if team environment is created successfully', async () => {
|
test('should send pubsub message to "team_environment/<teamID>/created" if team environment is created successfully', async () => {
|
||||||
mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment);
|
mockPrisma.teamEnvironment.create.mockResolvedValueOnce(teamEnvironment);
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.createTeamEnvironment(
|
const result = await teamEnvironmentsService.createTeamEnvironment(
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
teamEnvironment.teamID,
|
teamEnvironment.teamID,
|
||||||
JSON.stringify(teamEnvironment.variables),
|
JSON.stringify(teamEnvironment.variables),
|
||||||
);
|
)();
|
||||||
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_environment/${teamEnvironment.teamID}/created`,
|
`team_environment/${teamEnvironment.teamID}/created`,
|
||||||
{
|
result,
|
||||||
...teamEnvironment,
|
|
||||||
variables: JSON.stringify(teamEnvironment.variables),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteTeamEnvironment', () => {
|
describe('deleteTeamEnvironment', () => {
|
||||||
test('should successfully delete a TeamEnvironment with a valid ID', async () => {
|
test('should resolve to true given a valid team environment ID', async () => {
|
||||||
mockPrisma.teamEnvironment.delete.mockResolvedValueOnce(teamEnvironment);
|
mockPrisma.teamEnvironment.delete.mockResolvedValueOnce(teamEnvironment);
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
);
|
)();
|
||||||
|
|
||||||
expect(result).toEqualRight(true);
|
expect(result).toEqualRight(true);
|
||||||
});
|
});
|
||||||
@@ -119,7 +159,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
|
|
||||||
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
||||||
'invalidid',
|
'invalidid',
|
||||||
);
|
)();
|
||||||
|
|
||||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
});
|
});
|
||||||
@@ -129,7 +169,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
|
|
||||||
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
);
|
)();
|
||||||
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_environment/${teamEnvironment.teamID}/deleted`,
|
`team_environment/${teamEnvironment.teamID}/deleted`,
|
||||||
@@ -142,7 +182,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('updateVariablesInTeamEnvironment', () => {
|
describe('updateVariablesInTeamEnvironment', () => {
|
||||||
test('should successfully add new variable to a team environment', async () => {
|
test('should add new variable to a team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
variables: [{ key: 'value' }],
|
variables: [{ key: 'value' }],
|
||||||
@@ -152,7 +192,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
JSON.stringify([{ key: 'value' }]),
|
JSON.stringify([{ key: 'value' }]),
|
||||||
);
|
)();
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
@@ -160,7 +200,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should successfully add new variable to already existing list of variables in a team environment', async () => {
|
test('should add new variable to already existing list of variables in a team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
variables: [{ key: 'value' }, { key_2: 'value_2' }],
|
variables: [{ key: 'value' }, { key_2: 'value_2' }],
|
||||||
@@ -170,7 +210,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
JSON.stringify([{ key: 'value' }, { key_2: 'value_2' }]),
|
JSON.stringify([{ key: 'value' }, { key_2: 'value_2' }]),
|
||||||
);
|
)();
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
@@ -178,7 +218,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should successfully edit existing variables in a team environment', async () => {
|
test('should edit existing variables in a team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
variables: [{ key: '1234' }],
|
variables: [{ key: '1234' }],
|
||||||
@@ -188,7 +228,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
JSON.stringify([{ key: '1234' }]),
|
JSON.stringify([{ key: '1234' }]),
|
||||||
);
|
)();
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
@@ -196,7 +236,22 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should successfully edit name of an existing team environment', async () => {
|
test('should delete existing variable in a team environment', async () => {
|
||||||
|
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
|
||||||
|
|
||||||
|
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
||||||
|
teamEnvironment.id,
|
||||||
|
teamEnvironment.name,
|
||||||
|
JSON.stringify([{}]),
|
||||||
|
)();
|
||||||
|
|
||||||
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
|
...teamEnvironment,
|
||||||
|
variables: JSON.stringify([{}]),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should edit name of an existing team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
variables: [{ key: '123' }],
|
variables: [{ key: '123' }],
|
||||||
@@ -206,7 +261,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
JSON.stringify([{ key: '123' }]),
|
JSON.stringify([{ key: '123' }]),
|
||||||
);
|
)();
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
@@ -214,24 +269,14 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw TEAM_ENVIRONMENT_SHORT_NAME if input TeamEnvironment name is invalid', async () => {
|
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||||
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
|
||||||
teamEnvironment.id,
|
|
||||||
'12',
|
|
||||||
JSON.stringify([{ key: 'value' }]),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_SHORT_NAME);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
|
||||||
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
|
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
||||||
'invalidid',
|
'invalidid',
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
JSON.stringify(teamEnvironment.variables),
|
JSON.stringify(teamEnvironment.variables),
|
||||||
);
|
)();
|
||||||
|
|
||||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
});
|
});
|
||||||
@@ -243,7 +288,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
JSON.stringify([{ key: 'value' }]),
|
JSON.stringify([{ key: 'value' }]),
|
||||||
);
|
)();
|
||||||
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_environment/${teamEnvironment.teamID}/updated`,
|
`team_environment/${teamEnvironment.teamID}/updated`,
|
||||||
@@ -256,13 +301,13 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteAllVariablesFromTeamEnvironment', () => {
|
describe('deleteAllVariablesFromTeamEnvironment', () => {
|
||||||
test('should successfully delete all variables in a team environment', async () => {
|
test('should delete all variables in a team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
|
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
|
||||||
|
|
||||||
const result =
|
const result =
|
||||||
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
);
|
)();
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
@@ -270,13 +315,13 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
|
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
|
||||||
|
|
||||||
const result =
|
const result =
|
||||||
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||||
'invalidid',
|
'invalidid',
|
||||||
);
|
)();
|
||||||
|
|
||||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
});
|
});
|
||||||
@@ -287,7 +332,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
const result =
|
const result =
|
||||||
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
);
|
)();
|
||||||
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_environment/${teamEnvironment.teamID}/updated`,
|
`team_environment/${teamEnvironment.teamID}/updated`,
|
||||||
@@ -300,33 +345,33 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('createDuplicateEnvironment', () => {
|
describe('createDuplicateEnvironment', () => {
|
||||||
test('should successfully duplicate an existing team environment', async () => {
|
test('should duplicate an existing team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
|
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
|
||||||
teamEnvironment,
|
teamEnvironment,
|
||||||
);
|
);
|
||||||
|
|
||||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
||||||
id: 'newid',
|
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
|
id: 'newid',
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
);
|
)();
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
id: 'newid',
|
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
|
id: 'newid',
|
||||||
variables: JSON.stringify(teamEnvironment.variables),
|
variables: JSON.stringify(teamEnvironment.variables),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||||
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
|
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
);
|
)();
|
||||||
|
|
||||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
});
|
});
|
||||||
@@ -337,19 +382,19 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
||||||
id: 'newid',
|
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
|
id: 'newid',
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
);
|
)();
|
||||||
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_environment/${teamEnvironment.teamID}/created`,
|
`team_environment/${teamEnvironment.teamID}/created`,
|
||||||
{
|
{
|
||||||
id: 'newid',
|
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
|
id: 'newid',
|
||||||
variables: JSON.stringify([{}]),
|
variables: JSON.stringify([{}]),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { TeamEnvironment as DBTeamEnvironment, Prisma } from '@prisma/client';
|
import { pipe } from 'fp-ts/function';
|
||||||
|
import * as T from 'fp-ts/Task';
|
||||||
|
import * as TO from 'fp-ts/TaskOption';
|
||||||
|
import * as TE from 'fp-ts/TaskEither';
|
||||||
|
import * as A from 'fp-ts/Array';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { TeamEnvironment } from './team-environments.model';
|
import { TeamEnvironment } from './team-environments.model';
|
||||||
import {
|
import { TEAM_ENVIRONMENT_NOT_FOUND } from 'src/errors';
|
||||||
TEAM_ENVIRONMENT_NOT_FOUND,
|
|
||||||
TEAM_ENVIRONMENT_SHORT_NAME,
|
|
||||||
} from 'src/errors';
|
|
||||||
import * as E from 'fp-ts/Either';
|
|
||||||
import { isValidLength } from 'src/utils';
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamEnvironmentsService {
|
export class TeamEnvironmentsService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -16,218 +17,219 @@ export class TeamEnvironmentsService {
|
|||||||
private readonly pubsub: PubSubService,
|
private readonly pubsub: PubSubService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
TITLE_LENGTH = 3;
|
getTeamEnvironment(id: string) {
|
||||||
|
return TO.tryCatch(() =>
|
||||||
/**
|
this.prisma.teamEnvironment.findFirst({
|
||||||
* TeamEnvironments are saved in the DB in the following way
|
where: { id },
|
||||||
* [{ key: value }, { key: value },....]
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Typecast a database TeamEnvironment to a TeamEnvironment model
|
|
||||||
* @param teamEnvironment database TeamEnvironment
|
|
||||||
* @returns TeamEnvironment model
|
|
||||||
*/
|
|
||||||
private cast(teamEnvironment: DBTeamEnvironment): TeamEnvironment {
|
|
||||||
return {
|
|
||||||
id: teamEnvironment.id,
|
|
||||||
name: teamEnvironment.name,
|
|
||||||
teamID: teamEnvironment.teamID,
|
|
||||||
variables: JSON.stringify(teamEnvironment.variables),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get details of a TeamEnvironment.
|
|
||||||
*
|
|
||||||
* @param id TeamEnvironment ID
|
|
||||||
* @returns Either of a TeamEnvironment or error message
|
|
||||||
*/
|
|
||||||
async getTeamEnvironment(id: string) {
|
|
||||||
try {
|
|
||||||
const teamEnvironment =
|
|
||||||
await this.prisma.teamEnvironment.findFirstOrThrow({
|
|
||||||
where: { id },
|
|
||||||
});
|
|
||||||
return E.right(teamEnvironment);
|
|
||||||
} catch (error) {
|
|
||||||
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new TeamEnvironment.
|
|
||||||
*
|
|
||||||
* @param name name of new TeamEnvironment
|
|
||||||
* @param teamID teamID of new TeamEnvironment
|
|
||||||
* @param variables JSONified string of contents of new TeamEnvironment
|
|
||||||
* @returns Either of a TeamEnvironment or error message
|
|
||||||
*/
|
|
||||||
async createTeamEnvironment(name: string, teamID: string, variables: string) {
|
|
||||||
const isTitleValid = isValidLength(name, this.TITLE_LENGTH);
|
|
||||||
if (!isTitleValid) return E.left(TEAM_ENVIRONMENT_SHORT_NAME);
|
|
||||||
|
|
||||||
const result = await this.prisma.teamEnvironment.create({
|
|
||||||
data: {
|
|
||||||
name: name,
|
|
||||||
teamID: teamID,
|
|
||||||
variables: JSON.parse(variables),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const createdTeamEnvironment = this.cast(result);
|
|
||||||
|
|
||||||
this.pubsub.publish(
|
|
||||||
`team_environment/${createdTeamEnvironment.teamID}/created`,
|
|
||||||
createdTeamEnvironment,
|
|
||||||
);
|
|
||||||
|
|
||||||
return E.right(createdTeamEnvironment);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a TeamEnvironment.
|
|
||||||
*
|
|
||||||
* @param id TeamEnvironment ID
|
|
||||||
* @returns Either of boolean or error message
|
|
||||||
*/
|
|
||||||
async deleteTeamEnvironment(id: string) {
|
|
||||||
try {
|
|
||||||
const result = await this.prisma.teamEnvironment.delete({
|
|
||||||
where: {
|
|
||||||
id: id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const deletedTeamEnvironment = this.cast(result);
|
|
||||||
|
|
||||||
this.pubsub.publish(
|
|
||||||
`team_environment/${deletedTeamEnvironment.teamID}/deleted`,
|
|
||||||
deletedTeamEnvironment,
|
|
||||||
);
|
|
||||||
|
|
||||||
return E.right(true);
|
|
||||||
} catch (error) {
|
|
||||||
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a TeamEnvironment.
|
|
||||||
*
|
|
||||||
* @param id TeamEnvironment ID
|
|
||||||
* @param name TeamEnvironment name
|
|
||||||
* @param variables JSONified string of contents of new TeamEnvironment
|
|
||||||
* @returns Either of a TeamEnvironment or error message
|
|
||||||
*/
|
|
||||||
async updateTeamEnvironment(id: string, name: string, variables: string) {
|
|
||||||
try {
|
|
||||||
const isTitleValid = isValidLength(name, this.TITLE_LENGTH);
|
|
||||||
if (!isTitleValid) return E.left(TEAM_ENVIRONMENT_SHORT_NAME);
|
|
||||||
|
|
||||||
const result = await this.prisma.teamEnvironment.update({
|
|
||||||
where: { id: id },
|
|
||||||
data: {
|
|
||||||
name,
|
|
||||||
variables: JSON.parse(variables),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedTeamEnvironment = this.cast(result);
|
|
||||||
|
|
||||||
this.pubsub.publish(
|
|
||||||
`team_environment/${updatedTeamEnvironment.teamID}/updated`,
|
|
||||||
updatedTeamEnvironment,
|
|
||||||
);
|
|
||||||
|
|
||||||
return E.right(updatedTeamEnvironment);
|
|
||||||
} catch (error) {
|
|
||||||
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear contents of a TeamEnvironment.
|
|
||||||
*
|
|
||||||
* @param id TeamEnvironment ID
|
|
||||||
* @returns Either of a TeamEnvironment or error message
|
|
||||||
*/
|
|
||||||
async deleteAllVariablesFromTeamEnvironment(id: string) {
|
|
||||||
try {
|
|
||||||
const result = await this.prisma.teamEnvironment.update({
|
|
||||||
where: { id: id },
|
|
||||||
data: {
|
|
||||||
variables: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const teamEnvironment = this.cast(result);
|
|
||||||
|
|
||||||
this.pubsub.publish(
|
|
||||||
`team_environment/${teamEnvironment.teamID}/updated`,
|
|
||||||
teamEnvironment,
|
|
||||||
);
|
|
||||||
|
|
||||||
return E.right(teamEnvironment);
|
|
||||||
} catch (error) {
|
|
||||||
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a duplicate of a existing TeamEnvironment.
|
|
||||||
*
|
|
||||||
* @param id TeamEnvironment ID
|
|
||||||
* @returns Either of a TeamEnvironment or error message
|
|
||||||
*/
|
|
||||||
async createDuplicateEnvironment(id: string) {
|
|
||||||
try {
|
|
||||||
const environment = await this.prisma.teamEnvironment.findFirst({
|
|
||||||
where: {
|
|
||||||
id: id,
|
|
||||||
},
|
|
||||||
rejectOnNotFound: true,
|
rejectOnNotFound: true,
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
const result = await this.prisma.teamEnvironment.create({
|
|
||||||
data: {
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: environment.variables as Prisma.JsonArray,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const duplicatedTeamEnvironment = this.cast(result);
|
|
||||||
|
|
||||||
this.pubsub.publish(
|
|
||||||
`team_environment/${duplicatedTeamEnvironment.teamID}/created`,
|
|
||||||
duplicatedTeamEnvironment,
|
|
||||||
);
|
|
||||||
|
|
||||||
return E.right(duplicatedTeamEnvironment);
|
|
||||||
} catch (error) {
|
|
||||||
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
createTeamEnvironment(name: string, teamID: string, variables: string) {
|
||||||
* Fetch all TeamEnvironments of a team.
|
return pipe(
|
||||||
*
|
() =>
|
||||||
* @param teamID teamID of new TeamEnvironment
|
this.prisma.teamEnvironment.create({
|
||||||
* @returns List of TeamEnvironments
|
data: {
|
||||||
*/
|
name: name,
|
||||||
async fetchAllTeamEnvironments(teamID: string) {
|
teamID: teamID,
|
||||||
const result = await this.prisma.teamEnvironment.findMany({
|
variables: JSON.parse(variables),
|
||||||
where: {
|
},
|
||||||
teamID: teamID,
|
}),
|
||||||
},
|
T.chainFirst(
|
||||||
});
|
(environment) => () =>
|
||||||
const teamEnvironments = result.map((item) => {
|
this.pubsub.publish(
|
||||||
return this.cast(item);
|
`team_environment/${environment.teamID}/created`,
|
||||||
});
|
<TeamEnvironment>{
|
||||||
|
id: environment.id,
|
||||||
|
name: environment.name,
|
||||||
|
teamID: environment.teamID,
|
||||||
|
variables: JSON.stringify(environment.variables),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
T.map((data) => {
|
||||||
|
return <TeamEnvironment>{
|
||||||
|
id: data.id,
|
||||||
|
name: data.name,
|
||||||
|
teamID: data.teamID,
|
||||||
|
variables: JSON.stringify(data.variables),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return teamEnvironments;
|
deleteTeamEnvironment(id: string) {
|
||||||
|
return pipe(
|
||||||
|
TE.tryCatch(
|
||||||
|
() =>
|
||||||
|
this.prisma.teamEnvironment.delete({
|
||||||
|
where: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
||||||
|
),
|
||||||
|
TE.chainFirst((environment) =>
|
||||||
|
TE.fromTask(() =>
|
||||||
|
this.pubsub.publish(
|
||||||
|
`team_environment/${environment.teamID}/deleted`,
|
||||||
|
<TeamEnvironment>{
|
||||||
|
id: environment.id,
|
||||||
|
name: environment.name,
|
||||||
|
teamID: environment.teamID,
|
||||||
|
variables: JSON.stringify(environment.variables),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TE.map((data) => true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTeamEnvironment(id: string, name: string, variables: string) {
|
||||||
|
return pipe(
|
||||||
|
TE.tryCatch(
|
||||||
|
() =>
|
||||||
|
this.prisma.teamEnvironment.update({
|
||||||
|
where: { id: id },
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
variables: JSON.parse(variables),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
||||||
|
),
|
||||||
|
TE.chainFirst((environment) =>
|
||||||
|
TE.fromTask(() =>
|
||||||
|
this.pubsub.publish(
|
||||||
|
`team_environment/${environment.teamID}/updated`,
|
||||||
|
<TeamEnvironment>{
|
||||||
|
id: environment.id,
|
||||||
|
name: environment.name,
|
||||||
|
teamID: environment.teamID,
|
||||||
|
variables: JSON.stringify(environment.variables),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TE.map(
|
||||||
|
(environment) =>
|
||||||
|
<TeamEnvironment>{
|
||||||
|
id: environment.id,
|
||||||
|
name: environment.name,
|
||||||
|
teamID: environment.teamID,
|
||||||
|
variables: JSON.stringify(environment.variables),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAllVariablesFromTeamEnvironment(id: string) {
|
||||||
|
return pipe(
|
||||||
|
TE.tryCatch(
|
||||||
|
() =>
|
||||||
|
this.prisma.teamEnvironment.update({
|
||||||
|
where: { id: id },
|
||||||
|
data: {
|
||||||
|
variables: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
||||||
|
),
|
||||||
|
TE.chainFirst((environment) =>
|
||||||
|
TE.fromTask(() =>
|
||||||
|
this.pubsub.publish(
|
||||||
|
`team_environment/${environment.teamID}/updated`,
|
||||||
|
<TeamEnvironment>{
|
||||||
|
id: environment.id,
|
||||||
|
name: environment.name,
|
||||||
|
teamID: environment.teamID,
|
||||||
|
variables: JSON.stringify(environment.variables),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TE.map(
|
||||||
|
(environment) =>
|
||||||
|
<TeamEnvironment>{
|
||||||
|
id: environment.id,
|
||||||
|
name: environment.name,
|
||||||
|
teamID: environment.teamID,
|
||||||
|
variables: JSON.stringify(environment.variables),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
createDuplicateEnvironment(id: string) {
|
||||||
|
return pipe(
|
||||||
|
TE.tryCatch(
|
||||||
|
() =>
|
||||||
|
this.prisma.teamEnvironment.findFirst({
|
||||||
|
where: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
rejectOnNotFound: true,
|
||||||
|
}),
|
||||||
|
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
||||||
|
),
|
||||||
|
TE.chain((environment) =>
|
||||||
|
TE.fromTask(() =>
|
||||||
|
this.prisma.teamEnvironment.create({
|
||||||
|
data: {
|
||||||
|
name: environment.name,
|
||||||
|
teamID: environment.teamID,
|
||||||
|
variables: environment.variables as Prisma.JsonArray,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TE.chainFirst((environment) =>
|
||||||
|
TE.fromTask(() =>
|
||||||
|
this.pubsub.publish(
|
||||||
|
`team_environment/${environment.teamID}/created`,
|
||||||
|
<TeamEnvironment>{
|
||||||
|
id: environment.id,
|
||||||
|
name: environment.name,
|
||||||
|
teamID: environment.teamID,
|
||||||
|
variables: JSON.stringify(environment.variables),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TE.map(
|
||||||
|
(environment) =>
|
||||||
|
<TeamEnvironment>{
|
||||||
|
id: environment.id,
|
||||||
|
name: environment.name,
|
||||||
|
teamID: environment.teamID,
|
||||||
|
variables: JSON.stringify(environment.variables),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAllTeamEnvironments(teamID: string) {
|
||||||
|
return pipe(
|
||||||
|
() =>
|
||||||
|
this.prisma.teamEnvironment.findMany({
|
||||||
|
where: {
|
||||||
|
teamID: teamID,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
T.map(
|
||||||
|
A.map(
|
||||||
|
(environment) =>
|
||||||
|
<TeamEnvironment>{
|
||||||
|
id: environment.id,
|
||||||
|
name: environment.name,
|
||||||
|
teamID: environment.teamID,
|
||||||
|
variables: JSON.stringify(environment.variables),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ export class TeamEnvsTeamResolver {
|
|||||||
description: 'Returns all Team Environments for the given Team',
|
description: 'Returns all Team Environments for the given Team',
|
||||||
})
|
})
|
||||||
teamEnvironments(@Parent() team: Team): Promise<TeamEnvironment[]> {
|
teamEnvironments(@Parent() team: Team): Promise<TeamEnvironment[]> {
|
||||||
return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id);
|
return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id)();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import { ArgsType, Field, ID } from '@nestjs/graphql';
|
|
||||||
import { TeamMemberRole } from 'src/team/team.model';
|
|
||||||
|
|
||||||
@ArgsType()
|
|
||||||
export class CreateTeamInvitationArgs {
|
|
||||||
@Field(() => ID, {
|
|
||||||
name: 'teamID',
|
|
||||||
description: 'ID of the Team ID to invite from',
|
|
||||||
})
|
|
||||||
teamID: string;
|
|
||||||
|
|
||||||
@Field({ name: 'inviteeEmail', description: 'Email of the user to invite' })
|
|
||||||
inviteeEmail: string;
|
|
||||||
|
|
||||||
@Field(() => TeamMemberRole, {
|
|
||||||
name: 'inviteeRole',
|
|
||||||
description: 'Role to be given to the user',
|
|
||||||
})
|
|
||||||
inviteeRole: TeamMemberRole;
|
|
||||||
}
|
|
||||||
@@ -12,10 +12,15 @@ import { TeamInvitation } from './team-invitation.model';
|
|||||||
import { TeamInvitationService } from './team-invitation.service';
|
import { TeamInvitationService } from './team-invitation.service';
|
||||||
import { pipe } from 'fp-ts/function';
|
import { pipe } from 'fp-ts/function';
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
import * as TE from 'fp-ts/TaskEither';
|
||||||
import * as E from 'fp-ts/Either';
|
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import { Team, TeamMember, TeamMemberRole } from 'src/team/team.model';
|
import { Team, TeamMember, TeamMemberRole } from 'src/team/team.model';
|
||||||
import { TEAM_INVITE_NO_INVITE_FOUND, USER_NOT_FOUND } from 'src/errors';
|
import { EmailCodec } from 'src/types/Email';
|
||||||
|
import {
|
||||||
|
INVALID_EMAIL,
|
||||||
|
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
||||||
|
TEAM_INVITE_NO_INVITE_FOUND,
|
||||||
|
USER_NOT_FOUND,
|
||||||
|
} from 'src/errors';
|
||||||
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||||
import { User } from 'src/user/user.model';
|
import { User } from 'src/user/user.model';
|
||||||
import { UseGuards } from '@nestjs/common';
|
import { UseGuards } from '@nestjs/common';
|
||||||
@@ -31,8 +36,6 @@ import { UserService } from 'src/user/user.service';
|
|||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||||
import { SkipThrottle } from '@nestjs/throttler';
|
import { SkipThrottle } from '@nestjs/throttler';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
|
||||||
import { CreateTeamInvitationArgs } from './input-type.args';
|
|
||||||
|
|
||||||
@UseGuards(GqlThrottlerGuard)
|
@UseGuards(GqlThrottlerGuard)
|
||||||
@Resolver(() => TeamInvitation)
|
@Resolver(() => TeamInvitation)
|
||||||
@@ -76,8 +79,8 @@ export class TeamInvitationResolver {
|
|||||||
'Gets the Team Invitation with the given ID, or null if not exists',
|
'Gets the Team Invitation with the given ID, or null if not exists',
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, TeamInviteViewerGuard)
|
@UseGuards(GqlAuthGuard, TeamInviteViewerGuard)
|
||||||
async teamInvitation(
|
teamInvitation(
|
||||||
@GqlUser() user: AuthUser,
|
@GqlUser() user: User,
|
||||||
@Args({
|
@Args({
|
||||||
name: 'inviteID',
|
name: 'inviteID',
|
||||||
description: 'ID of the Team Invitation to lookup',
|
description: 'ID of the Team Invitation to lookup',
|
||||||
@@ -85,11 +88,17 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
inviteID: string,
|
inviteID: string,
|
||||||
): Promise<TeamInvitation> {
|
): Promise<TeamInvitation> {
|
||||||
const teamInvitation = await this.teamInvitationService.getInvitation(
|
return pipe(
|
||||||
inviteID,
|
this.teamInvitationService.getInvitation(inviteID),
|
||||||
);
|
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
||||||
if (O.isNone(teamInvitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
TE.chainW(
|
||||||
return teamInvitation.value;
|
TE.fromPredicate(
|
||||||
|
(a) => a.inviteeEmail.toLowerCase() === user.email?.toLowerCase(),
|
||||||
|
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TE.getOrElse(throwErr),
|
||||||
|
)();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TeamInvitation, {
|
@Mutation(() => TeamInvitation, {
|
||||||
@@ -97,19 +106,56 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER)
|
@RequiresTeamRole(TeamMemberRole.OWNER)
|
||||||
async createTeamInvitation(
|
createTeamInvitation(
|
||||||
@GqlUser() user: AuthUser,
|
@GqlUser()
|
||||||
@Args() args: CreateTeamInvitationArgs,
|
user: User,
|
||||||
): Promise<TeamInvitation> {
|
|
||||||
const teamInvitation = await this.teamInvitationService.createInvitation(
|
|
||||||
user,
|
|
||||||
args.teamID,
|
|
||||||
args.inviteeEmail,
|
|
||||||
args.inviteeRole,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (E.isLeft(teamInvitation)) throwErr(teamInvitation.left);
|
@Args({
|
||||||
return teamInvitation.right;
|
name: 'teamID',
|
||||||
|
description: 'ID of the Team ID to invite from',
|
||||||
|
type: () => ID,
|
||||||
|
})
|
||||||
|
teamID: string,
|
||||||
|
@Args({
|
||||||
|
name: 'inviteeEmail',
|
||||||
|
description: 'Email of the user to invite',
|
||||||
|
})
|
||||||
|
inviteeEmail: string,
|
||||||
|
@Args({
|
||||||
|
name: 'inviteeRole',
|
||||||
|
type: () => TeamMemberRole,
|
||||||
|
description: 'Role to be given to the user',
|
||||||
|
})
|
||||||
|
inviteeRole: TeamMemberRole,
|
||||||
|
): Promise<TeamInvitation> {
|
||||||
|
return pipe(
|
||||||
|
TE.Do,
|
||||||
|
|
||||||
|
// Validate email
|
||||||
|
TE.bindW('email', () =>
|
||||||
|
pipe(
|
||||||
|
EmailCodec.decode(inviteeEmail),
|
||||||
|
TE.fromEither,
|
||||||
|
TE.mapLeft(() => INVALID_EMAIL),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Validate and get Team
|
||||||
|
TE.bindW('team', () => this.teamService.getTeamWithIDTE(teamID)),
|
||||||
|
|
||||||
|
// Create team
|
||||||
|
TE.chainW(({ email, team }) =>
|
||||||
|
this.teamInvitationService.createInvitation(
|
||||||
|
user,
|
||||||
|
team,
|
||||||
|
email,
|
||||||
|
inviteeRole,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// If failed, throw err (so the message is passed) else return value
|
||||||
|
TE.getOrElse(throwErr),
|
||||||
|
)();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
@Mutation(() => Boolean, {
|
||||||
@@ -117,7 +163,7 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, TeamInviteTeamOwnerGuard)
|
@UseGuards(GqlAuthGuard, TeamInviteTeamOwnerGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER)
|
@RequiresTeamRole(TeamMemberRole.OWNER)
|
||||||
async revokeTeamInvitation(
|
revokeTeamInvitation(
|
||||||
@Args({
|
@Args({
|
||||||
name: 'inviteID',
|
name: 'inviteID',
|
||||||
type: () => ID,
|
type: () => ID,
|
||||||
@@ -125,19 +171,19 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
inviteID: string,
|
inviteID: string,
|
||||||
): Promise<true> {
|
): Promise<true> {
|
||||||
const isRevoked = await this.teamInvitationService.revokeInvitation(
|
return pipe(
|
||||||
inviteID,
|
this.teamInvitationService.revokeInvitation(inviteID),
|
||||||
);
|
TE.map(() => true as const),
|
||||||
if (E.isLeft(isRevoked)) throwErr(isRevoked.left);
|
TE.getOrElse(throwErr),
|
||||||
return true;
|
)();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TeamMember, {
|
@Mutation(() => TeamMember, {
|
||||||
description: 'Accept an Invitation',
|
description: 'Accept an Invitation',
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, TeamInviteeGuard)
|
@UseGuards(GqlAuthGuard, TeamInviteeGuard)
|
||||||
async acceptTeamInvitation(
|
acceptTeamInvitation(
|
||||||
@GqlUser() user: AuthUser,
|
@GqlUser() user: User,
|
||||||
@Args({
|
@Args({
|
||||||
name: 'inviteID',
|
name: 'inviteID',
|
||||||
type: () => ID,
|
type: () => ID,
|
||||||
@@ -145,12 +191,10 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
inviteID: string,
|
inviteID: string,
|
||||||
): Promise<TeamMember> {
|
): Promise<TeamMember> {
|
||||||
const teamMember = await this.teamInvitationService.acceptInvitation(
|
return pipe(
|
||||||
inviteID,
|
this.teamInvitationService.acceptInvitation(inviteID, user),
|
||||||
user,
|
TE.getOrElse(throwErr),
|
||||||
);
|
)();
|
||||||
if (E.isLeft(teamMember)) throwErr(teamMember.left);
|
|
||||||
return teamMember.right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscriptions
|
// Subscriptions
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import * as T from 'fp-ts/Task';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
|
import * as TO from 'fp-ts/TaskOption';
|
||||||
|
import * as TE from 'fp-ts/TaskEither';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
|
import { pipe, flow, constVoid } from 'fp-ts/function';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { TeamInvitation as DBTeamInvitation } from '@prisma/client';
|
import { Team, TeamMemberRole } from 'src/team/team.model';
|
||||||
import { TeamMember, TeamMemberRole } from 'src/team/team.model';
|
import { Email } from 'src/types/Email';
|
||||||
|
import { User } from 'src/user/user.model';
|
||||||
import { TeamService } from 'src/team/team.service';
|
import { TeamService } from 'src/team/team.service';
|
||||||
import {
|
import {
|
||||||
INVALID_EMAIL,
|
INVALID_EMAIL,
|
||||||
TEAM_INVALID_ID,
|
|
||||||
TEAM_INVITE_ALREADY_MEMBER,
|
TEAM_INVITE_ALREADY_MEMBER,
|
||||||
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
||||||
TEAM_INVITE_MEMBER_HAS_INVITE,
|
TEAM_INVITE_MEMBER_HAS_INVITE,
|
||||||
TEAM_INVITE_NO_INVITE_FOUND,
|
TEAM_INVITE_NO_INVITE_FOUND,
|
||||||
TEAM_MEMBER_NOT_FOUND,
|
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { TeamInvitation } from './team-invitation.model';
|
import { TeamInvitation } from './team-invitation.model';
|
||||||
import { MailerService } from 'src/mailer/mailer.service';
|
import { MailerService } from 'src/mailer/mailer.service';
|
||||||
import { UserService } from 'src/user/user.service';
|
import { UserService } from 'src/user/user.service';
|
||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { validateEmail } from '../utils';
|
import { validateEmail } from '../utils';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamInvitationService {
|
export class TeamInvitationService {
|
||||||
@@ -30,37 +32,38 @@ export class TeamInvitationService {
|
|||||||
private readonly mailerService: MailerService,
|
private readonly mailerService: MailerService,
|
||||||
|
|
||||||
private readonly pubsub: PubSubService,
|
private readonly pubsub: PubSubService,
|
||||||
) {}
|
) {
|
||||||
|
this.getInvitation = this.getInvitation.bind(this);
|
||||||
/**
|
|
||||||
* Cast a DBTeamInvitation to a TeamInvitation
|
|
||||||
* @param dbTeamInvitation database TeamInvitation
|
|
||||||
* @returns TeamInvitation model
|
|
||||||
*/
|
|
||||||
cast(dbTeamInvitation: DBTeamInvitation): TeamInvitation {
|
|
||||||
return {
|
|
||||||
...dbTeamInvitation,
|
|
||||||
inviteeRole: TeamMemberRole[dbTeamInvitation.inviteeRole],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getInvitation(inviteID: string): TO.TaskOption<TeamInvitation> {
|
||||||
* Get the team invite
|
return pipe(
|
||||||
* @param inviteID invite id
|
() =>
|
||||||
* @returns an Option of team invitation or none
|
this.prisma.teamInvitation.findUnique({
|
||||||
*/
|
where: {
|
||||||
async getInvitation(inviteID: string) {
|
id: inviteID,
|
||||||
try {
|
},
|
||||||
const dbInvitation = await this.prisma.teamInvitation.findUniqueOrThrow({
|
}),
|
||||||
where: {
|
TO.fromTask,
|
||||||
id: inviteID,
|
TO.chain(flow(O.fromNullable, TO.fromOption)),
|
||||||
},
|
TO.map((x) => x as TeamInvitation),
|
||||||
});
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return O.some(this.cast(dbInvitation));
|
getInvitationWithEmail(email: Email, team: Team) {
|
||||||
} catch (e) {
|
return pipe(
|
||||||
return O.none;
|
() =>
|
||||||
}
|
this.prisma.teamInvitation.findUnique({
|
||||||
|
where: {
|
||||||
|
teamID_inviteeEmail: {
|
||||||
|
inviteeEmail: email,
|
||||||
|
teamID: team.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
TO.fromTask,
|
||||||
|
TO.chain(flow(O.fromNullable, TO.fromOption)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,162 +92,211 @@ export class TeamInvitationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
createInvitation(
|
||||||
* Create a team invitation
|
creator: User,
|
||||||
* @param creator creator of the invitation
|
team: Team,
|
||||||
* @param teamID team id
|
inviteeEmail: Email,
|
||||||
* @param inviteeEmail invitee email
|
|
||||||
* @param inviteeRole invitee role
|
|
||||||
* @returns an Either of team invitation or error message
|
|
||||||
*/
|
|
||||||
async createInvitation(
|
|
||||||
creator: AuthUser,
|
|
||||||
teamID: string,
|
|
||||||
inviteeEmail: string,
|
|
||||||
inviteeRole: TeamMemberRole,
|
inviteeRole: TeamMemberRole,
|
||||||
) {
|
) {
|
||||||
// validate email
|
return pipe(
|
||||||
const isEmailValid = validateEmail(inviteeEmail);
|
// Perform all validation checks
|
||||||
if (!isEmailValid) return E.left(INVALID_EMAIL);
|
TE.sequenceArray([
|
||||||
|
// creator should be a TeamMember
|
||||||
|
pipe(
|
||||||
|
this.teamService.getTeamMemberTE(team.id, creator.uid),
|
||||||
|
TE.map(constVoid),
|
||||||
|
),
|
||||||
|
|
||||||
// team ID should valid
|
// Invitee should not be a team member
|
||||||
const team = await this.teamService.getTeamWithID(teamID);
|
pipe(
|
||||||
if (!team) return E.left(TEAM_INVALID_ID);
|
async () => await this.userService.findUserByEmail(inviteeEmail),
|
||||||
|
TO.foldW(
|
||||||
|
() => TE.right(undefined), // If no user, short circuit to completion
|
||||||
|
(user) =>
|
||||||
|
pipe(
|
||||||
|
// If user is found, check if team member
|
||||||
|
this.teamService.getTeamMemberTE(team.id, user.uid),
|
||||||
|
TE.foldW(
|
||||||
|
() => TE.right(undefined), // Not team-member, this is good
|
||||||
|
() => TE.left(TEAM_INVITE_ALREADY_MEMBER), // Is team member, not good
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TE.map(constVoid),
|
||||||
|
),
|
||||||
|
|
||||||
// invitation creator should be a TeamMember
|
// Should not have an existing invite
|
||||||
const isTeamMember = await this.teamService.getTeamMember(
|
pipe(
|
||||||
team.id,
|
this.getInvitationWithEmail(inviteeEmail, team),
|
||||||
creator.uid,
|
TE.fromTaskOption(() => null),
|
||||||
|
TE.swap,
|
||||||
|
TE.map(constVoid),
|
||||||
|
TE.mapLeft(() => TEAM_INVITE_MEMBER_HAS_INVITE),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Create the invitation
|
||||||
|
TE.chainTaskK(
|
||||||
|
() => () =>
|
||||||
|
this.prisma.teamInvitation.create({
|
||||||
|
data: {
|
||||||
|
teamID: team.id,
|
||||||
|
inviteeEmail,
|
||||||
|
inviteeRole,
|
||||||
|
creatorUid: creator.uid,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Send email, this is a side effect
|
||||||
|
TE.chainFirstTaskK((invitation) =>
|
||||||
|
pipe(
|
||||||
|
this.mailerService.sendMail(inviteeEmail, {
|
||||||
|
template: 'team-invitation',
|
||||||
|
variables: {
|
||||||
|
invitee: creator.displayName ?? 'A Hoppscotch User',
|
||||||
|
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${invitation.id}`,
|
||||||
|
invite_team_name: team.name,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
TE.getOrElseW(() => T.of(undefined)), // This value doesn't matter as we don't mind the return value (chainFirst) as long as the task completes
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Send PubSub topic
|
||||||
|
TE.chainFirstTaskK((invitation) =>
|
||||||
|
TE.fromTask(async () => {
|
||||||
|
const inv: TeamInvitation = {
|
||||||
|
id: invitation.id,
|
||||||
|
teamID: invitation.teamID,
|
||||||
|
creatorUid: invitation.creatorUid,
|
||||||
|
inviteeEmail: invitation.inviteeEmail,
|
||||||
|
inviteeRole: TeamMemberRole[invitation.inviteeRole],
|
||||||
|
};
|
||||||
|
|
||||||
|
this.pubsub.publish(`team/${inv.teamID}/invite_added`, inv);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Map to model type
|
||||||
|
TE.map((x) => x as TeamInvitation),
|
||||||
);
|
);
|
||||||
if (!isTeamMember) return E.left(TEAM_MEMBER_NOT_FOUND);
|
}
|
||||||
|
|
||||||
// Checking to see if the invitee is already part of the team or not
|
revokeInvitation(inviteID: string) {
|
||||||
const inviteeUser = await this.userService.findUserByEmail(inviteeEmail);
|
return pipe(
|
||||||
if (O.isSome(inviteeUser)) {
|
// Make sure invite exists
|
||||||
// invitee should not already a member
|
this.getInvitation(inviteID),
|
||||||
const isTeamMember = await this.teamService.getTeamMember(
|
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
||||||
team.id,
|
|
||||||
inviteeUser.value.uid,
|
|
||||||
);
|
|
||||||
if (isTeamMember) return E.left(TEAM_INVITE_ALREADY_MEMBER);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check invitee already invited earlier or not
|
// Delete team invitation
|
||||||
const teamInvitation = await this.getTeamInviteByEmailAndTeamID(
|
TE.chainTaskK(
|
||||||
inviteeEmail,
|
() => () =>
|
||||||
team.id,
|
this.prisma.teamInvitation.delete({
|
||||||
|
where: {
|
||||||
|
id: inviteID,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Emit Pubsub Event
|
||||||
|
TE.chainFirst((invitation) =>
|
||||||
|
TE.fromTask(() =>
|
||||||
|
this.pubsub.publish(
|
||||||
|
`team/${invitation.teamID}/invite_removed`,
|
||||||
|
invitation.id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// We are not returning anything
|
||||||
|
TE.map(constVoid),
|
||||||
);
|
);
|
||||||
if (E.isRight(teamInvitation)) return E.left(TEAM_INVITE_MEMBER_HAS_INVITE);
|
}
|
||||||
|
|
||||||
// create the invitation
|
getAllInvitationsInTeam(team: Team) {
|
||||||
const dbInvitation = await this.prisma.teamInvitation.create({
|
return pipe(
|
||||||
data: {
|
() =>
|
||||||
teamID: team.id,
|
this.prisma.teamInvitation.findMany({
|
||||||
inviteeEmail,
|
where: {
|
||||||
inviteeRole,
|
teamID: team.id,
|
||||||
creatorUid: creator.uid,
|
},
|
||||||
},
|
}),
|
||||||
});
|
T.map((x) => x as TeamInvitation[]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await this.mailerService.sendEmail(inviteeEmail, {
|
acceptInvitation(inviteID: string, acceptedBy: User) {
|
||||||
template: 'team-invitation',
|
return pipe(
|
||||||
variables: {
|
TE.Do,
|
||||||
invitee: creator.displayName ?? 'A Hoppscotch User',
|
|
||||||
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${dbInvitation.id}`,
|
|
||||||
invite_team_name: team.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const invitation = this.cast(dbInvitation);
|
// First get the invitation
|
||||||
this.pubsub.publish(`team/${invitation.teamID}/invite_added`, invitation);
|
TE.bindW('invitation', () =>
|
||||||
|
pipe(
|
||||||
|
this.getInvitation(inviteID),
|
||||||
|
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
return E.right(invitation);
|
// Validation checks
|
||||||
|
TE.chainFirstW(({ invitation }) =>
|
||||||
|
TE.sequenceArray([
|
||||||
|
// Make sure the invited user is not part of the team
|
||||||
|
pipe(
|
||||||
|
this.teamService.getTeamMemberTE(invitation.teamID, acceptedBy.uid),
|
||||||
|
TE.swap,
|
||||||
|
TE.bimap(
|
||||||
|
() => TEAM_INVITE_ALREADY_MEMBER,
|
||||||
|
constVoid, // The return type is ignored
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Make sure the invited user and accepting user has the same email
|
||||||
|
pipe(
|
||||||
|
undefined,
|
||||||
|
TE.fromPredicate(
|
||||||
|
(a) => acceptedBy.email === invitation.inviteeEmail,
|
||||||
|
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Add the team member
|
||||||
|
// TODO: Somehow bring subscriptions to this ?
|
||||||
|
TE.bindW('teamMember', ({ invitation }) =>
|
||||||
|
pipe(
|
||||||
|
TE.tryCatch(
|
||||||
|
() =>
|
||||||
|
this.teamService.addMemberToTeam(
|
||||||
|
invitation.teamID,
|
||||||
|
acceptedBy.uid,
|
||||||
|
invitation.inviteeRole,
|
||||||
|
),
|
||||||
|
() => TEAM_INVITE_ALREADY_MEMBER, // Can only fail if Team Member already exists, which we checked, but due to async lets assert that here too
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
TE.chainFirstW(({ invitation }) => this.revokeInvitation(invitation.id)),
|
||||||
|
|
||||||
|
TE.map(({ teamMember }) => teamMember),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Revoke a team invitation
|
* Fetch the count invitations for a given team.
|
||||||
* @param inviteID invite id
|
|
||||||
* @returns an Either of true or error message
|
|
||||||
*/
|
|
||||||
async revokeInvitation(inviteID: string) {
|
|
||||||
// check if the invite exists
|
|
||||||
const invitation = await this.getInvitation(inviteID);
|
|
||||||
if (O.isNone(invitation)) return E.left(TEAM_INVITE_NO_INVITE_FOUND);
|
|
||||||
|
|
||||||
// delete the invite
|
|
||||||
await this.prisma.teamInvitation.delete({
|
|
||||||
where: {
|
|
||||||
id: inviteID,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.pubsub.publish(
|
|
||||||
`team/${invitation.value.teamID}/invite_removed`,
|
|
||||||
invitation.value.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
return E.right(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Accept a team invitation
|
|
||||||
* @param inviteID invite id
|
|
||||||
* @param acceptedBy user who accepted the invitation
|
|
||||||
* @returns an Either of team member or error message
|
|
||||||
*/
|
|
||||||
async acceptInvitation(inviteID: string, acceptedBy: AuthUser) {
|
|
||||||
// check if the invite exists
|
|
||||||
const invitation = await this.getInvitation(inviteID);
|
|
||||||
if (O.isNone(invitation)) return E.left(TEAM_INVITE_NO_INVITE_FOUND);
|
|
||||||
|
|
||||||
// make sure the user is not already a member of the team
|
|
||||||
const teamMemberInvitee = await this.teamService.getTeamMember(
|
|
||||||
invitation.value.teamID,
|
|
||||||
acceptedBy.uid,
|
|
||||||
);
|
|
||||||
if (teamMemberInvitee) return E.left(TEAM_INVITE_ALREADY_MEMBER);
|
|
||||||
|
|
||||||
// make sure the user is the same as the invitee
|
|
||||||
if (
|
|
||||||
acceptedBy.email.toLowerCase() !==
|
|
||||||
invitation.value.inviteeEmail.toLowerCase()
|
|
||||||
)
|
|
||||||
return E.left(TEAM_INVITE_EMAIL_DO_NOT_MATCH);
|
|
||||||
|
|
||||||
// add the user to the team
|
|
||||||
let teamMember: TeamMember;
|
|
||||||
try {
|
|
||||||
teamMember = await this.teamService.addMemberToTeam(
|
|
||||||
invitation.value.teamID,
|
|
||||||
acceptedBy.uid,
|
|
||||||
invitation.value.inviteeRole,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
return E.left(TEAM_INVITE_ALREADY_MEMBER);
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete the invite
|
|
||||||
await this.revokeInvitation(inviteID);
|
|
||||||
|
|
||||||
return E.right(teamMember);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch all team invitations for a given team.
|
|
||||||
* @param teamID team id
|
* @param teamID team id
|
||||||
* @returns array of team invitations for a team
|
* @returns a count team invitations for a team
|
||||||
*/
|
*/
|
||||||
async getTeamInvitations(teamID: string) {
|
async getAllTeamInvitations(teamID: string) {
|
||||||
const dbInvitations = await this.prisma.teamInvitation.findMany({
|
const invitations = await this.prisma.teamInvitation.findMany({
|
||||||
where: {
|
where: {
|
||||||
teamID: teamID,
|
teamID: teamID,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const invitations: TeamInvitation[] = dbInvitations.map((dbInvitation) =>
|
|
||||||
this.cast(dbInvitation),
|
|
||||||
);
|
|
||||||
|
|
||||||
return invitations;
|
return invitations;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
|
import { pipe } from 'fp-ts/function';
|
||||||
import { TeamService } from 'src/team/team.service';
|
import { TeamService } from 'src/team/team.service';
|
||||||
import { TeamInvitationService } from './team-invitation.service';
|
import { TeamInvitationService } from './team-invitation.service';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
|
import * as T from 'fp-ts/Task';
|
||||||
|
import * as TE from 'fp-ts/TaskEither';
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
import {
|
import {
|
||||||
BUG_AUTH_NO_USER_CTX,
|
BUG_AUTH_NO_USER_CTX,
|
||||||
BUG_TEAM_INVITE_NO_INVITE_ID,
|
BUG_TEAM_INVITE_NO_INVITE_ID,
|
||||||
TEAM_INVITE_NO_INVITE_FOUND,
|
TEAM_INVITE_NO_INVITE_FOUND,
|
||||||
TEAM_MEMBER_NOT_FOUND,
|
|
||||||
TEAM_NOT_REQUIRED_ROLE,
|
TEAM_NOT_REQUIRED_ROLE,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
|
import { User } from 'src/user/user.model';
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
import { TeamMemberRole } from 'src/team/team.model';
|
import { TeamMemberRole } from 'src/team/team.model';
|
||||||
|
|
||||||
/**
|
|
||||||
* This guard only allows team owner to execute the resolver
|
|
||||||
*/
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamInviteTeamOwnerGuard implements CanActivate {
|
export class TeamInviteTeamOwnerGuard implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -24,30 +24,48 @@ export class TeamInviteTeamOwnerGuard implements CanActivate {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
// Get GQL context
|
return pipe(
|
||||||
const gqlExecCtx = GqlExecutionContext.create(context);
|
TE.Do,
|
||||||
|
|
||||||
// Get user
|
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
|
||||||
const { user } = gqlExecCtx.getContext().req;
|
|
||||||
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
|
|
||||||
|
|
||||||
// Get the invite
|
// Get the invite
|
||||||
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
|
TE.bindW('invite', ({ gqlCtx }) =>
|
||||||
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
|
pipe(
|
||||||
|
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
|
||||||
|
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
|
||||||
|
TE.chainW((inviteID) =>
|
||||||
|
pipe(
|
||||||
|
this.teamInviteService.getInvitation(inviteID),
|
||||||
|
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const invitation = await this.teamInviteService.getInvitation(inviteID);
|
TE.bindW('user', ({ gqlCtx }) =>
|
||||||
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
pipe(
|
||||||
|
gqlCtx.getContext().req.user,
|
||||||
|
O.fromNullable,
|
||||||
|
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Fetch team member details of this user
|
TE.bindW('userMember', ({ invite, user }) =>
|
||||||
const teamMember = await this.teamService.getTeamMember(
|
this.teamService.getTeamMemberTE(invite.teamID, user.uid),
|
||||||
invitation.value.teamID,
|
),
|
||||||
user.uid,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!teamMember) throwErr(TEAM_MEMBER_NOT_FOUND);
|
TE.chainW(
|
||||||
if (teamMember.role !== TeamMemberRole.OWNER)
|
TE.fromPredicate(
|
||||||
throwErr(TEAM_NOT_REQUIRED_ROLE);
|
({ userMember }) => userMember.role === TeamMemberRole.OWNER,
|
||||||
|
() => TEAM_NOT_REQUIRED_ROLE,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
return true;
|
TE.fold(
|
||||||
|
(err) => throwErr(err),
|
||||||
|
() => T.of(true),
|
||||||
|
),
|
||||||
|
)();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { TeamInvitationService } from './team-invitation.service';
|
import { TeamInvitationService } from './team-invitation.service';
|
||||||
|
import { pipe, flow } from 'fp-ts/function';
|
||||||
|
import * as TE from 'fp-ts/TaskEither';
|
||||||
|
import * as T from 'fp-ts/Task';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
import {
|
import {
|
||||||
BUG_AUTH_NO_USER_CTX,
|
BUG_AUTH_NO_USER_CTX,
|
||||||
BUG_TEAM_INVITE_NO_INVITE_ID,
|
BUG_TEAM_INVITE_NO_INVITE_ID,
|
||||||
|
TEAM_INVITE_NOT_VALID_VIEWER,
|
||||||
TEAM_INVITE_NO_INVITE_FOUND,
|
TEAM_INVITE_NO_INVITE_FOUND,
|
||||||
TEAM_MEMBER_NOT_FOUND,
|
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
|
import { User } from 'src/user/user.model';
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
import { TeamService } from 'src/team/team.service';
|
import { TeamService } from 'src/team/team.service';
|
||||||
|
|
||||||
/**
|
|
||||||
* This guard only allows user to execute the resolver
|
|
||||||
* 1. If user is invitee, allow
|
|
||||||
* 2. Or else, if user is team member, allow
|
|
||||||
*
|
|
||||||
* TLDR: Allow if user is invitee or team member
|
|
||||||
*/
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamInviteViewerGuard implements CanActivate {
|
export class TeamInviteViewerGuard implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -26,32 +23,50 @@ export class TeamInviteViewerGuard implements CanActivate {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
// Get GQL context
|
return pipe(
|
||||||
const gqlExecCtx = GqlExecutionContext.create(context);
|
TE.Do,
|
||||||
|
|
||||||
// Get user
|
// Get GQL Context
|
||||||
const { user } = gqlExecCtx.getContext().req;
|
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
|
||||||
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
|
|
||||||
|
|
||||||
// Get the invite
|
// Get user
|
||||||
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
|
TE.bindW('user', ({ gqlCtx }) =>
|
||||||
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
|
pipe(
|
||||||
|
O.fromNullable(gqlCtx.getContext().req.user),
|
||||||
|
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const invitation = await this.teamInviteService.getInvitation(inviteID);
|
// Get the invite
|
||||||
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
TE.bindW('invite', ({ gqlCtx }) =>
|
||||||
|
pipe(
|
||||||
|
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
|
||||||
|
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
|
||||||
|
TE.chainW(
|
||||||
|
flow(
|
||||||
|
this.teamInviteService.getInvitation,
|
||||||
|
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Check if the user and the invite email match, else if user is a team member
|
// Check if the user and the invite email match, else if we can resolver the user as a team member
|
||||||
if (
|
// any better solution ?
|
||||||
user.email?.toLowerCase() !== invitation.value.inviteeEmail.toLowerCase()
|
TE.chainW(({ user, invite }) =>
|
||||||
) {
|
user.email?.toLowerCase() === invite.inviteeEmail.toLowerCase()
|
||||||
const teamMember = await this.teamService.getTeamMember(
|
? TE.of(true)
|
||||||
invitation.value.teamID,
|
: pipe(
|
||||||
user.uid,
|
this.teamService.getTeamMemberTE(invite.teamID, user.uid),
|
||||||
);
|
TE.map(() => true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
if (!teamMember) throwErr(TEAM_MEMBER_NOT_FOUND);
|
TE.mapLeft((e) =>
|
||||||
}
|
e === 'team/member_not_found' ? TEAM_INVITE_NOT_VALID_VIEWER : e,
|
||||||
|
),
|
||||||
|
|
||||||
return true;
|
TE.fold(throwErr, () => T.of(true)),
|
||||||
|
)();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { TeamInvitationService } from './team-invitation.service';
|
import { TeamInvitationService } from './team-invitation.service';
|
||||||
|
import { pipe, flow } from 'fp-ts/function';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
|
import * as T from 'fp-ts/Task';
|
||||||
|
import * as TE from 'fp-ts/TaskEither';
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
|
import { User } from 'src/user/user.model';
|
||||||
import {
|
import {
|
||||||
BUG_AUTH_NO_USER_CTX,
|
BUG_AUTH_NO_USER_CTX,
|
||||||
BUG_TEAM_INVITE_NO_INVITE_ID,
|
BUG_TEAM_INVITE_NO_INVITE_ID,
|
||||||
@@ -20,26 +24,44 @@ export class TeamInviteeGuard implements CanActivate {
|
|||||||
constructor(private readonly teamInviteService: TeamInvitationService) {}
|
constructor(private readonly teamInviteService: TeamInvitationService) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
// Get GQL Context
|
return pipe(
|
||||||
const gqlExecCtx = GqlExecutionContext.create(context);
|
TE.Do,
|
||||||
|
|
||||||
// Get user
|
// Get execution context
|
||||||
const { user } = gqlExecCtx.getContext().req;
|
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
|
||||||
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
|
|
||||||
|
|
||||||
// Get the invite
|
// Get user
|
||||||
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
|
TE.bindW('user', ({ gqlCtx }) =>
|
||||||
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
|
pipe(
|
||||||
|
O.fromNullable(gqlCtx.getContext().req.user),
|
||||||
|
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const invitation = await this.teamInviteService.getInvitation(inviteID);
|
// Get invite
|
||||||
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
TE.bindW('invite', ({ gqlCtx }) =>
|
||||||
|
pipe(
|
||||||
|
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
|
||||||
|
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
|
||||||
|
TE.chainW(
|
||||||
|
flow(
|
||||||
|
this.teamInviteService.getInvitation,
|
||||||
|
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
if (
|
// Check if the emails match
|
||||||
user.email.toLowerCase() !== invitation.value.inviteeEmail.toLowerCase()
|
TE.chainW(
|
||||||
) {
|
TE.fromPredicate(
|
||||||
throwErr(TEAM_INVITE_EMAIL_DO_NOT_MATCH);
|
({ user, invite }) => user.email === invite.inviteeEmail,
|
||||||
}
|
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
return true;
|
// Fold it to a promise
|
||||||
|
TE.fold(throwErr, () => T.of(true)),
|
||||||
|
)();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ export class TeamTeamInviteExtResolver {
|
|||||||
complexity: 10,
|
complexity: 10,
|
||||||
})
|
})
|
||||||
teamInvitations(@Parent() team: Team): Promise<TeamInvitation[]> {
|
teamInvitations(@Parent() team: Team): Promise<TeamInvitation[]> {
|
||||||
return this.teamInviteService.getTeamInvitations(team.id);
|
return this.teamInviteService.getAllInvitationsInTeam(team)();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
TEAM_REQ_NOT_FOUND,
|
TEAM_REQ_NOT_FOUND,
|
||||||
TEAM_REQ_REORDERING_FAILED,
|
TEAM_REQ_REORDERING_FAILED,
|
||||||
TEAM_COLL_NOT_FOUND,
|
TEAM_COLL_NOT_FOUND,
|
||||||
JSON_INVALID,
|
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
@@ -240,7 +239,7 @@ describe('deleteTeamRequest', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('createTeamRequest', () => {
|
describe('createTeamRequest', () => {
|
||||||
test('should rejects for invalid collection id', async () => {
|
test('rejects for invalid collection id', async () => {
|
||||||
mockTeamCollectionService.getTeamOfCollection.mockResolvedValue(
|
mockTeamCollectionService.getTeamOfCollection.mockResolvedValue(
|
||||||
E.left(TEAM_INVALID_COLL_ID),
|
E.left(TEAM_INVALID_COLL_ID),
|
||||||
);
|
);
|
||||||
@@ -256,42 +255,7 @@ describe('createTeamRequest', () => {
|
|||||||
expect(mockPrisma.teamRequest.create).not.toHaveBeenCalled();
|
expect(mockPrisma.teamRequest.create).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should rejects for invalid team ID', async () => {
|
test('resolves for valid collection id', async () => {
|
||||||
mockTeamCollectionService.getTeamOfCollection.mockResolvedValue(
|
|
||||||
E.right(team),
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await teamRequestService.createTeamRequest(
|
|
||||||
'testcoll',
|
|
||||||
'invalidteamid',
|
|
||||||
'Test Request',
|
|
||||||
'{}',
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(response).toEqualLeft(TEAM_INVALID_ID);
|
|
||||||
expect(mockPrisma.teamRequest.create).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should reject for invalid request body', async () => {
|
|
||||||
mockTeamCollectionService.getTeamOfCollection.mockResolvedValue(
|
|
||||||
E.right(team),
|
|
||||||
);
|
|
||||||
teamRequestService.getRequestsCountInCollection = jest
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValueOnce(0);
|
|
||||||
|
|
||||||
const response = await teamRequestService.createTeamRequest(
|
|
||||||
'testcoll',
|
|
||||||
team.id,
|
|
||||||
'Test Request',
|
|
||||||
'invalidjson',
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(response).toEqualLeft(JSON_INVALID);
|
|
||||||
expect(mockPrisma.teamRequest.create).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should resolves and create team request', async () => {
|
|
||||||
const dbRequest = dbTeamRequests[0];
|
const dbRequest = dbTeamRequests[0];
|
||||||
const teamRequest = teamRequests[0];
|
const teamRequest = teamRequests[0];
|
||||||
|
|
||||||
@@ -572,52 +536,6 @@ describe('findRequestAndNextRequest', () => {
|
|||||||
|
|
||||||
expect(result).resolves.toEqualLeft(TEAM_REQ_NOT_FOUND);
|
expect(result).resolves.toEqualLeft(TEAM_REQ_NOT_FOUND);
|
||||||
});
|
});
|
||||||
test('should resolve left if the next request and given destCollId are different', () => {
|
|
||||||
const args: MoveTeamRequestArgs = {
|
|
||||||
srcCollID: teamRequests[0].collectionID,
|
|
||||||
destCollID: 'different_coll_id',
|
|
||||||
requestID: teamRequests[0].id,
|
|
||||||
nextRequestID: teamRequests[4].id,
|
|
||||||
};
|
|
||||||
|
|
||||||
mockPrisma.teamRequest.findFirst
|
|
||||||
.mockResolvedValueOnce(dbTeamRequests[0])
|
|
||||||
.mockResolvedValueOnce(dbTeamRequests[4]);
|
|
||||||
|
|
||||||
const result = teamRequestService.findRequestAndNextRequest(
|
|
||||||
args.srcCollID,
|
|
||||||
args.requestID,
|
|
||||||
args.destCollID,
|
|
||||||
args.nextRequestID,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).resolves.toEqualLeft(TEAM_REQ_INVALID_TARGET_COLL_ID);
|
|
||||||
});
|
|
||||||
test('should resolve left if the request and the next request are from different teams', async () => {
|
|
||||||
const args: MoveTeamRequestArgs = {
|
|
||||||
srcCollID: teamRequests[0].collectionID,
|
|
||||||
destCollID: teamRequests[4].collectionID,
|
|
||||||
requestID: teamRequests[0].id,
|
|
||||||
nextRequestID: teamRequests[4].id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const request = {
|
|
||||||
...dbTeamRequests[0],
|
|
||||||
teamID: 'different_team_id',
|
|
||||||
};
|
|
||||||
mockPrisma.teamRequest.findFirst
|
|
||||||
.mockResolvedValueOnce(request)
|
|
||||||
.mockResolvedValueOnce(dbTeamRequests[4]);
|
|
||||||
|
|
||||||
const result = await teamRequestService.findRequestAndNextRequest(
|
|
||||||
args.srcCollID,
|
|
||||||
args.requestID,
|
|
||||||
args.destCollID,
|
|
||||||
args.nextRequestID,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqualLeft(TEAM_REQ_INVALID_TARGET_COLL_ID);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('moveRequest', () => {
|
describe('moveRequest', () => {
|
||||||
@@ -807,12 +725,13 @@ describe('totalRequestsInATeam', () => {
|
|||||||
});
|
});
|
||||||
expect(result).toEqual(0);
|
expect(result).toEqual(0);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
describe('getTeamRequestsCount', () => {
|
|
||||||
test('should return count of all Team Collections in the organization', async () => {
|
|
||||||
mockPrisma.teamRequest.count.mockResolvedValueOnce(10);
|
|
||||||
|
|
||||||
const result = await teamRequestService.getTeamRequestsCount();
|
describe('getTeamRequestsCount', () => {
|
||||||
expect(result).toEqual(10);
|
test('should return count of all Team Collections in the organization', async () => {
|
||||||
|
mockPrisma.teamRequest.count.mockResolvedValueOnce(10);
|
||||||
|
|
||||||
|
const result = await teamRequestService.getTeamRequestsCount();
|
||||||
|
expect(result).toEqual(10);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { UserCollection, UserRequest as DbUserRequest } from '@prisma/client';
|
import { UserCollection } from '@prisma/client';
|
||||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
import {
|
import {
|
||||||
USER_COLL_DEST_SAME,
|
USER_COLL_DEST_SAME,
|
||||||
@@ -11,17 +11,12 @@ import {
|
|||||||
USER_COLL_SHORT_TITLE,
|
USER_COLL_SHORT_TITLE,
|
||||||
USER_COLL_ALREADY_ROOT,
|
USER_COLL_ALREADY_ROOT,
|
||||||
USER_NOT_OWNER,
|
USER_NOT_OWNER,
|
||||||
USER_NOT_FOUND,
|
|
||||||
USER_COLL_INVALID_JSON,
|
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { ReqType } from 'src/types/RequestTypes';
|
import { ReqType } from 'src/types/RequestTypes';
|
||||||
import { UserCollectionService } from './user-collection.service';
|
import { UserCollectionService } from './user-collection.service';
|
||||||
import * as E from 'fp-ts/Either';
|
|
||||||
import { CollectionFolder } from 'src/types/CollectionFolder';
|
|
||||||
import { UserCollectionExportJSONData } from './user-collections.model';
|
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockPubSub = mockDeep<PubSubService>();
|
const mockPubSub = mockDeep<PubSubService>();
|
||||||
@@ -346,485 +341,11 @@ const rootGQLGQLUserCollectionList: UserCollection[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const userRESTRequestList: DbUserRequest[] = [
|
|
||||||
{
|
|
||||||
id: '123',
|
|
||||||
collectionID: rootRESTUserCollection.id,
|
|
||||||
userUid: user.uid,
|
|
||||||
title: 'Request 1',
|
|
||||||
request: {},
|
|
||||||
type: ReqType.REST,
|
|
||||||
orderIndex: 1,
|
|
||||||
createdOn: new Date(),
|
|
||||||
updatedOn: new Date(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockReset(mockPrisma);
|
mockReset(mockPrisma);
|
||||||
mockPubSub.publish.mockClear();
|
mockPubSub.publish.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('importCollectionsFromJSON', () => {
|
|
||||||
test('should resolve left for invalid JSON string', async () => {
|
|
||||||
const result = await userCollectionService.importCollectionsFromJSON(
|
|
||||||
'invalidJSONString',
|
|
||||||
user.uid,
|
|
||||||
rootRESTUserCollection.id,
|
|
||||||
ReqType.REST,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.left(USER_COLL_INVALID_JSON));
|
|
||||||
});
|
|
||||||
test('should resolve left if JSON string is not an array', async () => {
|
|
||||||
const result = await userCollectionService.importCollectionsFromJSON(
|
|
||||||
JSON.stringify({}),
|
|
||||||
user.uid,
|
|
||||||
rootRESTUserCollection.id,
|
|
||||||
ReqType.REST,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual(E.left(USER_COLL_INVALID_JSON));
|
|
||||||
});
|
|
||||||
test('should resolve left if destCollectionID is invalid', async () => {
|
|
||||||
jest
|
|
||||||
.spyOn(userCollectionService, 'getUserCollection')
|
|
||||||
.mockResolvedValueOnce(E.left(USER_COLL_NOT_FOUND));
|
|
||||||
|
|
||||||
const result = await userCollectionService.importCollectionsFromJSON(
|
|
||||||
JSON.stringify([]),
|
|
||||||
user.uid,
|
|
||||||
'invalidID',
|
|
||||||
ReqType.REST,
|
|
||||||
);
|
|
||||||
expect(result).toEqual(E.left(USER_COLL_NOT_FOUND));
|
|
||||||
});
|
|
||||||
test('should resolve left if destCollectionID is not owned by this user', async () => {
|
|
||||||
jest
|
|
||||||
.spyOn(userCollectionService, 'getUserCollection')
|
|
||||||
.mockResolvedValueOnce(E.right(rootRESTUserCollection));
|
|
||||||
|
|
||||||
const result = await userCollectionService.importCollectionsFromJSON(
|
|
||||||
JSON.stringify([]),
|
|
||||||
'anotherUserUid',
|
|
||||||
rootRESTUserCollection.id,
|
|
||||||
ReqType.REST,
|
|
||||||
);
|
|
||||||
expect(result).toEqual(E.left(USER_NOT_OWNER));
|
|
||||||
});
|
|
||||||
test('should resolve left if destCollection type miss match', async () => {
|
|
||||||
jest
|
|
||||||
.spyOn(userCollectionService, 'getUserCollection')
|
|
||||||
.mockResolvedValueOnce(E.right(rootRESTUserCollection));
|
|
||||||
|
|
||||||
const result = await userCollectionService.importCollectionsFromJSON(
|
|
||||||
JSON.stringify([]),
|
|
||||||
user.uid,
|
|
||||||
rootRESTUserCollection.id,
|
|
||||||
ReqType.GQL,
|
|
||||||
);
|
|
||||||
expect(result).toEqual(E.left(USER_COLL_NOT_SAME_TYPE));
|
|
||||||
});
|
|
||||||
test('should resolve right for valid JSON and destCollectionID provided', async () => {
|
|
||||||
jest
|
|
||||||
.spyOn(userCollectionService, 'getUserCollection')
|
|
||||||
.mockResolvedValueOnce(E.right(rootRESTUserCollection));
|
|
||||||
|
|
||||||
// private getChildCollectionsCount function call
|
|
||||||
mockPrisma.userCollection.findMany.mockResolvedValueOnce([]);
|
|
||||||
|
|
||||||
mockPrisma.$transaction.mockResolvedValueOnce([]);
|
|
||||||
|
|
||||||
const result = await userCollectionService.importCollectionsFromJSON(
|
|
||||||
JSON.stringify([]),
|
|
||||||
user.uid,
|
|
||||||
rootRESTUserCollection.id,
|
|
||||||
ReqType.REST,
|
|
||||||
);
|
|
||||||
expect(result).toEqual(E.right(true));
|
|
||||||
});
|
|
||||||
test('should resolve right for importing in root directory (destCollectionID == null)', async () => {
|
|
||||||
// private getChildCollectionsCount function call
|
|
||||||
mockPrisma.userCollection.findMany.mockResolvedValueOnce([]);
|
|
||||||
|
|
||||||
mockPrisma.$transaction.mockResolvedValueOnce([]);
|
|
||||||
|
|
||||||
const result = await userCollectionService.importCollectionsFromJSON(
|
|
||||||
JSON.stringify([
|
|
||||||
{
|
|
||||||
name: 'collection-name',
|
|
||||||
folders: [],
|
|
||||||
requests: [{ name: 'request-name' }],
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
user.uid,
|
|
||||||
null,
|
|
||||||
ReqType.REST,
|
|
||||||
);
|
|
||||||
expect(result).toEqual(E.right(true));
|
|
||||||
});
|
|
||||||
test('should resolve right and publish event', async () => {
|
|
||||||
jest
|
|
||||||
.spyOn(userCollectionService, 'getUserCollection')
|
|
||||||
.mockResolvedValueOnce(E.right(rootRESTUserCollection));
|
|
||||||
|
|
||||||
// private getChildCollectionsCount function call
|
|
||||||
mockPrisma.userCollection.findMany.mockResolvedValueOnce([]);
|
|
||||||
|
|
||||||
mockPrisma.$transaction.mockResolvedValueOnce([{}]);
|
|
||||||
|
|
||||||
const result = await userCollectionService.importCollectionsFromJSON(
|
|
||||||
JSON.stringify([
|
|
||||||
{
|
|
||||||
name: 'collection-name',
|
|
||||||
folders: [],
|
|
||||||
requests: [{ name: 'request-name' }],
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
user.uid,
|
|
||||||
rootRESTUserCollection.id,
|
|
||||||
ReqType.REST,
|
|
||||||
);
|
|
||||||
expect(result).toEqual(E.right(true));
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('exportUserCollectionsToJSON', () => {
|
|
||||||
test('should return a list of user collections successfully for valid collectionID input and structure - 1', async () => {
|
|
||||||
/*
|
|
||||||
Assuming collection and request structure is as follows:
|
|
||||||
|
|
||||||
rootTeamCollection (id: 1 [exporting this collection])
|
|
||||||
|-> childTeamCollection
|
|
||||||
| |-> <no request of root coll>
|
|
||||||
|-> <no request of root coll>
|
|
||||||
*/
|
|
||||||
|
|
||||||
mockPrisma.userCollection.findMany.mockResolvedValueOnce([
|
|
||||||
childRESTUserCollection,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// RCV CALL 1: exportUserCollectionToJSONObject
|
|
||||||
jest
|
|
||||||
.spyOn(userCollectionService, 'getUserCollection')
|
|
||||||
.mockResolvedValueOnce(E.right(childRESTUserCollection));
|
|
||||||
mockPrisma.userCollection.findMany.mockResolvedValueOnce([]);
|
|
||||||
mockPrisma.userRequest.findMany.mockResolvedValueOnce([]);
|
|
||||||
const returnFromCallee: CollectionFolder = {
|
|
||||||
id: childRESTUserCollection.id,
|
|
||||||
name: childRESTUserCollection.title,
|
|
||||||
folders: [],
|
|
||||||
requests: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Back to exportUserCollectionsToJSON
|
|
||||||
jest
|
|
||||||
.spyOn(userCollectionService, 'getUserCollection')
|
|
||||||
.mockResolvedValueOnce(E.right(rootRESTUserCollection));
|
|
||||||
mockPrisma.userRequest.findMany.mockResolvedValueOnce([]);
|
|
||||||
|
|
||||||
const returnedValue: UserCollectionExportJSONData = {
|
|
||||||
exportedCollection: JSON.stringify({
|
|
||||||
id: rootRESTUserCollection.id,
|
|
||||||
name: rootRESTUserCollection.title,
|
|
||||||
folders: [returnFromCallee],
|
|
||||||
requests: [],
|
|
||||||
}),
|
|
||||||
collectionType: ReqType.REST,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await userCollectionService.exportUserCollectionsToJSON(
|
|
||||||
user.uid,
|
|
||||||
rootRESTUserCollection.id,
|
|
||||||
ReqType.REST,
|
|
||||||
);
|
|
||||||
expect(result).toEqualRight(returnedValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return a list of user collections successfully for valid collectionID input and structure - 2', async () => {
|
|
||||||
/*
|
|
||||||
Assuming collection and request structure is as follows:
|
|
||||||
|
|
||||||
rootTeamCollection (id: 1 [exporting this collection])
|
|
||||||
|-> childTeamCollection
|
|
||||||
| |-> request1
|
|
||||||
|-> <no request of root coll>
|
|
||||||
*/
|
|
||||||
|
|
||||||
mockPrisma.userCollection.findMany.mockResolvedValueOnce([
|
|
||||||
childRESTUserCollection,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// RCV CALL 1: exportUserCollectionToJSONObject
|
|
||||||
jest
|
|
||||||
.spyOn(userCollectionService, 'getUserCollection')
|
|
||||||
.mockResolvedValueOnce(E.right(childRESTUserCollection));
|
|
||||||
mockPrisma.userCollection.findMany.mockResolvedValueOnce([]);
|
|
||||||
mockPrisma.userRequest.findMany.mockResolvedValueOnce(userRESTRequestList);
|
|
||||||
const returnFromCallee: CollectionFolder = {
|
|
||||||
id: childRESTUserCollection.id,
|
|
||||||
name: childRESTUserCollection.title,
|
|
||||||
folders: [],
|
|
||||||
requests: userRESTRequestList.map((r) => {
|
|
||||||
return {
|
|
||||||
id: r.id,
|
|
||||||
name: r.title,
|
|
||||||
...(r.request as Record<string, unknown>),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Back to exportUserCollectionsToJSON
|
|
||||||
jest
|
|
||||||
.spyOn(userCollectionService, 'getUserCollection')
|
|
||||||
.mockResolvedValueOnce(E.right(rootRESTUserCollection));
|
|
||||||
mockPrisma.userRequest.findMany.mockResolvedValueOnce([]);
|
|
||||||
|
|
||||||
const returnedValue: UserCollectionExportJSONData = {
|
|
||||||
exportedCollection: JSON.stringify({
|
|
||||||
id: rootRESTUserCollection.id,
|
|
||||||
name: rootRESTUserCollection.title,
|
|
||||||
folders: [returnFromCallee],
|
|
||||||
requests: [],
|
|
||||||
}),
|
|
||||||
collectionType: ReqType.REST,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await userCollectionService.exportUserCollectionsToJSON(
|
|
||||||
user.uid,
|
|
||||||
rootRESTUserCollection.id,
|
|
||||||
ReqType.REST,
|
|
||||||
);
|
|
||||||
expect(result).toEqualRight(returnedValue);
|
|
||||||
});
|
|
||||||
test('should return a list of user collections successfully for valid collectionID input and structure - 3', async () => {
|
|
||||||
/*
|
|
||||||
Assuming collection and request structure is as follows:
|
|
||||||
|
|
||||||
rootTeamCollection (id: 1 [exporting this collection])
|
|
||||||
|-> childTeamCollection
|
|
||||||
| |-> request1
|
|
||||||
|-> request2
|
|
||||||
*/
|
|
||||||
|
|
||||||
mockPrisma.userCollection.findMany.mockResolvedValueOnce([
|
|
||||||
childRESTUserCollection,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// RCV CALL 1: exportUserCollectionToJSONObject
|
|
||||||
jest
|
|
||||||
.spyOn(userCollectionService, 'getUserCollection')
|
|
||||||
.mockResolvedValueOnce(E.right(childRESTUserCollection));
|
|
||||||
mockPrisma.userCollection.findMany.mockResolvedValueOnce([]);
|
|
||||||
mockPrisma.userRequest.findMany.mockResolvedValueOnce(userRESTRequestList);
|
|
||||||
const returnFromCallee: CollectionFolder = {
|
|
||||||
id: childRESTUserCollection.id,
|
|
||||||
name: childRESTUserCollection.title,
|
|
||||||
folders: [],
|
|
||||||
requests: userRESTRequestList.map((r) => {
|
|
||||||
return {
|
|
||||||
id: r.id,
|
|
||||||
name: r.title,
|
|
||||||
...(r.request as Record<string, unknown>),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Back to exportUserCollectionsToJSON
|
|
||||||
jest
|
|
||||||
.spyOn(userCollectionService, 'getUserCollection')
|
|
||||||
.mockResolvedValueOnce(E.right(rootRESTUserCollection));
|
|
||||||
mockPrisma.userRequest.findMany.mockResolvedValueOnce(userRESTRequestList);
|
|
||||||
|
|
||||||
const returnedValue: UserCollectionExportJSONData = {
|
|
||||||
exportedCollection: JSON.stringify({
|
|
||||||
id: rootRESTUserCollection.id,
|
|
||||||
name: rootRESTUserCollection.title,
|
|
||||||
folders: [returnFromCallee],
|
|
||||||
requests: userRESTRequestList.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
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
collectionType: ReqType.REST,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await userCollectionService.exportUserCollectionsToJSON(
|
|
||||||
user.uid,
|
|
||||||
rootRESTUserCollection.id,
|
|
||||||
ReqType.REST,
|
|
||||||
);
|
|
||||||
expect(result).toEqualRight(returnedValue);
|
|
||||||
});
|
|
||||||
test('should return a list of user collections successfully for collectionID == null', async () => {
|
|
||||||
/*
|
|
||||||
Assuming collection and request structure is as follows:
|
|
||||||
|
|
||||||
rootTeamCollection (id: 1 [exporting this collection])
|
|
||||||
|-> childTeamCollection
|
|
||||||
| |-> request1
|
|
||||||
|-> request2
|
|
||||||
*/
|
|
||||||
|
|
||||||
mockPrisma.userCollection.findMany.mockResolvedValueOnce([
|
|
||||||
childRESTUserCollection,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// RCV CALL 1: exportUserCollectionToJSONObject
|
|
||||||
jest
|
|
||||||
.spyOn(userCollectionService, 'getUserCollection')
|
|
||||||
.mockResolvedValueOnce(E.right(childRESTUserCollection));
|
|
||||||
mockPrisma.userCollection.findMany.mockResolvedValueOnce([]);
|
|
||||||
mockPrisma.userRequest.findMany.mockResolvedValueOnce(userRESTRequestList);
|
|
||||||
const returnFromCallee: CollectionFolder = {
|
|
||||||
id: childRESTUserCollection.id,
|
|
||||||
name: childRESTUserCollection.title,
|
|
||||||
folders: [],
|
|
||||||
requests: userRESTRequestList.map((r) => {
|
|
||||||
return {
|
|
||||||
id: r.id,
|
|
||||||
name: r.title,
|
|
||||||
...(r.request as Record<string, unknown>),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Back to exportUserCollectionsToJSON
|
|
||||||
|
|
||||||
const returnedValue: UserCollectionExportJSONData = {
|
|
||||||
exportedCollection: JSON.stringify([returnFromCallee]),
|
|
||||||
collectionType: ReqType.REST,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await userCollectionService.exportUserCollectionsToJSON(
|
|
||||||
user.uid,
|
|
||||||
null,
|
|
||||||
ReqType.REST,
|
|
||||||
);
|
|
||||||
expect(result).toEqualRight(returnedValue);
|
|
||||||
});
|
|
||||||
test('should return USER_COLL_NOT_FOUND if collectionID or its child not found in DB', async () => {
|
|
||||||
/*
|
|
||||||
Assuming collection and request structure is as follows:
|
|
||||||
|
|
||||||
rootTeamCollection (id: 1 [exporting this collection])
|
|
||||||
|-> childTeamCollection
|
|
||||||
| |-> request1 <NOT FOUND IN DATABASE>
|
|
||||||
|-> request2
|
|
||||||
*/
|
|
||||||
|
|
||||||
mockPrisma.userCollection.findMany.mockResolvedValueOnce([
|
|
||||||
childRESTUserCollection,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// RCV CALL 1: exportUserCollectionToJSONObject
|
|
||||||
jest
|
|
||||||
.spyOn(userCollectionService, 'getUserCollection')
|
|
||||||
.mockResolvedValueOnce(E.left(USER_COLL_NOT_FOUND));
|
|
||||||
|
|
||||||
// Back to exportUserCollectionsToJSON
|
|
||||||
|
|
||||||
const result = await userCollectionService.exportUserCollectionsToJSON(
|
|
||||||
user.uid,
|
|
||||||
null,
|
|
||||||
ReqType.REST,
|
|
||||||
);
|
|
||||||
expect(result).toEqualLeft(USER_COLL_NOT_FOUND);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getUserOfCollection', () => {
|
|
||||||
test('should return a user successfully with valid collectionID', async () => {
|
|
||||||
mockPrisma.userCollection.findUniqueOrThrow.mockResolvedValueOnce({
|
|
||||||
...rootRESTUserCollection,
|
|
||||||
user: user,
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
const result = await userCollectionService.getUserOfCollection(
|
|
||||||
rootRESTUserCollection.id,
|
|
||||||
);
|
|
||||||
expect(result).toEqualRight(user);
|
|
||||||
});
|
|
||||||
test('should return null with invalid collectionID', async () => {
|
|
||||||
mockPrisma.userCollection.findUniqueOrThrow.mockRejectedValue('error');
|
|
||||||
|
|
||||||
const result = await userCollectionService.getUserOfCollection('invalidId');
|
|
||||||
expect(result).toEqualLeft(USER_NOT_FOUND);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getUserChildCollections', () => {
|
|
||||||
test('should return a list of child collections successfully with valid collectionID and userID', async () => {
|
|
||||||
mockPrisma.userCollection.findMany.mockResolvedValueOnce(
|
|
||||||
childRESTUserCollectionList,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await userCollectionService.getUserChildCollections(
|
|
||||||
user,
|
|
||||||
rootRESTUserCollection.id,
|
|
||||||
null,
|
|
||||||
10,
|
|
||||||
ReqType.REST,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual(childRESTUserCollectionList);
|
|
||||||
expect(mockPrisma.userCollection.findMany).toHaveBeenCalledWith({
|
|
||||||
where: {
|
|
||||||
userUid: user.uid,
|
|
||||||
parentID: rootRESTUserCollection.id,
|
|
||||||
type: ReqType.REST,
|
|
||||||
},
|
|
||||||
take: 10,
|
|
||||||
skip: 0,
|
|
||||||
cursor: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('should return an empty list if no child collections found', async () => {
|
|
||||||
mockPrisma.userCollection.findMany.mockResolvedValueOnce([]);
|
|
||||||
|
|
||||||
const result = await userCollectionService.getUserChildCollections(
|
|
||||||
user,
|
|
||||||
rootRESTUserCollection.id,
|
|
||||||
null,
|
|
||||||
10,
|
|
||||||
ReqType.REST,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
expect(mockPrisma.userCollection.findMany).toHaveBeenCalledWith({
|
|
||||||
where: {
|
|
||||||
userUid: user.uid,
|
|
||||||
parentID: rootRESTUserCollection.id,
|
|
||||||
type: ReqType.REST,
|
|
||||||
},
|
|
||||||
take: 10,
|
|
||||||
skip: 0,
|
|
||||||
cursor: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getCollectionCount', () => {
|
|
||||||
test('should return the count of collections', async () => {
|
|
||||||
const collectionID = 'collection123';
|
|
||||||
const count = 5;
|
|
||||||
|
|
||||||
mockPrisma.userCollection.count.mockResolvedValueOnce(count);
|
|
||||||
|
|
||||||
const result = await userCollectionService.getCollectionCount(collectionID);
|
|
||||||
|
|
||||||
expect(result).toEqual(count);
|
|
||||||
expect(mockPrisma.userCollection.count).toHaveBeenCalledTimes(1);
|
|
||||||
expect(mockPrisma.userCollection.count).toHaveBeenCalledWith({
|
|
||||||
where: { parentID: collectionID },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getParentOfUserCollection', () => {
|
describe('getParentOfUserCollection', () => {
|
||||||
test('should return a user-collection successfully with valid collectionID', async () => {
|
test('should return a user-collection successfully with valid collectionID', async () => {
|
||||||
mockPrisma.userCollection.findUnique.mockResolvedValueOnce({
|
mockPrisma.userCollection.findUnique.mockResolvedValueOnce({
|
||||||
|
|||||||
@@ -140,15 +140,13 @@ describe('UserHistoryService', () => {
|
|||||||
});
|
});
|
||||||
describe('createUserHistory', () => {
|
describe('createUserHistory', () => {
|
||||||
test('Should resolve right and create a REST request to users history and return a `UserHistory` object', async () => {
|
test('Should resolve right and create a REST request to users history and return a `UserHistory` object', async () => {
|
||||||
const executedOn = new Date();
|
|
||||||
|
|
||||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||||
userUid: 'abc',
|
userUid: 'abc',
|
||||||
id: '1',
|
id: '1',
|
||||||
request: [{}],
|
request: [{}],
|
||||||
responseMetadata: [{}],
|
responseMetadata: [{}],
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn,
|
executedOn: new Date(),
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -158,7 +156,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: JSON.stringify([{}]),
|
request: JSON.stringify([{}]),
|
||||||
responseMetadata: JSON.stringify([{}]),
|
responseMetadata: JSON.stringify([{}]),
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn,
|
executedOn: new Date(),
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -172,15 +170,13 @@ describe('UserHistoryService', () => {
|
|||||||
).toEqualRight(userHistory);
|
).toEqualRight(userHistory);
|
||||||
});
|
});
|
||||||
test('Should resolve right and create a GQL request to users history and return a `UserHistory` object', async () => {
|
test('Should resolve right and create a GQL request to users history and return a `UserHistory` object', async () => {
|
||||||
const executedOn = new Date();
|
|
||||||
|
|
||||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||||
userUid: 'abc',
|
userUid: 'abc',
|
||||||
id: '1',
|
id: '1',
|
||||||
request: [{}],
|
request: [{}],
|
||||||
responseMetadata: [{}],
|
responseMetadata: [{}],
|
||||||
reqType: ReqType.GQL,
|
reqType: ReqType.GQL,
|
||||||
executedOn,
|
executedOn: new Date(),
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -190,7 +186,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: JSON.stringify([{}]),
|
request: JSON.stringify([{}]),
|
||||||
responseMetadata: JSON.stringify([{}]),
|
responseMetadata: JSON.stringify([{}]),
|
||||||
reqType: ReqType.GQL,
|
reqType: ReqType.GQL,
|
||||||
executedOn,
|
executedOn: new Date(),
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -214,15 +210,13 @@ describe('UserHistoryService', () => {
|
|||||||
).toEqualLeft(USER_HISTORY_INVALID_REQ_TYPE);
|
).toEqualLeft(USER_HISTORY_INVALID_REQ_TYPE);
|
||||||
});
|
});
|
||||||
test('Should create a GQL request to users history and publish a created subscription', async () => {
|
test('Should create a GQL request to users history and publish a created subscription', async () => {
|
||||||
const executedOn = new Date();
|
|
||||||
|
|
||||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||||
userUid: 'abc',
|
userUid: 'abc',
|
||||||
id: '1',
|
id: '1',
|
||||||
request: [{}],
|
request: [{}],
|
||||||
responseMetadata: [{}],
|
responseMetadata: [{}],
|
||||||
reqType: ReqType.GQL,
|
reqType: ReqType.GQL,
|
||||||
executedOn,
|
executedOn: new Date(),
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -232,7 +226,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: JSON.stringify([{}]),
|
request: JSON.stringify([{}]),
|
||||||
responseMetadata: JSON.stringify([{}]),
|
responseMetadata: JSON.stringify([{}]),
|
||||||
reqType: ReqType.GQL,
|
reqType: ReqType.GQL,
|
||||||
executedOn,
|
executedOn: new Date(),
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -249,15 +243,13 @@ describe('UserHistoryService', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
test('Should create a REST request to users history and publish a created subscription', async () => {
|
test('Should create a REST request to users history and publish a created subscription', async () => {
|
||||||
const executedOn = new Date();
|
|
||||||
|
|
||||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||||
userUid: 'abc',
|
userUid: 'abc',
|
||||||
id: '1',
|
id: '1',
|
||||||
request: [{}],
|
request: [{}],
|
||||||
responseMetadata: [{}],
|
responseMetadata: [{}],
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn,
|
executedOn: new Date(),
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -267,7 +259,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: JSON.stringify([{}]),
|
request: JSON.stringify([{}]),
|
||||||
responseMetadata: JSON.stringify([{}]),
|
responseMetadata: JSON.stringify([{}]),
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn,
|
executedOn: new Date(),
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import {
|
|||||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
import {
|
import {
|
||||||
JSON_INVALID,
|
JSON_INVALID,
|
||||||
USER_COLLECTION_NOT_FOUND,
|
|
||||||
USER_COLL_NOT_FOUND,
|
|
||||||
USER_REQUEST_INVALID_TYPE,
|
|
||||||
USER_REQUEST_NOT_FOUND,
|
USER_REQUEST_NOT_FOUND,
|
||||||
USER_REQUEST_REORDERING_FAILED,
|
USER_REQUEST_REORDERING_FAILED,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
@@ -376,101 +373,6 @@ describe('UserRequestService', () => {
|
|||||||
|
|
||||||
expect(result).resolves.toEqualLeft(JSON_INVALID);
|
expect(result).resolves.toEqualLeft(JSON_INVALID);
|
||||||
});
|
});
|
||||||
test('Should resolve left for invalid collection ID', () => {
|
|
||||||
const args: CreateUserRequestArgs = {
|
|
||||||
collectionID: 'invalid-collection-id',
|
|
||||||
title: userRequests[0].title,
|
|
||||||
request: userRequests[0].request,
|
|
||||||
type: userRequests[0].type,
|
|
||||||
};
|
|
||||||
|
|
||||||
mockPrisma.userRequest.count.mockResolvedValue(
|
|
||||||
dbUserRequests[0].orderIndex - 1,
|
|
||||||
);
|
|
||||||
mockUserCollectionService.getUserCollection.mockResolvedValue(
|
|
||||||
E.left(USER_COLL_NOT_FOUND),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = userRequestService.createRequest(
|
|
||||||
args.collectionID,
|
|
||||||
args.title,
|
|
||||||
args.request,
|
|
||||||
args.type,
|
|
||||||
user,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).resolves.toEqualLeft(USER_COLL_NOT_FOUND);
|
|
||||||
});
|
|
||||||
test('Should resolve left for wrong collection ID (using other users collection ID)', () => {
|
|
||||||
const args: CreateUserRequestArgs = {
|
|
||||||
collectionID: userRequests[0].collectionID,
|
|
||||||
title: userRequests[0].title,
|
|
||||||
request: userRequests[0].request,
|
|
||||||
type: userRequests[0].type,
|
|
||||||
};
|
|
||||||
|
|
||||||
mockPrisma.userRequest.count.mockResolvedValue(
|
|
||||||
dbUserRequests[0].orderIndex - 1,
|
|
||||||
);
|
|
||||||
mockUserCollectionService.getUserCollection.mockResolvedValue(
|
|
||||||
E.right({ type: userRequests[0].type, userUid: 'another-user' } as any),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = userRequestService.createRequest(
|
|
||||||
args.collectionID,
|
|
||||||
args.title,
|
|
||||||
args.request,
|
|
||||||
args.type,
|
|
||||||
user,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).resolves.toEqualLeft(USER_COLLECTION_NOT_FOUND);
|
|
||||||
});
|
|
||||||
test('Should resolve left for collection type and request type miss match', () => {
|
|
||||||
const args: CreateUserRequestArgs = {
|
|
||||||
collectionID: userRequests[0].collectionID,
|
|
||||||
title: userRequests[0].title,
|
|
||||||
request: userRequests[0].request,
|
|
||||||
type: userRequests[0].type,
|
|
||||||
};
|
|
||||||
|
|
||||||
mockUserCollectionService.getUserCollection.mockResolvedValue(
|
|
||||||
E.right({ type: 'invalid-type', userUid: user.uid } as any),
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = userRequestService.createRequest(
|
|
||||||
args.collectionID,
|
|
||||||
args.title,
|
|
||||||
args.request,
|
|
||||||
args.type,
|
|
||||||
user,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).resolves.toEqualLeft(USER_REQUEST_INVALID_TYPE);
|
|
||||||
});
|
|
||||||
test('Should resolve left if DB request type and parameter type is different', () => {
|
|
||||||
const args: CreateUserRequestArgs = {
|
|
||||||
collectionID: userRequests[0].collectionID,
|
|
||||||
title: userRequests[0].title,
|
|
||||||
request: userRequests[0].request,
|
|
||||||
type: userRequests[0].type,
|
|
||||||
};
|
|
||||||
|
|
||||||
mockPrisma.userRequest.count.mockResolvedValue(
|
|
||||||
dbUserRequests[0].orderIndex - 1,
|
|
||||||
);
|
|
||||||
mockPrisma.userRequest.create.mockResolvedValue(dbUserRequests[0]);
|
|
||||||
|
|
||||||
const result = userRequestService.createRequest(
|
|
||||||
args.collectionID,
|
|
||||||
args.title,
|
|
||||||
args.request,
|
|
||||||
ReqType.GQL,
|
|
||||||
user,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).resolves.toEqualLeft(USER_REQUEST_INVALID_TYPE);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('updateRequest', () => {
|
describe('updateRequest', () => {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 337 B |
@@ -19,7 +19,7 @@
|
|||||||
"edit": "編輯",
|
"edit": "編輯",
|
||||||
"filter": "篩選回應",
|
"filter": "篩選回應",
|
||||||
"go_back": "返回",
|
"go_back": "返回",
|
||||||
"go_forward": "向前",
|
"go_forward": "Go forward",
|
||||||
"group_by": "分組方式",
|
"group_by": "分組方式",
|
||||||
"label": "標籤",
|
"label": "標籤",
|
||||||
"learn_more": "瞭解更多",
|
"learn_more": "瞭解更多",
|
||||||
@@ -117,37 +117,37 @@
|
|||||||
"username": "使用者名稱"
|
"username": "使用者名稱"
|
||||||
},
|
},
|
||||||
"collection": {
|
"collection": {
|
||||||
"created": "集合已建立",
|
"created": "組合已建立",
|
||||||
"different_parent": "無法為父集合不同的集合重新排序",
|
"different_parent": "Cannot reorder collection with different parent",
|
||||||
"edit": "編輯集合",
|
"edit": "編輯組合",
|
||||||
"invalid_name": "請提供有效的集合名稱",
|
"invalid_name": "請提供有效的組合名稱",
|
||||||
"invalid_root_move": "集合已在根目錄",
|
"invalid_root_move": "Collection already in the root",
|
||||||
"moved": "移動成功",
|
"moved": "Moved Successfully",
|
||||||
"my_collections": "我的集合",
|
"my_collections": "我的組合",
|
||||||
"name": "我的新集合",
|
"name": "我的新組合",
|
||||||
"name_length_insufficient": "集合名稱至少要有 3 個字元。",
|
"name_length_insufficient": "組合名稱至少要有 3 個字元。",
|
||||||
"new": "建立集合",
|
"new": "建立組合",
|
||||||
"order_changed": "集合順序已更新",
|
"order_changed": "Collection Order Updated",
|
||||||
"renamed": "集合已重新命名",
|
"renamed": "組合已重新命名",
|
||||||
"request_in_use": "請求正在使用中",
|
"request_in_use": "請求正在使用中",
|
||||||
"save_as": "另存為",
|
"save_as": "另存為",
|
||||||
"select": "選擇一個集合",
|
"select": "選擇一個組合",
|
||||||
"select_location": "選擇位置",
|
"select_location": "選擇位置",
|
||||||
"select_team": "選擇一個團隊",
|
"select_team": "選擇一個團隊",
|
||||||
"team_collections": "團隊集合"
|
"team_collections": "團隊組合"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"exit_team": "您確定要離開此團隊嗎?",
|
"exit_team": "您確定要離開此團隊嗎?",
|
||||||
"logout": "您確定要登出嗎?",
|
"logout": "您確定要登出嗎?",
|
||||||
"remove_collection": "您確定要永久刪除該集合嗎?",
|
"remove_collection": "您確定要永久刪除該組合嗎?",
|
||||||
"remove_environment": "您確定要永久刪除該環境嗎?",
|
"remove_environment": "您確定要永久刪除該環境嗎?",
|
||||||
"remove_folder": "您確定要永久刪除該資料夾嗎?",
|
"remove_folder": "您確定要永久刪除該資料夾嗎?",
|
||||||
"remove_history": "您確定要永久刪除全部歷史記錄嗎?",
|
"remove_history": "您確定要永久刪除全部歷史記錄嗎?",
|
||||||
"remove_request": "您確定要永久刪除該請求嗎?",
|
"remove_request": "您確定要永久刪除該請求嗎?",
|
||||||
"remove_team": "您確定要刪除該團隊嗎?",
|
"remove_team": "您確定要刪除該團隊嗎?",
|
||||||
"remove_telemetry": "您確定要退出遙測服務嗎?",
|
"remove_telemetry": "您確定要退出遙測服務嗎?",
|
||||||
"request_change": "您確定要捨棄目前的請求嗎?未儲存的變更將遺失。",
|
"request_change": "您確定要捨棄當前請求嗎?未儲存的變更將遺失。",
|
||||||
"save_unsaved_tab": "您要儲存在此分頁做出的改動嗎?",
|
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||||
"sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。"
|
"sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。"
|
||||||
},
|
},
|
||||||
"count": {
|
"count": {
|
||||||
@@ -160,13 +160,13 @@
|
|||||||
},
|
},
|
||||||
"documentation": {
|
"documentation": {
|
||||||
"generate": "產生文件",
|
"generate": "產生文件",
|
||||||
"generate_message": "匯入 Hoppscotch 集合以隨時隨地產生 API 文件。"
|
"generate_message": "匯入 Hoppscotch 組合以隨時隨地產生 API 文件。"
|
||||||
},
|
},
|
||||||
"empty": {
|
"empty": {
|
||||||
"authorization": "該請求沒有使用任何授權",
|
"authorization": "該請求沒有使用任何授權",
|
||||||
"body": "該請求沒有任何請求主體",
|
"body": "該請求沒有任何請求主體",
|
||||||
"collection": "集合為空",
|
"collection": "組合為空",
|
||||||
"collections": "集合為空",
|
"collections": "組合為空",
|
||||||
"documentation": "連線到 GraphQL 端點以檢視文件",
|
"documentation": "連線到 GraphQL 端點以檢視文件",
|
||||||
"endpoint": "端點不能留空",
|
"endpoint": "端點不能留空",
|
||||||
"environments": "環境為空",
|
"environments": "環境為空",
|
||||||
@@ -209,7 +209,7 @@
|
|||||||
"browser_support_sse": "此瀏覽器似乎不支援 SSE。",
|
"browser_support_sse": "此瀏覽器似乎不支援 SSE。",
|
||||||
"check_console_details": "檢查控制台日誌以獲悉詳情",
|
"check_console_details": "檢查控制台日誌以獲悉詳情",
|
||||||
"curl_invalid_format": "cURL 格式不正確",
|
"curl_invalid_format": "cURL 格式不正確",
|
||||||
"danger_zone": "危險地帶",
|
"danger_zone": "Danger zone",
|
||||||
"delete_account": "您的帳號目前為這些團隊的擁有者:",
|
"delete_account": "您的帳號目前為這些團隊的擁有者:",
|
||||||
"delete_account_description": "您在刪除帳號前必須先將您自己從團隊中移除、轉移擁有權,或是刪除團隊。",
|
"delete_account_description": "您在刪除帳號前必須先將您自己從團隊中移除、轉移擁有權,或是刪除團隊。",
|
||||||
"empty_req_name": "空請求名稱",
|
"empty_req_name": "空請求名稱",
|
||||||
@@ -277,38 +277,38 @@
|
|||||||
"tests": "編寫測試指令碼以自動除錯。"
|
"tests": "編寫測試指令碼以自動除錯。"
|
||||||
},
|
},
|
||||||
"hide": {
|
"hide": {
|
||||||
"collection": "隱藏集合面板",
|
"collection": "隱藏組合面板",
|
||||||
"more": "隱藏更多",
|
"more": "隱藏更多",
|
||||||
"preview": "隱藏預覽",
|
"preview": "隱藏預覽",
|
||||||
"sidebar": "隱藏側邊欄"
|
"sidebar": "隱藏側邊欄"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"collections": "匯入集合",
|
"collections": "匯入組合",
|
||||||
"curl": "匯入 cURL",
|
"curl": "匯入 cURL",
|
||||||
"failed": "匯入失敗",
|
"failed": "匯入失敗",
|
||||||
"from_gist": "從 Gist 匯入",
|
"from_gist": "從 Gist 匯入",
|
||||||
"from_gist_description": "從 Gist 網址匯入",
|
"from_gist_description": "從 Gist 網址匯入",
|
||||||
"from_insomnia": "從 Insomnia 匯入",
|
"from_insomnia": "從 Insomnia 匯入",
|
||||||
"from_insomnia_description": "從 Insomnia 集合匯入",
|
"from_insomnia_description": "從 Insomnia 組合匯入",
|
||||||
"from_json": "從 Hoppscotch 匯入",
|
"from_json": "從 Hoppscotch 匯入",
|
||||||
"from_json_description": "從 Hoppscotch 集合檔匯入",
|
"from_json_description": "從 Hoppscotch 組合檔匯入",
|
||||||
"from_my_collections": "從我的集合匯入",
|
"from_my_collections": "從我的組合匯入",
|
||||||
"from_my_collections_description": "從我的集合檔匯入",
|
"from_my_collections_description": "從我的組合檔匯入",
|
||||||
"from_openapi": "從 OpenAPI 匯入",
|
"from_openapi": "從 OpenAPI 匯入",
|
||||||
"from_openapi_description": "從 OpenAPI 規格檔 (YML/JSON) 匯入",
|
"from_openapi_description": "從 OpenAPI 規格檔 (YML/JSON) 匯入",
|
||||||
"from_postman": "從 Postman 匯入",
|
"from_postman": "從 Postman 匯入",
|
||||||
"from_postman_description": "從 Postman 集合匯入",
|
"from_postman_description": "從 Postman 組合匯入",
|
||||||
"from_url": "從網址匯入",
|
"from_url": "從網址匯入",
|
||||||
"gist_url": "輸入 Gist 網址",
|
"gist_url": "輸入 Gist 網址",
|
||||||
"import_from_url_invalid_fetch": "無法從網址取得資料",
|
"import_from_url_invalid_fetch": "無法從網址取得資料",
|
||||||
"import_from_url_invalid_file_format": "匯入集合時發生錯誤",
|
"import_from_url_invalid_file_format": "匯入組合時發生錯誤",
|
||||||
"import_from_url_invalid_type": "不支援此類型。可接受的值為 'hoppscotch'、'openapi'、'postman'、'insomnia'",
|
"import_from_url_invalid_type": "不支援此類型。可接受的值為 'hoppscotch'、'openapi'、'postman'、'insomnia'",
|
||||||
"import_from_url_success": "已匯入集合",
|
"import_from_url_success": "已匯入組合",
|
||||||
"json_description": "從 Hoppscotch 集合 JSON 檔匯入集合",
|
"json_description": "從 Hoppscotch 組合 JSON 檔匯入組合",
|
||||||
"title": "匯入"
|
"title": "匯入"
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
"collapse_collection": "隱藏或顯示集合",
|
"collapse_collection": "隱藏或顯示組合",
|
||||||
"collapse_sidebar": "隱藏或顯示側邊欄",
|
"collapse_sidebar": "隱藏或顯示側邊欄",
|
||||||
"column": "垂直版面",
|
"column": "垂直版面",
|
||||||
"name": "配置",
|
"name": "配置",
|
||||||
@@ -316,8 +316,8 @@
|
|||||||
"zen_mode": "專注模式"
|
"zen_mode": "專注模式"
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"close_unsaved_tab": "您有未儲存的改動",
|
"close_unsaved_tab": "You have unsaved changes",
|
||||||
"collections": "集合",
|
"collections": "組合",
|
||||||
"confirm": "確認",
|
"confirm": "確認",
|
||||||
"edit_request": "編輯請求",
|
"edit_request": "編輯請求",
|
||||||
"import_export": "匯入/匯出"
|
"import_export": "匯入/匯出"
|
||||||
@@ -374,9 +374,9 @@
|
|||||||
"email_verification_mail": "已將驗證信寄送至您的電子郵件地址。請點擊信中連結以驗證您的電子郵件地址。",
|
"email_verification_mail": "已將驗證信寄送至您的電子郵件地址。請點擊信中連結以驗證您的電子郵件地址。",
|
||||||
"no_permission": "您沒有權限執行此操作。",
|
"no_permission": "您沒有權限執行此操作。",
|
||||||
"owner": "擁有者",
|
"owner": "擁有者",
|
||||||
"owner_description": "擁有者可以新增、編輯和刪除請求、集合和團隊成員。",
|
"owner_description": "擁有者可以新增、編輯和刪除請求、組合和團隊成員。",
|
||||||
"roles": "角色",
|
"roles": "角色",
|
||||||
"roles_description": "角色用來控制對共用集合的存取權。",
|
"roles_description": "角色用來控制對共用組合的存取權。",
|
||||||
"updated": "已更新個人檔案",
|
"updated": "已更新個人檔案",
|
||||||
"viewer": "檢視者",
|
"viewer": "檢視者",
|
||||||
"viewer_description": "檢視者只能檢視和使用請求。"
|
"viewer_description": "檢視者只能檢視和使用請求。"
|
||||||
@@ -396,8 +396,8 @@
|
|||||||
"text": "文字"
|
"text": "文字"
|
||||||
},
|
},
|
||||||
"copy_link": "複製連結",
|
"copy_link": "複製連結",
|
||||||
"different_collection": "無法重新排列來自不同集合的請求",
|
"different_collection": "Cannot reorder requests from different collections",
|
||||||
"duplicated": "已複製請求",
|
"duplicated": "Request duplicated",
|
||||||
"duration": "持續時間",
|
"duration": "持續時間",
|
||||||
"enter_curl": "輸入 cURL",
|
"enter_curl": "輸入 cURL",
|
||||||
"generate_code": "產生程式碼",
|
"generate_code": "產生程式碼",
|
||||||
@@ -405,10 +405,10 @@
|
|||||||
"header_list": "請求標頭列表",
|
"header_list": "請求標頭列表",
|
||||||
"invalid_name": "請提供請求名稱",
|
"invalid_name": "請提供請求名稱",
|
||||||
"method": "方法",
|
"method": "方法",
|
||||||
"moved": "已移動請求",
|
"moved": "Request moved",
|
||||||
"name": "請求名稱",
|
"name": "請求名稱",
|
||||||
"new": "新請求",
|
"new": "新請求",
|
||||||
"order_changed": "已更新請求順序",
|
"order_changed": "Request Order Updated",
|
||||||
"override": "覆寫",
|
"override": "覆寫",
|
||||||
"override_help": "在標頭設置 <kbd>Content-Type</kbd>",
|
"override_help": "在標頭設置 <kbd>Content-Type</kbd>",
|
||||||
"overriden": "已覆寫",
|
"overriden": "已覆寫",
|
||||||
@@ -432,7 +432,7 @@
|
|||||||
"view_my_links": "檢視我的連結"
|
"view_my_links": "檢視我的連結"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"audio": "音訊",
|
"audio": "Audio",
|
||||||
"body": "回應本體",
|
"body": "回應本體",
|
||||||
"filter_response_body": "篩選 JSON 回應本體 (使用 JSONPath 語法)",
|
"filter_response_body": "篩選 JSON 回應本體 (使用 JSONPath 語法)",
|
||||||
"headers": "回應標頭",
|
"headers": "回應標頭",
|
||||||
@@ -446,7 +446,7 @@
|
|||||||
"status": "狀態",
|
"status": "狀態",
|
||||||
"time": "時間",
|
"time": "時間",
|
||||||
"title": "回應",
|
"title": "回應",
|
||||||
"video": "視訊",
|
"video": "Video",
|
||||||
"waiting_for_connection": "等待連線",
|
"waiting_for_connection": "等待連線",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
@@ -494,7 +494,7 @@
|
|||||||
"short_codes_description": "我們為您打造的快捷碼。",
|
"short_codes_description": "我們為您打造的快捷碼。",
|
||||||
"sidebar_on_left": "左側邊欄",
|
"sidebar_on_left": "左側邊欄",
|
||||||
"sync": "同步",
|
"sync": "同步",
|
||||||
"sync_collections": "集合",
|
"sync_collections": "組合",
|
||||||
"sync_description": "這些設定會同步到雲端。",
|
"sync_description": "這些設定會同步到雲端。",
|
||||||
"sync_environments": "環境",
|
"sync_environments": "環境",
|
||||||
"sync_history": "歷史",
|
"sync_history": "歷史",
|
||||||
@@ -551,7 +551,7 @@
|
|||||||
"previous_method": "選擇上一個方法",
|
"previous_method": "選擇上一個方法",
|
||||||
"put_method": "選擇 PUT 方法",
|
"put_method": "選擇 PUT 方法",
|
||||||
"reset_request": "重置請求",
|
"reset_request": "重置請求",
|
||||||
"save_to_collections": "儲存到集合",
|
"save_to_collections": "儲存到組合",
|
||||||
"send_request": "傳送請求",
|
"send_request": "傳送請求",
|
||||||
"title": "請求"
|
"title": "請求"
|
||||||
},
|
},
|
||||||
@@ -570,7 +570,7 @@
|
|||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"code": "顯示程式碼",
|
"code": "顯示程式碼",
|
||||||
"collection": "顯示集合面板",
|
"collection": "顯示組合面板",
|
||||||
"more": "顯示更多",
|
"more": "顯示更多",
|
||||||
"sidebar": "顯示側邊欄"
|
"sidebar": "顯示側邊欄"
|
||||||
},
|
},
|
||||||
@@ -639,9 +639,9 @@
|
|||||||
"tab": {
|
"tab": {
|
||||||
"authorization": "授權",
|
"authorization": "授權",
|
||||||
"body": "請求本體",
|
"body": "請求本體",
|
||||||
"collections": "集合",
|
"collections": "組合",
|
||||||
"documentation": "幫助文件",
|
"documentation": "幫助文件",
|
||||||
"environments": "環境",
|
"environments": "Environments",
|
||||||
"headers": "請求標頭",
|
"headers": "請求標頭",
|
||||||
"history": "歷史記錄",
|
"history": "歷史記錄",
|
||||||
"mqtt": "MQTT",
|
"mqtt": "MQTT",
|
||||||
@@ -666,7 +666,7 @@
|
|||||||
"email_do_not_match": "電子信箱與您的帳號資料不一致。請聯絡您的團隊擁有者。",
|
"email_do_not_match": "電子信箱與您的帳號資料不一致。請聯絡您的團隊擁有者。",
|
||||||
"exit": "退出團隊",
|
"exit": "退出團隊",
|
||||||
"exit_disabled": "團隊擁有者無法退出團隊",
|
"exit_disabled": "團隊擁有者無法退出團隊",
|
||||||
"invalid_coll_id": "集合 ID 無效",
|
"invalid_coll_id": "Invalid collection ID",
|
||||||
"invalid_email_format": "電子信箱格式無效",
|
"invalid_email_format": "電子信箱格式無效",
|
||||||
"invalid_id": "團隊 ID 無效。請聯絡您的團隊擁有者。",
|
"invalid_id": "團隊 ID 無效。請聯絡您的團隊擁有者。",
|
||||||
"invalid_invite_link": "邀請連結無效",
|
"invalid_invite_link": "邀請連結無效",
|
||||||
@@ -690,21 +690,21 @@
|
|||||||
"member_removed": "使用者已移除",
|
"member_removed": "使用者已移除",
|
||||||
"member_role_updated": "使用者角色已更新",
|
"member_role_updated": "使用者角色已更新",
|
||||||
"members": "成員",
|
"members": "成員",
|
||||||
"more_members": "還有 {count} 位",
|
"more_members": "+{count} more",
|
||||||
"name_length_insufficient": "團隊名稱至少為 6 個字元",
|
"name_length_insufficient": "團隊名稱至少為 6 個字元",
|
||||||
"name_updated": "團隊名稱已更新",
|
"name_updated": "團隊名稱已更新",
|
||||||
"new": "新團隊",
|
"new": "新團隊",
|
||||||
"new_created": "已建立新團隊",
|
"new_created": "已建立新團隊",
|
||||||
"new_name": "我的新團隊",
|
"new_name": "我的新團隊",
|
||||||
"no_access": "您沒有編輯集合的許可權",
|
"no_access": "您沒有編輯組合的許可權",
|
||||||
"no_invite_found": "未找到邀請。請聯絡您的團隊擁有者。",
|
"no_invite_found": "未找到邀請。請聯絡您的團隊擁有者。",
|
||||||
"no_request_found": "找不到請求。",
|
"no_request_found": "Request not found.",
|
||||||
"not_found": "找不到團隊。請聯絡您的團隊擁有者。",
|
"not_found": "找不到團隊。請聯絡您的團隊擁有者。",
|
||||||
"not_valid_viewer": "您不是一個有效的檢視者。請聯絡您的團隊擁有者。",
|
"not_valid_viewer": "您不是一個有效的檢視者。請聯絡您的團隊擁有者。",
|
||||||
"parent_coll_move": "無法將集合移動至子集合",
|
"parent_coll_move": "Cannot move collection to a child collection",
|
||||||
"pending_invites": "待定邀請",
|
"pending_invites": "待定邀請",
|
||||||
"permissions": "許可權",
|
"permissions": "許可權",
|
||||||
"same_target_destination": "目標和目的地相同",
|
"same_target_destination": "Same target and destination",
|
||||||
"saved": "團隊已儲存",
|
"saved": "團隊已儲存",
|
||||||
"select_a_team": "選擇團隊",
|
"select_a_team": "選擇團隊",
|
||||||
"title": "團隊",
|
"title": "團隊",
|
||||||
@@ -734,9 +734,9 @@
|
|||||||
"url": "網址"
|
"url": "網址"
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"change": "切換工作區",
|
"change": "Change workspace",
|
||||||
"personal": "我的工作區",
|
"personal": "My Workspace",
|
||||||
"team": "團隊工作區",
|
"team": "Team Workspace",
|
||||||
"title": "工作區"
|
"title": "Workspaces"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/common",
|
"name": "@hoppscotch/common",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2023.4.7",
|
"version": "2023.4.6",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||||
"dev:vite": "vite",
|
"dev:vite": "vite",
|
||||||
|
|||||||
@@ -44,9 +44,8 @@
|
|||||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
>
|
>
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||||
<span class="my-2 text-center flex flex-col">
|
<span class="my-2 text-center">
|
||||||
{{ t("state.nothing_found") }}
|
{{ t("state.nothing_found") }} "{{ filterText }}"
|
||||||
<span class="break-all">"{{ filterText }}"</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -284,14 +284,6 @@ const importerAction = async (stepResults: StepReturnValue[]) => {
|
|||||||
emit("import-to-teams", result)
|
emit("import-to-teams", result)
|
||||||
} else {
|
} else {
|
||||||
appendRESTCollections(result)
|
appendRESTCollections(result)
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_IMPORT_COLLECTION",
|
|
||||||
importer: importerModule.value!.name,
|
|
||||||
platform: "rest",
|
|
||||||
workspaceType: "personal",
|
|
||||||
})
|
|
||||||
|
|
||||||
fileImported()
|
fileImported()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-col flex-1 bg-primaryContrast">
|
||||||
<div
|
<div
|
||||||
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
|
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
|
||||||
:style="
|
:style="
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ import {
|
|||||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||||
import { computedWithControl } from "@vueuse/core"
|
import { computedWithControl } from "@vueuse/core"
|
||||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||||
import { platform } from "~/platform"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -224,13 +223,6 @@ const saveRequestAs = async () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_SAVE_REQUEST",
|
|
||||||
createdNow: true,
|
|
||||||
platform: "rest",
|
|
||||||
workspaceType: "personal",
|
|
||||||
})
|
|
||||||
|
|
||||||
requestSaved()
|
requestSaved()
|
||||||
} else if (picked.value.pickedType === "my-folder") {
|
} else if (picked.value.pickedType === "my-folder") {
|
||||||
if (!isHoppRESTRequest(requestUpdated))
|
if (!isHoppRESTRequest(requestUpdated))
|
||||||
@@ -251,13 +243,6 @@ const saveRequestAs = async () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_SAVE_REQUEST",
|
|
||||||
createdNow: true,
|
|
||||||
platform: "rest",
|
|
||||||
workspaceType: "personal",
|
|
||||||
})
|
|
||||||
|
|
||||||
requestSaved()
|
requestSaved()
|
||||||
} else if (picked.value.pickedType === "my-request") {
|
} else if (picked.value.pickedType === "my-request") {
|
||||||
if (!isHoppRESTRequest(requestUpdated))
|
if (!isHoppRESTRequest(requestUpdated))
|
||||||
@@ -279,38 +264,17 @@ const saveRequestAs = async () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_SAVE_REQUEST",
|
|
||||||
createdNow: false,
|
|
||||||
platform: "rest",
|
|
||||||
workspaceType: "personal",
|
|
||||||
})
|
|
||||||
|
|
||||||
requestSaved()
|
requestSaved()
|
||||||
} else if (picked.value.pickedType === "teams-collection") {
|
} else if (picked.value.pickedType === "teams-collection") {
|
||||||
if (!isHoppRESTRequest(requestUpdated))
|
if (!isHoppRESTRequest(requestUpdated))
|
||||||
throw new Error("requestUpdated is not a REST Request")
|
throw new Error("requestUpdated is not a REST Request")
|
||||||
|
|
||||||
updateTeamCollectionOrFolder(picked.value.collectionID, requestUpdated)
|
updateTeamCollectionOrFolder(picked.value.collectionID, requestUpdated)
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_SAVE_REQUEST",
|
|
||||||
createdNow: true,
|
|
||||||
platform: "rest",
|
|
||||||
workspaceType: "team",
|
|
||||||
})
|
|
||||||
} else if (picked.value.pickedType === "teams-folder") {
|
} else if (picked.value.pickedType === "teams-folder") {
|
||||||
if (!isHoppRESTRequest(requestUpdated))
|
if (!isHoppRESTRequest(requestUpdated))
|
||||||
throw new Error("requestUpdated is not a REST Request")
|
throw new Error("requestUpdated is not a REST Request")
|
||||||
|
|
||||||
updateTeamCollectionOrFolder(picked.value.folderID, requestUpdated)
|
updateTeamCollectionOrFolder(picked.value.folderID, requestUpdated)
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_SAVE_REQUEST",
|
|
||||||
createdNow: true,
|
|
||||||
platform: "rest",
|
|
||||||
workspaceType: "team",
|
|
||||||
})
|
|
||||||
} else if (picked.value.pickedType === "teams-request") {
|
} else if (picked.value.pickedType === "teams-request") {
|
||||||
if (!isHoppRESTRequest(requestUpdated))
|
if (!isHoppRESTRequest(requestUpdated))
|
||||||
throw new Error("requestUpdated is not a REST Request")
|
throw new Error("requestUpdated is not a REST Request")
|
||||||
@@ -328,13 +292,6 @@ const saveRequestAs = async () => {
|
|||||||
title: requestUpdated.name,
|
title: requestUpdated.name,
|
||||||
}
|
}
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_SAVE_REQUEST",
|
|
||||||
createdNow: false,
|
|
||||||
platform: "rest",
|
|
||||||
workspaceType: "team",
|
|
||||||
})
|
|
||||||
|
|
||||||
pipe(
|
pipe(
|
||||||
updateTeamRequest(picked.value.requestID, data),
|
updateTeamRequest(picked.value.requestID, data),
|
||||||
TE.match(
|
TE.match(
|
||||||
@@ -356,13 +313,6 @@ const saveRequestAs = async () => {
|
|||||||
requestUpdated as HoppGQLRequest
|
requestUpdated as HoppGQLRequest
|
||||||
)
|
)
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_SAVE_REQUEST",
|
|
||||||
createdNow: false,
|
|
||||||
platform: "gql",
|
|
||||||
workspaceType: "team",
|
|
||||||
})
|
|
||||||
|
|
||||||
requestSaved()
|
requestSaved()
|
||||||
} else if (picked.value.pickedType === "gql-my-folder") {
|
} else if (picked.value.pickedType === "gql-my-folder") {
|
||||||
// TODO: Check for GQL request ?
|
// TODO: Check for GQL request ?
|
||||||
@@ -371,13 +321,6 @@ const saveRequestAs = async () => {
|
|||||||
requestUpdated as HoppGQLRequest
|
requestUpdated as HoppGQLRequest
|
||||||
)
|
)
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_SAVE_REQUEST",
|
|
||||||
createdNow: true,
|
|
||||||
platform: "gql",
|
|
||||||
workspaceType: "team",
|
|
||||||
})
|
|
||||||
|
|
||||||
requestSaved()
|
requestSaved()
|
||||||
} else if (picked.value.pickedType === "gql-my-collection") {
|
} else if (picked.value.pickedType === "gql-my-collection") {
|
||||||
// TODO: Check for GQL request ?
|
// TODO: Check for GQL request ?
|
||||||
@@ -386,13 +329,6 @@ const saveRequestAs = async () => {
|
|||||||
requestUpdated as HoppGQLRequest
|
requestUpdated as HoppGQLRequest
|
||||||
)
|
)
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_SAVE_REQUEST",
|
|
||||||
createdNow: true,
|
|
||||||
platform: "gql",
|
|
||||||
workspaceType: "team",
|
|
||||||
})
|
|
||||||
|
|
||||||
requestSaved()
|
requestSaved()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-col flex-1 bg-primaryContrast">
|
||||||
<div
|
<div
|
||||||
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
|
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
|
||||||
:style="
|
:style="
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ import { useToast } from "@composables/toast"
|
|||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { HoppGQLRequest, makeCollection } from "@hoppscotch/data"
|
import { HoppGQLRequest, makeCollection } from "@hoppscotch/data"
|
||||||
import { addGraphqlCollection } from "~/newstore/collections"
|
import { addGraphqlCollection } from "~/newstore/collections"
|
||||||
import { platform } from "~/platform"
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -80,13 +79,6 @@ export default defineComponent({
|
|||||||
)
|
)
|
||||||
|
|
||||||
this.hideModal()
|
this.hideModal()
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_CREATE_COLLECTION",
|
|
||||||
isRootCollection: true,
|
|
||||||
platform: "gql",
|
|
||||||
workspaceType: "personal",
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
hideModal() {
|
hideModal() {
|
||||||
this.name = null
|
this.name = null
|
||||||
|
|||||||
@@ -244,14 +244,6 @@ const importFromJSON = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
appendGraphqlCollections(collections)
|
appendGraphqlCollections(collections)
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_IMPORT_COLLECTION",
|
|
||||||
importer: "json",
|
|
||||||
workspaceType: "personal",
|
|
||||||
platform: "gql",
|
|
||||||
})
|
|
||||||
|
|
||||||
fileImported()
|
fileImported()
|
||||||
}
|
}
|
||||||
reader.readAsText(inputChooseFileToImportFrom.value.files[0])
|
reader.readAsText(inputChooseFileToImportFrom.value.files[0])
|
||||||
@@ -265,12 +257,6 @@ const exportJSON = () => {
|
|||||||
const url = URL.createObjectURL(file)
|
const url = URL.createObjectURL(file)
|
||||||
a.href = url
|
a.href = url
|
||||||
|
|
||||||
platform?.analytics?.logEvent({
|
|
||||||
type: "HOPP_EXPORT_COLLECTION",
|
|
||||||
exporter: "json",
|
|
||||||
platform: "gql",
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: get uri from meta
|
// TODO: get uri from meta
|
||||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||||
document.body.appendChild(a)
|
document.body.appendChild(a)
|
||||||
|
|||||||
@@ -153,7 +153,6 @@ import IconArchive from "~icons/lucide/archive"
|
|||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { platform } from "~/platform"
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -286,13 +285,6 @@ export default defineComponent({
|
|||||||
response: "",
|
response: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_SAVE_REQUEST",
|
|
||||||
platform: "gql",
|
|
||||||
createdNow: true,
|
|
||||||
workspaceType: "personal",
|
|
||||||
})
|
|
||||||
|
|
||||||
this.displayModalAddRequest(false)
|
this.displayModalAddRequest(false)
|
||||||
},
|
},
|
||||||
addRequest(payload) {
|
addRequest(payload) {
|
||||||
@@ -302,14 +294,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
onAddFolder({ name, path }) {
|
onAddFolder({ name, path }) {
|
||||||
addGraphqlFolder(name, path)
|
addGraphqlFolder(name, path)
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_CREATE_COLLECTION",
|
|
||||||
isRootCollection: false,
|
|
||||||
platform: "gql",
|
|
||||||
workspaceType: "personal",
|
|
||||||
})
|
|
||||||
|
|
||||||
this.displayModalAddFolder(false)
|
this.displayModalAddFolder(false)
|
||||||
},
|
},
|
||||||
addFolder(payload) {
|
addFolder(payload) {
|
||||||
|
|||||||
@@ -599,25 +599,11 @@ const addNewRootCollection = (name: string) => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_CREATE_COLLECTION",
|
|
||||||
platform: "rest",
|
|
||||||
workspaceType: "personal",
|
|
||||||
isRootCollection: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
displayModalAdd(false)
|
displayModalAdd(false)
|
||||||
} else if (hasTeamWriteAccess.value) {
|
} else if (hasTeamWriteAccess.value) {
|
||||||
if (!collectionsType.value.selectedTeam) return
|
if (!collectionsType.value.selectedTeam) return
|
||||||
modalLoadingState.value = true
|
modalLoadingState.value = true
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_CREATE_COLLECTION",
|
|
||||||
platform: "rest",
|
|
||||||
workspaceType: "team",
|
|
||||||
isRootCollection: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
pipe(
|
pipe(
|
||||||
createNewRootCollection(name, collectionsType.value.selectedTeam.id),
|
createNewRootCollection(name, collectionsType.value.selectedTeam.id),
|
||||||
TE.match(
|
TE.match(
|
||||||
@@ -666,13 +652,6 @@ const onAddRequest = (requestName: string) => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_SAVE_REQUEST",
|
|
||||||
workspaceType: "personal",
|
|
||||||
createdNow: true,
|
|
||||||
platform: "rest",
|
|
||||||
})
|
|
||||||
|
|
||||||
displayModalAddRequest(false)
|
displayModalAddRequest(false)
|
||||||
} else if (hasTeamWriteAccess.value) {
|
} else if (hasTeamWriteAccess.value) {
|
||||||
const folder = editingFolder.value
|
const folder = editingFolder.value
|
||||||
@@ -688,13 +667,6 @@ const onAddRequest = (requestName: string) => {
|
|||||||
title: requestName,
|
title: requestName,
|
||||||
}
|
}
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_SAVE_REQUEST",
|
|
||||||
workspaceType: "team",
|
|
||||||
platform: "rest",
|
|
||||||
createdNow: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
pipe(
|
pipe(
|
||||||
createRequestInCollection(folder.id, data),
|
createRequestInCollection(folder.id, data),
|
||||||
TE.match(
|
TE.match(
|
||||||
@@ -740,14 +712,6 @@ const onAddFolder = (folderName: string) => {
|
|||||||
if (collectionsType.value.type === "my-collections") {
|
if (collectionsType.value.type === "my-collections") {
|
||||||
if (!path) return
|
if (!path) return
|
||||||
addRESTFolder(folderName, path)
|
addRESTFolder(folderName, path)
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_CREATE_COLLECTION",
|
|
||||||
workspaceType: "personal",
|
|
||||||
isRootCollection: false,
|
|
||||||
platform: "rest",
|
|
||||||
})
|
|
||||||
|
|
||||||
displayModalAddFolder(false)
|
displayModalAddFolder(false)
|
||||||
} else if (hasTeamWriteAccess.value) {
|
} else if (hasTeamWriteAccess.value) {
|
||||||
const folder = editingFolder.value
|
const folder = editingFolder.value
|
||||||
@@ -755,13 +719,6 @@ const onAddFolder = (folderName: string) => {
|
|||||||
|
|
||||||
modalLoadingState.value = true
|
modalLoadingState.value = true
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_CREATE_COLLECTION",
|
|
||||||
workspaceType: "personal",
|
|
||||||
isRootCollection: false,
|
|
||||||
platform: "rest",
|
|
||||||
})
|
|
||||||
|
|
||||||
pipe(
|
pipe(
|
||||||
createChildCollection(folderName, folder.id),
|
createChildCollection(folderName, folder.id),
|
||||||
TE.match(
|
TE.match(
|
||||||
@@ -1927,12 +1884,6 @@ const exportData = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const exportJSONCollection = async () => {
|
const exportJSONCollection = async () => {
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_EXPORT_COLLECTION",
|
|
||||||
exporter: "json",
|
|
||||||
platform: "rest",
|
|
||||||
})
|
|
||||||
|
|
||||||
await getJSONCollection()
|
await getJSONCollection()
|
||||||
|
|
||||||
initializeDownloadCollection(collectionJSON.value, null)
|
initializeDownloadCollection(collectionJSON.value, null)
|
||||||
@@ -1944,12 +1895,6 @@ const createCollectionGist = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_EXPORT_COLLECTION",
|
|
||||||
exporter: "gist",
|
|
||||||
platform: "rest",
|
|
||||||
})
|
|
||||||
|
|
||||||
creatingGistCollection.value = true
|
creatingGistCollection.value = true
|
||||||
await getJSONCollection()
|
await getJSONCollection()
|
||||||
|
|
||||||
@@ -1980,12 +1925,6 @@ const importToTeams = async (collection: HoppCollection<HoppRESTRequest>[]) => {
|
|||||||
|
|
||||||
importingMyCollections.value = true
|
importingMyCollections.value = true
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_EXPORT_COLLECTION",
|
|
||||||
exporter: "import-to-teams",
|
|
||||||
platform: "rest",
|
|
||||||
})
|
|
||||||
|
|
||||||
pipe(
|
pipe(
|
||||||
importJSONToTeam(
|
importJSONToTeam(
|
||||||
JSON.stringify(collection),
|
JSON.stringify(collection),
|
||||||
|
|||||||
@@ -190,12 +190,6 @@ const createEnvironmentGist = async () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
toast.success(t("export.gist_created").toString())
|
toast.success(t("export.gist_created").toString())
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_EXPORT_ENVIRONMENT",
|
|
||||||
platform: "rest",
|
|
||||||
})
|
|
||||||
|
|
||||||
window.open(res.data.html_url)
|
window.open(res.data.html_url)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(t("error.something_went_wrong").toString())
|
toast.error(t("error.something_went_wrong").toString())
|
||||||
@@ -255,13 +249,6 @@ const openDialogChooseFileToImportFrom = () => {
|
|||||||
|
|
||||||
const importToTeams = async (content: Environment[]) => {
|
const importToTeams = async (content: Environment[]) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
|
||||||
platform: "rest",
|
|
||||||
workspaceType: "team",
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const [i, env] of content.entries()) {
|
for (const [i, env] of content.entries()) {
|
||||||
if (i === content.length - 1) {
|
if (i === content.length - 1) {
|
||||||
await pipe(
|
await pipe(
|
||||||
@@ -314,12 +301,6 @@ const importFromJSON = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
|
||||||
platform: "rest",
|
|
||||||
workspaceType: "personal",
|
|
||||||
})
|
|
||||||
|
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
|
|
||||||
reader.onload = ({ target }) => {
|
reader.onload = ({ target }) => {
|
||||||
@@ -371,7 +352,6 @@ const importFromPostman = ({
|
|||||||
const environment: Environment = { name, variables: [] }
|
const environment: Environment = { name, variables: [] }
|
||||||
values.forEach(({ key, value }) => environment.variables.push({ key, value }))
|
values.forEach(({ key, value }) => environment.variables.push({ key, value }))
|
||||||
const environments = [environment]
|
const environments = [environment]
|
||||||
|
|
||||||
importFromHoppscotch(environments)
|
importFromHoppscotch(environments)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -148,7 +148,6 @@ import { useToast } from "@composables/toast"
|
|||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { environmentsStore } from "~/newstore/environments"
|
import { environmentsStore } from "~/newstore/environments"
|
||||||
import { platform } from "~/platform"
|
|
||||||
|
|
||||||
type EnvironmentVariable = {
|
type EnvironmentVariable = {
|
||||||
id: number
|
id: number
|
||||||
@@ -312,11 +311,6 @@ const saveEnvironment = () => {
|
|||||||
index: envList.value.length - 1,
|
index: envList.value.length - 1,
|
||||||
})
|
})
|
||||||
toast.success(`${t("environment.created")}`)
|
toast.success(`${t("environment.created")}`)
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_CREATE_ENVIRONMENT",
|
|
||||||
workspaceType: "personal",
|
|
||||||
})
|
|
||||||
} else if (props.editingEnvironmentIndex === "Global") {
|
} else if (props.editingEnvironmentIndex === "Global") {
|
||||||
// Editing the Global environment
|
// Editing the Global environment
|
||||||
setGlobalEnvVariables(environmentUpdated.variables)
|
setGlobalEnvVariables(environmentUpdated.variables)
|
||||||
|
|||||||
@@ -156,7 +156,6 @@ import IconTrash from "~icons/lucide/trash"
|
|||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
import IconDone from "~icons/lucide/check"
|
import IconDone from "~icons/lucide/check"
|
||||||
import IconPlus from "~icons/lucide/plus"
|
import IconPlus from "~icons/lucide/plus"
|
||||||
import { platform } from "~/platform"
|
|
||||||
|
|
||||||
type EnvironmentVariable = {
|
type EnvironmentVariable = {
|
||||||
id: number
|
id: number
|
||||||
@@ -295,11 +294,6 @@ const saveEnvironment = async () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (props.action === "new") {
|
if (props.action === "new") {
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_CREATE_ENVIRONMENT",
|
|
||||||
workspaceType: "team",
|
|
||||||
})
|
|
||||||
|
|
||||||
await pipe(
|
await pipe(
|
||||||
createTeamEnvironment(
|
createTeamEnvironment(
|
||||||
JSON.stringify(filterdVariables),
|
JSON.stringify(filterdVariables),
|
||||||
|
|||||||
@@ -142,15 +142,15 @@
|
|||||||
<div v-if="authType === 'basic'">
|
<div v-if="authType === 'basic'">
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
|
:environmentHighlights="false"
|
||||||
v-model="basicUsername"
|
v-model="basicUsername"
|
||||||
:environment-highlights="false"
|
|
||||||
:placeholder="t('authorization.username')"
|
:placeholder="t('authorization.username')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
|
:environmentHighlights="false"
|
||||||
v-model="basicPassword"
|
v-model="basicPassword"
|
||||||
:environment-highlights="false"
|
|
||||||
:placeholder="t('authorization.password')"
|
:placeholder="t('authorization.password')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -158,8 +158,8 @@
|
|||||||
<div v-if="authType === 'bearer'">
|
<div v-if="authType === 'bearer'">
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
|
:environmentHighlights="false"
|
||||||
v-model="bearerToken"
|
v-model="bearerToken"
|
||||||
:environment-highlights="false"
|
|
||||||
placeholder="Token"
|
placeholder="Token"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,8 +167,8 @@
|
|||||||
<div v-if="authType === 'oauth-2'">
|
<div v-if="authType === 'oauth-2'">
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
|
:environmentHighlights="false"
|
||||||
v-model="oauth2Token"
|
v-model="oauth2Token"
|
||||||
:environment-highlights="false"
|
|
||||||
placeholder="Token"
|
placeholder="Token"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,15 +177,15 @@
|
|||||||
<div v-if="authType === 'api-key'">
|
<div v-if="authType === 'api-key'">
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
|
:environmentHighlights="false"
|
||||||
v-model="apiKey"
|
v-model="apiKey"
|
||||||
:environment-highlights="false"
|
|
||||||
placeholder="Key"
|
placeholder="Key"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
|
:environmentHighlights="false"
|
||||||
v-model="apiValue"
|
v-model="apiValue"
|
||||||
:environment-highlights="false"
|
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -59,8 +59,7 @@ const onConnectClick = () => {
|
|||||||
if (!connected.value) {
|
if (!connected.value) {
|
||||||
props.conn.connect(url.value, headers.value as any, auth.value)
|
props.conn.connect(url.value, headers.value as any, auth.value)
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||||
type: "HOPP_REQUEST_RUN",
|
|
||||||
platform: "graphql-schema",
|
platform: "graphql-schema",
|
||||||
strategy: getCurrentStrategyID(),
|
strategy: getCurrentStrategyID(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -748,8 +748,7 @@ const runQuery = async () => {
|
|||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||||
type: "HOPP_REQUEST_RUN",
|
|
||||||
platform: "graphql-query",
|
platform: "graphql-query",
|
||||||
strategy: getCurrentStrategyID(),
|
strategy: getCurrentStrategyID(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ import { GQLHistoryEntry } from "~/newstore/history"
|
|||||||
import { shortDateTime } from "~/helpers/utils/date"
|
import { shortDateTime } from "~/helpers/utils/date"
|
||||||
|
|
||||||
import IconStar from "~icons/lucide/star"
|
import IconStar from "~icons/lucide/star"
|
||||||
import IconStarOff from "~icons/hopp/star-off"
|
import IconStarOff from "~icons/lucide/star-off"
|
||||||
import IconTrash from "~icons/lucide/trash"
|
import IconTrash from "~icons/lucide/trash"
|
||||||
import IconMinimize2 from "~icons/lucide/minimize-2"
|
import IconMinimize2 from "~icons/lucide/minimize-2"
|
||||||
import IconMaximize2 from "~icons/lucide/maximize-2"
|
import IconMaximize2 from "~icons/lucide/maximize-2"
|
||||||
|
|||||||
@@ -72,11 +72,9 @@
|
|||||||
class="flex items-center justify-between flex-1 min-w-0 transition cursor-pointer focus:outline-none text-secondaryLight text-tiny group"
|
class="flex items-center justify-between flex-1 min-w-0 transition cursor-pointer focus:outline-none text-secondaryLight text-tiny group"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center justify-center px-4 py-2 transition group-hover:text-secondary truncate"
|
class="inline-flex items-center justify-center px-4 py-2 transition group-hover:text-secondary"
|
||||||
>
|
>
|
||||||
<icon-lucide-chevron-right
|
<icon-lucide-chevron-right class="mr-2 indicator" />
|
||||||
class="mr-2 indicator flex flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<span
|
<span
|
||||||
:class="[
|
:class="[
|
||||||
{ 'capitalize-first': groupSelection === 'TIME' },
|
{ 'capitalize-first': groupSelection === 'TIME' },
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ import { RESTHistoryEntry } from "~/newstore/history"
|
|||||||
import { shortDateTime } from "~/helpers/utils/date"
|
import { shortDateTime } from "~/helpers/utils/date"
|
||||||
|
|
||||||
import IconStar from "~icons/lucide/star"
|
import IconStar from "~icons/lucide/star"
|
||||||
import IconStarOff from "~icons/hopp/star-off"
|
import IconStarOff from "~icons/lucide/star-off"
|
||||||
import IconTrash from "~icons/lucide/trash"
|
import IconTrash from "~icons/lucide/trash"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -165,7 +165,6 @@ import IconCheck from "~icons/lucide/check"
|
|||||||
import IconWrapText from "~icons/lucide/wrap-text"
|
import IconWrapText from "~icons/lucide/wrap-text"
|
||||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||||
import cloneDeep from "lodash-es/cloneDeep"
|
import cloneDeep from "lodash-es/cloneDeep"
|
||||||
import { platform } from "~/platform"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -249,10 +248,6 @@ watch(
|
|||||||
(goingToShow) => {
|
(goingToShow) => {
|
||||||
if (goingToShow) {
|
if (goingToShow) {
|
||||||
request.value = cloneDeep(currentActiveTab.value.document.request)
|
request.value = cloneDeep(currentActiveTab.value.document.request)
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_REST_CODEGEN_OPENED",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ watch(workingHeaders, (headersList) => {
|
|||||||
|
|
||||||
// Sync logic between headers and working/bulk headers
|
// Sync logic between headers and working/bulk headers
|
||||||
watch(
|
watch(
|
||||||
() => request.value.headers,
|
request.value.headers,
|
||||||
(newHeadersList) => {
|
(newHeadersList) => {
|
||||||
// Sync should overwrite working headers
|
// Sync should overwrite working headers
|
||||||
const filteredWorkingHeaders = pipe(
|
const filteredWorkingHeaders = pipe(
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ import IconClipboard from "~icons/lucide/clipboard"
|
|||||||
import IconCheck from "~icons/lucide/check"
|
import IconCheck from "~icons/lucide/check"
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||||
import { platform } from "~/platform"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -145,10 +144,6 @@ const handleImport = () => {
|
|||||||
try {
|
try {
|
||||||
const req = parseCurlToHoppRESTReq(text)
|
const req = parseCurlToHoppRESTReq(text)
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_REST_IMPORT_CURL",
|
|
||||||
})
|
|
||||||
|
|
||||||
currentActiveTab.value.document.request = req
|
currentActiveTab.value.document.request = req
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|||||||
@@ -324,8 +324,7 @@ const newSendRequest = async () => {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
// Log the request run into analytics
|
// Log the request run into analytics
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||||
type: "HOPP_REQUEST_RUN",
|
|
||||||
platform: "rest",
|
platform: "rest",
|
||||||
strategy: getCurrentStrategyID(),
|
strategy: getCurrentStrategyID(),
|
||||||
})
|
})
|
||||||
@@ -447,11 +446,6 @@ const copyRequest = async () => {
|
|||||||
shareLink.value = ""
|
shareLink.value = ""
|
||||||
fetchingShareLink.value = true
|
fetchingShareLink.value = true
|
||||||
const shortcodeResult = await createShortcode(tab.value.document.request)()
|
const shortcodeResult = await createShortcode(tab.value.document.request)()
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_SHORTCODE_CREATED",
|
|
||||||
})
|
|
||||||
|
|
||||||
if (E.isLeft(shortcodeResult)) {
|
if (E.isLeft(shortcodeResult)) {
|
||||||
toast.error(`${shortcodeResult.left.error}`)
|
toast.error(`${shortcodeResult.left.error}`)
|
||||||
shareLink.value = `${t("error.something_went_wrong")}`
|
shareLink.value = `${t("error.something_went_wrong")}`
|
||||||
@@ -522,14 +516,6 @@ const saveRequest = () => {
|
|||||||
editRESTRequest(saveCtx.folderPath, saveCtx.requestIndex, req)
|
editRESTRequest(saveCtx.folderPath, saveCtx.requestIndex, req)
|
||||||
|
|
||||||
tab.value.document.isDirty = false
|
tab.value.document.isDirty = false
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_SAVE_REQUEST",
|
|
||||||
platform: "rest",
|
|
||||||
createdNow: false,
|
|
||||||
workspaceType: "personal",
|
|
||||||
})
|
|
||||||
|
|
||||||
toast.success(`${t("request.saved")}`)
|
toast.success(`${t("request.saved")}`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
tab.value.document.saveContext = undefined
|
tab.value.document.saveContext = undefined
|
||||||
@@ -540,13 +526,6 @@ const saveRequest = () => {
|
|||||||
|
|
||||||
// TODO: handle error case (NOTE: overwriteRequestTeams is async)
|
// TODO: handle error case (NOTE: overwriteRequestTeams is async)
|
||||||
try {
|
try {
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_SAVE_REQUEST",
|
|
||||||
platform: "rest",
|
|
||||||
createdNow: false,
|
|
||||||
workspaceType: "team",
|
|
||||||
})
|
|
||||||
|
|
||||||
runMutation(UpdateRequestDocument, {
|
runMutation(UpdateRequestDocument, {
|
||||||
requestID: saveCtx.requestID,
|
requestID: saveCtx.requestID,
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -54,8 +54,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { useVModel } from "@vueuse/core"
|
import { computed, ref, watch } from "vue"
|
||||||
import { computed, ref } from "vue"
|
|
||||||
|
|
||||||
export type RequestOptionTabs =
|
export type RequestOptionTabs =
|
||||||
| "params"
|
| "params"
|
||||||
@@ -71,7 +70,15 @@ const emit = defineEmits<{
|
|||||||
(e: "update:modelValue", value: HoppRESTRequest): void
|
(e: "update:modelValue", value: HoppRESTRequest): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const request = useVModel(props, "modelValue", emit)
|
const request = ref(props.modelValue)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => request.value,
|
||||||
|
(newVal) => {
|
||||||
|
emit("update:modelValue", newVal)
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
const selectedRealtimeTab = ref<RequestOptionTabs>("params")
|
const selectedRealtimeTab = ref<RequestOptionTabs>("params")
|
||||||
|
|
||||||
|
|||||||
@@ -28,11 +28,9 @@
|
|||||||
class="flex items-center justify-between flex-1 min-w-0 transition cursor-pointer focus:outline-none text-secondaryLight text-tiny group"
|
class="flex items-center justify-between flex-1 min-w-0 transition cursor-pointer focus:outline-none text-secondaryLight text-tiny group"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center justify-center px-4 py-2 transition group-hover:text-secondary truncate"
|
class="inline-flex items-center justify-center px-4 py-2 transition group-hover:text-secondary"
|
||||||
>
|
>
|
||||||
<icon-lucide-chevron-right
|
<icon-lucide-chevron-right class="mr-2 indicator" />
|
||||||
class="mr-2 indicator flex flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<span class="truncate capitalize-first">
|
<span class="truncate capitalize-first">
|
||||||
{{ t("environment.title") }}
|
{{ t("environment.title") }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ import { createTeam } from "~/helpers/backend/mutations/Team"
|
|||||||
import { TeamNameCodec } from "~/helpers/backend/types/TeamName"
|
import { TeamNameCodec } from "~/helpers/backend/types/TeamName"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { platform } from "~/platform"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -69,12 +68,6 @@ const addNewTeam = async () => {
|
|||||||
TE.fromEither,
|
TE.fromEither,
|
||||||
TE.mapLeft(() => "invalid_name" as const),
|
TE.mapLeft(() => "invalid_name" as const),
|
||||||
TE.chainW(createTeam),
|
TE.chainW(createTeam),
|
||||||
TE.chainFirstIOK(
|
|
||||||
() => () =>
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_CREATE_TEAM",
|
|
||||||
})
|
|
||||||
),
|
|
||||||
TE.match(
|
TE.match(
|
||||||
(err) => {
|
(err) => {
|
||||||
// err is of type "invalid_name" | GQLError<Err>
|
// err is of type "invalid_name" | GQLError<Err>
|
||||||
|
|||||||
@@ -71,11 +71,9 @@ const parseURL = (urlText: string | number) =>
|
|||||||
* @returns URL object
|
* @returns URL object
|
||||||
*/
|
*/
|
||||||
export function getURLObject(parsedArguments: parser.Arguments) {
|
export function getURLObject(parsedArguments: parser.Arguments) {
|
||||||
const location = parsedArguments.location ?? undefined
|
|
||||||
|
|
||||||
return pipe(
|
return pipe(
|
||||||
// contains raw url strings
|
// contains raw url strings
|
||||||
[...parsedArguments._.slice(1), location],
|
parsedArguments._.slice(1),
|
||||||
A.findFirstMap(parseURL),
|
A.findFirstMap(parseURL),
|
||||||
// no url found
|
// no url found
|
||||||
O.getOrElse(() => new URL(defaultRESTReq.endpoint))
|
O.getOrElse(() => new URL(defaultRESTReq.endpoint))
|
||||||
|
|||||||
@@ -105,8 +105,7 @@ export class MQTTConnection {
|
|||||||
this.handleError(e)
|
this.handleError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||||
type: "HOPP_REQUEST_RUN",
|
|
||||||
platform: "mqtt",
|
platform: "mqtt",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,8 +113,7 @@ export class SIOConnection {
|
|||||||
this.handleError(error, "CONNECTION")
|
this.handleError(error, "CONNECTION")
|
||||||
}
|
}
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||||
type: "HOPP_REQUEST_RUN",
|
|
||||||
platform: "socketio",
|
platform: "socketio",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,8 +63,7 @@ export class SSEConnection {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||||
type: "HOPP_REQUEST_RUN",
|
|
||||||
platform: "sse",
|
platform: "sse",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,8 +71,7 @@ export class WSConnection {
|
|||||||
this.handleError(error as SyntaxError)
|
this.handleError(error as SyntaxError)
|
||||||
}
|
}
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||||
type: "HOPP_REQUEST_RUN",
|
|
||||||
platform: "wss",
|
platform: "wss",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { refWithControl } from "@vueuse/core"
|
|||||||
import { HoppRESTResponse } from "../types/HoppRESTResponse"
|
import { HoppRESTResponse } from "../types/HoppRESTResponse"
|
||||||
import { getDefaultRESTRequest } from "./default"
|
import { getDefaultRESTRequest } from "./default"
|
||||||
import { HoppTestResult } from "../types/HoppTestResult"
|
import { HoppTestResult } from "../types/HoppTestResult"
|
||||||
import { platform } from "~/platform"
|
|
||||||
|
|
||||||
export type HoppRESTTab = {
|
export type HoppRESTTab = {
|
||||||
id: string
|
id: string
|
||||||
@@ -148,10 +147,6 @@ export function createNewTab(document: HoppRESTDocument, switchToIt = true) {
|
|||||||
currentTabID.value = id
|
currentTabID.value = id
|
||||||
}
|
}
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_REST_NEW_TAB_OPENED",
|
|
||||||
})
|
|
||||||
|
|
||||||
return tab
|
return tab
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ import IconHome from "~icons/lucide/home"
|
|||||||
import IconRefreshCW from "~icons/lucide/refresh-cw"
|
import IconRefreshCW from "~icons/lucide/refresh-cw"
|
||||||
import { createNewTab } from "~/helpers/rest/tab"
|
import { createNewTab } from "~/helpers/rest/tab"
|
||||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
import { platform } from "~/platform"
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -121,10 +120,6 @@ const addRequestToTab = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_SHORTCODE_RESOLVED",
|
|
||||||
})
|
|
||||||
|
|
||||||
const request: unknown = JSON.parse(data.right.shortcode?.request as string)
|
const request: unknown = JSON.parse(data.right.shortcode?.request as string)
|
||||||
|
|
||||||
createNewTab({
|
createNewTab({
|
||||||
|
|||||||
@@ -5,50 +5,8 @@ export type HoppRequestEvent =
|
|||||||
}
|
}
|
||||||
| { platform: "wss" | "sse" | "socketio" | "mqtt" }
|
| { platform: "wss" | "sse" | "socketio" | "mqtt" }
|
||||||
|
|
||||||
export type AnalyticsEvent =
|
|
||||||
| ({ type: "HOPP_REQUEST_RUN" } & HoppRequestEvent)
|
|
||||||
| {
|
|
||||||
type: "HOPP_CREATE_ENVIRONMENT"
|
|
||||||
workspaceType: "personal" | "team"
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "HOPP_CREATE_COLLECTION"
|
|
||||||
platform: "rest" | "gql"
|
|
||||||
isRootCollection: boolean
|
|
||||||
workspaceType: "personal" | "team"
|
|
||||||
}
|
|
||||||
| { type: "HOPP_CREATE_TEAM" }
|
|
||||||
| {
|
|
||||||
type: "HOPP_SAVE_REQUEST"
|
|
||||||
createdNow: boolean
|
|
||||||
workspaceType: "personal" | "team"
|
|
||||||
platform: "rest" | "gql"
|
|
||||||
}
|
|
||||||
| { type: "HOPP_SHORTCODE_CREATED" }
|
|
||||||
| { type: "HOPP_SHORTCODE_RESOLVED" }
|
|
||||||
| { type: "HOPP_REST_NEW_TAB_OPENED" }
|
|
||||||
| {
|
|
||||||
type: "HOPP_IMPORT_COLLECTION"
|
|
||||||
importer: string
|
|
||||||
workspaceType: "personal" | "team"
|
|
||||||
platform: "rest" | "gql"
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "HOPP_IMPORT_ENVIRONMENT"
|
|
||||||
workspaceType: "personal" | "team"
|
|
||||||
platform: "rest" | "gql"
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: "HOPP_EXPORT_COLLECTION"
|
|
||||||
exporter: string
|
|
||||||
platform: "rest" | "gql"
|
|
||||||
}
|
|
||||||
| { type: "HOPP_EXPORT_ENVIRONMENT"; platform: "rest" | "gql" }
|
|
||||||
| { type: "HOPP_REST_CODEGEN_OPENED" }
|
|
||||||
| { type: "HOPP_REST_IMPORT_CURL" }
|
|
||||||
|
|
||||||
export type AnalyticsPlatformDef = {
|
export type AnalyticsPlatformDef = {
|
||||||
initAnalytics: () => void
|
initAnalytics: () => void
|
||||||
logEvent: (ev: AnalyticsEvent) => void
|
logHoppRequestRunToAnalytics: (ev: HoppRequestEvent) => void
|
||||||
logPageView: (pagePath: string) => void
|
logPageView: (pagePath: string) => void
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/selfhost-web",
|
"name": "@hoppscotch/selfhost-web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2023.4.7",
|
"version": "2023.4.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:vite": "vite",
|
"dev:vite": "vite",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "hoppscotch-sh-admin",
|
"name": "hoppscotch-sh-admin",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2023.4.7",
|
"version": "2023.4.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||||
|
|||||||
Reference in New Issue
Block a user