Compare commits
45 Commits
fix/db-url
...
test/backe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c188f865a2 | ||
|
|
a2a675dd86 | ||
|
|
b867ba9139 | ||
|
|
d2ca631492 | ||
|
|
39a4fd8ab2 | ||
|
|
6928eb7992 | ||
|
|
525ba77739 | ||
|
|
6bc748a267 | ||
|
|
b29c04c28d | ||
|
|
b2af353941 | ||
|
|
2ec29c47ad | ||
|
|
399a238bf4 | ||
|
|
b20ab72298 | ||
|
|
f723e6496a | ||
|
|
8c0aff8863 | ||
|
|
64c5077506 | ||
|
|
2afc87847d | ||
|
|
878ec833ce | ||
|
|
039de8015f | ||
|
|
f67b366b90 | ||
|
|
77e8a36ab0 | ||
|
|
d7cc9f5dbc | ||
|
|
4ba135f3b9 | ||
|
|
24894e05dc | ||
|
|
e2b668bee2 | ||
|
|
f112c46bb4 | ||
|
|
84b0c30d64 | ||
|
|
e3dd9e99a1 | ||
|
|
e3091cb6db | ||
|
|
270f796683 | ||
|
|
24c6bce02d | ||
|
|
2db567589f | ||
|
|
1fe83ebdc8 | ||
|
|
8320d4f222 | ||
|
|
e76c1bc64c | ||
|
|
1f3f8464ea | ||
|
|
76d52a3b05 | ||
|
|
b83cc38a1c | ||
|
|
db42073d42 | ||
|
|
6c928e72d4 | ||
|
|
ddd0a67da3 | ||
|
|
295304feeb | ||
|
|
e75391cdf1 | ||
|
|
a213c0c26c | ||
|
|
15424903ed |
@@ -31,6 +31,7 @@ MICROSOFT_CLIENT_ID="************************************************"
|
||||
MICROSOFT_CLIENT_SECRET="************************************************"
|
||||
MICROSOFT_CALLBACK_URL="http://localhost:3170/v1/auth/microsoft/callback"
|
||||
MICROSOFT_SCOPE="user.read"
|
||||
MICROSOFT_TENANT="common"
|
||||
|
||||
# Mailer config
|
||||
MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com"
|
||||
|
||||
42
.github/workflows/ui.yml
vendored
Normal file
42
.github/workflows/ui.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Deploy to Netlify (ui)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
# run this workflow only if an update is made to the ui package
|
||||
paths:
|
||||
- "packages/hoppscotch-ui/**"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup environment
|
||||
run: mv .env.example .env
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 8
|
||||
run_install: true
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: pnpm
|
||||
|
||||
- name: Build site
|
||||
run: pnpm run generate-ui
|
||||
|
||||
# Deploy the ui site with netlify-cli
|
||||
- name: Deploy to Netlify (ui)
|
||||
run: npx netlify-cli deploy --dir=packages/hoppscotch-ui/.histoire/dist --prod
|
||||
env:
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_UI_SITE_ID }}
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hoppscotch-backend",
|
||||
"version": "2023.4.4",
|
||||
"version": "2023.4.7",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -10,11 +10,23 @@ import { TeamInvitationService } from '../team-invitation/team-invitation.servic
|
||||
import { TeamCollectionService } from '../team-collection/team-collection.service';
|
||||
import { MailerService } from '../mailer/mailer.service';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { User as DbUser } from '@prisma/client';
|
||||
import {
|
||||
DUPLICATE_EMAIL,
|
||||
INVALID_EMAIL,
|
||||
ONLY_ONE_ADMIN_ACCOUNT,
|
||||
TEAM_INVITE_ALREADY_MEMBER,
|
||||
TEAM_MEMBER_NOT_FOUND,
|
||||
USER_ALREADY_INVITED,
|
||||
USER_IS_ADMIN,
|
||||
USER_NOT_FOUND,
|
||||
} 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 mockPubSub = mockDeep<PubSubService>();
|
||||
@@ -52,7 +64,582 @@ const invitedUsers: InvitedUsers[] = [
|
||||
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('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', () => {
|
||||
test('should resolve right and return an array of invited users', async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
|
||||
@@ -181,7 +181,7 @@ export class AdminService {
|
||||
* @returns an array team invitations
|
||||
*/
|
||||
async pendingInvitationCountInTeam(teamID: string) {
|
||||
const invitations = await this.teamInvitationService.getAllTeamInvitations(
|
||||
const invitations = await this.teamInvitationService.getTeamInvitations(
|
||||
teamID,
|
||||
);
|
||||
|
||||
@@ -236,11 +236,12 @@ export class AdminService {
|
||||
const user = await this.userService.findUserByEmail(userEmail);
|
||||
if (O.isNone(user)) return E.left(USER_NOT_FOUND);
|
||||
|
||||
const isUserAlreadyMember = await this.teamService.getTeamMemberTE(
|
||||
const teamMember = await this.teamService.getTeamMemberTE(
|
||||
teamID,
|
||||
user.value.uid,
|
||||
)();
|
||||
if (E.left(isUserAlreadyMember)) {
|
||||
|
||||
if (E.isLeft(teamMember)) {
|
||||
const addedUser = await this.teamService.addMemberToTeamWithEmail(
|
||||
teamID,
|
||||
userEmail,
|
||||
@@ -248,6 +249,18 @@ export class AdminService {
|
||||
);
|
||||
if (E.isLeft(addedUser)) return E.left(addedUser.left);
|
||||
|
||||
const userInvitation =
|
||||
await this.teamInvitationService.getTeamInviteByEmailAndTeamID(
|
||||
userEmail,
|
||||
teamID,
|
||||
);
|
||||
|
||||
if (E.isRight(userInvitation)) {
|
||||
await this.teamInvitationService.revokeInvitation(
|
||||
userInvitation.right.id,
|
||||
);
|
||||
}
|
||||
|
||||
return E.right(addedUser.right);
|
||||
}
|
||||
|
||||
|
||||
@@ -228,7 +228,7 @@ export class AuthService {
|
||||
url = process.env.VITE_BASE_URL;
|
||||
}
|
||||
|
||||
await this.mailerService.sendAuthEmail(email, {
|
||||
await this.mailerService.sendEmail(email, {
|
||||
template: 'code-your-own',
|
||||
variables: {
|
||||
inviteeEmail: email,
|
||||
|
||||
@@ -17,7 +17,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
||||
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
|
||||
callbackURL: process.env.MICROSOFT_CALLBACK_URL,
|
||||
scope: [process.env.MICROSOFT_SCOPE],
|
||||
passReqToCallback: true,
|
||||
tenant: process.env.MICROSOFT_TENANT,
|
||||
store: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const AUTH_FAIL = 'auth/fail';
|
||||
export const JSON_INVALID = 'json_invalid';
|
||||
|
||||
/**
|
||||
* Tried to delete an user data document from fb firestore but failed.
|
||||
* Tried to delete a user data document from fb firestore but failed.
|
||||
* (FirebaseService)
|
||||
*/
|
||||
export const USER_FB_DOCUMENT_DELETION_FAILED =
|
||||
@@ -231,7 +231,7 @@ export const TEAM_COLL_INVALID_JSON = 'team_coll/invalid_json';
|
||||
export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
|
||||
|
||||
/**
|
||||
* Tried to perform action on a request that doesn't accept their member role level
|
||||
* Tried to perform an action on a request that doesn't accept their member role level
|
||||
* (GqlRequestTeamMemberGuard)
|
||||
*/
|
||||
export const TEAM_REQ_NOT_REQUIRED_ROLE = 'team_req/not_required_role';
|
||||
@@ -262,7 +262,7 @@ export const TEAM_REQ_REORDERING_FAILED = 'team_req/reordering_failed' as const;
|
||||
export const SENDER_EMAIL_INVALID = 'mailer/sender_email_invalid' as const;
|
||||
|
||||
/**
|
||||
* Tried to perform action on a request when the user is not even member of the team
|
||||
* Tried to perform an action on a request when the user is not even a member of the team
|
||||
* (GqlRequestTeamMemberGuard, GqlCollectionTeamMemberGuard)
|
||||
*/
|
||||
export const TEAM_REQ_NOT_MEMBER = 'team_req/not_member';
|
||||
@@ -307,11 +307,18 @@ export const SHORTCODE_INVALID_JSON = 'shortcode/invalid_json' as const;
|
||||
export const SHORTCODE_ALREADY_EXISTS = 'shortcode/already_exists' as const;
|
||||
|
||||
/**
|
||||
* Invalid or non-existent TEAM ENVIRONMMENT ID
|
||||
* Invalid or non-existent TEAM ENVIRONMENT ID
|
||||
* (TeamEnvironmentsService)
|
||||
*/
|
||||
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
|
||||
* (GqlTeamEnvTeamGuard)
|
||||
@@ -340,7 +347,7 @@ export const USER_SETTINGS_NULL_SETTINGS =
|
||||
'user_settings/null_settings' as const;
|
||||
|
||||
/*
|
||||
* Global environment doesnt exists for the user
|
||||
* Global environment doesn't exist for the user
|
||||
* (UserEnvironmentsService)
|
||||
*/
|
||||
export const USER_ENVIRONMENT_GLOBAL_ENV_DOES_NOT_EXISTS =
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
UserMagicLinkMailDescription,
|
||||
} from './MailDescriptions';
|
||||
import { throwErr } from 'src/utils';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
import { EMAIL_FAILED } from 'src/errors';
|
||||
import { MailerService as NestMailerService } from '@nestjs-modules/mailer';
|
||||
|
||||
@@ -35,33 +34,14 @@ export class MailerService {
|
||||
|
||||
/**
|
||||
* Sends an email to the given email address given a mail description
|
||||
* @param to The email address to be sent to (NOTE: this is not validated)
|
||||
* @param to Receiver's email id
|
||||
* @param mailDesc Definition of what email to be sent
|
||||
* @returns Response if email was send successfully or not
|
||||
*/
|
||||
sendMail(
|
||||
async sendEmail(
|
||||
to: string,
|
||||
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 {
|
||||
await this.nestMailerService.sendMail({
|
||||
to,
|
||||
|
||||
@@ -9,7 +9,6 @@ import { emitGQLSchemaFile } from './gql-schema';
|
||||
async function bootstrap() {
|
||||
console.log(`Running in production: ${process.env.PRODUCTION}`);
|
||||
console.log(`Port: ${process.env.PORT}`);
|
||||
console.log(`Database: ${process.env.DATABASE_URL}`);
|
||||
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Team, TeamCollection as DBTeamCollection } from '@prisma/client';
|
||||
import { mock, mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import {
|
||||
Team,
|
||||
TeamCollection as DBTeamCollection,
|
||||
TeamRequest as DBTeamRequest,
|
||||
} from '@prisma/client';
|
||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import {
|
||||
TEAM_COLL_DEST_SAME,
|
||||
TEAM_COLL_INVALID_JSON,
|
||||
@@ -17,9 +21,8 @@ import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
import { TeamCollectionService } from './team-collection.service';
|
||||
import { TeamCollection } from './team-collection.model';
|
||||
import { TeamCollectionModule } from './team-collection.module';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { CollectionFolder } from 'src/types/CollectionFolder';
|
||||
|
||||
const mockPrisma = mockDeep<PrismaService>();
|
||||
const mockPubSub = mockDeep<PubSubService>();
|
||||
@@ -276,11 +279,188 @@ 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(() => {
|
||||
mockReset(mockPrisma);
|
||||
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', () => {
|
||||
test('should return the team of a collection successfully with valid collectionID', async () => {
|
||||
mockPrisma.teamCollection.findUnique.mockResolvedValueOnce({
|
||||
@@ -1460,5 +1640,3 @@ describe('totalCollectionsInTeam', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//ToDo: write test cases for exportCollectionsToJSON
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
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 {
|
||||
BUG_AUTH_NO_USER_CTX,
|
||||
@@ -19,6 +9,10 @@ import {
|
||||
TEAM_ENVIRONMENT_NOT_FOUND,
|
||||
} from 'src/errors';
|
||||
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
|
||||
@@ -33,50 +27,31 @@ export class GqlTeamEnvTeamGuard implements CanActivate {
|
||||
private readonly teamService: TeamService,
|
||||
) {}
|
||||
|
||||
canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
return pipe(
|
||||
TE.Do,
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const requireRoles = this.reflector.get<TeamMemberRole[]>(
|
||||
'requiresTeamRole',
|
||||
context.getHandler(),
|
||||
);
|
||||
if (!requireRoles) throw new Error(BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES);
|
||||
|
||||
TE.bindW('requiredRoles', () =>
|
||||
pipe(
|
||||
getAnnotatedRequiredRoles(this.reflector, context),
|
||||
TE.fromOption(() => BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES),
|
||||
),
|
||||
),
|
||||
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||
|
||||
TE.bindW('user', () =>
|
||||
pipe(
|
||||
getUserFromGQLContext(context),
|
||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
||||
),
|
||||
),
|
||||
const { user } = gqlExecCtx.getContext().req;
|
||||
if (user == undefined) throw new Error(BUG_AUTH_NO_USER_CTX);
|
||||
|
||||
TE.bindW('envID', () =>
|
||||
pipe(
|
||||
getGqlArg('id', context),
|
||||
O.fromPredicate(S.isString),
|
||||
TE.fromOption(() => BUG_TEAM_ENV_GUARD_NO_ENV_ID),
|
||||
),
|
||||
),
|
||||
const { id } = gqlExecCtx.getArgs<{ id: string }>();
|
||||
if (!id) throwErr(BUG_TEAM_ENV_GUARD_NO_ENV_ID);
|
||||
|
||||
TE.bindW('membership', ({ envID, user }) =>
|
||||
pipe(
|
||||
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 teamEnvironment =
|
||||
await this.teamEnvironmentService.getTeamEnvironment(id);
|
||||
if (E.isLeft(teamEnvironment)) throwErr(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
|
||||
TE.map(({ membership, requiredRoles }) =>
|
||||
requiredRoles.includes(membership.role),
|
||||
),
|
||||
const member = await this.teamService.getTeamMember(
|
||||
teamEnvironment.right.teamID,
|
||||
user.uid,
|
||||
);
|
||||
if (!member) throwErr(TEAM_ENVIRONMENT_NOT_TEAM_MEMBER);
|
||||
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
return requireRoles.includes(member.role);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
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,6 +13,11 @@ import { throwErr } from 'src/utils';
|
||||
import { GqlTeamEnvTeamGuard } from './gql-team-env-team.guard';
|
||||
import { TeamEnvironment } from './team-environments.model';
|
||||
import { TeamEnvironmentsService } from './team-environments.service';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import {
|
||||
CreateTeamEnvironmentArgs,
|
||||
UpdateTeamEnvironmentArgs,
|
||||
} from './input-type.args';
|
||||
|
||||
@UseGuards(GqlThrottlerGuard)
|
||||
@Resolver(() => 'TeamEnvironment')
|
||||
@@ -29,29 +34,18 @@ export class TeamEnvironmentsResolver {
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||
createTeamEnvironment(
|
||||
@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,
|
||||
async createTeamEnvironment(
|
||||
@Args() args: CreateTeamEnvironmentArgs,
|
||||
): Promise<TeamEnvironment> {
|
||||
return this.teamEnvironmentsService.createTeamEnvironment(
|
||||
name,
|
||||
teamID,
|
||||
variables,
|
||||
)();
|
||||
const teamEnvironment =
|
||||
await this.teamEnvironmentsService.createTeamEnvironment(
|
||||
args.name,
|
||||
args.teamID,
|
||||
args.variables,
|
||||
);
|
||||
|
||||
if (E.isLeft(teamEnvironment)) throwErr(teamEnvironment.left);
|
||||
return teamEnvironment.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
@@ -59,7 +53,7 @@ export class TeamEnvironmentsResolver {
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||
deleteTeamEnvironment(
|
||||
async deleteTeamEnvironment(
|
||||
@Args({
|
||||
name: 'id',
|
||||
description: 'ID of the Team Environment',
|
||||
@@ -67,10 +61,12 @@ export class TeamEnvironmentsResolver {
|
||||
})
|
||||
id: string,
|
||||
): Promise<boolean> {
|
||||
return pipe(
|
||||
this.teamEnvironmentsService.deleteTeamEnvironment(id),
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
const isDeleted = await this.teamEnvironmentsService.deleteTeamEnvironment(
|
||||
id,
|
||||
);
|
||||
|
||||
if (E.isLeft(isDeleted)) throwErr(isDeleted.left);
|
||||
return isDeleted.right;
|
||||
}
|
||||
|
||||
@Mutation(() => TeamEnvironment, {
|
||||
@@ -79,28 +75,19 @@ export class TeamEnvironmentsResolver {
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||
updateTeamEnvironment(
|
||||
@Args({
|
||||
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,
|
||||
async updateTeamEnvironment(
|
||||
@Args()
|
||||
args: UpdateTeamEnvironmentArgs,
|
||||
): Promise<TeamEnvironment> {
|
||||
return pipe(
|
||||
this.teamEnvironmentsService.updateTeamEnvironment(id, name, variables),
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
const updatedTeamEnvironment =
|
||||
await this.teamEnvironmentsService.updateTeamEnvironment(
|
||||
args.id,
|
||||
args.name,
|
||||
args.variables,
|
||||
);
|
||||
|
||||
if (E.isLeft(updatedTeamEnvironment)) throwErr(updatedTeamEnvironment.left);
|
||||
return updatedTeamEnvironment.right;
|
||||
}
|
||||
|
||||
@Mutation(() => TeamEnvironment, {
|
||||
@@ -108,7 +95,7 @@ export class TeamEnvironmentsResolver {
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||
deleteAllVariablesFromTeamEnvironment(
|
||||
async deleteAllVariablesFromTeamEnvironment(
|
||||
@Args({
|
||||
name: 'id',
|
||||
description: 'ID of the Team Environment',
|
||||
@@ -116,10 +103,13 @@ export class TeamEnvironmentsResolver {
|
||||
})
|
||||
id: string,
|
||||
): Promise<TeamEnvironment> {
|
||||
return pipe(
|
||||
this.teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(id),
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
const teamEnvironment =
|
||||
await this.teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||
id,
|
||||
);
|
||||
|
||||
if (E.isLeft(teamEnvironment)) throwErr(teamEnvironment.left);
|
||||
return teamEnvironment.right;
|
||||
}
|
||||
|
||||
@Mutation(() => TeamEnvironment, {
|
||||
@@ -127,7 +117,7 @@ export class TeamEnvironmentsResolver {
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||
createDuplicateEnvironment(
|
||||
async createDuplicateEnvironment(
|
||||
@Args({
|
||||
name: 'id',
|
||||
description: 'ID of the Team Environment',
|
||||
@@ -135,10 +125,12 @@ export class TeamEnvironmentsResolver {
|
||||
})
|
||||
id: string,
|
||||
): Promise<TeamEnvironment> {
|
||||
return pipe(
|
||||
this.teamEnvironmentsService.createDuplicateEnvironment(id),
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
const res = await this.teamEnvironmentsService.createDuplicateEnvironment(
|
||||
id,
|
||||
);
|
||||
|
||||
if (E.isLeft(res)) throwErr(res.left);
|
||||
return res.right;
|
||||
}
|
||||
|
||||
/* Subscriptions */
|
||||
|
||||
@@ -2,7 +2,11 @@ import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { TeamEnvironment } from './team-environments.model';
|
||||
import { TeamEnvironmentsService } from './team-environments.service';
|
||||
import { TEAM_ENVIRONMENT_NOT_FOUND } from 'src/errors';
|
||||
import {
|
||||
JSON_INVALID,
|
||||
TEAM_ENVIRONMENT_NOT_FOUND,
|
||||
TEAM_ENVIRONMENT_SHORT_NAME,
|
||||
} from 'src/errors';
|
||||
|
||||
const mockPrisma = mockDeep<PrismaService>();
|
||||
|
||||
@@ -31,125 +35,81 @@ beforeEach(() => {
|
||||
|
||||
describe('TeamEnvironmentsService', () => {
|
||||
describe('getTeamEnvironment', () => {
|
||||
test('queries the db with the id', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
|
||||
|
||||
await teamEnvironmentsService.getTeamEnvironment('123')();
|
||||
|
||||
expect(mockPrisma.teamEnvironment.findFirst).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: {
|
||||
id: '123',
|
||||
},
|
||||
}),
|
||||
test('should successfully return a TeamEnvironment with valid ID', async () => {
|
||||
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
|
||||
teamEnvironment,
|
||||
);
|
||||
});
|
||||
|
||||
test('requests prisma to reject the query promise if not found', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
|
||||
|
||||
await teamEnvironmentsService.getTeamEnvironment('123')();
|
||||
|
||||
expect(mockPrisma.teamEnvironment.findFirst).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rejectOnNotFound: true,
|
||||
}),
|
||||
const result = await teamEnvironmentsService.getTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
);
|
||||
expect(result).toEqualRight(teamEnvironment);
|
||||
});
|
||||
|
||||
test('should return a Some of the correct environment if exists', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
|
||||
test('should throw TEAM_ENVIRONMENT_NOT_FOUND with invalid ID', async () => {
|
||||
mockPrisma.teamEnvironment.findFirstOrThrow.mockRejectedValueOnce(
|
||||
'RejectOnNotFound',
|
||||
);
|
||||
|
||||
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();
|
||||
const result = await teamEnvironmentsService.getTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
);
|
||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTeamEnvironment', () => {
|
||||
test('should create and return a new team environment given a valid name,variable and team ID', async () => {
|
||||
test('should successfully create and return a new team environment given valid inputs', async () => {
|
||||
mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment);
|
||||
|
||||
const result = await teamEnvironmentsService.createTeamEnvironment(
|
||||
teamEnvironment.name,
|
||||
teamEnvironment.teamID,
|
||||
JSON.stringify(teamEnvironment.variables),
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqual(<TeamEnvironment>{
|
||||
id: teamEnvironment.id,
|
||||
name: teamEnvironment.name,
|
||||
teamID: teamEnvironment.teamID,
|
||||
expect(result).toEqualRight({
|
||||
...teamEnvironment,
|
||||
variables: JSON.stringify(teamEnvironment.variables),
|
||||
});
|
||||
});
|
||||
|
||||
test('should reject if given team ID is invalid', async () => {
|
||||
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
|
||||
test('should throw TEAM_ENVIRONMENT_SHORT_NAME if input TeamEnvironment name is invalid', async () => {
|
||||
const result = await teamEnvironmentsService.createTeamEnvironment(
|
||||
'12',
|
||||
teamEnvironment.teamID,
|
||||
JSON.stringify(teamEnvironment.variables),
|
||||
);
|
||||
|
||||
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();
|
||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_SHORT_NAME);
|
||||
});
|
||||
|
||||
test('should send pubsub message to "team_environment/<teamID>/created" if team environment is created successfully', async () => {
|
||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce(teamEnvironment);
|
||||
mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment);
|
||||
|
||||
const result = await teamEnvironmentsService.createTeamEnvironment(
|
||||
teamEnvironment.name,
|
||||
teamEnvironment.teamID,
|
||||
JSON.stringify(teamEnvironment.variables),
|
||||
)();
|
||||
);
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_environment/${teamEnvironment.teamID}/created`,
|
||||
result,
|
||||
{
|
||||
...teamEnvironment,
|
||||
variables: JSON.stringify(teamEnvironment.variables),
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteTeamEnvironment', () => {
|
||||
test('should resolve to true given a valid team environment ID', async () => {
|
||||
test('should successfully delete a TeamEnvironment with a valid ID', async () => {
|
||||
mockPrisma.teamEnvironment.delete.mockResolvedValueOnce(teamEnvironment);
|
||||
|
||||
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualRight(true);
|
||||
});
|
||||
@@ -159,7 +119,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
|
||||
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
||||
'invalidid',
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
});
|
||||
@@ -169,7 +129,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
|
||||
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
);
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_environment/${teamEnvironment.teamID}/deleted`,
|
||||
@@ -182,7 +142,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
|
||||
describe('updateVariablesInTeamEnvironment', () => {
|
||||
test('should add new variable to a team environment', async () => {
|
||||
test('should successfully add new variable to a team environment', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||
...teamEnvironment,
|
||||
variables: [{ key: 'value' }],
|
||||
@@ -192,7 +152,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
teamEnvironment.id,
|
||||
teamEnvironment.name,
|
||||
JSON.stringify([{ key: 'value' }]),
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
@@ -200,7 +160,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should add new variable to already existing list of variables in a team environment', async () => {
|
||||
test('should successfully add new variable to already existing list of variables in a team environment', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||
...teamEnvironment,
|
||||
variables: [{ key: 'value' }, { key_2: 'value_2' }],
|
||||
@@ -210,7 +170,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
teamEnvironment.id,
|
||||
teamEnvironment.name,
|
||||
JSON.stringify([{ key: 'value' }, { key_2: 'value_2' }]),
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
@@ -218,7 +178,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should edit existing variables in a team environment', async () => {
|
||||
test('should successfully edit existing variables in a team environment', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||
...teamEnvironment,
|
||||
variables: [{ key: '1234' }],
|
||||
@@ -228,7 +188,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
teamEnvironment.id,
|
||||
teamEnvironment.name,
|
||||
JSON.stringify([{ key: '1234' }]),
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
@@ -236,22 +196,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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 () => {
|
||||
test('should successfully edit name of an existing team environment', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||
...teamEnvironment,
|
||||
variables: [{ key: '123' }],
|
||||
@@ -261,7 +206,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
teamEnvironment.id,
|
||||
teamEnvironment.name,
|
||||
JSON.stringify([{ key: '123' }]),
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
@@ -269,14 +214,24 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||
test('should throw TEAM_ENVIRONMENT_SHORT_NAME if input TeamEnvironment name 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');
|
||||
|
||||
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
||||
'invalidid',
|
||||
teamEnvironment.name,
|
||||
JSON.stringify(teamEnvironment.variables),
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
});
|
||||
@@ -288,7 +243,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
teamEnvironment.id,
|
||||
teamEnvironment.name,
|
||||
JSON.stringify([{ key: 'value' }]),
|
||||
)();
|
||||
);
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_environment/${teamEnvironment.teamID}/updated`,
|
||||
@@ -301,13 +256,13 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
|
||||
describe('deleteAllVariablesFromTeamEnvironment', () => {
|
||||
test('should delete all variables in a team environment', async () => {
|
||||
test('should successfully delete all variables in a team environment', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
|
||||
|
||||
const result =
|
||||
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
@@ -315,13 +270,13 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
|
||||
|
||||
const result =
|
||||
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||
'invalidid',
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
});
|
||||
@@ -332,7 +287,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
const result =
|
||||
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
);
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_environment/${teamEnvironment.teamID}/updated`,
|
||||
@@ -345,33 +300,33 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
|
||||
describe('createDuplicateEnvironment', () => {
|
||||
test('should duplicate an existing team environment', async () => {
|
||||
test('should successfully duplicate an existing team environment', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
|
||||
teamEnvironment,
|
||||
);
|
||||
|
||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
||||
...teamEnvironment,
|
||||
id: 'newid',
|
||||
...teamEnvironment,
|
||||
});
|
||||
|
||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
id: 'newid',
|
||||
...teamEnvironment,
|
||||
variables: JSON.stringify(teamEnvironment.variables),
|
||||
});
|
||||
});
|
||||
|
||||
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
|
||||
|
||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
});
|
||||
@@ -382,19 +337,19 @@ describe('TeamEnvironmentsService', () => {
|
||||
);
|
||||
|
||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
||||
...teamEnvironment,
|
||||
id: 'newid',
|
||||
...teamEnvironment,
|
||||
});
|
||||
|
||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
);
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_environment/${teamEnvironment.teamID}/created`,
|
||||
{
|
||||
...teamEnvironment,
|
||||
id: 'newid',
|
||||
...teamEnvironment,
|
||||
variables: JSON.stringify([{}]),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
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 { TeamEnvironment as DBTeamEnvironment, Prisma } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { TeamEnvironment } from './team-environments.model';
|
||||
import { TEAM_ENVIRONMENT_NOT_FOUND } from 'src/errors';
|
||||
|
||||
import {
|
||||
TEAM_ENVIRONMENT_NOT_FOUND,
|
||||
TEAM_ENVIRONMENT_SHORT_NAME,
|
||||
} from 'src/errors';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { isValidLength } from 'src/utils';
|
||||
@Injectable()
|
||||
export class TeamEnvironmentsService {
|
||||
constructor(
|
||||
@@ -17,219 +16,218 @@ export class TeamEnvironmentsService {
|
||||
private readonly pubsub: PubSubService,
|
||||
) {}
|
||||
|
||||
getTeamEnvironment(id: string) {
|
||||
return TO.tryCatch(() =>
|
||||
this.prisma.teamEnvironment.findFirst({
|
||||
where: { id },
|
||||
TITLE_LENGTH = 3;
|
||||
|
||||
/**
|
||||
* TeamEnvironments are saved in the DB in the following way
|
||||
* [{ 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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
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) {
|
||||
return pipe(
|
||||
() =>
|
||||
this.prisma.teamEnvironment.create({
|
||||
data: {
|
||||
name: name,
|
||||
teamID: teamID,
|
||||
variables: JSON.parse(variables),
|
||||
},
|
||||
}),
|
||||
T.chainFirst(
|
||||
(environment) => () =>
|
||||
this.pubsub.publish(
|
||||
`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),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Fetch all TeamEnvironments of a team.
|
||||
*
|
||||
* @param teamID teamID of new TeamEnvironment
|
||||
* @returns List of TeamEnvironments
|
||||
*/
|
||||
async fetchAllTeamEnvironments(teamID: string) {
|
||||
const result = await this.prisma.teamEnvironment.findMany({
|
||||
where: {
|
||||
teamID: teamID,
|
||||
},
|
||||
});
|
||||
const teamEnvironments = result.map((item) => {
|
||||
return this.cast(item);
|
||||
});
|
||||
|
||||
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),
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
return teamEnvironments;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,6 @@ export class TeamEnvsTeamResolver {
|
||||
description: 'Returns all Team Environments for the given Team',
|
||||
})
|
||||
teamEnvironments(@Parent() team: Team): Promise<TeamEnvironment[]> {
|
||||
return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id)();
|
||||
return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
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,15 +12,10 @@ import { TeamInvitation } from './team-invitation.model';
|
||||
import { TeamInvitationService } from './team-invitation.service';
|
||||
import { pipe } from 'fp-ts/function';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import { Team, TeamMember, TeamMemberRole } from 'src/team/team.model';
|
||||
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 { TEAM_INVITE_NO_INVITE_FOUND, USER_NOT_FOUND } from 'src/errors';
|
||||
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||
import { User } from 'src/user/user.model';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
@@ -36,6 +31,8 @@ import { UserService } from 'src/user/user.service';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||
import { SkipThrottle } from '@nestjs/throttler';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
import { CreateTeamInvitationArgs } from './input-type.args';
|
||||
|
||||
@UseGuards(GqlThrottlerGuard)
|
||||
@Resolver(() => TeamInvitation)
|
||||
@@ -79,8 +76,8 @@ export class TeamInvitationResolver {
|
||||
'Gets the Team Invitation with the given ID, or null if not exists',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, TeamInviteViewerGuard)
|
||||
teamInvitation(
|
||||
@GqlUser() user: User,
|
||||
async teamInvitation(
|
||||
@GqlUser() user: AuthUser,
|
||||
@Args({
|
||||
name: 'inviteID',
|
||||
description: 'ID of the Team Invitation to lookup',
|
||||
@@ -88,17 +85,11 @@ export class TeamInvitationResolver {
|
||||
})
|
||||
inviteID: string,
|
||||
): Promise<TeamInvitation> {
|
||||
return pipe(
|
||||
this.teamInvitationService.getInvitation(inviteID),
|
||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
||||
TE.chainW(
|
||||
TE.fromPredicate(
|
||||
(a) => a.inviteeEmail.toLowerCase() === user.email?.toLowerCase(),
|
||||
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
||||
),
|
||||
),
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
const teamInvitation = await this.teamInvitationService.getInvitation(
|
||||
inviteID,
|
||||
);
|
||||
if (O.isNone(teamInvitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||
return teamInvitation.value;
|
||||
}
|
||||
|
||||
@Mutation(() => TeamInvitation, {
|
||||
@@ -106,56 +97,19 @@ export class TeamInvitationResolver {
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER)
|
||||
createTeamInvitation(
|
||||
@GqlUser()
|
||||
user: User,
|
||||
|
||||
@Args({
|
||||
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,
|
||||
async createTeamInvitation(
|
||||
@GqlUser() user: AuthUser,
|
||||
@Args() args: CreateTeamInvitationArgs,
|
||||
): Promise<TeamInvitation> {
|
||||
return pipe(
|
||||
TE.Do,
|
||||
const teamInvitation = await this.teamInvitationService.createInvitation(
|
||||
user,
|
||||
args.teamID,
|
||||
args.inviteeEmail,
|
||||
args.inviteeRole,
|
||||
);
|
||||
|
||||
// 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),
|
||||
)();
|
||||
if (E.isLeft(teamInvitation)) throwErr(teamInvitation.left);
|
||||
return teamInvitation.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
@@ -163,7 +117,7 @@ export class TeamInvitationResolver {
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, TeamInviteTeamOwnerGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER)
|
||||
revokeTeamInvitation(
|
||||
async revokeTeamInvitation(
|
||||
@Args({
|
||||
name: 'inviteID',
|
||||
type: () => ID,
|
||||
@@ -171,19 +125,19 @@ export class TeamInvitationResolver {
|
||||
})
|
||||
inviteID: string,
|
||||
): Promise<true> {
|
||||
return pipe(
|
||||
this.teamInvitationService.revokeInvitation(inviteID),
|
||||
TE.map(() => true as const),
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
const isRevoked = await this.teamInvitationService.revokeInvitation(
|
||||
inviteID,
|
||||
);
|
||||
if (E.isLeft(isRevoked)) throwErr(isRevoked.left);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Mutation(() => TeamMember, {
|
||||
description: 'Accept an Invitation',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, TeamInviteeGuard)
|
||||
acceptTeamInvitation(
|
||||
@GqlUser() user: User,
|
||||
async acceptTeamInvitation(
|
||||
@GqlUser() user: AuthUser,
|
||||
@Args({
|
||||
name: 'inviteID',
|
||||
type: () => ID,
|
||||
@@ -191,10 +145,12 @@ export class TeamInvitationResolver {
|
||||
})
|
||||
inviteID: string,
|
||||
): Promise<TeamMember> {
|
||||
return pipe(
|
||||
this.teamInvitationService.acceptInvitation(inviteID, user),
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
const teamMember = await this.teamInvitationService.acceptInvitation(
|
||||
inviteID,
|
||||
user,
|
||||
);
|
||||
if (E.isLeft(teamMember)) throwErr(teamMember.left);
|
||||
return teamMember.right;
|
||||
}
|
||||
|
||||
// Subscriptions
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as T from 'fp-ts/Task';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import * as TO from 'fp-ts/TaskOption';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
import { pipe, flow, constVoid } from 'fp-ts/function';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Team, TeamMemberRole } from 'src/team/team.model';
|
||||
import { Email } from 'src/types/Email';
|
||||
import { User } from 'src/user/user.model';
|
||||
import { TeamInvitation as DBTeamInvitation } from '@prisma/client';
|
||||
import { TeamMember, TeamMemberRole } from 'src/team/team.model';
|
||||
import { TeamService } from 'src/team/team.service';
|
||||
import {
|
||||
INVALID_EMAIL,
|
||||
TEAM_INVALID_ID,
|
||||
TEAM_INVITE_ALREADY_MEMBER,
|
||||
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
||||
TEAM_INVITE_MEMBER_HAS_INVITE,
|
||||
TEAM_INVITE_NO_INVITE_FOUND,
|
||||
TEAM_MEMBER_NOT_FOUND,
|
||||
} from 'src/errors';
|
||||
import { TeamInvitation } from './team-invitation.model';
|
||||
import { MailerService } from 'src/mailer/mailer.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { validateEmail } from '../utils';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
|
||||
@Injectable()
|
||||
export class TeamInvitationService {
|
||||
@@ -29,245 +30,221 @@ export class TeamInvitationService {
|
||||
private readonly mailerService: MailerService,
|
||||
|
||||
private readonly pubsub: PubSubService,
|
||||
) {
|
||||
this.getInvitation = this.getInvitation.bind(this);
|
||||
}
|
||||
) {}
|
||||
|
||||
getInvitation(inviteID: string): TO.TaskOption<TeamInvitation> {
|
||||
return pipe(
|
||||
() =>
|
||||
this.prisma.teamInvitation.findUnique({
|
||||
where: {
|
||||
id: inviteID,
|
||||
},
|
||||
}),
|
||||
TO.fromTask,
|
||||
TO.chain(flow(O.fromNullable, TO.fromOption)),
|
||||
TO.map((x) => x as TeamInvitation),
|
||||
);
|
||||
}
|
||||
|
||||
getInvitationWithEmail(email: Email, team: Team) {
|
||||
return pipe(
|
||||
() =>
|
||||
this.prisma.teamInvitation.findUnique({
|
||||
where: {
|
||||
teamID_inviteeEmail: {
|
||||
inviteeEmail: email,
|
||||
teamID: team.id,
|
||||
},
|
||||
},
|
||||
}),
|
||||
TO.fromTask,
|
||||
TO.chain(flow(O.fromNullable, TO.fromOption)),
|
||||
);
|
||||
}
|
||||
|
||||
createInvitation(
|
||||
creator: User,
|
||||
team: Team,
|
||||
inviteeEmail: Email,
|
||||
inviteeRole: TeamMemberRole,
|
||||
) {
|
||||
return pipe(
|
||||
// Perform all validation checks
|
||||
TE.sequenceArray([
|
||||
// creator should be a TeamMember
|
||||
pipe(
|
||||
this.teamService.getTeamMemberTE(team.id, creator.uid),
|
||||
TE.map(constVoid),
|
||||
),
|
||||
|
||||
// Invitee should not be a team member
|
||||
pipe(
|
||||
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),
|
||||
),
|
||||
|
||||
// Should not have an existing invite
|
||||
pipe(
|
||||
this.getInvitationWithEmail(inviteeEmail, team),
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
revokeInvitation(inviteID: string) {
|
||||
return pipe(
|
||||
// Make sure invite exists
|
||||
this.getInvitation(inviteID),
|
||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
||||
|
||||
// Delete team invitation
|
||||
TE.chainTaskK(
|
||||
() => () =>
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
getAllInvitationsInTeam(team: Team) {
|
||||
return pipe(
|
||||
() =>
|
||||
this.prisma.teamInvitation.findMany({
|
||||
where: {
|
||||
teamID: team.id,
|
||||
},
|
||||
}),
|
||||
T.map((x) => x as TeamInvitation[]),
|
||||
);
|
||||
}
|
||||
|
||||
acceptInvitation(inviteID: string, acceptedBy: User) {
|
||||
return pipe(
|
||||
TE.Do,
|
||||
|
||||
// First get the invitation
|
||||
TE.bindW('invitation', () =>
|
||||
pipe(
|
||||
this.getInvitation(inviteID),
|
||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
||||
),
|
||||
),
|
||||
|
||||
// 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),
|
||||
);
|
||||
/**
|
||||
* Cast a DBTeamInvitation to a TeamInvitation
|
||||
* @param dbTeamInvitation database TeamInvitation
|
||||
* @returns TeamInvitation model
|
||||
*/
|
||||
cast(dbTeamInvitation: DBTeamInvitation): TeamInvitation {
|
||||
return {
|
||||
...dbTeamInvitation,
|
||||
inviteeRole: TeamMemberRole[dbTeamInvitation.inviteeRole],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the count invitations for a given team.
|
||||
* @param teamID team id
|
||||
* @returns a count team invitations for a team
|
||||
* Get the team invite
|
||||
* @param inviteID invite id
|
||||
* @returns an Option of team invitation or none
|
||||
*/
|
||||
async getAllTeamInvitations(teamID: string) {
|
||||
const invitations = await this.prisma.teamInvitation.findMany({
|
||||
async getInvitation(inviteID: string) {
|
||||
try {
|
||||
const dbInvitation = await this.prisma.teamInvitation.findUniqueOrThrow({
|
||||
where: {
|
||||
id: inviteID,
|
||||
},
|
||||
});
|
||||
|
||||
return O.some(this.cast(dbInvitation));
|
||||
} catch (e) {
|
||||
return O.none;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the team invite for an invitee with email and teamID.
|
||||
* @param inviteeEmail invitee email
|
||||
* @param teamID team id
|
||||
* @returns an Either of team invitation for the invitee or error
|
||||
*/
|
||||
async getTeamInviteByEmailAndTeamID(inviteeEmail: string, teamID: string) {
|
||||
const isEmailValid = validateEmail(inviteeEmail);
|
||||
if (!isEmailValid) return E.left(INVALID_EMAIL);
|
||||
|
||||
try {
|
||||
const teamInvite = await this.prisma.teamInvitation.findUniqueOrThrow({
|
||||
where: {
|
||||
teamID_inviteeEmail: {
|
||||
inviteeEmail: inviteeEmail,
|
||||
teamID: teamID,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return E.right(teamInvite);
|
||||
} catch (e) {
|
||||
return E.left(TEAM_INVITE_NO_INVITE_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a team invitation
|
||||
* @param creator creator of the invitation
|
||||
* @param teamID team id
|
||||
* @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,
|
||||
) {
|
||||
// validate email
|
||||
const isEmailValid = validateEmail(inviteeEmail);
|
||||
if (!isEmailValid) return E.left(INVALID_EMAIL);
|
||||
|
||||
// team ID should valid
|
||||
const team = await this.teamService.getTeamWithID(teamID);
|
||||
if (!team) return E.left(TEAM_INVALID_ID);
|
||||
|
||||
// invitation creator should be a TeamMember
|
||||
const isTeamMember = await this.teamService.getTeamMember(
|
||||
team.id,
|
||||
creator.uid,
|
||||
);
|
||||
if (!isTeamMember) return E.left(TEAM_MEMBER_NOT_FOUND);
|
||||
|
||||
// Checking to see if the invitee is already part of the team or not
|
||||
const inviteeUser = await this.userService.findUserByEmail(inviteeEmail);
|
||||
if (O.isSome(inviteeUser)) {
|
||||
// invitee should not already a member
|
||||
const isTeamMember = await this.teamService.getTeamMember(
|
||||
team.id,
|
||||
inviteeUser.value.uid,
|
||||
);
|
||||
if (isTeamMember) return E.left(TEAM_INVITE_ALREADY_MEMBER);
|
||||
}
|
||||
|
||||
// check invitee already invited earlier or not
|
||||
const teamInvitation = await this.getTeamInviteByEmailAndTeamID(
|
||||
inviteeEmail,
|
||||
team.id,
|
||||
);
|
||||
if (E.isRight(teamInvitation)) return E.left(TEAM_INVITE_MEMBER_HAS_INVITE);
|
||||
|
||||
// create the invitation
|
||||
const dbInvitation = await this.prisma.teamInvitation.create({
|
||||
data: {
|
||||
teamID: team.id,
|
||||
inviteeEmail,
|
||||
inviteeRole,
|
||||
creatorUid: creator.uid,
|
||||
},
|
||||
});
|
||||
|
||||
await this.mailerService.sendEmail(inviteeEmail, {
|
||||
template: 'team-invitation',
|
||||
variables: {
|
||||
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);
|
||||
this.pubsub.publish(`team/${invitation.teamID}/invite_added`, invitation);
|
||||
|
||||
return E.right(invitation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke a team invitation
|
||||
* @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
|
||||
* @returns array of team invitations for a team
|
||||
*/
|
||||
async getTeamInvitations(teamID: string) {
|
||||
const dbInvitations = await this.prisma.teamInvitation.findMany({
|
||||
where: {
|
||||
teamID: teamID,
|
||||
},
|
||||
});
|
||||
|
||||
const invitations: TeamInvitation[] = dbInvitations.map((dbInvitation) =>
|
||||
this.cast(dbInvitation),
|
||||
);
|
||||
|
||||
return invitations;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { pipe } from 'fp-ts/function';
|
||||
import { TeamService } from 'src/team/team.service';
|
||||
import { TeamInvitationService } from './team-invitation.service';
|
||||
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 {
|
||||
BUG_AUTH_NO_USER_CTX,
|
||||
BUG_TEAM_INVITE_NO_INVITE_ID,
|
||||
TEAM_INVITE_NO_INVITE_FOUND,
|
||||
TEAM_MEMBER_NOT_FOUND,
|
||||
TEAM_NOT_REQUIRED_ROLE,
|
||||
} from 'src/errors';
|
||||
import { User } from 'src/user/user.model';
|
||||
import { throwErr } from 'src/utils';
|
||||
import { TeamMemberRole } from 'src/team/team.model';
|
||||
|
||||
/**
|
||||
* This guard only allows team owner to execute the resolver
|
||||
*/
|
||||
@Injectable()
|
||||
export class TeamInviteTeamOwnerGuard implements CanActivate {
|
||||
constructor(
|
||||
@@ -24,48 +24,30 @@ export class TeamInviteTeamOwnerGuard implements CanActivate {
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
return pipe(
|
||||
TE.Do,
|
||||
// Get GQL context
|
||||
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||
|
||||
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
|
||||
// Get user
|
||||
const { user } = gqlExecCtx.getContext().req;
|
||||
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
|
||||
|
||||
// Get the invite
|
||||
TE.bindW('invite', ({ gqlCtx }) =>
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Get the invite
|
||||
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
|
||||
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
|
||||
|
||||
TE.bindW('user', ({ gqlCtx }) =>
|
||||
pipe(
|
||||
gqlCtx.getContext().req.user,
|
||||
O.fromNullable,
|
||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
||||
),
|
||||
),
|
||||
const invitation = await this.teamInviteService.getInvitation(inviteID);
|
||||
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||
|
||||
TE.bindW('userMember', ({ invite, user }) =>
|
||||
this.teamService.getTeamMemberTE(invite.teamID, user.uid),
|
||||
),
|
||||
// Fetch team member details of this user
|
||||
const teamMember = await this.teamService.getTeamMember(
|
||||
invitation.value.teamID,
|
||||
user.uid,
|
||||
);
|
||||
|
||||
TE.chainW(
|
||||
TE.fromPredicate(
|
||||
({ userMember }) => userMember.role === TeamMemberRole.OWNER,
|
||||
() => TEAM_NOT_REQUIRED_ROLE,
|
||||
),
|
||||
),
|
||||
if (!teamMember) throwErr(TEAM_MEMBER_NOT_FOUND);
|
||||
if (teamMember.role !== TeamMemberRole.OWNER)
|
||||
throwErr(TEAM_NOT_REQUIRED_ROLE);
|
||||
|
||||
TE.fold(
|
||||
(err) => throwErr(err),
|
||||
() => T.of(true),
|
||||
),
|
||||
)();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
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 { GqlExecutionContext } from '@nestjs/graphql';
|
||||
import {
|
||||
BUG_AUTH_NO_USER_CTX,
|
||||
BUG_TEAM_INVITE_NO_INVITE_ID,
|
||||
TEAM_INVITE_NOT_VALID_VIEWER,
|
||||
TEAM_INVITE_NO_INVITE_FOUND,
|
||||
TEAM_MEMBER_NOT_FOUND,
|
||||
} from 'src/errors';
|
||||
import { User } from 'src/user/user.model';
|
||||
import { throwErr } from 'src/utils';
|
||||
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()
|
||||
export class TeamInviteViewerGuard implements CanActivate {
|
||||
constructor(
|
||||
@@ -23,50 +26,32 @@ export class TeamInviteViewerGuard implements CanActivate {
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
return pipe(
|
||||
TE.Do,
|
||||
// Get GQL context
|
||||
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||
|
||||
// Get GQL Context
|
||||
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
|
||||
// Get user
|
||||
const { user } = gqlExecCtx.getContext().req;
|
||||
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
|
||||
|
||||
// Get user
|
||||
TE.bindW('user', ({ gqlCtx }) =>
|
||||
pipe(
|
||||
O.fromNullable(gqlCtx.getContext<{ user?: User }>().user),
|
||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
||||
),
|
||||
),
|
||||
// Get the invite
|
||||
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
|
||||
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
|
||||
|
||||
// Get the invite
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const invitation = await this.teamInviteService.getInvitation(inviteID);
|
||||
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||
|
||||
// Check if the user and the invite email match, else if we can resolver the user as a team member
|
||||
// any better solution ?
|
||||
TE.chainW(({ user, invite }) =>
|
||||
user.email?.toLowerCase() === invite.inviteeEmail.toLowerCase()
|
||||
? TE.of(true)
|
||||
: pipe(
|
||||
this.teamService.getTeamMemberTE(invite.teamID, user.uid),
|
||||
TE.map(() => true),
|
||||
),
|
||||
),
|
||||
// Check if the user and the invite email match, else if user is a team member
|
||||
if (
|
||||
user.email?.toLowerCase() !== invitation.value.inviteeEmail.toLowerCase()
|
||||
) {
|
||||
const teamMember = await this.teamService.getTeamMember(
|
||||
invitation.value.teamID,
|
||||
user.uid,
|
||||
);
|
||||
|
||||
TE.mapLeft((e) =>
|
||||
e === 'team/member_not_found' ? TEAM_INVITE_NOT_VALID_VIEWER : e,
|
||||
),
|
||||
if (!teamMember) throwErr(TEAM_MEMBER_NOT_FOUND);
|
||||
}
|
||||
|
||||
TE.fold(throwErr, () => T.of(true)),
|
||||
)();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { TeamInvitationService } from './team-invitation.service';
|
||||
import { pipe, flow } from 'fp-ts/function';
|
||||
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 { User } from 'src/user/user.model';
|
||||
import {
|
||||
BUG_AUTH_NO_USER_CTX,
|
||||
BUG_TEAM_INVITE_NO_INVITE_ID,
|
||||
@@ -24,44 +20,26 @@ export class TeamInviteeGuard implements CanActivate {
|
||||
constructor(private readonly teamInviteService: TeamInvitationService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
return pipe(
|
||||
TE.Do,
|
||||
// Get GQL Context
|
||||
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||
|
||||
// Get execution context
|
||||
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
|
||||
// Get user
|
||||
const { user } = gqlExecCtx.getContext().req;
|
||||
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
|
||||
|
||||
// Get user
|
||||
TE.bindW('user', ({ gqlCtx }) =>
|
||||
pipe(
|
||||
O.fromNullable(gqlCtx.getContext<{ user?: User }>().user),
|
||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
||||
),
|
||||
),
|
||||
// Get the invite
|
||||
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
|
||||
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
|
||||
|
||||
// Get invite
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const invitation = await this.teamInviteService.getInvitation(inviteID);
|
||||
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||
|
||||
// Check if the emails match
|
||||
TE.chainW(
|
||||
TE.fromPredicate(
|
||||
({ user, invite }) => user.email === invite.inviteeEmail,
|
||||
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
||||
),
|
||||
),
|
||||
if (
|
||||
user.email.toLowerCase() !== invitation.value.inviteeEmail.toLowerCase()
|
||||
) {
|
||||
throwErr(TEAM_INVITE_EMAIL_DO_NOT_MATCH);
|
||||
}
|
||||
|
||||
// Fold it to a promise
|
||||
TE.fold(throwErr, () => T.of(true)),
|
||||
)();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ export class TeamTeamInviteExtResolver {
|
||||
complexity: 10,
|
||||
})
|
||||
teamInvitations(@Parent() team: Team): Promise<TeamInvitation[]> {
|
||||
return this.teamInviteService.getAllInvitationsInTeam(team)();
|
||||
return this.teamInviteService.getTeamInvitations(team.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
TEAM_REQ_NOT_FOUND,
|
||||
TEAM_REQ_REORDERING_FAILED,
|
||||
TEAM_COLL_NOT_FOUND,
|
||||
JSON_INVALID,
|
||||
} from 'src/errors';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
@@ -239,7 +240,7 @@ describe('deleteTeamRequest', () => {
|
||||
});
|
||||
|
||||
describe('createTeamRequest', () => {
|
||||
test('rejects for invalid collection id', async () => {
|
||||
test('should rejects for invalid collection id', async () => {
|
||||
mockTeamCollectionService.getTeamOfCollection.mockResolvedValue(
|
||||
E.left(TEAM_INVALID_COLL_ID),
|
||||
);
|
||||
@@ -255,7 +256,42 @@ describe('createTeamRequest', () => {
|
||||
expect(mockPrisma.teamRequest.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('resolves for valid collection id', async () => {
|
||||
test('should rejects for invalid team 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 teamRequest = teamRequests[0];
|
||||
|
||||
@@ -536,6 +572,52 @@ describe('findRequestAndNextRequest', () => {
|
||||
|
||||
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', () => {
|
||||
@@ -725,13 +807,12 @@ describe('totalRequestsInATeam', () => {
|
||||
});
|
||||
expect(result).toEqual(0);
|
||||
});
|
||||
});
|
||||
describe('getTeamRequestsCount', () => {
|
||||
test('should return count of all Team Collections in the organization', async () => {
|
||||
mockPrisma.teamRequest.count.mockResolvedValueOnce(10);
|
||||
|
||||
describe('getTeamRequestsCount', () => {
|
||||
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);
|
||||
});
|
||||
const result = await teamRequestService.getTeamRequestsCount();
|
||||
expect(result).toEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { HttpStatus } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
** Custom interface to handle errors specific to Auth module
|
||||
** Since its REST we need to return HTTP status code along with error message
|
||||
** Since its REST we need to return the HTTP status code along with the error message
|
||||
*/
|
||||
export type AuthError = {
|
||||
message: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UserCollection } from '@prisma/client';
|
||||
import { UserCollection, UserRequest as DbUserRequest } from '@prisma/client';
|
||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import {
|
||||
USER_COLL_DEST_SAME,
|
||||
@@ -11,12 +11,17 @@ import {
|
||||
USER_COLL_SHORT_TITLE,
|
||||
USER_COLL_ALREADY_ROOT,
|
||||
USER_NOT_OWNER,
|
||||
USER_NOT_FOUND,
|
||||
USER_COLL_INVALID_JSON,
|
||||
} from 'src/errors';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
import { ReqType } from 'src/types/RequestTypes';
|
||||
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 mockPubSub = mockDeep<PubSubService>();
|
||||
@@ -341,11 +346,485 @@ 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(() => {
|
||||
mockReset(mockPrisma);
|
||||
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', () => {
|
||||
test('should return a user-collection successfully with valid collectionID', async () => {
|
||||
mockPrisma.userCollection.findUnique.mockResolvedValueOnce({
|
||||
|
||||
@@ -140,13 +140,15 @@ describe('UserHistoryService', () => {
|
||||
});
|
||||
describe('createUserHistory', () => {
|
||||
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({
|
||||
userUid: 'abc',
|
||||
id: '1',
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
});
|
||||
|
||||
@@ -156,7 +158,7 @@ describe('UserHistoryService', () => {
|
||||
request: JSON.stringify([{}]),
|
||||
responseMetadata: JSON.stringify([{}]),
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
};
|
||||
|
||||
@@ -170,13 +172,15 @@ describe('UserHistoryService', () => {
|
||||
).toEqualRight(userHistory);
|
||||
});
|
||||
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({
|
||||
userUid: 'abc',
|
||||
id: '1',
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.GQL,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
});
|
||||
|
||||
@@ -186,7 +190,7 @@ describe('UserHistoryService', () => {
|
||||
request: JSON.stringify([{}]),
|
||||
responseMetadata: JSON.stringify([{}]),
|
||||
reqType: ReqType.GQL,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
};
|
||||
|
||||
@@ -210,13 +214,15 @@ describe('UserHistoryService', () => {
|
||||
).toEqualLeft(USER_HISTORY_INVALID_REQ_TYPE);
|
||||
});
|
||||
test('Should create a GQL request to users history and publish a created subscription', async () => {
|
||||
const executedOn = new Date();
|
||||
|
||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||
userUid: 'abc',
|
||||
id: '1',
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.GQL,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
});
|
||||
|
||||
@@ -226,7 +232,7 @@ describe('UserHistoryService', () => {
|
||||
request: JSON.stringify([{}]),
|
||||
responseMetadata: JSON.stringify([{}]),
|
||||
reqType: ReqType.GQL,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
};
|
||||
|
||||
@@ -243,13 +249,15 @@ describe('UserHistoryService', () => {
|
||||
);
|
||||
});
|
||||
test('Should create a REST request to users history and publish a created subscription', async () => {
|
||||
const executedOn = new Date();
|
||||
|
||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||
userUid: 'abc',
|
||||
id: '1',
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
});
|
||||
|
||||
@@ -259,7 +267,7 @@ describe('UserHistoryService', () => {
|
||||
request: JSON.stringify([{}]),
|
||||
responseMetadata: JSON.stringify([{}]),
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ import {
|
||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import {
|
||||
JSON_INVALID,
|
||||
USER_COLLECTION_NOT_FOUND,
|
||||
USER_COLL_NOT_FOUND,
|
||||
USER_REQUEST_INVALID_TYPE,
|
||||
USER_REQUEST_NOT_FOUND,
|
||||
USER_REQUEST_REORDERING_FAILED,
|
||||
} from 'src/errors';
|
||||
@@ -373,6 +376,101 @@ describe('UserRequestService', () => {
|
||||
|
||||
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', () => {
|
||||
|
||||
1
packages/hoppscotch-common/assets/icons/star-off.svg
Normal file
1
packages/hoppscotch-common/assets/icons/star-off.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 337 B |
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Reaksie liggaam",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Opskrifte",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Tyd",
|
||||
"title": "Reaksie",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "wag vir verbinding",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "هيئة الاستجابة",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "الرؤوس",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "حالة",
|
||||
"time": "وقت",
|
||||
"title": "إجابة",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "في انتظار الاتصال",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "Visualitzar els meus enllaços"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Cos de resposta",
|
||||
"filter_response_body": "Filtrar el cos de la resposta JSON (utilitza la sintaxi JSONPath)",
|
||||
"headers": "Capçaleres",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Estat",
|
||||
"time": "Temps",
|
||||
"title": "Resposta",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "esperant la connexió",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "查看我的链接"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "响应体",
|
||||
"filter_response_body": "筛选JSON响应本体(使用JSONPath语法)",
|
||||
"headers": "响应头",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "状态",
|
||||
"time": "时间",
|
||||
"title": "响应",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "等待连接",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Odpovědní orgán",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Záhlaví",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Postavení",
|
||||
"time": "Čas",
|
||||
"title": "Odezva",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "čekání na připojení",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Svarorgan",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Overskrifter",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Tid",
|
||||
"title": "Respons",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "venter på forbindelse",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Antworttext",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Header",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Zeit",
|
||||
"title": "Antwort",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "auf Verbindung warten",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "Προβολή των links μου"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Σώμα απόκρισης",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Κεφαλίδες",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Κατάσταση",
|
||||
"time": "χρόνος",
|
||||
"title": "Απάντηση",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "περιμένοντας τη σύνδεση",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "Ver mis enlaces"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Cuerpo de respuesta",
|
||||
"filter_response_body": "Filtrar el cuerpo de la respuesta JSON (utiliza la sintaxis JSONPath)",
|
||||
"headers": "Encabezados",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Estado",
|
||||
"time": "Tiempo",
|
||||
"title": "Respuesta",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "esperando la conexión",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Vastauselin",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Otsikot",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Tila",
|
||||
"time": "Aika",
|
||||
"title": "Vastaus",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "yhteyttä odotellessa",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "Voir mes liens"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Corps de réponse",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "En-têtes",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Statut",
|
||||
"time": "Temps",
|
||||
"title": "Réponse",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "En attente de connexion",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "גוף תגובה",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "כותרות",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "סטָטוּס",
|
||||
"time": "זְמַן",
|
||||
"title": "תְגוּבָה",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "מחכה לחיבור",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -433,6 +433,7 @@
|
||||
"view_my_links": "मेरे लिंक देखें"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "प्रतिक्रिया निकाय",
|
||||
"filter_response_body": "फ़िल्टर JSON रिस्पांस बॉडी (JSONPATH सिंटैक्स का उपयोग करता है)",
|
||||
"headers": "हेडर",
|
||||
@@ -446,6 +447,7 @@
|
||||
"status": "दर्जा",
|
||||
"time": "समय",
|
||||
"title": "जवाब",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "जुडने के लिए इंतजार",
|
||||
"xml": "एक्सएमएल"
|
||||
},
|
||||
|
||||
@@ -5,29 +5,29 @@
|
||||
"choose_file": "Válasszon egy fájlt",
|
||||
"clear": "Törlés",
|
||||
"clear_all": "Összes törlése",
|
||||
"close": "Close",
|
||||
"close": "Bezárás",
|
||||
"connect": "Kapcsolódás",
|
||||
"connecting": "Connecting",
|
||||
"connecting": "Kapcsolódás",
|
||||
"copy": "Másolás",
|
||||
"delete": "Törlés",
|
||||
"disconnect": "Leválasztás",
|
||||
"dismiss": "Eltüntetés",
|
||||
"dont_save": "Ne mentse",
|
||||
"download_file": "Fájl letöltése",
|
||||
"drag_to_reorder": "Drag to reorder",
|
||||
"drag_to_reorder": "Húzza az átrendezéshez",
|
||||
"duplicate": "Kettőzés",
|
||||
"edit": "Szerkesztés",
|
||||
"filter": "Filter",
|
||||
"filter": "Szűrő",
|
||||
"go_back": "Vissza",
|
||||
"go_forward": "Go forward",
|
||||
"group_by": "Group by",
|
||||
"go_forward": "Előre",
|
||||
"group_by": "Csoportosítás",
|
||||
"label": "Címke",
|
||||
"learn_more": "Tudjon meg többet",
|
||||
"less": "Kevesebb",
|
||||
"more": "Több",
|
||||
"new": "Új",
|
||||
"no": "Nem",
|
||||
"open_workspace": "Open workspace",
|
||||
"open_workspace": "Munkaterület megnyitása",
|
||||
"paste": "Beillesztés",
|
||||
"prettify": "Csinosítás",
|
||||
"remove": "Eltávolítás",
|
||||
@@ -38,7 +38,7 @@
|
||||
"search": "Keresés",
|
||||
"send": "Küldés",
|
||||
"start": "Indítás",
|
||||
"starting": "Starting",
|
||||
"starting": "Indítás",
|
||||
"stop": "Leállítás",
|
||||
"to_close": "a bezáráshoz",
|
||||
"to_navigate": "a navigáláshoz",
|
||||
@@ -118,16 +118,16 @@
|
||||
},
|
||||
"collection": {
|
||||
"created": "Gyűjtemény létrehozva",
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"different_parent": "Nem lehet átrendezni a különböző szülővel rendelkező gyűjteményt",
|
||||
"edit": "Gyűjtemény szerkesztése",
|
||||
"invalid_name": "Adjon nevet a gyűjteménynek",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
"invalid_root_move": "A gyűjtemény már a gyökérben van",
|
||||
"moved": "Sikeresen áthelyezve",
|
||||
"my_collections": "Saját gyűjtemények",
|
||||
"name": "Saját új gyűjtemény",
|
||||
"name_length_insufficient": "A gyűjtemény nevének legalább 3 karakter hosszúságúnak kell lennie",
|
||||
"new": "Új gyűjtemény",
|
||||
"order_changed": "Collection Order Updated",
|
||||
"order_changed": "Gyűjtemény sorrendje frissítve",
|
||||
"renamed": "Gyűjtemény átnevezve",
|
||||
"request_in_use": "A kérés használatban",
|
||||
"save_as": "Mentés másként",
|
||||
@@ -147,7 +147,7 @@
|
||||
"remove_team": "Biztosan törölni szeretné ezt a csapatot?",
|
||||
"remove_telemetry": "Biztosan ki szeretné kapcsolni a telemetriát?",
|
||||
"request_change": "Biztosan el szeretné vetni a jelenlegi kérést? Minden mentetlen változtatás el fog veszni.",
|
||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||
"save_unsaved_tab": "Szeretné menteni az ezen a lapon elvégzett változtatásokat?",
|
||||
"sync": "Szeretné visszaállítani a munkaterületét a felhőből? Ez el fogja vetni a helyi folyamatát."
|
||||
},
|
||||
"count": {
|
||||
@@ -180,8 +180,8 @@
|
||||
"profile": "Jelentkezzen be a profilja megtekintéséhez",
|
||||
"protocols": "A protokollok üresek",
|
||||
"schema": "Kapcsolódjon egy GraphQL-végponthoz a séma megtekintéséhez",
|
||||
"shortcodes": "Shortcodes are empty",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"shortcodes": "A rövid kódok üresek",
|
||||
"subscription": "A feliratkozások üresek",
|
||||
"team_name": "A csapat neve üres",
|
||||
"teams": "Ön nem tartozik semmilyen csapathoz",
|
||||
"tests": "Nincsenek tesztek ehhez a kéréshez"
|
||||
@@ -194,13 +194,13 @@
|
||||
"deleted": "Környezet törlése",
|
||||
"edit": "Környezet szerkesztése",
|
||||
"invalid_name": "Adjon nevet a környezetnek",
|
||||
"my_environments": "My Environments",
|
||||
"my_environments": "Saját környezetek",
|
||||
"nested_overflow": "az egymásba ágyazott környezeti változók 10 szintre vannak korlátozva",
|
||||
"new": "Új környezet",
|
||||
"no_environment": "Nincs környezet",
|
||||
"no_environment_description": "Nem lettek környezetek kiválasztva. Válassza ki, hogy mit kell tenni a következő változókkal.",
|
||||
"select": "Környezet kiválasztása",
|
||||
"team_environments": "Team Environments",
|
||||
"team_environments": "Csapatkörnyezetek",
|
||||
"title": "Környezetek",
|
||||
"updated": "Környezet frissítve",
|
||||
"variable_list": "Változólista"
|
||||
@@ -209,9 +209,9 @@
|
||||
"browser_support_sse": "Úgy tűnik, hogy ez a böngésző nem támogatja a kiszolgáló által küldött eseményeket.",
|
||||
"check_console_details": "Nézze meg a konzolnaplót a részletekért.",
|
||||
"curl_invalid_format": "A cURL nincs megfelelően formázva",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
|
||||
"danger_zone": "Veszélyes zóna",
|
||||
"delete_account": "Az Ön fiókja jelenleg tulajdonos ezekben a csapatokban:",
|
||||
"delete_account_description": "El kell távolítani magát, át kell adnia a tulajdonjogot vagy törölnie kell ezeket a csapatokat, mielőtt törölhetné a fiókját.",
|
||||
"empty_req_name": "Üres kérésnév",
|
||||
"f12_details": "(F12 a részletekért)",
|
||||
"gql_prettify_invalid_query": "Nem sikerült csinosítani egy érvénytelen lekérdezést, oldja meg a lekérdezés szintaktikai hibáit, és próbálja újra",
|
||||
@@ -219,13 +219,13 @@
|
||||
"incorrect_email": "Hibás e-mail",
|
||||
"invalid_link": "Érvénytelen hivatkozás",
|
||||
"invalid_link_description": "A kattintott hivatkozás érvénytelen vagy lejárt.",
|
||||
"json_parsing_failed": "Invalid JSON",
|
||||
"json_parsing_failed": "Érvénytelen JSON",
|
||||
"json_prettify_invalid_body": "Nem sikerült csinosítani egy érvénytelen törzset, oldja meg a JSON szintaktikai hibáit, és próbálja újra",
|
||||
"network_error": "Úgy tűnik, hogy hálózati hiba van. Próbálja újra.",
|
||||
"network_fail": "Nem sikerült elküldeni a kérést",
|
||||
"no_duration": "Nincs időtartam",
|
||||
"no_results_found": "No matches found",
|
||||
"page_not_found": "This page could not be found",
|
||||
"no_results_found": "Nincs találat",
|
||||
"page_not_found": "Ez az oldal nem található",
|
||||
"script_fail": "Nem sikerült végrehajtani a kérés előtti parancsfájlt",
|
||||
"something_went_wrong": "Valami elromlott",
|
||||
"test_script_fail": "Nem sikerült végrehajtani a kérés utáni parancsfájlt"
|
||||
@@ -238,9 +238,9 @@
|
||||
"title": "Exportálás"
|
||||
},
|
||||
"filter": {
|
||||
"all": "All",
|
||||
"none": "None",
|
||||
"starred": "Starred"
|
||||
"all": "Összes",
|
||||
"none": "Nincs",
|
||||
"starred": "Csillagozott"
|
||||
},
|
||||
"folder": {
|
||||
"created": "Mappa létrehozva",
|
||||
@@ -256,7 +256,7 @@
|
||||
"subscriptions": "Feliratkozások"
|
||||
},
|
||||
"group": {
|
||||
"time": "Time",
|
||||
"time": "Idő",
|
||||
"url": "URL"
|
||||
},
|
||||
"header": {
|
||||
@@ -316,32 +316,32 @@
|
||||
"zen_mode": "Zen mód"
|
||||
},
|
||||
"modal": {
|
||||
"close_unsaved_tab": "You have unsaved changes",
|
||||
"close_unsaved_tab": "Elmentetlen változtatásai vannak",
|
||||
"collections": "Gyűjtemények",
|
||||
"confirm": "Megerősítés",
|
||||
"edit_request": "Kérés szerkesztése",
|
||||
"import_export": "Importálás és exportálás"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "You are already subscribed to this topic.",
|
||||
"clean_session": "Clean Session",
|
||||
"clear_input": "Clear input",
|
||||
"clear_input_on_send": "Clear input on send",
|
||||
"client_id": "Client ID",
|
||||
"color": "Pick a color",
|
||||
"already_subscribed": "Ön már feliratkozott erre a témára.",
|
||||
"clean_session": "Munkamenet törlése",
|
||||
"clear_input": "Bevitel törlése",
|
||||
"clear_input_on_send": "Bevitel törlése küldéskor",
|
||||
"client_id": "Ügyfél-azonosító",
|
||||
"color": "Válasszon színt",
|
||||
"communication": "Kommunikáció",
|
||||
"connection_config": "Connection Config",
|
||||
"connection_not_authorized": "This MQTT connection does not use any authentication.",
|
||||
"invalid_topic": "Please provide a topic for the subscription",
|
||||
"keep_alive": "Keep Alive",
|
||||
"connection_config": "Kapcsolat beállításai",
|
||||
"connection_not_authorized": "Ez az MQTT-kapcsolat nem használ semmilyen hitelesítést.",
|
||||
"invalid_topic": "Adjon témát a feliratkozáshoz",
|
||||
"keep_alive": "Életben tartás",
|
||||
"log": "Napló",
|
||||
"lw_message": "Last-Will Message",
|
||||
"lw_qos": "Last-Will QoS",
|
||||
"lw_retain": "Last-Will Retain",
|
||||
"lw_topic": "Last-Will Topic",
|
||||
"lw_message": "Utolsó kívánság üzenet",
|
||||
"lw_qos": "Utolsó kívánság QoS",
|
||||
"lw_retain": "Utolsó kívánság megtartás",
|
||||
"lw_topic": "Utolsó kívánság téma",
|
||||
"message": "Üzenet",
|
||||
"new": "New Subscription",
|
||||
"not_connected": "Please start a MQTT connection first.",
|
||||
"new": "Új feliratkozás",
|
||||
"not_connected": "Először indítson egy MQTT-kapcsolatot.",
|
||||
"publish": "Közzététel",
|
||||
"qos": "QoS",
|
||||
"ssl": "SSL",
|
||||
@@ -368,7 +368,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"app_settings": "Alkalmazás beállításai",
|
||||
"default_hopp_displayname": "Unnamed User",
|
||||
"default_hopp_displayname": "Névtelen felhasználó",
|
||||
"editor": "Szerkesztő",
|
||||
"editor_description": "A szerkesztők hozzáadhatnak, szerkeszthetnek és törölhetnek kéréseket.",
|
||||
"email_verification_mail": "Egy ellenőrző e-mail el lett küldve az e-mail-címére. Kattintson a hivatkozásra az e-mail-címe ellenőrzéséhez.",
|
||||
@@ -391,26 +391,26 @@
|
||||
"choose_language": "Nyelv kiválasztása",
|
||||
"content_type": "Tartalom típusa",
|
||||
"content_type_titles": {
|
||||
"others": "Others",
|
||||
"structured": "Structured",
|
||||
"text": "Text"
|
||||
"others": "Egyebek",
|
||||
"structured": "Szerkesztett",
|
||||
"text": "Szöveg"
|
||||
},
|
||||
"copy_link": "Hivatkozás másolása",
|
||||
"different_collection": "Cannot reorder requests from different collections",
|
||||
"duplicated": "Request duplicated",
|
||||
"different_collection": "Nem lehet átrendezni a különböző gyűjteményekből érkező kéréseket",
|
||||
"duplicated": "Kérés megkettőzve",
|
||||
"duration": "Időtartam",
|
||||
"enter_curl": "cURL megadása",
|
||||
"enter_curl": "cURL-parancs megadása",
|
||||
"generate_code": "Kód előállítása",
|
||||
"generated_code": "Előállított kód",
|
||||
"header_list": "Fejléclista",
|
||||
"invalid_name": "Adjon nevet a kérésnek",
|
||||
"method": "Módszer",
|
||||
"moved": "Request moved",
|
||||
"moved": "Kérés áthelyezve",
|
||||
"name": "Kérés neve",
|
||||
"new": "Új kérés",
|
||||
"order_changed": "Request Order Updated",
|
||||
"order_changed": "Kérés sorrendje frissítve",
|
||||
"override": "Felülbírálás",
|
||||
"override_help": "A <kbd>Content-Type</kbd> beállítása a fejlécekben",
|
||||
"override_help": "<kbd>Content-Type</kbd> beállítása a fejlécekben",
|
||||
"overriden": "Felülbírálva",
|
||||
"parameter_list": "Lekérdezési paraméterek",
|
||||
"parameters": "Paraméterek",
|
||||
@@ -429,11 +429,12 @@
|
||||
"type": "Kérés típusa",
|
||||
"url": "URL",
|
||||
"variables": "Változók",
|
||||
"view_my_links": "View my links"
|
||||
"view_my_links": "Saját hivatkozások megtekintése"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Hang",
|
||||
"body": "Válasz törzse",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"filter_response_body": "JSON-válasz törzsének szűrése (JSONPath szintaxist használ)",
|
||||
"headers": "Fejlécek",
|
||||
"html": "HTML",
|
||||
"image": "Kép",
|
||||
@@ -445,13 +446,14 @@
|
||||
"status": "Állapot",
|
||||
"time": "Idő",
|
||||
"title": "Válasz",
|
||||
"video": "Videó",
|
||||
"waiting_for_connection": "várakozás kapcsolódásra",
|
||||
"xml": "XML"
|
||||
},
|
||||
"settings": {
|
||||
"accent_color": "Kiemelőszín",
|
||||
"account": "Fiók",
|
||||
"account_deleted": "Your account has been deleted",
|
||||
"account_deleted": "A fiókja törölve lett",
|
||||
"account_description": "A fiókbeállítások személyre szabása.",
|
||||
"account_email_description": "Az Ön elsődleges e-mail-címe.",
|
||||
"account_name_description": "Ez a megjelenített neve.",
|
||||
@@ -460,8 +462,8 @@
|
||||
"change_font_size": "Betűméret megváltoztatása",
|
||||
"choose_language": "Nyelv kiválasztása",
|
||||
"dark_mode": "Sötét",
|
||||
"delete_account": "Delete account",
|
||||
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
||||
"delete_account": "Fiók törlése",
|
||||
"delete_account_description": "Ha törli a fiókját, akkor az összes adata véglegesen törlésre kerül. Ezt a műveletet nem lehet visszavonni.",
|
||||
"expand_navigation": "Navigáció kinyitása",
|
||||
"experiments": "Kísérletek",
|
||||
"experiments_notice": "Ez olyan kísérletek gyűjteménye, amelyeken dolgozunk, és amelyek hasznosak, szórakoztatóak lehetnek, mindkettő, vagy egyik sem. Ezek nem véglegesek és nem stabilak, ezért ha valami túl furcsa dolog történik, ne essen pánikba. Egyszerűen kapcsolja ki a hibás dolgot. Viccet félretéve, ",
|
||||
@@ -488,8 +490,8 @@
|
||||
"proxy_use_toggle": "A proxy középprogram használata a kérések küldéséhez",
|
||||
"read_the": "Olvassa el:",
|
||||
"reset_default": "Visszaállítás az alapértelmezettre",
|
||||
"short_codes": "Short codes",
|
||||
"short_codes_description": "Short codes which were created by you.",
|
||||
"short_codes": "Rövid kódok",
|
||||
"short_codes_description": "Az Ön által létrehozott rövid kódok.",
|
||||
"sidebar_on_left": "Oldalsáv a bal oldalon",
|
||||
"sync": "Szinkronizálás",
|
||||
"sync_collections": "Gyűjtemények",
|
||||
@@ -503,16 +505,16 @@
|
||||
"theme_description": "Az alkalmazás témájának személyre szabása.",
|
||||
"use_experimental_url_bar": "Kísérleti URL-sáv használata a környezet kiemelésével",
|
||||
"user": "Felhasználó",
|
||||
"verified_email": "Verified email",
|
||||
"verified_email": "Ellenőrzött e-mail-cím",
|
||||
"verify_email": "E-mail-cím ellenőrzése"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Actions",
|
||||
"created_on": "Created on",
|
||||
"deleted": "Shortcode deleted",
|
||||
"method": "Method",
|
||||
"not_found": "Shortcode not found",
|
||||
"short_code": "Short code",
|
||||
"actions": "Műveletek",
|
||||
"created_on": "Létrehozva",
|
||||
"deleted": "Rövid kód törölve",
|
||||
"method": "Módszer",
|
||||
"not_found": "A rövid kód nem található",
|
||||
"short_code": "Rövid kód",
|
||||
"url": "URL"
|
||||
},
|
||||
"shortcut": {
|
||||
@@ -554,9 +556,9 @@
|
||||
"title": "Kérés"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copy response to clipboard",
|
||||
"download": "Download response as file",
|
||||
"title": "Response"
|
||||
"copy": "Válasz másolása a vágólapra",
|
||||
"download": "Válasz letöltés fájlként",
|
||||
"title": "Válasz"
|
||||
},
|
||||
"theme": {
|
||||
"black": "Téma átváltása fekete módra",
|
||||
@@ -574,8 +576,8 @@
|
||||
},
|
||||
"socketio": {
|
||||
"communication": "Kommunikáció",
|
||||
"connection_not_authorized": "This SocketIO connection does not use any authentication.",
|
||||
"event_name": "Esemény neve",
|
||||
"connection_not_authorized": "Ez a SocketIO-kapcsolat nem használ semmilyen hitelesítést.",
|
||||
"event_name": "Esemény vagy téma neve",
|
||||
"events": "Események",
|
||||
"log": "Napló",
|
||||
"url": "URL"
|
||||
@@ -592,9 +594,9 @@
|
||||
"connected": "Kapcsolódva",
|
||||
"connected_to": "Kapcsolódva ehhez: {name}",
|
||||
"connecting_to": "Kapcsolódás ehhez: {name}…",
|
||||
"connection_error": "Failed to connect",
|
||||
"connection_failed": "Connection failed",
|
||||
"connection_lost": "Connection lost",
|
||||
"connection_error": "Nem sikerült kapcsolódni",
|
||||
"connection_failed": "A kapcsolódás sikertelen",
|
||||
"connection_lost": "A kapcsolat elveszett",
|
||||
"copied_to_clipboard": "Vágólapra másolva",
|
||||
"deleted": "Törölve",
|
||||
"deprecated": "ELAVULT",
|
||||
@@ -609,17 +611,17 @@
|
||||
"history_deleted": "Előzmények törölve",
|
||||
"linewrap": "Sorok tördelése",
|
||||
"loading": "Betöltés…",
|
||||
"message_received": "Message: {message} arrived on topic: {topic}",
|
||||
"mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}",
|
||||
"message_received": "Üzenet: {message} érkezett ehhez a témához: {topic}",
|
||||
"mqtt_subscription_failed": "Valami elromlott a következő témára való feliratkozás során: {topic}",
|
||||
"none": "Nincs",
|
||||
"nothing_found": "Semmi sem található ehhez:",
|
||||
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
||||
"published_message": "Published message: {message} to topic: {topic}",
|
||||
"reconnection_error": "Failed to reconnect",
|
||||
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
||||
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
||||
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
||||
"unsubscribed_success": "Successfully unsubscribed from topic: {topic}",
|
||||
"published_error": "Valami elromlott a következő üzenet közzététele során: {topic}, ehhez a témához: {message}",
|
||||
"published_message": "Közzétett üzenet: {message}, ehhez a témához: {topic}",
|
||||
"reconnection_error": "Nem sikerült újrakapcsolódni",
|
||||
"subscribed_failed": "Nem sikerült feliratkozni erre a témára: {topic}",
|
||||
"subscribed_success": "Sikeresen feliratkozott erre a témára: {topic}",
|
||||
"unsubscribed_failed": "Nem sikerült leiratkozni erről a témáról: {topic}",
|
||||
"unsubscribed_success": "Sikeresen leiratkozott erről a témáról: {topic}",
|
||||
"waiting_send_request": "Várakozás a kérés elküldésére"
|
||||
},
|
||||
"support": {
|
||||
@@ -639,7 +641,7 @@
|
||||
"body": "Törzs",
|
||||
"collections": "Gyűjtemények",
|
||||
"documentation": "Dokumentáció",
|
||||
"environments": "Environments",
|
||||
"environments": "Környezetek",
|
||||
"headers": "Fejlécek",
|
||||
"history": "Előzmények",
|
||||
"mqtt": "MQTT",
|
||||
@@ -664,7 +666,7 @@
|
||||
"email_do_not_match": "Az e-mail-cím nem egyezik a fiókja részleteivel. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
||||
"exit": "Kilépés a csapatból",
|
||||
"exit_disabled": "Csak a tulajdonos nem léphet ki a csapatból",
|
||||
"invalid_coll_id": "Invalid collection ID",
|
||||
"invalid_coll_id": "Érvénytelen gyűjteményazonosító",
|
||||
"invalid_email_format": "Az e-mail formátuma érvénytelen",
|
||||
"invalid_id": "Érvénytelen csapatazonosító. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
||||
"invalid_invite_link": "Érvénytelen meghívási hivatkozás",
|
||||
@@ -688,7 +690,7 @@
|
||||
"member_removed": "Felhasználó eltávolítva",
|
||||
"member_role_updated": "Felhasználói szerepek frissítve",
|
||||
"members": "Tagok",
|
||||
"more_members": "+{count} more",
|
||||
"more_members": "+{count} további",
|
||||
"name_length_insufficient": "A csapat nevének legalább 6 karakter hosszúságúnak kell lennie",
|
||||
"name_updated": "Csapatnév frissítve",
|
||||
"new": "Új csapat",
|
||||
@@ -696,13 +698,13 @@
|
||||
"new_name": "Saját új csapat",
|
||||
"no_access": "Nincs szerkesztési jogosultsága ezekhez a gyűjteményekhez",
|
||||
"no_invite_found": "A meghívás nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
||||
"no_request_found": "Request not found.",
|
||||
"no_request_found": "A kérés nem található.",
|
||||
"not_found": "A csapat nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
||||
"not_valid_viewer": "Ön nem érvényes megtekintő. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
||||
"parent_coll_move": "Cannot move collection to a child collection",
|
||||
"parent_coll_move": "Nem lehet áthelyezni a gyűjteményt egy gyermekgyűjteménybe",
|
||||
"pending_invites": "Függőben lévő meghívások",
|
||||
"permissions": "Jogosultságok",
|
||||
"same_target_destination": "Same target and destination",
|
||||
"same_target_destination": "Ugyanaz a cél és célhely",
|
||||
"saved": "Csapat elmentve",
|
||||
"select_a_team": "Csapat kiválasztása",
|
||||
"title": "Csapatok",
|
||||
@@ -710,9 +712,9 @@
|
||||
"we_sent_invite_link_description": "Kérje meg az összes meghívottat, hogy nézzék meg a beérkező leveleiket. Kattintsanak a hivatkozásra a csapathoz való csatlakozáshoz."
|
||||
},
|
||||
"team_environment": {
|
||||
"deleted": "Environment Deleted",
|
||||
"duplicate": "Environment Duplicated",
|
||||
"not_found": "Environment not found."
|
||||
"deleted": "Környezet törölve",
|
||||
"duplicate": "Környezet megkettőzve",
|
||||
"not_found": "A környezet nem található."
|
||||
},
|
||||
"test": {
|
||||
"failed": "teszt sikertelen",
|
||||
@@ -732,9 +734,9 @@
|
||||
"url": "URL"
|
||||
},
|
||||
"workspace": {
|
||||
"change": "Change workspace",
|
||||
"personal": "My Workspace",
|
||||
"team": "Team Workspace",
|
||||
"title": "Workspaces"
|
||||
"change": "Munkaterület váltása",
|
||||
"personal": "Saját munkaterület",
|
||||
"team": "Csapat-munkaterület",
|
||||
"title": "Munkaterületek"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "Lihat tautan saya"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Response Body",
|
||||
"filter_response_body": "Filter body respons JSON (menggunakan sintaks JSONPath)",
|
||||
"headers": "Headers",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Waktu",
|
||||
"title": "Response",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "Menunggu koneksi",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Corpo della risposta",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Intestazioni",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Stato",
|
||||
"time": "Tempo impiegato",
|
||||
"title": "Risposta",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "In attesa di connessione",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "自分のリンクを見る"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "レスポンスボディ",
|
||||
"filter_response_body": "JSONレスポンスボディをフィルタ (JSONPathシンタックスを使用)",
|
||||
"headers": "ヘッダー",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "ステータス",
|
||||
"time": "時間",
|
||||
"title": "レスポンス",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "接続を待っています",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "내 링크 보기"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "응답 본문",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "헤더",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "상태",
|
||||
"time": "시간",
|
||||
"title": "제목",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "연결 대기 중",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Reactie inhoud",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Headers",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Tijd",
|
||||
"title": "Antwoord",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "wachten op verbinding",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Svarkropp",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Overskrifter",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Tid",
|
||||
"title": "Respons",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "venter på tilkobling",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Ciało odpowiedzi",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Nagłówki",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Czas",
|
||||
"title": "Odpowiedź",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "oczekiwanie na połączenie",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Corpo de Resposta",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Cabeçalhos",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Tempo",
|
||||
"title": "Resposta",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "aguardando conexão",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Corpo de Resposta",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Cabeçalhos",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Tempo",
|
||||
"title": "Resposta",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "aguardando conexão",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "Vizualizare link-uri"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Corpul de răspuns",
|
||||
"filter_response_body": "Filtrează corpul răspunsului JSON (folosește sintaxa JSONPath)",
|
||||
"headers": "Anteturi",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Stare",
|
||||
"time": "Timp",
|
||||
"title": "Raspuns",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "Așteptând conexiunea",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Тело ответа",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Заголовки",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Статус",
|
||||
"time": "Время",
|
||||
"title": "Ответ",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "Ожидание соединения",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Тело за одговор",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Заглавља",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Статус",
|
||||
"time": "време",
|
||||
"title": "Одговор",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "чека везу",
|
||||
"xml": "КСМЛ"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Svarskommitté",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Rubriker",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Tid",
|
||||
"title": "Svar",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "väntar på anslutning",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"edit": "Koleksiyonu düzenle",
|
||||
"invalid_name": "Lütfen koleksiyon için geçerli bir ad girin",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
"moved": "Başarıyla taşındı",
|
||||
"my_collections": "Koleksiyonlarım",
|
||||
"name": "Yeni Koleksiyonum",
|
||||
"name_length_insufficient": "Koleksiyon adı en az 3 karakter uzunluğunda olmalıdır",
|
||||
@@ -147,7 +147,7 @@
|
||||
"remove_team": "Bu takımı silmek istediğinizden emin misiniz?",
|
||||
"remove_telemetry": "Telemetriden çıkmak istediğinizden emin misiniz?",
|
||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||
"save_unsaved_tab": "Bu sekmede yapılan değişiklikleri kaydetmek istiyor musunuz?",
|
||||
"sync": "Bu çalışma alanını senkronize etmek istediğinizden emin misiniz?"
|
||||
},
|
||||
"count": {
|
||||
@@ -368,9 +368,9 @@
|
||||
},
|
||||
"profile": {
|
||||
"app_settings": "Uygulama ayarları",
|
||||
"default_hopp_displayname": "Unnamed User",
|
||||
"editor": "Düzenleyici",
|
||||
"editor_description": "Editors can add, edit, and delete requests.",
|
||||
"default_hopp_displayname": "Adsız Kullanıcı",
|
||||
"editor": "Editör",
|
||||
"editor_description": "Editörler istekleri ekleyebilir, düzenleyebilir ve silebilir.",
|
||||
"email_verification_mail": "Doğrulama bağlantısı e-postanıza gönderildi. E-postanızı doğrulamak için gelen bağlantıya tıklayınız.",
|
||||
"no_permission": "Bu eylemi gerçekleştirmek için gerekli yetkiniz yok.",
|
||||
"owner": "Kurucu",
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Yanıt gövdesi",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Başlıklar",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Durum",
|
||||
"time": "Zaman",
|
||||
"title": "Cevap",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "Bağlantı için bekleniyor",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"edit": "編輯",
|
||||
"filter": "篩選回應",
|
||||
"go_back": "返回",
|
||||
"go_forward": "Go forward",
|
||||
"go_forward": "向前",
|
||||
"group_by": "分組方式",
|
||||
"label": "標籤",
|
||||
"learn_more": "瞭解更多",
|
||||
@@ -117,37 +117,37 @@
|
||||
"username": "使用者名稱"
|
||||
},
|
||||
"collection": {
|
||||
"created": "組合已建立",
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"edit": "編輯組合",
|
||||
"invalid_name": "請提供有效的組合名稱",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
"my_collections": "我的組合",
|
||||
"name": "我的新組合",
|
||||
"name_length_insufficient": "組合名稱至少要有 3 個字元。",
|
||||
"new": "建立組合",
|
||||
"order_changed": "Collection Order Updated",
|
||||
"renamed": "組合已重新命名",
|
||||
"created": "集合已建立",
|
||||
"different_parent": "無法為父集合不同的集合重新排序",
|
||||
"edit": "編輯集合",
|
||||
"invalid_name": "請提供有效的集合名稱",
|
||||
"invalid_root_move": "集合已在根目錄",
|
||||
"moved": "移動成功",
|
||||
"my_collections": "我的集合",
|
||||
"name": "我的新集合",
|
||||
"name_length_insufficient": "集合名稱至少要有 3 個字元。",
|
||||
"new": "建立集合",
|
||||
"order_changed": "集合順序已更新",
|
||||
"renamed": "集合已重新命名",
|
||||
"request_in_use": "請求正在使用中",
|
||||
"save_as": "另存為",
|
||||
"select": "選擇一個組合",
|
||||
"select": "選擇一個集合",
|
||||
"select_location": "選擇位置",
|
||||
"select_team": "選擇一個團隊",
|
||||
"team_collections": "團隊組合"
|
||||
"team_collections": "團隊集合"
|
||||
},
|
||||
"confirm": {
|
||||
"exit_team": "您確定要離開此團隊嗎?",
|
||||
"logout": "您確定要登出嗎?",
|
||||
"remove_collection": "您確定要永久刪除該組合嗎?",
|
||||
"remove_collection": "您確定要永久刪除該集合嗎?",
|
||||
"remove_environment": "您確定要永久刪除該環境嗎?",
|
||||
"remove_folder": "您確定要永久刪除該資料夾嗎?",
|
||||
"remove_history": "您確定要永久刪除全部歷史記錄嗎?",
|
||||
"remove_request": "您確定要永久刪除該請求嗎?",
|
||||
"remove_team": "您確定要刪除該團隊嗎?",
|
||||
"remove_telemetry": "您確定要退出遙測服務嗎?",
|
||||
"request_change": "您確定要捨棄當前請求嗎?未儲存的變更將遺失。",
|
||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||
"request_change": "您確定要捨棄目前的請求嗎?未儲存的變更將遺失。",
|
||||
"save_unsaved_tab": "您要儲存在此分頁做出的改動嗎?",
|
||||
"sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。"
|
||||
},
|
||||
"count": {
|
||||
@@ -160,13 +160,13 @@
|
||||
},
|
||||
"documentation": {
|
||||
"generate": "產生文件",
|
||||
"generate_message": "匯入 Hoppscotch 組合以隨時隨地產生 API 文件。"
|
||||
"generate_message": "匯入 Hoppscotch 集合以隨時隨地產生 API 文件。"
|
||||
},
|
||||
"empty": {
|
||||
"authorization": "該請求沒有使用任何授權",
|
||||
"body": "該請求沒有任何請求主體",
|
||||
"collection": "組合為空",
|
||||
"collections": "組合為空",
|
||||
"collection": "集合為空",
|
||||
"collections": "集合為空",
|
||||
"documentation": "連線到 GraphQL 端點以檢視文件",
|
||||
"endpoint": "端點不能留空",
|
||||
"environments": "環境為空",
|
||||
@@ -209,7 +209,7 @@
|
||||
"browser_support_sse": "此瀏覽器似乎不支援 SSE。",
|
||||
"check_console_details": "檢查控制台日誌以獲悉詳情",
|
||||
"curl_invalid_format": "cURL 格式不正確",
|
||||
"danger_zone": "Danger zone",
|
||||
"danger_zone": "危險地帶",
|
||||
"delete_account": "您的帳號目前為這些團隊的擁有者:",
|
||||
"delete_account_description": "您在刪除帳號前必須先將您自己從團隊中移除、轉移擁有權,或是刪除團隊。",
|
||||
"empty_req_name": "空請求名稱",
|
||||
@@ -277,38 +277,38 @@
|
||||
"tests": "編寫測試指令碼以自動除錯。"
|
||||
},
|
||||
"hide": {
|
||||
"collection": "隱藏組合面板",
|
||||
"collection": "隱藏集合面板",
|
||||
"more": "隱藏更多",
|
||||
"preview": "隱藏預覽",
|
||||
"sidebar": "隱藏側邊欄"
|
||||
},
|
||||
"import": {
|
||||
"collections": "匯入組合",
|
||||
"collections": "匯入集合",
|
||||
"curl": "匯入 cURL",
|
||||
"failed": "匯入失敗",
|
||||
"from_gist": "從 Gist 匯入",
|
||||
"from_gist_description": "從 Gist 網址匯入",
|
||||
"from_insomnia": "從 Insomnia 匯入",
|
||||
"from_insomnia_description": "從 Insomnia 組合匯入",
|
||||
"from_insomnia_description": "從 Insomnia 集合匯入",
|
||||
"from_json": "從 Hoppscotch 匯入",
|
||||
"from_json_description": "從 Hoppscotch 組合檔匯入",
|
||||
"from_my_collections": "從我的組合匯入",
|
||||
"from_my_collections_description": "從我的組合檔匯入",
|
||||
"from_json_description": "從 Hoppscotch 集合檔匯入",
|
||||
"from_my_collections": "從我的集合匯入",
|
||||
"from_my_collections_description": "從我的集合檔匯入",
|
||||
"from_openapi": "從 OpenAPI 匯入",
|
||||
"from_openapi_description": "從 OpenAPI 規格檔 (YML/JSON) 匯入",
|
||||
"from_postman": "從 Postman 匯入",
|
||||
"from_postman_description": "從 Postman 組合匯入",
|
||||
"from_postman_description": "從 Postman 集合匯入",
|
||||
"from_url": "從網址匯入",
|
||||
"gist_url": "輸入 Gist 網址",
|
||||
"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_success": "已匯入組合",
|
||||
"json_description": "從 Hoppscotch 組合 JSON 檔匯入組合",
|
||||
"import_from_url_success": "已匯入集合",
|
||||
"json_description": "從 Hoppscotch 集合 JSON 檔匯入集合",
|
||||
"title": "匯入"
|
||||
},
|
||||
"layout": {
|
||||
"collapse_collection": "隱藏或顯示組合",
|
||||
"collapse_collection": "隱藏或顯示集合",
|
||||
"collapse_sidebar": "隱藏或顯示側邊欄",
|
||||
"column": "垂直版面",
|
||||
"name": "配置",
|
||||
@@ -316,8 +316,8 @@
|
||||
"zen_mode": "專注模式"
|
||||
},
|
||||
"modal": {
|
||||
"close_unsaved_tab": "You have unsaved changes",
|
||||
"collections": "組合",
|
||||
"close_unsaved_tab": "您有未儲存的改動",
|
||||
"collections": "集合",
|
||||
"confirm": "確認",
|
||||
"edit_request": "編輯請求",
|
||||
"import_export": "匯入/匯出"
|
||||
@@ -374,9 +374,9 @@
|
||||
"email_verification_mail": "已將驗證信寄送至您的電子郵件地址。請點擊信中連結以驗證您的電子郵件地址。",
|
||||
"no_permission": "您沒有權限執行此操作。",
|
||||
"owner": "擁有者",
|
||||
"owner_description": "擁有者可以新增、編輯和刪除請求、組合和團隊成員。",
|
||||
"owner_description": "擁有者可以新增、編輯和刪除請求、集合和團隊成員。",
|
||||
"roles": "角色",
|
||||
"roles_description": "角色用來控制對共用組合的存取權。",
|
||||
"roles_description": "角色用來控制對共用集合的存取權。",
|
||||
"updated": "已更新個人檔案",
|
||||
"viewer": "檢視者",
|
||||
"viewer_description": "檢視者只能檢視和使用請求。"
|
||||
@@ -396,8 +396,8 @@
|
||||
"text": "文字"
|
||||
},
|
||||
"copy_link": "複製連結",
|
||||
"different_collection": "Cannot reorder requests from different collections",
|
||||
"duplicated": "Request duplicated",
|
||||
"different_collection": "無法重新排列來自不同集合的請求",
|
||||
"duplicated": "已複製請求",
|
||||
"duration": "持續時間",
|
||||
"enter_curl": "輸入 cURL",
|
||||
"generate_code": "產生程式碼",
|
||||
@@ -405,10 +405,10 @@
|
||||
"header_list": "請求標頭列表",
|
||||
"invalid_name": "請提供請求名稱",
|
||||
"method": "方法",
|
||||
"moved": "Request moved",
|
||||
"moved": "已移動請求",
|
||||
"name": "請求名稱",
|
||||
"new": "新請求",
|
||||
"order_changed": "Request Order Updated",
|
||||
"order_changed": "已更新請求順序",
|
||||
"override": "覆寫",
|
||||
"override_help": "在標頭設置 <kbd>Content-Type</kbd>",
|
||||
"overriden": "已覆寫",
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "檢視我的連結"
|
||||
},
|
||||
"response": {
|
||||
"audio": "音訊",
|
||||
"body": "回應本體",
|
||||
"filter_response_body": "篩選 JSON 回應本體 (使用 JSONPath 語法)",
|
||||
"headers": "回應標頭",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "狀態",
|
||||
"time": "時間",
|
||||
"title": "回應",
|
||||
"video": "視訊",
|
||||
"waiting_for_connection": "等待連線",
|
||||
"xml": "XML"
|
||||
},
|
||||
@@ -492,7 +494,7 @@
|
||||
"short_codes_description": "我們為您打造的快捷碼。",
|
||||
"sidebar_on_left": "左側邊欄",
|
||||
"sync": "同步",
|
||||
"sync_collections": "組合",
|
||||
"sync_collections": "集合",
|
||||
"sync_description": "這些設定會同步到雲端。",
|
||||
"sync_environments": "環境",
|
||||
"sync_history": "歷史",
|
||||
@@ -549,7 +551,7 @@
|
||||
"previous_method": "選擇上一個方法",
|
||||
"put_method": "選擇 PUT 方法",
|
||||
"reset_request": "重置請求",
|
||||
"save_to_collections": "儲存到組合",
|
||||
"save_to_collections": "儲存到集合",
|
||||
"send_request": "傳送請求",
|
||||
"title": "請求"
|
||||
},
|
||||
@@ -568,7 +570,7 @@
|
||||
},
|
||||
"show": {
|
||||
"code": "顯示程式碼",
|
||||
"collection": "顯示組合面板",
|
||||
"collection": "顯示集合面板",
|
||||
"more": "顯示更多",
|
||||
"sidebar": "顯示側邊欄"
|
||||
},
|
||||
@@ -637,9 +639,9 @@
|
||||
"tab": {
|
||||
"authorization": "授權",
|
||||
"body": "請求本體",
|
||||
"collections": "組合",
|
||||
"collections": "集合",
|
||||
"documentation": "幫助文件",
|
||||
"environments": "Environments",
|
||||
"environments": "環境",
|
||||
"headers": "請求標頭",
|
||||
"history": "歷史記錄",
|
||||
"mqtt": "MQTT",
|
||||
@@ -664,7 +666,7 @@
|
||||
"email_do_not_match": "電子信箱與您的帳號資料不一致。請聯絡您的團隊擁有者。",
|
||||
"exit": "退出團隊",
|
||||
"exit_disabled": "團隊擁有者無法退出團隊",
|
||||
"invalid_coll_id": "Invalid collection ID",
|
||||
"invalid_coll_id": "集合 ID 無效",
|
||||
"invalid_email_format": "電子信箱格式無效",
|
||||
"invalid_id": "團隊 ID 無效。請聯絡您的團隊擁有者。",
|
||||
"invalid_invite_link": "邀請連結無效",
|
||||
@@ -688,21 +690,21 @@
|
||||
"member_removed": "使用者已移除",
|
||||
"member_role_updated": "使用者角色已更新",
|
||||
"members": "成員",
|
||||
"more_members": "+{count} more",
|
||||
"more_members": "還有 {count} 位",
|
||||
"name_length_insufficient": "團隊名稱至少為 6 個字元",
|
||||
"name_updated": "團隊名稱已更新",
|
||||
"new": "新團隊",
|
||||
"new_created": "已建立新團隊",
|
||||
"new_name": "我的新團隊",
|
||||
"no_access": "您沒有編輯組合的許可權",
|
||||
"no_access": "您沒有編輯集合的許可權",
|
||||
"no_invite_found": "未找到邀請。請聯絡您的團隊擁有者。",
|
||||
"no_request_found": "Request not found.",
|
||||
"no_request_found": "找不到請求。",
|
||||
"not_found": "找不到團隊。請聯絡您的團隊擁有者。",
|
||||
"not_valid_viewer": "您不是一個有效的檢視者。請聯絡您的團隊擁有者。",
|
||||
"parent_coll_move": "Cannot move collection to a child collection",
|
||||
"parent_coll_move": "無法將集合移動至子集合",
|
||||
"pending_invites": "待定邀請",
|
||||
"permissions": "許可權",
|
||||
"same_target_destination": "Same target and destination",
|
||||
"same_target_destination": "目標和目的地相同",
|
||||
"saved": "團隊已儲存",
|
||||
"select_a_team": "選擇團隊",
|
||||
"title": "團隊",
|
||||
@@ -732,9 +734,9 @@
|
||||
"url": "網址"
|
||||
},
|
||||
"workspace": {
|
||||
"change": "Change workspace",
|
||||
"personal": "My Workspace",
|
||||
"team": "Team Workspace",
|
||||
"title": "Workspaces"
|
||||
"change": "切換工作區",
|
||||
"personal": "我的工作區",
|
||||
"team": "團隊工作區",
|
||||
"title": "工作區"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "Переглянути мої посилання"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Орган реагування",
|
||||
"filter_response_body": "Фільтр тіла відповідей JSON (використовує синтаксис JSONPath)",
|
||||
"headers": "Заголовки",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Статус",
|
||||
"time": "Час",
|
||||
"title": "Відповідь",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "очікування підключення",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"cancel": "Hủy bỏ",
|
||||
"choose_file": "Chọn một tệp",
|
||||
"clear": "Thông thoáng",
|
||||
"clear_all": "Quet sạch tât cả",
|
||||
"clear_all": "Quet sạch tất cả",
|
||||
"close": "Close",
|
||||
"connect": "Liên kết",
|
||||
"connecting": "Connecting",
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Cơ quan phản hồi",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Tiêu đề",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Tình trạng",
|
||||
"time": "Thời gian",
|
||||
"title": "Phản ứng",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "Đang đợi kết nối",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hoppscotch/common",
|
||||
"private": true,
|
||||
"version": "2023.4.4",
|
||||
"version": "2023.4.7",
|
||||
"scripts": {
|
||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||
"dev:vite": "vite",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 12 KiB |
@@ -86,6 +86,7 @@ declare module '@vue/runtime-core' {
|
||||
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
||||
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
||||
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
||||
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
|
||||
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
||||
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
||||
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
||||
@@ -134,11 +135,13 @@ declare module '@vue/runtime-core' {
|
||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
||||
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
|
||||
LensesRenderersHTMLLensRenderer: typeof import('./components/lenses/renderers/HTMLLensRenderer.vue')['default']
|
||||
LensesRenderersImageLensRenderer: typeof import('./components/lenses/renderers/ImageLensRenderer.vue')['default']
|
||||
LensesRenderersJSONLensRenderer: typeof import('./components/lenses/renderers/JSONLensRenderer.vue')['default']
|
||||
LensesRenderersPDFLensRenderer: typeof import('./components/lenses/renderers/PDFLensRenderer.vue')['default']
|
||||
LensesRenderersRawLensRenderer: typeof import('./components/lenses/renderers/RawLensRenderer.vue')['default']
|
||||
LensesRenderersVideoLensRenderer: typeof import('./components/lenses/renderers/VideoLensRenderer.vue')['default']
|
||||
LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default']
|
||||
LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default']
|
||||
ProfileShortcode: typeof import('./components/profile/Shortcode.vue')['default']
|
||||
|
||||
@@ -44,8 +44,9 @@
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||
<span class="my-2 text-center">
|
||||
{{ t("state.nothing_found") }} "{{ filterText }}"
|
||||
<span class="my-2 text-center flex flex-col">
|
||||
{{ t("state.nothing_found") }}
|
||||
<span class="break-all">"{{ filterText }}"</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -284,6 +284,14 @@ const importerAction = async (stepResults: StepReturnValue[]) => {
|
||||
emit("import-to-teams", result)
|
||||
} else {
|
||||
appendRESTCollections(result)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_COLLECTION",
|
||||
importer: importerModule.value!.name,
|
||||
platform: "rest",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
fileImported()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 bg-primary">
|
||||
<div class="flex flex-col flex-1">
|
||||
<div
|
||||
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
|
||||
:style="
|
||||
|
||||
@@ -89,6 +89,7 @@ import {
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import { computedWithControl } from "@vueuse/core"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -223,6 +224,13 @@ const saveRequestAs = async () => {
|
||||
},
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
createdNow: true,
|
||||
platform: "rest",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "my-folder") {
|
||||
if (!isHoppRESTRequest(requestUpdated))
|
||||
@@ -243,6 +251,13 @@ const saveRequestAs = async () => {
|
||||
},
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
createdNow: true,
|
||||
platform: "rest",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "my-request") {
|
||||
if (!isHoppRESTRequest(requestUpdated))
|
||||
@@ -264,17 +279,38 @@ const saveRequestAs = async () => {
|
||||
},
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
createdNow: false,
|
||||
platform: "rest",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "teams-collection") {
|
||||
if (!isHoppRESTRequest(requestUpdated))
|
||||
throw new Error("requestUpdated is not a REST Request")
|
||||
|
||||
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") {
|
||||
if (!isHoppRESTRequest(requestUpdated))
|
||||
throw new Error("requestUpdated is not a REST Request")
|
||||
|
||||
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") {
|
||||
if (!isHoppRESTRequest(requestUpdated))
|
||||
throw new Error("requestUpdated is not a REST Request")
|
||||
@@ -292,6 +328,13 @@ const saveRequestAs = async () => {
|
||||
title: requestUpdated.name,
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
createdNow: false,
|
||||
platform: "rest",
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
pipe(
|
||||
updateTeamRequest(picked.value.requestID, data),
|
||||
TE.match(
|
||||
@@ -313,6 +356,13 @@ const saveRequestAs = async () => {
|
||||
requestUpdated as HoppGQLRequest
|
||||
)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
createdNow: false,
|
||||
platform: "gql",
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "gql-my-folder") {
|
||||
// TODO: Check for GQL request ?
|
||||
@@ -321,6 +371,13 @@ const saveRequestAs = async () => {
|
||||
requestUpdated as HoppGQLRequest
|
||||
)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
createdNow: true,
|
||||
platform: "gql",
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "gql-my-collection") {
|
||||
// TODO: Check for GQL request ?
|
||||
@@ -329,6 +386,13 @@ const saveRequestAs = async () => {
|
||||
requestUpdated as HoppGQLRequest
|
||||
)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
createdNow: true,
|
||||
platform: "gql",
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
requestSaved()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 bg-primary">
|
||||
<div class="flex flex-col flex-1">
|
||||
<div
|
||||
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
|
||||
:style="
|
||||
|
||||
@@ -46,6 +46,7 @@ import { useToast } from "@composables/toast"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { HoppGQLRequest, makeCollection } from "@hoppscotch/data"
|
||||
import { addGraphqlCollection } from "~/newstore/collections"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -79,6 +80,13 @@ export default defineComponent({
|
||||
)
|
||||
|
||||
this.hideModal()
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_COLLECTION",
|
||||
isRootCollection: true,
|
||||
platform: "gql",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
},
|
||||
hideModal() {
|
||||
this.name = null
|
||||
|
||||
@@ -244,6 +244,14 @@ const importFromJSON = () => {
|
||||
return
|
||||
}
|
||||
appendGraphqlCollections(collections)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_COLLECTION",
|
||||
importer: "json",
|
||||
workspaceType: "personal",
|
||||
platform: "gql",
|
||||
})
|
||||
|
||||
fileImported()
|
||||
}
|
||||
reader.readAsText(inputChooseFileToImportFrom.value.files[0])
|
||||
@@ -257,6 +265,12 @@ const exportJSON = () => {
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
|
||||
platform?.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "json",
|
||||
platform: "gql",
|
||||
})
|
||||
|
||||
// TODO: get uri from meta
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
document.body.appendChild(a)
|
||||
|
||||
@@ -153,6 +153,7 @@ import IconArchive from "~icons/lucide/archive"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -285,6 +286,13 @@ export default defineComponent({
|
||||
response: "",
|
||||
})
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
platform: "gql",
|
||||
createdNow: true,
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
this.displayModalAddRequest(false)
|
||||
},
|
||||
addRequest(payload) {
|
||||
@@ -294,6 +302,14 @@ export default defineComponent({
|
||||
},
|
||||
onAddFolder({ name, path }) {
|
||||
addGraphqlFolder(name, path)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_COLLECTION",
|
||||
isRootCollection: false,
|
||||
platform: "gql",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
this.displayModalAddFolder(false)
|
||||
},
|
||||
addFolder(payload) {
|
||||
|
||||
@@ -125,8 +125,8 @@
|
||||
@hide-modal="displayModalEditFolder(false)"
|
||||
/>
|
||||
<CollectionsEditRequest
|
||||
v-model="editingRequestName"
|
||||
:show="showModalEditRequest"
|
||||
:model-value="editingRequest ? editingRequest.name : ''"
|
||||
:loading-state="modalLoadingState"
|
||||
@submit="updateEditingRequest"
|
||||
@hide-modal="displayModalEditRequest(false)"
|
||||
@@ -157,7 +157,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, PropType, ref, watch } from "vue"
|
||||
import { computed, nextTick, PropType, ref, watch } from "vue"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { Picked } from "~/helpers/types/HoppPicked"
|
||||
@@ -288,6 +288,7 @@ const editingFolder = ref<
|
||||
const editingFolderName = ref<string | null>(null)
|
||||
const editingFolderPath = ref<string | null>(null)
|
||||
const editingRequest = ref<HoppRESTRequest | null>(null)
|
||||
const editingRequestName = ref("")
|
||||
const editingRequestIndex = ref<number | null>(null)
|
||||
const editingRequestID = ref<string | null>(null)
|
||||
|
||||
@@ -598,11 +599,25 @@ const addNewRootCollection = (name: string) => {
|
||||
})
|
||||
)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_COLLECTION",
|
||||
platform: "rest",
|
||||
workspaceType: "personal",
|
||||
isRootCollection: true,
|
||||
})
|
||||
|
||||
displayModalAdd(false)
|
||||
} else if (hasTeamWriteAccess.value) {
|
||||
if (!collectionsType.value.selectedTeam) return
|
||||
modalLoadingState.value = true
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_COLLECTION",
|
||||
platform: "rest",
|
||||
workspaceType: "team",
|
||||
isRootCollection: true,
|
||||
})
|
||||
|
||||
pipe(
|
||||
createNewRootCollection(name, collectionsType.value.selectedTeam.id),
|
||||
TE.match(
|
||||
@@ -651,6 +666,13 @@ const onAddRequest = (requestName: string) => {
|
||||
},
|
||||
})
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
workspaceType: "personal",
|
||||
createdNow: true,
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
displayModalAddRequest(false)
|
||||
} else if (hasTeamWriteAccess.value) {
|
||||
const folder = editingFolder.value
|
||||
@@ -666,6 +688,13 @@ const onAddRequest = (requestName: string) => {
|
||||
title: requestName,
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
workspaceType: "team",
|
||||
platform: "rest",
|
||||
createdNow: true,
|
||||
})
|
||||
|
||||
pipe(
|
||||
createRequestInCollection(folder.id, data),
|
||||
TE.match(
|
||||
@@ -711,6 +740,14 @@ const onAddFolder = (folderName: string) => {
|
||||
if (collectionsType.value.type === "my-collections") {
|
||||
if (!path) return
|
||||
addRESTFolder(folderName, path)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_COLLECTION",
|
||||
workspaceType: "personal",
|
||||
isRootCollection: false,
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
displayModalAddFolder(false)
|
||||
} else if (hasTeamWriteAccess.value) {
|
||||
const folder = editingFolder.value
|
||||
@@ -718,6 +755,13 @@ const onAddFolder = (folderName: string) => {
|
||||
|
||||
modalLoadingState.value = true
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_COLLECTION",
|
||||
workspaceType: "personal",
|
||||
isRootCollection: false,
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
pipe(
|
||||
createChildCollection(folderName, folder.id),
|
||||
TE.match(
|
||||
@@ -860,6 +904,7 @@ const editRequest = (payload: {
|
||||
}) => {
|
||||
const { folderPath, requestIndex, request } = payload
|
||||
editingRequest.value = request
|
||||
editingRequestName.value = request.name ?? ""
|
||||
if (collectionsType.value.type === "my-collections" && folderPath) {
|
||||
editingFolderPath.value = folderPath
|
||||
editingRequestIndex.value = parseInt(requestIndex)
|
||||
@@ -893,6 +938,9 @@ const updateEditingRequest = (newName: string) => {
|
||||
|
||||
if (possibleActiveTab) {
|
||||
possibleActiveTab.value.document.request.name = requestUpdated.name
|
||||
nextTick(() => {
|
||||
possibleActiveTab.value.document.isDirty = false
|
||||
})
|
||||
}
|
||||
|
||||
displayModalEditRequest(false)
|
||||
@@ -931,6 +979,9 @@ const updateEditingRequest = (newName: string) => {
|
||||
|
||||
if (possibleTab) {
|
||||
possibleTab.value.document.request.name = requestName
|
||||
nextTick(() => {
|
||||
possibleTab.value.document.isDirty = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1876,6 +1927,12 @@ const exportData = async (
|
||||
}
|
||||
|
||||
const exportJSONCollection = async () => {
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "json",
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
await getJSONCollection()
|
||||
|
||||
initializeDownloadCollection(collectionJSON.value, null)
|
||||
@@ -1887,6 +1944,12 @@ const createCollectionGist = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "gist",
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
creatingGistCollection.value = true
|
||||
await getJSONCollection()
|
||||
|
||||
@@ -1917,6 +1980,12 @@ const importToTeams = async (collection: HoppCollection<HoppRESTRequest>[]) => {
|
||||
|
||||
importingMyCollections.value = true
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "import-to-teams",
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
pipe(
|
||||
importJSONToTeam(
|
||||
JSON.stringify(collection),
|
||||
|
||||
@@ -190,6 +190,12 @@ const createEnvironmentGist = async () => {
|
||||
)
|
||||
|
||||
toast.success(t("export.gist_created").toString())
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_ENVIRONMENT",
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
window.open(res.data.html_url)
|
||||
} catch (e) {
|
||||
toast.error(t("error.something_went_wrong").toString())
|
||||
@@ -249,6 +255,13 @@ const openDialogChooseFileToImportFrom = () => {
|
||||
|
||||
const importToTeams = async (content: Environment[]) => {
|
||||
loading.value = true
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||
platform: "rest",
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
for (const [i, env] of content.entries()) {
|
||||
if (i === content.length - 1) {
|
||||
await pipe(
|
||||
@@ -301,6 +314,12 @@ const importFromJSON = () => {
|
||||
return
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||
platform: "rest",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = ({ target }) => {
|
||||
@@ -352,6 +371,7 @@ const importFromPostman = ({
|
||||
const environment: Environment = { name, variables: [] }
|
||||
values.forEach(({ key, value }) => environment.variables.push({ key, value }))
|
||||
const environments = [environment]
|
||||
|
||||
importFromHoppscotch(environments)
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
/>
|
||||
<HoppSmartTabs
|
||||
v-model="selectedEnvTab"
|
||||
styles="sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-0 top-0 bg-primary"
|
||||
styles="sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-10 top-0 bg-primary"
|
||||
render-inactive-tabs
|
||||
>
|
||||
<HoppSmartTab
|
||||
@@ -97,7 +97,7 @@
|
||||
<HoppSmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<div v-if="isTeamSelected" class="flex flex-col">
|
||||
<div v-else-if="isTeamSelected" class="flex flex-col">
|
||||
<HoppSmartItem
|
||||
v-for="(gen, index) in teamEnvironmentList"
|
||||
:key="`gen-team-${index}`"
|
||||
@@ -161,10 +161,14 @@ import {
|
||||
selectedEnvironmentIndex$,
|
||||
setSelectedEnvironmentIndex,
|
||||
} from "~/newstore/environments"
|
||||
import { workspaceStatus$ } from "~/newstore/workspace"
|
||||
import { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
|
||||
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { useLocalState } from "~/newstore/localstate"
|
||||
import { onLoggedIn } from "~/composables/auth"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
const mdAndLarger = breakpoints.greater("md")
|
||||
@@ -213,6 +217,38 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
// TeamList-Adapter
|
||||
const teamListAdapter = new TeamListAdapter(true)
|
||||
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
|
||||
const teamListFetched = ref(false)
|
||||
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
|
||||
|
||||
onLoggedIn(() => {
|
||||
!teamListAdapter.isInitialized && teamListAdapter.initialize()
|
||||
})
|
||||
|
||||
const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
|
||||
REMEMBERED_TEAM_ID.value = team.id
|
||||
changeWorkspace({
|
||||
teamID: team.id,
|
||||
teamName: team.name,
|
||||
type: "team",
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => myTeams.value,
|
||||
(newTeams) => {
|
||||
if (newTeams && !teamListFetched.value) {
|
||||
teamListFetched.value = true
|
||||
if (REMEMBERED_TEAM_ID.value) {
|
||||
const team = newTeams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
|
||||
if (team) switchToTeamWorkspace(team)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const selectedEnv = computed(() => {
|
||||
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
||||
return {
|
||||
|
||||
@@ -148,6 +148,7 @@ import { useToast } from "@composables/toast"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { environmentsStore } from "~/newstore/environments"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
type EnvironmentVariable = {
|
||||
id: number
|
||||
@@ -311,6 +312,11 @@ const saveEnvironment = () => {
|
||||
index: envList.value.length - 1,
|
||||
})
|
||||
toast.success(`${t("environment.created")}`)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_ENVIRONMENT",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
} else if (props.editingEnvironmentIndex === "Global") {
|
||||
// Editing the Global environment
|
||||
setGlobalEnvVariables(environmentUpdated.variables)
|
||||
|
||||
@@ -156,6 +156,7 @@ import IconTrash from "~icons/lucide/trash"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconDone from "~icons/lucide/check"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
type EnvironmentVariable = {
|
||||
id: number
|
||||
@@ -294,6 +295,11 @@ const saveEnvironment = async () => {
|
||||
)
|
||||
|
||||
if (props.action === "new") {
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_ENVIRONMENT",
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
await pipe(
|
||||
createTeamEnvironment(
|
||||
JSON.stringify(filterdVariables),
|
||||
|
||||
@@ -143,33 +143,51 @@
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="basicUsername"
|
||||
:environment-highlights="false"
|
||||
:placeholder="t('authorization.username')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="basicPassword"
|
||||
:environment-highlights="false"
|
||||
:placeholder="t('authorization.password')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="authType === 'bearer'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="bearerToken" placeholder="Token" />
|
||||
<SmartEnvInput
|
||||
v-model="bearerToken"
|
||||
:environment-highlights="false"
|
||||
placeholder="Token"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="authType === 'oauth-2'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="oauth2Token" placeholder="Token" />
|
||||
<SmartEnvInput
|
||||
v-model="oauth2Token"
|
||||
:environment-highlights="false"
|
||||
placeholder="Token"
|
||||
/>
|
||||
</div>
|
||||
<HttpOAuth2Authorization />
|
||||
</div>
|
||||
<div v-if="authType === 'api-key'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="apiKey" placeholder="Key" />
|
||||
<SmartEnvInput
|
||||
v-model="apiKey"
|
||||
:environment-highlights="false"
|
||||
placeholder="Key"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="apiValue" placeholder="Value" />
|
||||
<SmartEnvInput
|
||||
v-model="apiValue"
|
||||
:environment-highlights="false"
|
||||
placeholder="Value"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center border-b border-dividerLight">
|
||||
<span class="flex items-center">
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<HoppButtonPrimary
|
||||
id="get"
|
||||
name="get"
|
||||
:loading="isLoading"
|
||||
:label="!connected ? t('action.connect') : t('action.disconnect')"
|
||||
class="w-32"
|
||||
@click="onConnectClick"
|
||||
@@ -31,7 +32,12 @@ import { GQLConnection } from "~/helpers/GQLConnection"
|
||||
import { getCurrentStrategyID } from "~/helpers/network"
|
||||
import { useReadonlyStream, useStream } from "@composables/stream"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { gqlHeaders$, gqlURL$, setGQLURL } from "~/newstore/GQLSession"
|
||||
import {
|
||||
gqlAuth$,
|
||||
gqlHeaders$,
|
||||
gqlURL$,
|
||||
setGQLURL,
|
||||
} from "~/newstore/GQLSession"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -40,15 +46,21 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const connected = useReadonlyStream(props.conn.connected$, false)
|
||||
const isLoading = useReadonlyStream(props.conn.isLoading$, false)
|
||||
const headers = useReadonlyStream(gqlHeaders$, [])
|
||||
const auth = useReadonlyStream(gqlAuth$, {
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
})
|
||||
|
||||
const url = useStream(gqlURL$, "", setGQLURL)
|
||||
|
||||
const onConnectClick = () => {
|
||||
if (!connected.value) {
|
||||
props.conn.connect(url.value, headers.value as any)
|
||||
props.conn.connect(url.value, headers.value as any, auth.value)
|
||||
|
||||
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "graphql-schema",
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
|
||||
@@ -748,7 +748,8 @@ const runQuery = async () => {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "graphql-query",
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
|
||||
@@ -63,7 +63,7 @@ import { GQLHistoryEntry } from "~/newstore/history"
|
||||
import { shortDateTime } from "~/helpers/utils/date"
|
||||
|
||||
import IconStar from "~icons/lucide/star"
|
||||
import IconStarOff from "~icons/lucide/star-off"
|
||||
import IconStarOff from "~icons/hopp/star-off"
|
||||
import IconTrash from "~icons/lucide/trash"
|
||||
import IconMinimize2 from "~icons/lucide/minimize-2"
|
||||
import IconMaximize2 from "~icons/lucide/maximize-2"
|
||||
|
||||
@@ -72,9 +72,11 @@
|
||||
class="flex items-center justify-between flex-1 min-w-0 transition cursor-pointer focus:outline-none text-secondaryLight text-tiny group"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center justify-center px-4 py-2 transition group-hover:text-secondary"
|
||||
class="inline-flex items-center justify-center px-4 py-2 transition group-hover:text-secondary truncate"
|
||||
>
|
||||
<icon-lucide-chevron-right class="mr-2 indicator" />
|
||||
<icon-lucide-chevron-right
|
||||
class="mr-2 indicator flex flex-shrink-0"
|
||||
/>
|
||||
<span
|
||||
:class="[
|
||||
{ 'capitalize-first': groupSelection === 'TIME' },
|
||||
|
||||
@@ -55,7 +55,7 @@ import { RESTHistoryEntry } from "~/newstore/history"
|
||||
import { shortDateTime } from "~/helpers/utils/date"
|
||||
|
||||
import IconStar from "~icons/lucide/star"
|
||||
import IconStarOff from "~icons/lucide/star-off"
|
||||
import IconStarOff from "~icons/hopp/star-off"
|
||||
import IconTrash from "~icons/lucide/trash"
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -165,6 +165,7 @@ import IconCheck from "~icons/lucide/check"
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
import cloneDeep from "lodash-es/cloneDeep"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -248,6 +249,10 @@ watch(
|
||||
(goingToShow) => {
|
||||
if (goingToShow) {
|
||||
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
|
||||
watch(
|
||||
request.value.headers,
|
||||
() => request.value.headers,
|
||||
(newHeadersList) => {
|
||||
// Sync should overwrite working headers
|
||||
const filteredWorkingHeaders = pipe(
|
||||
|
||||
@@ -94,6 +94,7 @@ import IconClipboard from "~icons/lucide/clipboard"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -144,6 +145,10 @@ const handleImport = () => {
|
||||
try {
|
||||
const req = parseCurlToHoppRESTReq(text)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REST_IMPORT_CURL",
|
||||
})
|
||||
|
||||
currentActiveTab.value.document.request = req
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
@@ -217,6 +217,7 @@
|
||||
@hide-modal="showCodegenModal = false"
|
||||
/>
|
||||
<CollectionsSaveRequest
|
||||
v-if="showSaveRequestModal"
|
||||
mode="rest"
|
||||
:show="showSaveRequestModal"
|
||||
@hide-modal="showSaveRequestModal = false"
|
||||
@@ -323,7 +324,8 @@ const newSendRequest = async () => {
|
||||
loading.value = true
|
||||
|
||||
// Log the request run into analytics
|
||||
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "rest",
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
@@ -445,6 +447,11 @@ const copyRequest = async () => {
|
||||
shareLink.value = ""
|
||||
fetchingShareLink.value = true
|
||||
const shortcodeResult = await createShortcode(tab.value.document.request)()
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SHORTCODE_CREATED",
|
||||
})
|
||||
|
||||
if (E.isLeft(shortcodeResult)) {
|
||||
toast.error(`${shortcodeResult.left.error}`)
|
||||
shareLink.value = `${t("error.something_went_wrong")}`
|
||||
@@ -515,6 +522,14 @@ const saveRequest = () => {
|
||||
editRESTRequest(saveCtx.folderPath, saveCtx.requestIndex, req)
|
||||
|
||||
tab.value.document.isDirty = false
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
platform: "rest",
|
||||
createdNow: false,
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
toast.success(`${t("request.saved")}`)
|
||||
} catch (e) {
|
||||
tab.value.document.saveContext = undefined
|
||||
@@ -525,6 +540,13 @@ const saveRequest = () => {
|
||||
|
||||
// TODO: handle error case (NOTE: overwriteRequestTeams is async)
|
||||
try {
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
platform: "rest",
|
||||
createdNow: false,
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
runMutation(UpdateRequestDocument, {
|
||||
requestID: saveCtx.requestID,
|
||||
data: {
|
||||
|
||||
@@ -54,7 +54,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { computed, ref } from "vue"
|
||||
|
||||
export type RequestOptionTabs =
|
||||
| "params"
|
||||
@@ -70,15 +71,7 @@ const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: HoppRESTRequest): void
|
||||
}>()
|
||||
|
||||
const request = ref(props.modelValue)
|
||||
|
||||
watch(
|
||||
() => request.value,
|
||||
(newVal) => {
|
||||
emit("update:modelValue", newVal)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
const request = useVModel(props, "modelValue", emit)
|
||||
|
||||
const selectedRealtimeTab = ref<RequestOptionTabs>("params")
|
||||
|
||||
|
||||
@@ -28,9 +28,11 @@
|
||||
class="flex items-center justify-between flex-1 min-w-0 transition cursor-pointer focus:outline-none text-secondaryLight text-tiny group"
|
||||
>
|
||||
<span
|
||||
class="inline-flex items-center justify-center px-4 py-2 transition group-hover:text-secondary"
|
||||
class="inline-flex items-center justify-center px-4 py-2 transition group-hover:text-secondary truncate"
|
||||
>
|
||||
<icon-lucide-chevron-right class="mr-2 indicator" />
|
||||
<icon-lucide-chevron-right
|
||||
class="mr-2 indicator flex flex-shrink-0"
|
||||
/>
|
||||
<span class="truncate capitalize-first">
|
||||
{{ t("environment.title") }}
|
||||
</span>
|
||||
|
||||
@@ -44,6 +44,7 @@ const props = withDefaults(
|
||||
envs?: { key: string; value: string; source: string }[] | null
|
||||
focus?: boolean
|
||||
selectTextOnMount?: boolean
|
||||
environmentHighlights?: boolean
|
||||
readonly?: boolean
|
||||
}>(),
|
||||
{
|
||||
@@ -53,6 +54,7 @@ const props = withDefaults(
|
||||
envs: null,
|
||||
focus: false,
|
||||
readonly: false,
|
||||
environmentHighlights: true,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -142,7 +144,7 @@ const initView = (el: any) => {
|
||||
tooltips({
|
||||
position: "absolute",
|
||||
}),
|
||||
envTooltipPlugin,
|
||||
props.environmentHighlights ? envTooltipPlugin : [],
|
||||
placeholderExt(props.placeholder),
|
||||
EditorView.domEventHandlers({
|
||||
paste(ev) {
|
||||
|
||||
@@ -44,6 +44,7 @@ import { createTeam } from "~/helpers/backend/mutations/Team"
|
||||
import { TeamNameCodec } from "~/helpers/backend/types/TeamName"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -68,6 +69,12 @@ const addNewTeam = async () => {
|
||||
TE.fromEither,
|
||||
TE.mapLeft(() => "invalid_name" as const),
|
||||
TE.chainW(createTeam),
|
||||
TE.chainFirstIOK(
|
||||
() => () =>
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_TEAM",
|
||||
})
|
||||
),
|
||||
TE.match(
|
||||
(err) => {
|
||||
// err is of type "invalid_name" | GQLError<Err>
|
||||
|
||||
@@ -99,7 +99,7 @@ export class GQLConnection {
|
||||
|
||||
private timeoutSubscription: any
|
||||
|
||||
public connect(url: string, headers: GQLHeader[]) {
|
||||
public connect(url: string, headers: GQLHeader[], auth: HoppGQLAuth) {
|
||||
if (this.connected$.value) {
|
||||
throw new Error(
|
||||
"A connection is already running. Close it before starting another."
|
||||
@@ -110,7 +110,7 @@ export class GQLConnection {
|
||||
this.connected$.next(true)
|
||||
|
||||
const poll = async () => {
|
||||
await this.getSchema(url, headers)
|
||||
await this.getSchema(url, headers, auth)
|
||||
this.timeoutSubscription = setTimeout(() => {
|
||||
poll()
|
||||
}, GQL_SCHEMA_POLL_INTERVAL)
|
||||
@@ -135,7 +135,11 @@ export class GQLConnection {
|
||||
this.schema$.next(null)
|
||||
}
|
||||
|
||||
private async getSchema(url: string, headers: GQLHeader[]) {
|
||||
private async getSchema(
|
||||
url: string,
|
||||
reqHeaders: GQLHeader[],
|
||||
auth: HoppGQLAuth
|
||||
) {
|
||||
try {
|
||||
this.isLoading$.next(true)
|
||||
|
||||
@@ -143,10 +147,38 @@ export class GQLConnection {
|
||||
query: getIntrospectionQuery(),
|
||||
})
|
||||
|
||||
const headers = reqHeaders.filter((x) => x.active && x.key !== "")
|
||||
|
||||
// TODO: Support a better b64 implementation than btoa ?
|
||||
if (auth.authType === "basic") {
|
||||
const username = auth.username
|
||||
const password = auth.password
|
||||
|
||||
headers.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Basic ${btoa(`${username}:${password}`)}`,
|
||||
})
|
||||
} else if (auth.authType === "bearer" || auth.authType === "oauth-2") {
|
||||
headers.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Bearer ${auth.token}`,
|
||||
})
|
||||
} else if (auth.authType === "api-key") {
|
||||
const { key, value, addTo } = auth
|
||||
|
||||
if (addTo === "Headers") {
|
||||
headers.push({
|
||||
active: true,
|
||||
key,
|
||||
value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const finalHeaders: Record<string, string> = {}
|
||||
headers
|
||||
.filter((x) => x.active && x.key !== "")
|
||||
.forEach((x) => (finalHeaders[x.key] = x.value))
|
||||
headers.forEach((x) => (finalHeaders[x.key] = x.value))
|
||||
|
||||
const reqOptions = {
|
||||
method: "POST",
|
||||
|
||||
@@ -6,14 +6,18 @@ import { isJSONContentType } from "./utils/contenttypes"
|
||||
* Handles translations for all the hopp.io REST Shareable URL params
|
||||
*/
|
||||
export function translateExtURLParams(
|
||||
urlParams: Record<string, any>
|
||||
urlParams: Record<string, any>,
|
||||
initialReq?: HoppRESTRequest
|
||||
): HoppRESTRequest {
|
||||
if (urlParams.v) return parseV1ExtURL(urlParams)
|
||||
else return parseV0ExtURL(urlParams)
|
||||
if (urlParams.v) return parseV1ExtURL(urlParams, initialReq)
|
||||
else return parseV0ExtURL(urlParams, initialReq)
|
||||
}
|
||||
|
||||
function parseV0ExtURL(urlParams: Record<string, any>): HoppRESTRequest {
|
||||
const resolvedReq = getDefaultRESTRequest()
|
||||
function parseV0ExtURL(
|
||||
urlParams: Record<string, any>,
|
||||
initialReq?: HoppRESTRequest
|
||||
): HoppRESTRequest {
|
||||
const resolvedReq = initialReq ?? getDefaultRESTRequest()
|
||||
|
||||
if (urlParams.method && typeof urlParams.method === "string") {
|
||||
resolvedReq.method = urlParams.method
|
||||
@@ -89,8 +93,11 @@ function parseV0ExtURL(urlParams: Record<string, any>): HoppRESTRequest {
|
||||
return resolvedReq
|
||||
}
|
||||
|
||||
function parseV1ExtURL(urlParams: Record<string, any>): HoppRESTRequest {
|
||||
const resolvedReq = getDefaultRESTRequest()
|
||||
function parseV1ExtURL(
|
||||
urlParams: Record<string, any>,
|
||||
initialReq?: HoppRESTRequest
|
||||
): HoppRESTRequest {
|
||||
const resolvedReq = initialReq ?? getDefaultRESTRequest()
|
||||
|
||||
if (urlParams.headers && typeof urlParams.headers === "string") {
|
||||
resolvedReq.headers = JSON.parse(urlParams.headers)
|
||||
|
||||
@@ -71,9 +71,11 @@ const parseURL = (urlText: string | number) =>
|
||||
* @returns URL object
|
||||
*/
|
||||
export function getURLObject(parsedArguments: parser.Arguments) {
|
||||
const location = parsedArguments.location ?? undefined
|
||||
|
||||
return pipe(
|
||||
// contains raw url strings
|
||||
parsedArguments._.slice(1),
|
||||
[...parsedArguments._.slice(1), location],
|
||||
A.findFirstMap(parseURL),
|
||||
// no url found
|
||||
O.getOrElse(() => new URL(defaultRESTReq.endpoint))
|
||||
|
||||
@@ -105,7 +105,8 @@ export class MQTTConnection {
|
||||
this.handleError(e)
|
||||
}
|
||||
|
||||
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "mqtt",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -113,7 +113,8 @@ export class SIOConnection {
|
||||
this.handleError(error, "CONNECTION")
|
||||
}
|
||||
|
||||
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "socketio",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -63,7 +63,8 @@ export class SSEConnection {
|
||||
})
|
||||
}
|
||||
|
||||
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "sse",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -71,7 +71,8 @@ export class WSConnection {
|
||||
this.handleError(error as SyntaxError)
|
||||
}
|
||||
|
||||
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "wss",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { refWithControl } from "@vueuse/core"
|
||||
import { HoppRESTResponse } from "../types/HoppRESTResponse"
|
||||
import { getDefaultRESTRequest } from "./default"
|
||||
import { HoppTestResult } from "../types/HoppTestResult"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
export type HoppRESTTab = {
|
||||
id: string
|
||||
@@ -147,6 +148,10 @@ export function createNewTab(document: HoppRESTDocument, switchToIt = true) {
|
||||
currentTabID.value = id
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REST_NEW_TAB_OPENED",
|
||||
})
|
||||
|
||||
return tab
|
||||
}
|
||||
|
||||
|
||||
@@ -275,7 +275,4 @@ export const gqlResponse$ = gqlSessionStore.subject$.pipe(
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const gqlAuth$ = gqlSessionStore.subject$.pipe(
|
||||
pluck("request", "auth"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
export const gqlAuth$ = gqlSessionStore.subject$.pipe(pluck("request", "auth"))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user