Compare commits

..

8 Commits

Author SHA1 Message Date
nivedin
9581f1d747 refactor: asterik length same as real text length 2023-09-21 13:19:57 +05:30
nivedin
882aafdb43 refactor: update secret toggle icon 2023-09-21 13:18:50 +05:30
Daniel Maurer
f33fa6ac1a fix: adjusted tests to work with the secret flag 2023-09-19 12:37:31 +05:30
Daniel Maurer
b9a1cc21f1 fix: fixed bug in gui with masked secrets 2023-09-19 12:37:31 +05:30
Daniel Maurer
f530fc2853 fix: inputfield to readonly + asterisk if secret 2023-09-19 12:37:28 +05:30
Daniel Maurer
e0eb8af6f5 feat: added masking of secrets in cli in url 2023-09-19 12:37:13 +05:30
Daniel Maurer
088f1d6b47 feat: missing files from last commit 2023-09-19 12:37:13 +05:30
Daniel Maurer
7d61e69b3d feat: added toggle Button to Frontend 2023-09-19 12:37:10 +05:30
662 changed files with 14812 additions and 37507 deletions

View File

@@ -5,5 +5,5 @@
"features": {
"ghcr.io/NicoVIII/devcontainer-features/pnpm:1": {}
},
"postCreateCommand": "cp .env.example .env && pnpm i"
"postCreateCommand": "mv .env.example .env && pnpm i"
}

View File

@@ -12,8 +12,8 @@ SESSION_SECRET='add some secret here'
# Hoppscotch App Domain Config
REDIRECT_URL="http://localhost:3000"
WHITELISTED_ORIGINS="http://localhost:3170,http://localhost:3000,http://localhost:3100"
VITE_ALLOWED_AUTH_PROVIDERS=GOOGLE,GITHUB,MICROSOFT,EMAIL
WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100"
VITE_ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL
# Google Auth Config
GOOGLE_CLIENT_ID="************************************************"
@@ -59,6 +59,3 @@ VITE_BACKEND_API_URL=http://localhost:3170/v1
# Terms Of Service And Privacy Policy Links (Optional)
VITE_APP_TOS_LINK=https://docs.hoppscotch.io/support/terms
VITE_APP_PRIVACY_POLICY_LINK=https://docs.hoppscotch.io/support/privacy
# Set to `true` for subpath based access
ENABLE_SUBPATH_BASED_ACCESS=false

View File

@@ -18,9 +18,6 @@ jobs:
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:

View File

@@ -36,7 +36,7 @@ jobs:
# Deploy the ui site with netlify-cli
- name: Deploy to Netlify (ui)
run: npx netlify-cli@15.11.0 deploy --dir=packages/hoppscotch-ui/.histoire/dist --prod
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 }}

14
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,14 @@
{
"recommendations": [
"antfu.iconify",
"vue.volar",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"csstools.postcss",
"folke.vscode-monorepo-workspace"
],
"unwantedRecommendations": [
"octref.vetur"
]
}

View File

@@ -1,19 +0,0 @@
:3000 {
try_files {path} /
root * /site/selfhost-web
file_server
}
:3100 {
try_files {path} /
root * /site/sh-admin-multiport-setup
file_server
}
:3170 {
reverse_proxy localhost:8080
}
:80 {
respond 404
}

View File

@@ -1,37 +0,0 @@
:3000 {
respond 404
}
:3100 {
respond 404
}
:3170 {
reverse_proxy localhost:8080
}
:80 {
# Serve the `selfhost-web` SPA by default
root * /site/selfhost-web
file_server
handle_path /admin* {
root * /site/sh-admin-subpath-access
file_server
# Ensures any non-existent file in the server is routed to the SPA
try_files {path} /
}
# Handle requests under `/backend*` path
handle_path /backend* {
reverse_proxy localhost:8080
}
# Catch-all route for unknown paths, serves `selfhost-web` SPA
handle {
root * /site/selfhost-web
file_server
try_files {path} /
}
}

11
aio.Caddyfile Normal file
View File

@@ -0,0 +1,11 @@
:3000 {
try_files {path} /
root * /site/selfhost-web
file_server
}
:3100 {
try_files {path} /
root * /site/sh-admin
file_server
}

View File

@@ -49,8 +49,7 @@ execSync(`npx import-meta-env -x build.env -e build.env -p "/site/**/*"`)
fs.rmSync("build.env")
const caddyFileName = process.env.ENABLE_SUBPATH_BASED_ACCESS === 'true' ? 'aio-subpath-access.Caddyfile' : 'aio-multiport-setup.Caddyfile'
const caddyProcess = runChildProcessWithPrefix("caddy", ["run", "--config", `/etc/caddy/${caddyFileName}`, "--adapter", "caddyfile"], "App/Admin Dashboard Caddy")
const caddyProcess = runChildProcessWithPrefix("caddy", ["run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"], "App/Admin Dashboard Caddy")
const backendProcess = runChildProcessWithPrefix("pnpm", ["run", "start:prod"], "Backend Server")
caddyProcess.on("exit", (code) => {

View File

@@ -17,7 +17,7 @@ services:
environment:
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
- PORT=8080
- PORT=3170
volumes:
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
# - ./packages/hoppscotch-backend/:/usr/src/app
@@ -26,7 +26,6 @@ services:
hoppscotch-db:
condition: service_healthy
ports:
- "3180:80"
- "3170:3170"
# The main hoppscotch app. This will be hosted at port 3000
@@ -43,8 +42,7 @@ services:
depends_on:
- hoppscotch-backend
ports:
- "3080:80"
- "3000:3000"
- "3000:8080"
# The Self Host dashboard for managing the app. This will be hosted at port 3100
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
@@ -60,8 +58,7 @@ services:
depends_on:
- hoppscotch-backend
ports:
- "3280:80"
- "3100:3100"
- "3100:8080"
# The service that spins up all 3 services at once in one container
hoppscotch-aio:
@@ -79,7 +76,6 @@ services:
- "3000:3000"
- "3100:3100"
- "3170:3170"
- "3080:80"
# The preset DB service, you can delete/comment the below lines if
# you are using an external postgres instance

View File

@@ -22,19 +22,18 @@
"workspaces": [
"./packages/*"
],
"dependencies": {
"husky": "^7.0.4",
"lint-staged": "^12.3.8"
},
"devDependencies": {
"@commitlint/cli": "^16.2.3",
"@commitlint/config-conventional": "^16.2.1",
"@types/node": "17.0.27",
"@types/node": "^17.0.24",
"cross-env": "^7.0.3",
"http-server": "^14.1.1",
"husky": "^7.0.4",
"lint-staged": "12.4.0"
"http-server": "^14.1.1"
},
"pnpm": {
"overrides": {
"vue": "3.3.9"
},
"packageExtensions": {
"httpsnippet@^3.0.1": {
"peerDependencies": {

View File

@@ -17,16 +17,16 @@
"types": "dist/index.d.ts",
"sideEffects": false,
"dependencies": {
"@codemirror/language": "6.9.0",
"@lezer/highlight": "1.1.4",
"@lezer/lr": "^1.3.13"
"@codemirror/language": "^6.9.0",
"@lezer/highlight": "^1.1.6",
"@lezer/lr": "^1.3.10"
},
"devDependencies": {
"@lezer/generator": "^1.5.1",
"@lezer/generator": "^1.5.0",
"mocha": "^9.2.2",
"rollup": "^3.29.3",
"rollup-plugin-dts": "^6.0.2",
"rollup-plugin-ts": "^3.4.5",
"typescript": "^5.2.2"
"rollup": "^2.70.2",
"rollup-plugin-dts": "^4.2.1",
"rollup-plugin-ts": "^2.0.7",
"typescript": "^4.6.3"
}
}

View File

@@ -1,3 +0,0 @@
:80 :3170 {
reverse_proxy localhost:8080
}

View File

@@ -1,6 +1,6 @@
{
"name": "hoppscotch-backend",
"version": "2023.8.4-1",
"version": "2023.8.1",
"description": "",
"author": "",
"private": true,
@@ -24,17 +24,18 @@
"do-test": "pnpm run test"
},
"dependencies": {
"@apollo/server": "^4.9.4",
"@nestjs-modules/mailer": "^1.9.1",
"@nestjs/apollo": "^12.0.9",
"@nestjs/common": "^10.2.6",
"@nestjs/core": "^10.2.6",
"@nestjs/graphql": "^12.0.9",
"@nestjs/jwt": "^10.1.1",
"@nestjs/passport": "^10.0.2",
"@nestjs/platform-express": "^10.2.6",
"@nestjs/throttler": "^5.0.0",
"@nestjs-modules/mailer": "^1.8.1",
"@nestjs/apollo": "^10.1.6",
"@nestjs/common": "^9.2.1",
"@nestjs/core": "^9.2.1",
"@nestjs/graphql": "^10.1.6",
"@nestjs/jwt": "^10.0.1",
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.2.1",
"@nestjs/throttler": "^4.0.0",
"@prisma/client": "^4.16.2",
"apollo-server-express": "^3.11.1",
"apollo-server-plugin-base": "^3.7.1",
"argon2": "^0.30.3",
"bcrypt": "^5.1.0",
"cookie": "^0.5.0",
@@ -42,9 +43,9 @@
"express": "^4.17.1",
"express-session": "^1.17.3",
"fp-ts": "^2.13.1",
"graphql": "^16.8.1",
"graphql": "^15.5.0",
"graphql-query-complexity": "^0.12.0",
"graphql-redis-subscriptions": "^2.6.0",
"graphql-redis-subscriptions": "^2.5.0",
"graphql-subscriptions": "^2.0.0",
"handlebars": "^4.7.7",
"io-ts": "^2.2.16",
@@ -62,9 +63,9 @@
"rxjs": "^7.6.0"
},
"devDependencies": {
"@nestjs/cli": "^10.1.18",
"@nestjs/schematics": "^10.0.2",
"@nestjs/testing": "^10.2.6",
"@nestjs/cli": "^9.1.5",
"@nestjs/schematics": "^9.0.3",
"@nestjs/testing": "^9.2.1",
"@relmify/jest-fp-ts": "^2.0.2",
"@types/argon2": "^0.15.0",
"@types/bcrypt": "^5.0.0",

View File

@@ -1,15 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[id]` on the table `Shortcode` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "Shortcode" ADD COLUMN "embedProperties" JSONB,
ADD COLUMN "updatedOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- CreateIndex
CREATE UNIQUE INDEX "Shortcode_id_key" ON "Shortcode"("id");
-- AddForeignKey
ALTER TABLE "Shortcode" ADD CONSTRAINT "Shortcode_creatorUid_fkey" FOREIGN KEY ("creatorUid") REFERENCES "User"("uid") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE "TeamCollection" ADD COLUMN "data" JSONB;
-- AlterTable
ALTER TABLE "UserCollection" ADD COLUMN "data" JSONB;

View File

@@ -43,7 +43,6 @@ model TeamInvitation {
model TeamCollection {
id String @id @default(cuid())
parentID String?
data Json?
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
children TeamCollection[] @relation("TeamCollectionChildParent")
requests TeamRequest[]
@@ -69,13 +68,10 @@ model TeamRequest {
}
model Shortcode {
id String @id @unique
request Json
embedProperties Json?
creatorUid String?
User User? @relation(fields: [creatorUid], references: [uid])
createdOn DateTime @default(now())
updatedOn DateTime @default(now()) @updatedAt
id String @id
request Json
creatorUid String?
createdOn DateTime @default(now())
@@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
}
@@ -106,7 +102,6 @@ model User {
currentGQLSession Json?
createdOn DateTime @default(now()) @db.Timestamp(3)
invitedUsers InvitedUsers[]
shortcodes Shortcode[]
}
model Account {
@@ -197,7 +192,6 @@ model UserCollection {
userUid String
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
title String
data Json?
orderIndex Int
type ReqType
createdOn DateTime @default(now()) @db.Timestamp(3)

View File

@@ -1,66 +0,0 @@
#!/usr/local/bin/node
// @ts-check
import { spawn } from 'child_process';
import process from 'process';
function runChildProcessWithPrefix(command, args, prefix) {
const childProcess = spawn(command, args);
childProcess.stdout.on('data', (data) => {
const output = data.toString().trim().split('\n');
output.forEach((line) => {
console.log(`${prefix} | ${line}`);
});
});
childProcess.stderr.on('data', (data) => {
const error = data.toString().trim().split('\n');
error.forEach((line) => {
console.error(`${prefix} | ${line}`);
});
});
childProcess.on('close', (code) => {
console.log(`${prefix} Child process exited with code ${code}`);
});
childProcess.on('error', (stuff) => {
console.error('error');
console.error(stuff);
});
return childProcess;
}
const caddyProcess = runChildProcessWithPrefix(
'caddy',
['run', '--config', '/etc/caddy/backend.Caddyfile', '--adapter', 'caddyfile'],
'App/Admin Dashboard Caddy',
);
const backendProcess = runChildProcessWithPrefix(
'pnpm',
['run', 'start:prod'],
'Backend Server',
);
caddyProcess.on('exit', (code) => {
console.log(`Exiting process because Caddy Server exited with code ${code}`);
process.exit(code);
});
backendProcess.on('exit', (code) => {
console.log(
`Exiting process because Backend Server exited with code ${code}`,
);
process.exit(code);
});
process.on('SIGINT', () => {
console.log('SIGINT received, exiting...');
caddyProcess.kill('SIGINT');
backendProcess.kill('SIGINT');
process.exit(0);
});

View File

@@ -1,9 +1,4 @@
import { ObjectType, OmitType } from '@nestjs/graphql';
import { User } from 'src/user/user.model';
import { ObjectType } from '@nestjs/graphql';
@ObjectType()
export class Admin extends OmitType(User, [
'isAdmin',
'currentRESTSession',
'currentGQLSession',
]) {}
export class Admin {}

View File

@@ -10,8 +10,6 @@ import { TeamInvitationModule } from '../team-invitation/team-invitation.module'
import { TeamEnvironmentsModule } from '../team-environments/team-environments.module';
import { TeamCollectionModule } from '../team-collection/team-collection.module';
import { TeamRequestModule } from '../team-request/team-request.module';
import { InfraResolver } from './infra.resolver';
import { ShortcodeModule } from 'src/shortcode/shortcode.module';
@Module({
imports: [
@@ -24,9 +22,8 @@ import { ShortcodeModule } from 'src/shortcode/shortcode.module';
TeamEnvironmentsModule,
TeamCollectionModule,
TeamRequestModule,
ShortcodeModule,
],
providers: [InfraResolver, AdminResolver, AdminService],
providers: [AdminResolver, AdminService],
exports: [AdminService],
})
export class AdminModule {}

View File

@@ -21,15 +21,15 @@ import { InvitedUser } from './invited-user.model';
import { GqlUser } from '../decorators/gql-user.decorator';
import { PubSubService } from '../pubsub/pubsub.service';
import { Team, TeamMember } from '../team/team.model';
import { User } from '../user/user.model';
import { TeamInvitation } from '../team-invitation/team-invitation.model';
import { PaginationArgs } from '../types/input-types.args';
import {
AddUserToTeamArgs,
ChangeUserRoleInTeamArgs,
} from './input-types.args';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { SkipThrottle } from '@nestjs/throttler';
import { User } from 'src/user/user.model';
import { PaginationArgs } from 'src/types/input-types.args';
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
@UseGuards(GqlThrottlerGuard)
@Resolver(() => Admin)
@@ -51,7 +51,6 @@ export class AdminResolver {
@ResolveField(() => [User], {
description: 'Returns a list of all admin users in infra',
deprecationReason: 'Use `infra` query instead',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async admins() {
@@ -60,7 +59,6 @@ export class AdminResolver {
}
@ResolveField(() => User, {
description: 'Returns a user info by UID',
deprecationReason: 'Use `infra` query instead',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async userInfo(
@@ -78,7 +76,6 @@ export class AdminResolver {
@ResolveField(() => [User], {
description: 'Returns a list of all the users in infra',
deprecationReason: 'Use `infra` query instead',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async allUsers(
@@ -91,7 +88,6 @@ export class AdminResolver {
@ResolveField(() => [InvitedUser], {
description: 'Returns a list of all the invited users',
deprecationReason: 'Use `infra` query instead',
})
async invitedUsers(@Parent() admin: Admin): Promise<InvitedUser[]> {
const users = await this.adminService.fetchInvitedUsers();
@@ -100,7 +96,6 @@ export class AdminResolver {
@ResolveField(() => [Team], {
description: 'Returns a list of all the teams in the infra',
deprecationReason: 'Use `infra` query instead',
})
async allTeams(
@Parent() admin: Admin,
@@ -111,7 +106,6 @@ export class AdminResolver {
}
@ResolveField(() => Team, {
description: 'Returns a team info by ID when requested by Admin',
deprecationReason: 'Use `infra` query instead',
})
async teamInfo(
@Parent() admin: Admin,
@@ -129,7 +123,6 @@ export class AdminResolver {
@ResolveField(() => Number, {
description: 'Return count of all the members in a team',
deprecationReason: 'Use `infra` query instead',
})
async membersCountInTeam(
@Parent() admin: Admin,
@@ -147,7 +140,6 @@ export class AdminResolver {
@ResolveField(() => Number, {
description: 'Return count of all the stored collections in a team',
deprecationReason: 'Use `infra` query instead',
})
async collectionCountInTeam(
@Parent() admin: Admin,
@@ -163,7 +155,6 @@ export class AdminResolver {
}
@ResolveField(() => Number, {
description: 'Return count of all the stored requests in a team',
deprecationReason: 'Use `infra` query instead',
})
async requestCountInTeam(
@Parent() admin: Admin,
@@ -180,7 +171,6 @@ export class AdminResolver {
@ResolveField(() => Number, {
description: 'Return count of all the stored environments in a team',
deprecationReason: 'Use `infra` query instead',
})
async environmentCountInTeam(
@Parent() admin: Admin,
@@ -197,7 +187,6 @@ export class AdminResolver {
@ResolveField(() => [TeamInvitation], {
description: 'Return all the pending invitations in a team',
deprecationReason: 'Use `infra` query instead',
})
async pendingInvitationCountInTeam(
@Parent() admin: Admin,
@@ -216,7 +205,6 @@ export class AdminResolver {
@ResolveField(() => Number, {
description: 'Return total number of Users in organization',
deprecationReason: 'Use `infra` query instead',
})
async usersCount() {
return this.adminService.getUsersCount();
@@ -224,7 +212,6 @@ export class AdminResolver {
@ResolveField(() => Number, {
description: 'Return total number of Teams in organization',
deprecationReason: 'Use `infra` query instead',
})
async teamsCount() {
return this.adminService.getTeamsCount();
@@ -232,7 +219,6 @@ export class AdminResolver {
@ResolveField(() => Number, {
description: 'Return total number of Team Collections in organization',
deprecationReason: 'Use `infra` query instead',
})
async teamCollectionsCount() {
return this.adminService.getTeamCollectionsCount();
@@ -240,7 +226,6 @@ export class AdminResolver {
@ResolveField(() => Number, {
description: 'Return total number of Team Requests in organization',
deprecationReason: 'Use `infra` query instead',
})
async teamRequestsCount() {
return this.adminService.getTeamRequestsCount();
@@ -443,23 +428,6 @@ export class AdminResolver {
return true;
}
@Mutation(() => Boolean, {
description: 'Revoke Shortcode by ID',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async revokeShortcodeByAdmin(
@Args({
name: 'code',
description: 'The shortcode to delete',
type: () => ID,
})
code: string,
): Promise<boolean> {
const res = await this.adminService.deleteShortcode(code);
if (E.isLeft(res)) throwErr(res.left);
return true;
}
/* Subscriptions */
@Subscription(() => InvitedUser, {

View File

@@ -15,7 +15,6 @@ import {
INVALID_EMAIL,
USER_ALREADY_INVITED,
} from '../errors';
import { ShortcodeService } from 'src/shortcode/shortcode.service';
const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>();
@@ -26,7 +25,6 @@ const mockTeamRequestService = mockDeep<TeamRequestService>();
const mockTeamInvitationService = mockDeep<TeamInvitationService>();
const mockTeamCollectionService = mockDeep<TeamCollectionService>();
const mockMailerService = mockDeep<MailerService>();
const mockShortcodeService = mockDeep<ShortcodeService>();
const adminService = new AdminService(
mockUserService,
@@ -38,7 +36,6 @@ const adminService = new AdminService(
mockPubSub as any,
mockPrisma as any,
mockMailerService,
mockShortcodeService,
);
const invitedUsers: InvitedUsers[] = [

View File

@@ -24,7 +24,6 @@ import { TeamRequestService } from '../team-request/team-request.service';
import { TeamEnvironmentsService } from '../team-environments/team-environments.service';
import { TeamInvitationService } from '../team-invitation/team-invitation.service';
import { TeamMemberRole } from '../team/team.model';
import { ShortcodeService } from 'src/shortcode/shortcode.service';
@Injectable()
export class AdminService {
@@ -38,7 +37,6 @@ export class AdminService {
private readonly pubsub: PubSubService,
private readonly prisma: PrismaService,
private readonly mailerService: MailerService,
private readonly shortcodeService: ShortcodeService,
) {}
/**
@@ -76,7 +74,7 @@ export class AdminService {
try {
await this.mailerService.sendUserInvitationEmail(inviteeEmail, {
template: 'user-invitation',
template: 'code-your-own',
variables: {
inviteeEmail: inviteeEmail,
magicLink: `${process.env.VITE_BASE_URL}`,
@@ -434,35 +432,4 @@ export class AdminService {
return E.right(teamInvite.right);
}
/**
* Fetch all created ShortCodes
*
* @param args Pagination arguments
* @param userEmail User email
* @returns ShortcodeWithUserEmail
*/
async fetchAllShortcodes(
cursorID: string,
take: number,
userEmail: string = null,
) {
return this.shortcodeService.fetchAllShortcodes(
{ cursor: cursorID, take },
userEmail,
);
}
/**
* Delete a Shortcode
*
* @param shortcodeID ID of Shortcode being deleted
* @returns Boolean on successful deletion
*/
async deleteShortcode(shortcodeID: string) {
const result = await this.shortcodeService.deleteShortcode(shortcodeID);
if (E.isLeft(result)) return E.left(result.left);
return E.right(result.right);
}
}

View File

@@ -1,10 +0,0 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { Admin } from './admin.model';
@ObjectType()
export class Infra {
@Field(() => Admin, {
description: 'Admin who executed the action',
})
executedBy: Admin;
}

View File

@@ -1,225 +0,0 @@
import { UseGuards } from '@nestjs/common';
import { Args, ID, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { Infra } from './infra.model';
import { AdminService } from './admin.service';
import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
import { GqlAdminGuard } from './guards/gql-admin.guard';
import { User } from 'src/user/user.model';
import { AuthUser } from 'src/types/AuthUser';
import { throwErr } from 'src/utils';
import * as E from 'fp-ts/Either';
import { Admin } from './admin.model';
import { PaginationArgs } from 'src/types/input-types.args';
import { InvitedUser } from './invited-user.model';
import { Team } from 'src/team/team.model';
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
import { GqlAdmin } from './decorators/gql-admin.decorator';
import { ShortcodeWithUserEmail } from 'src/shortcode/shortcode.model';
@UseGuards(GqlThrottlerGuard)
@Resolver(() => Infra)
export class InfraResolver {
constructor(private adminService: AdminService) {}
@Query(() => Infra, {
description: 'Fetch details of the Infrastructure',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
infra(@GqlAdmin() admin: Admin) {
const infra: Infra = { executedBy: admin };
return infra;
}
@ResolveField(() => [User], {
description: 'Returns a list of all admin users in infra',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async admins() {
const admins = await this.adminService.fetchAdmins();
return admins;
}
@ResolveField(() => User, {
description: 'Returns a user info by UID',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async userInfo(
@Args({
name: 'userUid',
type: () => ID,
description: 'The user UID',
})
userUid: string,
): Promise<AuthUser> {
const user = await this.adminService.fetchUserInfo(userUid);
if (E.isLeft(user)) throwErr(user.left);
return user.right;
}
@ResolveField(() => [User], {
description: 'Returns a list of all the users in infra',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async allUsers(@Args() args: PaginationArgs): Promise<AuthUser[]> {
const users = await this.adminService.fetchUsers(args.cursor, args.take);
return users;
}
@ResolveField(() => [InvitedUser], {
description: 'Returns a list of all the invited users',
})
async invitedUsers(): Promise<InvitedUser[]> {
const users = await this.adminService.fetchInvitedUsers();
return users;
}
@ResolveField(() => [Team], {
description: 'Returns a list of all the teams in the infra',
})
async allTeams(@Args() args: PaginationArgs): Promise<Team[]> {
const teams = await this.adminService.fetchAllTeams(args.cursor, args.take);
return teams;
}
@ResolveField(() => Team, {
description: 'Returns a team info by ID when requested by Admin',
})
async teamInfo(
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which info to fetch',
})
teamID: string,
): Promise<Team> {
const team = await this.adminService.getTeamInfo(teamID);
if (E.isLeft(team)) throwErr(team.left);
return team.right;
}
@ResolveField(() => Number, {
description: 'Return count of all the members in a team',
})
async membersCountInTeam(
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
nullable: false,
})
teamID: string,
): Promise<number> {
const teamMembersCount = await this.adminService.membersCountInTeam(teamID);
return teamMembersCount;
}
@ResolveField(() => Number, {
description: 'Return count of all the stored collections in a team',
})
async collectionCountInTeam(
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
})
teamID: string,
): Promise<number> {
const teamCollCount = await this.adminService.collectionCountInTeam(teamID);
return teamCollCount;
}
@ResolveField(() => Number, {
description: 'Return count of all the stored requests in a team',
})
async requestCountInTeam(
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
})
teamID: string,
): Promise<number> {
const teamReqCount = await this.adminService.requestCountInTeam(teamID);
return teamReqCount;
}
@ResolveField(() => Number, {
description: 'Return count of all the stored environments in a team',
})
async environmentCountInTeam(
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
})
teamID: string,
): Promise<number> {
const envsCount = await this.adminService.environmentCountInTeam(teamID);
return envsCount;
}
@ResolveField(() => [TeamInvitation], {
description: 'Return all the pending invitations in a team',
})
async pendingInvitationCountInTeam(
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
})
teamID: string,
) {
const invitations = await this.adminService.pendingInvitationCountInTeam(
teamID,
);
return invitations;
}
@ResolveField(() => Number, {
description: 'Return total number of Users in organization',
})
async usersCount() {
return this.adminService.getUsersCount();
}
@ResolveField(() => Number, {
description: 'Return total number of Teams in organization',
})
async teamsCount() {
return this.adminService.getTeamsCount();
}
@ResolveField(() => Number, {
description: 'Return total number of Team Collections in organization',
})
async teamCollectionsCount() {
return this.adminService.getTeamCollectionsCount();
}
@ResolveField(() => Number, {
description: 'Return total number of Team Requests in organization',
})
async teamRequestsCount() {
return this.adminService.getTeamRequestsCount();
}
@ResolveField(() => [ShortcodeWithUserEmail], {
description: 'Returns a list of all the shortcodes in the infra',
})
async allShortcodes(
@Args() args: PaginationArgs,
@Args({
name: 'userEmail',
nullable: true,
description: 'Users email to filter shortcodes by',
})
userEmail: string,
) {
return await this.adminService.fetchAllShortcodes(
args.cursor,
args.take,
userEmail,
);
}
}

View File

@@ -27,7 +27,12 @@ import { AppController } from './app.controller';
buildSchemaOptions: {
numberScalarMode: 'integer',
},
cors: {
origin: process.env.WHITELISTED_ORIGINS.split(','),
credentials: true,
},
playground: process.env.PRODUCTION !== 'true',
debug: process.env.PRODUCTION !== 'true',
autoSchemaFile: true,
installSubscriptionHandlers: true,
subscriptions: {
@@ -57,12 +62,10 @@ import { AppController } from './app.controller';
}),
driver: ApolloDriver,
}),
ThrottlerModule.forRoot([
{
ttl: +process.env.RATE_LIMIT_TTL,
limit: +process.env.RATE_LIMIT_MAX,
},
]),
ThrottlerModule.forRoot({
ttl: +process.env.RATE_LIMIT_TTL,
limit: +process.env.RATE_LIMIT_MAX,
}),
UserModule,
AuthModule,
AdminModule,

View File

@@ -229,7 +229,7 @@ export class AuthService {
}
await this.mailerService.sendEmail(email, {
template: 'user-invitation',
template: 'code-your-own',
variables: {
inviteeEmail: email,
magicLink: `${url}/enter?token=${generatedTokens.token}`,

View File

@@ -254,13 +254,6 @@ export const TEAM_COLL_INVALID_JSON = 'team_coll/invalid_json';
*/
export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
/**
* The Team Collection data is not valid
* (TeamCollectionService)
*/
export const TEAM_COLL_DATA_INVALID =
'team_coll/team_coll_data_invalid' as const;
/**
* Tried to perform an action on a request that doesn't accept their member role level
* (GqlRequestTeamMemberGuard)
@@ -325,6 +318,18 @@ export const TEAM_INVITATION_NOT_FOUND =
*/
export const SHORTCODE_NOT_FOUND = 'shortcode/not_found' as const;
/**
* Invalid ShortCode format
* (ShortcodeService)
*/
export const SHORTCODE_INVALID_JSON = 'shortcode/invalid_json' as const;
/**
* ShortCode already exists in DB
* (ShortcodeService)
*/
export const SHORTCODE_ALREADY_EXISTS = 'shortcode/already_exists' as const;
/**
* Invalid or non-existent TEAM ENVIRONMENT ID
* (TeamEnvironmentsService)
@@ -592,13 +597,6 @@ export const USER_COLL_REORDERING_FAILED =
export const USER_COLL_SAME_NEXT_COLL =
'user_coll/user_collection_and_next_user_collection_are_same' as const;
/**
* The User Collection data is not valid
* (UserCollectionService)
*/
export const USER_COLL_DATA_INVALID =
'user_coll/user_coll_data_invalid' as const;
/**
* The User Collection does not belong to the logged-in user
* (UserCollectionService)
@@ -623,24 +621,3 @@ export const MAILER_SMTP_URL_UNDEFINED = 'mailer/smtp_url_undefined' as const;
*/
export const MAILER_FROM_ADDRESS_UNDEFINED =
'mailer/from_address_undefined' as const;
/**
* SharedRequest invalid request JSON format
* (ShortcodeService)
*/
export const SHORTCODE_INVALID_REQUEST_JSON =
'shortcode/request_invalid_format' as const;
/**
* SharedRequest invalid properties JSON format
* (ShortcodeService)
*/
export const SHORTCODE_INVALID_PROPERTIES_JSON =
'shortcode/properties_invalid_format' as const;
/**
* SharedRequest invalid properties not found
* (ShortcodeService)
*/
export const SHORTCODE_PROPERTIES_NOT_FOUND =
'shortcode/properties_not_found' as const;

View File

@@ -27,7 +27,6 @@ import { UserRequestUserCollectionResolver } from './user-request/resolvers/user
import { UserEnvsUserResolver } from './user-environment/user.resolver';
import { UserHistoryUserResolver } from './user-history/user.resolver';
import { UserSettingsUserResolver } from './user-settings/user.resolver';
import { InfraResolver } from './admin/infra.resolver';
/**
* All the resolvers present in the application.
@@ -35,7 +34,6 @@ import { InfraResolver } from './admin/infra.resolver';
* NOTE: This needs to be KEPT UP-TO-DATE to keep the schema accurate
*/
const RESOLVERS = [
InfraResolver,
AdminResolver,
ShortcodeResolver,
TeamResolver,
@@ -95,7 +93,9 @@ export async function emitGQLSchemaFile() {
numberScalarMode: 'integer',
});
const schemaString = printSchema(schema);
const schemaString = printSchema(schema, {
commentDescriptions: true,
});
logger.log(`Writing schema to GQL_SCHEMA_EMIT_LOCATION (${destination})`);

View File

@@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common';
@Injectable()
export class ThrottlerBehindProxyGuard extends ThrottlerGuard {
protected async getTracker(req: Record<string, any>): Promise<string> {
protected getTracker(req: Record<string, any>): string {
return req.ips.length ? req.ips[0] : req.ip; // individualize IP extraction to meet your own needs
// learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#directives
}
}

View File

@@ -8,7 +8,7 @@ export type MailDescription = {
};
export type UserMagicLinkMailDescription = {
template: 'user-invitation';
template: 'code-your-own';
variables: {
inviteeEmail: string;
magicLink: string;
@@ -16,7 +16,7 @@ export type UserMagicLinkMailDescription = {
};
export type AdminUserInvitationMailDescription = {
template: 'user-invitation';
template: 'code-your-own';
variables: {
inviteeEmail: string;
magicLink: string;

View File

@@ -27,7 +27,7 @@ export class MailerService {
case 'team-invitation':
return `${mailDesc.variables.invitee} invited you to join ${mailDesc.variables.invite_team_name} in Hoppscotch`;
case 'user-invitation':
case 'code-your-own':
return 'Sign in to Hoppscotch';
}
}

View File

@@ -14,7 +14,7 @@
-->
<style type="text/css" rel="stylesheet" media="all">
/* Base ------------------------------ */
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
body {
width: 100% !important;
@@ -22,19 +22,19 @@
margin: 0;
-webkit-text-size-adjust: none;
}
a {
color: #3869D4;
}
a img {
border: none;
}
td {
word-break: break-word;
}
.preheader {
display: none !important;
visibility: hidden;
@@ -47,13 +47,13 @@
overflow: hidden;
}
/* Type ------------------------------ */
body,
td,
th {
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
}
h1 {
margin-top: 0;
color: #333333;
@@ -61,7 +61,7 @@
font-weight: bold;
text-align: left;
}
h2 {
margin-top: 0;
color: #333333;
@@ -69,7 +69,7 @@
font-weight: bold;
text-align: left;
}
h3 {
margin-top: 0;
color: #333333;
@@ -77,12 +77,12 @@
font-weight: bold;
text-align: left;
}
td,
th {
font-size: 16px;
}
p,
ul,
ol,
@@ -91,25 +91,25 @@
font-size: 16px;
line-height: 1.625;
}
p.sub {
font-size: 13px;
}
/* Utilities ------------------------------ */
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
/* Buttons ------------------------------ */
.button {
background-color: #3869D4;
border-top: 10px solid #3869D4;
@@ -124,7 +124,7 @@
-webkit-text-size-adjust: none;
box-sizing: border-box;
}
.button--green {
background-color: #22BC66;
border-top: 10px solid #22BC66;
@@ -132,7 +132,7 @@
border-bottom: 10px solid #22BC66;
border-left: 18px solid #22BC66;
}
.button--red {
background-color: #FF6136;
border-top: 10px solid #FF6136;
@@ -140,7 +140,7 @@
border-bottom: 10px solid #FF6136;
border-left: 18px solid #FF6136;
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
@@ -148,21 +148,21 @@
}
}
/* Attribute list ------------------------------ */
.attributes {
margin: 0 0 21px;
}
.attributes_content {
background-color: #F4F4F7;
padding: 16px;
}
.attributes_item {
padding: 0;
}
/* Related Items ------------------------------ */
.related {
width: 100%;
margin: 0;
@@ -171,31 +171,31 @@
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.related_item {
padding: 10px 0;
color: #CBCCCF;
font-size: 15px;
line-height: 18px;
}
.related_item-title {
display: block;
margin: .5em 0 0;
}
.related_item-thumb {
display: block;
padding-bottom: 10px;
}
.related_heading {
border-top: 1px solid #CBCCCF;
text-align: center;
padding: 25px 0 10px;
}
/* Discount Code ------------------------------ */
.discount {
width: 100%;
margin: 0;
@@ -206,33 +206,33 @@
background-color: #F4F4F7;
border: 2px dashed #CBCCCF;
}
.discount_heading {
text-align: center;
}
.discount_body {
text-align: center;
font-size: 15px;
}
/* Social Icons ------------------------------ */
.social {
width: auto;
}
.social td {
padding: 0;
width: auto;
}
.social_icon {
height: 20px;
margin: 0 8px 10px 8px;
padding: 0;
}
/* Data table ------------------------------ */
.purchase {
width: 100%;
margin: 0;
@@ -241,7 +241,7 @@
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_content {
width: 100%;
margin: 0;
@@ -250,50 +250,50 @@
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_item {
padding: 10px 0;
color: #51545E;
font-size: 15px;
line-height: 18px;
}
.purchase_heading {
padding-bottom: 8px;
border-bottom: 1px solid #EAEAEC;
}
.purchase_heading p {
margin: 0;
color: #85878E;
font-size: 12px;
}
.purchase_footer {
padding-top: 15px;
border-top: 1px solid #EAEAEC;
}
.purchase_total {
margin: 0;
text-align: right;
font-weight: bold;
color: #333333;
}
.purchase_total--label {
padding: 0 15px 0 0;
}
body {
background-color: #F2F4F6;
color: #51545E;
}
p {
color: #51545E;
}
.email-wrapper {
width: 100%;
margin: 0;
@@ -303,7 +303,7 @@
-premailer-cellspacing: 0;
background-color: #F2F4F6;
}
.email-content {
width: 100%;
margin: 0;
@@ -313,16 +313,16 @@
-premailer-cellspacing: 0;
}
/* Masthead ----------------------- */
.email-masthead {
padding: 25px 0;
text-align: center;
}
.email-masthead_logo {
width: 94px;
}
.email-masthead_name {
font-size: 16px;
font-weight: bold;
@@ -331,7 +331,7 @@
text-shadow: 0 1px 0 white;
}
/* Body ------------------------------ */
.email-body {
width: 100%;
margin: 0;
@@ -340,7 +340,7 @@
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.email-body_inner {
width: 570px;
margin: 0 auto;
@@ -350,7 +350,7 @@
-premailer-cellspacing: 0;
background-color: #FFFFFF;
}
.email-footer {
width: 570px;
margin: 0 auto;
@@ -360,11 +360,11 @@
-premailer-cellspacing: 0;
text-align: center;
}
.email-footer p {
color: #A8AAAF;
}
.body-action {
width: 100%;
margin: 30px auto;
@@ -374,25 +374,25 @@
-premailer-cellspacing: 0;
text-align: center;
}
.body-sub {
margin-top: 25px;
padding-top: 25px;
border-top: 1px solid #EAEAEC;
}
.content-cell {
padding: 45px;
}
/*Media Queries ------------------------------ */
@media only screen and (max-width: 600px) {
.email-body_inner,
.email-footer {
width: 100% !important;
}
}
@media (prefers-color-scheme: dark) {
body,
.email-body,

View File

@@ -1,9 +1,8 @@
import { GraphQLSchemaHost } from '@nestjs/graphql';
import {
ApolloServerPlugin,
BaseContext,
GraphQLRequestListener,
} from '@apollo/server';
} from 'apollo-server-plugin-base';
import { Plugin } from '@nestjs/apollo';
import { GraphQLError } from 'graphql';
import {
@@ -18,7 +17,7 @@ const COMPLEXITY_LIMIT = 50;
export class GQLComplexityPlugin implements ApolloServerPlugin {
constructor(private gqlSchemaHost: GraphQLSchemaHost) {}
async requestDidStart(): Promise<GraphQLRequestListener<BaseContext>> {
async requestDidStart(): Promise<GraphQLRequestListener> {
const { schema } = this.gqlSchemaHost;
return {

View File

@@ -21,8 +21,8 @@ import {
} from 'src/team-request/team-request.model';
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
import { InvitedUser } from '../admin/invited-user.model';
import { UserCollection } from '@prisma/client';
import {
UserCollection,
UserCollectionRemovedData,
UserCollectionReorderData,
} from 'src/user-collection/user-collections.model';
@@ -69,7 +69,5 @@ export type TopicDef = {
[topic: `team_req/${string}/req_deleted`]: string;
[topic: `team/${string}/invite_added`]: TeamInvitation;
[topic: `team/${string}/invite_removed`]: string;
[
topic: `shortcode/${string}/${'created' | 'revoked' | 'updated'}`
]: Shortcode;
[topic: `shortcode/${string}/${'created' | 'revoked'}`]: Shortcode;
};

View File

@@ -1,10 +1,9 @@
import { Field, ID, ObjectType } from '@nestjs/graphql';
import { User } from 'src/user/user.model';
@ObjectType()
export class Shortcode {
@Field(() => ID, {
description: 'The 12 digit alphanumeric code',
description: 'The shortcode. 12 digit alphanumeric.',
})
id: string;
@@ -13,57 +12,8 @@ export class Shortcode {
})
request: string;
@Field({
description: 'JSON string representing the properties for an embed',
nullable: true,
})
properties: string;
@Field({
description: 'Timestamp of when the Shortcode was created',
})
createdOn: Date;
}
@ObjectType()
export class ShortcodeCreator {
@Field({
description: 'Uid of user who created the shortcode',
})
uid: string;
@Field({
description: 'Email of user who created the shortcode',
})
email: string;
}
@ObjectType()
export class ShortcodeWithUserEmail {
@Field(() => ID, {
description: 'The 12 digit alphanumeric code',
})
id: string;
@Field({
description: 'JSON string representing the request data',
})
request: string;
@Field({
description: 'JSON string representing the properties for an embed',
nullable: true,
})
properties: string;
@Field({
description: 'Timestamp of when the Shortcode was created',
})
createdOn: Date;
@Field({
description: 'Details of user who created the shortcode',
nullable: true,
})
creator: ShortcodeCreator;
}

View File

@@ -1,4 +1,5 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PrismaModule } from 'src/prisma/prisma.module';
import { PubSubModule } from 'src/pubsub/pubsub.module';
import { UserModule } from 'src/user/user.module';
@@ -6,7 +7,14 @@ import { ShortcodeResolver } from './shortcode.resolver';
import { ShortcodeService } from './shortcode.service';
@Module({
imports: [PrismaModule, UserModule, PubSubModule],
imports: [
PrismaModule,
UserModule,
PubSubModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
}),
],
providers: [ShortcodeService, ShortcodeResolver],
exports: [ShortcodeService],
})

View File

@@ -1,5 +1,6 @@
import {
Args,
Context,
ID,
Mutation,
Query,
@@ -8,25 +9,28 @@ import {
} from '@nestjs/graphql';
import * as E from 'fp-ts/Either';
import { UseGuards } from '@nestjs/common';
import { Shortcode, ShortcodeWithUserEmail } from './shortcode.model';
import { Shortcode } from './shortcode.model';
import { ShortcodeService } from './shortcode.service';
import { UserService } from 'src/user/user.service';
import { throwErr } from 'src/utils';
import { GqlUser } from 'src/decorators/gql-user.decorator';
import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
import { User } from 'src/user/user.model';
import { PubSubService } from 'src/pubsub/pubsub.service';
import { AuthUser } from '../types/AuthUser';
import { JwtService } from '@nestjs/jwt';
import { PaginationArgs } from 'src/types/input-types.args';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { SkipThrottle } from '@nestjs/throttler';
import { GqlAdminGuard } from 'src/admin/guards/gql-admin.guard';
@UseGuards(GqlThrottlerGuard)
@Resolver(() => Shortcode)
export class ShortcodeResolver {
constructor(
private readonly shortcodeService: ShortcodeService,
private readonly userService: UserService,
private readonly pubsub: PubSubService,
private jwtService: JwtService,
) {}
/* Queries */
@@ -60,53 +64,20 @@ export class ShortcodeResolver {
@Mutation(() => Shortcode, {
description: 'Create a shortcode for the given request.',
})
@UseGuards(GqlAuthGuard)
async createShortcode(
@GqlUser() user: AuthUser,
@Args({
name: 'request',
description: 'JSON string of the request object',
})
request: string,
@Args({
name: 'properties',
description: 'JSON string of the properties of the embed',
nullable: true,
})
properties: string,
@Context() ctx: any,
) {
const decodedAccessToken = this.jwtService.verify(
ctx.req.cookies['access_token'],
);
const result = await this.shortcodeService.createShortcode(
request,
properties,
user,
);
if (E.isLeft(result)) throwErr(result.left);
return result.right;
}
@Mutation(() => Shortcode, {
description: 'Update a user generated Shortcode',
})
@UseGuards(GqlAuthGuard)
async updateEmbedProperties(
@GqlUser() user: AuthUser,
@Args({
name: 'code',
type: () => ID,
description: 'The Shortcode to update',
})
code: string,
@Args({
name: 'properties',
description: 'JSON string of the properties of the embed',
})
properties: string,
) {
const result = await this.shortcodeService.updateEmbedProperties(
code,
user.uid,
properties,
decodedAccessToken?.sub,
);
if (E.isLeft(result)) throwErr(result.left);
@@ -122,7 +93,7 @@ export class ShortcodeResolver {
@Args({
name: 'code',
type: () => ID,
description: 'The shortcode to remove',
description: 'The shortcode to resolve',
})
code: string,
) {
@@ -143,16 +114,6 @@ export class ShortcodeResolver {
return this.pubsub.asyncIterator(`shortcode/${user.uid}/created`);
}
@Subscription(() => Shortcode, {
description: 'Listen for Shortcode updates',
resolve: (value) => value,
})
@SkipThrottle()
@UseGuards(GqlAuthGuard)
myShortcodesUpdated(@GqlUser() user: AuthUser) {
return this.pubsub.asyncIterator(`shortcode/${user.uid}/updated`);
}
@Subscription(() => Shortcode, {
description: 'Listen for shortcode deletion',
resolve: (value) => value,

View File

@@ -1,16 +1,13 @@
import { mockDeep, mockReset } from 'jest-mock-extended';
import { PrismaService } from '../prisma/prisma.service';
import {
INVALID_EMAIL,
SHORTCODE_INVALID_PROPERTIES_JSON,
SHORTCODE_INVALID_REQUEST_JSON,
SHORTCODE_ALREADY_EXISTS,
SHORTCODE_INVALID_JSON,
SHORTCODE_NOT_FOUND,
SHORTCODE_PROPERTIES_NOT_FOUND,
} from 'src/errors';
import { Shortcode, ShortcodeWithUserEmail } from './shortcode.model';
import { Shortcode } from './shortcode.model';
import { ShortcodeService } from './shortcode.service';
import { UserService } from 'src/user/user.service';
import { AuthUser } from 'src/types/AuthUser';
const mockPrisma = mockDeep<PrismaService>();
@@ -25,7 +22,7 @@ const mockFB = {
doc: mockDocFunc,
},
};
const mockUserService = new UserService(mockPrisma as any, mockPubSub as any);
const mockUserService = new UserService(mockFB as any, mockPubSub as any);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
@@ -41,34 +38,18 @@ beforeEach(() => {
});
const createdOn = new Date();
const user: AuthUser = {
uid: '123344',
email: 'dwight@dundermifflin.com',
displayName: 'Dwight Schrute',
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
isAdmin: false,
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
createdOn: createdOn,
currentGQLSession: {},
currentRESTSession: {},
};
const mockEmbed = {
const shortCodeWithOutUser = {
id: '123',
request: '{}',
embedProperties: '{}',
createdOn: createdOn,
creatorUid: user.uid,
updatedOn: createdOn,
creatorUid: null,
};
const mockShortcode = {
const shortCodeWithUser = {
id: '123',
request: '{}',
embedProperties: null,
createdOn: createdOn,
creatorUid: user.uid,
updatedOn: createdOn,
creatorUid: 'user_uid_1',
};
const shortcodes = [
@@ -77,67 +58,33 @@ const shortcodes = [
request: {
hello: 'there',
},
embedProperties: {
foo: 'bar',
},
creatorUid: user.uid,
creatorUid: 'testuser',
createdOn: new Date(),
updatedOn: createdOn,
},
{
id: 'blablabla1',
request: {
hello: 'there',
},
embedProperties: {
foo: 'bar',
},
creatorUid: user.uid,
creatorUid: 'testuser',
createdOn: new Date(),
updatedOn: createdOn,
},
];
const shortcodesWithUserEmail = [
{
id: 'blablabla',
request: {
hello: 'there',
},
embedProperties: {
foo: 'bar',
},
creatorUid: user.uid,
createdOn: new Date(),
updatedOn: createdOn,
User: user,
},
{
id: 'blablabla1',
request: {
hello: 'there',
},
embedProperties: {
foo: 'bar',
},
creatorUid: user.uid,
createdOn: new Date(),
updatedOn: createdOn,
User: user,
},
];
describe('ShortcodeService', () => {
describe('getShortCode', () => {
test('should return a valid Shortcode with valid Shortcode ID', async () => {
mockPrisma.shortcode.findFirstOrThrow.mockResolvedValueOnce(mockEmbed);
test('should return a valid shortcode with valid shortcode ID', async () => {
mockPrisma.shortcode.findFirstOrThrow.mockResolvedValueOnce(
shortCodeWithOutUser,
);
const result = await shortcodeService.getShortCode(mockEmbed.id);
const result = await shortcodeService.getShortCode(
shortCodeWithOutUser.id,
);
expect(result).toEqualRight(<Shortcode>{
id: mockEmbed.id,
createdOn: mockEmbed.createdOn,
request: JSON.stringify(mockEmbed.request),
properties: JSON.stringify(mockEmbed.embedProperties),
id: shortCodeWithOutUser.id,
createdOn: shortCodeWithOutUser.createdOn,
request: JSON.stringify(shortCodeWithOutUser.request),
});
});
@@ -152,10 +99,10 @@ describe('ShortcodeService', () => {
});
describe('fetchUserShortCodes', () => {
test('should return list of Shortcode with valid inputs and no cursor', async () => {
test('should return list of shortcodes with valid inputs and no cursor', async () => {
mockPrisma.shortcode.findMany.mockResolvedValueOnce(shortcodes);
const result = await shortcodeService.fetchUserShortCodes(user.uid, {
const result = await shortcodeService.fetchUserShortCodes('testuser', {
cursor: null,
take: 10,
});
@@ -163,22 +110,20 @@ describe('ShortcodeService', () => {
{
id: shortcodes[0].id,
request: JSON.stringify(shortcodes[0].request),
properties: JSON.stringify(shortcodes[0].embedProperties),
createdOn: shortcodes[0].createdOn,
},
{
id: shortcodes[1].id,
request: JSON.stringify(shortcodes[1].request),
properties: JSON.stringify(shortcodes[1].embedProperties),
createdOn: shortcodes[1].createdOn,
},
]);
});
test('should return list of Shortcode with valid inputs and cursor', async () => {
test('should return list of shortcodes with valid inputs and cursor', async () => {
mockPrisma.shortcode.findMany.mockResolvedValue([shortcodes[1]]);
const result = await shortcodeService.fetchUserShortCodes(user.uid, {
const result = await shortcodeService.fetchUserShortCodes('testuser', {
cursor: 'blablabla',
take: 10,
});
@@ -186,7 +131,6 @@ describe('ShortcodeService', () => {
{
id: shortcodes[1].id,
request: JSON.stringify(shortcodes[1].request),
properties: JSON.stringify(shortcodes[1].embedProperties),
createdOn: shortcodes[1].createdOn,
},
]);
@@ -195,7 +139,7 @@ describe('ShortcodeService', () => {
test('should return an empty array for an invalid cursor', async () => {
mockPrisma.shortcode.findMany.mockResolvedValue([]);
const result = await shortcodeService.fetchUserShortCodes(user.uid, {
const result = await shortcodeService.fetchUserShortCodes('testuser', {
cursor: 'invalidcursor',
take: 10,
});
@@ -227,111 +171,77 @@ describe('ShortcodeService', () => {
});
describe('createShortcode', () => {
test('should throw SHORTCODE_INVALID_REQUEST_JSON error if incoming request data is invalid', async () => {
test('should throw SHORTCODE_INVALID_JSON error if incoming request data is invalid', async () => {
const result = await shortcodeService.createShortcode(
'invalidRequest',
null,
user,
'user_uid_1',
);
expect(result).toEqualLeft(SHORTCODE_INVALID_REQUEST_JSON);
expect(result).toEqualLeft(SHORTCODE_INVALID_JSON);
});
test('should throw SHORTCODE_INVALID_PROPERTIES_JSON error if incoming properties data is invalid', async () => {
const result = await shortcodeService.createShortcode(
'{}',
'invalid_data',
user,
);
expect(result).toEqualLeft(SHORTCODE_INVALID_PROPERTIES_JSON);
});
test('should successfully create a new Embed with valid user uid', async () => {
// generateUniqueShortCodeID --> getShortcode
test('should successfully create a new shortcode with valid user uid', async () => {
// generateUniqueShortCodeID --> getShortCode
mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
'NotFoundError',
);
mockPrisma.shortcode.create.mockResolvedValueOnce(mockEmbed);
mockPrisma.shortcode.create.mockResolvedValueOnce(shortCodeWithUser);
const result = await shortcodeService.createShortcode('{}', '{}', user);
expect(result).toEqualRight(<Shortcode>{
id: mockEmbed.id,
createdOn: mockEmbed.createdOn,
request: JSON.stringify(mockEmbed.request),
properties: JSON.stringify(mockEmbed.embedProperties),
const result = await shortcodeService.createShortcode('{}', 'user_uid_1');
expect(result).toEqualRight({
id: shortCodeWithUser.id,
createdOn: shortCodeWithUser.createdOn,
request: JSON.stringify(shortCodeWithUser.request),
});
});
test('should successfully create a new ShortCode with valid user uid', async () => {
// generateUniqueShortCodeID --> getShortcode
test('should successfully create a new shortcode with null user uid', async () => {
// generateUniqueShortCodeID --> getShortCode
mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
'NotFoundError',
);
mockPrisma.shortcode.create.mockResolvedValueOnce(mockShortcode);
mockPrisma.shortcode.create.mockResolvedValueOnce(shortCodeWithUser);
const result = await shortcodeService.createShortcode('{}', null, user);
expect(result).toEqualRight(<Shortcode>{
id: mockShortcode.id,
createdOn: mockShortcode.createdOn,
request: JSON.stringify(mockShortcode.request),
properties: mockShortcode.embedProperties,
const result = await shortcodeService.createShortcode('{}', null);
expect(result).toEqualRight({
id: shortCodeWithUser.id,
createdOn: shortCodeWithUser.createdOn,
request: JSON.stringify(shortCodeWithOutUser.request),
});
});
test('should send pubsub message to `shortcode/{uid}/created` on successful creation of a Shortcode', async () => {
// generateUniqueShortCodeID --> getShortcode
test('should send pubsub message to `shortcode/{uid}/created` on successful creation of shortcode', async () => {
// generateUniqueShortCodeID --> getShortCode
mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
'NotFoundError',
);
mockPrisma.shortcode.create.mockResolvedValueOnce(mockShortcode);
const result = await shortcodeService.createShortcode('{}', null, user);
mockPrisma.shortcode.create.mockResolvedValueOnce(shortCodeWithUser);
const result = await shortcodeService.createShortcode('{}', 'user_uid_1');
expect(mockPubSub.publish).toHaveBeenCalledWith(
`shortcode/${mockShortcode.creatorUid}/created`,
<Shortcode>{
id: mockShortcode.id,
createdOn: mockShortcode.createdOn,
request: JSON.stringify(mockShortcode.request),
properties: mockShortcode.embedProperties,
},
);
});
test('should send pubsub message to `shortcode/{uid}/created` on successful creation of an Embed', async () => {
// generateUniqueShortCodeID --> getShortcode
mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
'NotFoundError',
);
mockPrisma.shortcode.create.mockResolvedValueOnce(mockEmbed);
const result = await shortcodeService.createShortcode('{}', '{}', user);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`shortcode/${mockEmbed.creatorUid}/created`,
<Shortcode>{
id: mockEmbed.id,
createdOn: mockEmbed.createdOn,
request: JSON.stringify(mockEmbed.request),
properties: JSON.stringify(mockEmbed.embedProperties),
`shortcode/${shortCodeWithUser.creatorUid}/created`,
{
id: shortCodeWithUser.id,
createdOn: shortCodeWithUser.createdOn,
request: JSON.stringify(shortCodeWithUser.request),
},
);
});
});
describe('revokeShortCode', () => {
test('should return true on successful deletion of Shortcode with valid inputs', async () => {
mockPrisma.shortcode.delete.mockResolvedValueOnce(mockEmbed);
test('should return true on successful deletion of shortcode with valid inputs', async () => {
mockPrisma.shortcode.delete.mockResolvedValueOnce(shortCodeWithUser);
const result = await shortcodeService.revokeShortCode(
mockEmbed.id,
mockEmbed.creatorUid,
shortCodeWithUser.id,
shortCodeWithUser.creatorUid,
);
expect(mockPrisma.shortcode.delete).toHaveBeenCalledWith({
where: {
creator_uid_shortcode_unique: {
creatorUid: mockEmbed.creatorUid,
id: mockEmbed.id,
creatorUid: shortCodeWithUser.creatorUid,
id: shortCodeWithUser.id,
},
},
});
@@ -339,53 +249,52 @@ describe('ShortcodeService', () => {
expect(result).toEqualRight(true);
});
test('should return SHORTCODE_NOT_FOUND error when Shortcode is invalid and user uid is valid', async () => {
test('should return SHORTCODE_NOT_FOUND error when shortcode is invalid and user uid is valid', async () => {
mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
expect(
shortcodeService.revokeShortCode('invalid', 'testuser'),
).resolves.toEqualLeft(SHORTCODE_NOT_FOUND);
});
test('should return SHORTCODE_NOT_FOUND error when Shortcode is valid and user uid is invalid', async () => {
test('should return SHORTCODE_NOT_FOUND error when shortcode is valid and user uid is invalid', async () => {
mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
expect(
shortcodeService.revokeShortCode('blablablabla', 'invalidUser'),
).resolves.toEqualLeft(SHORTCODE_NOT_FOUND);
});
test('should return SHORTCODE_NOT_FOUND error when both Shortcode and user uid are invalid', async () => {
test('should return SHORTCODE_NOT_FOUND error when both shortcode and user uid are invalid', async () => {
mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
expect(
shortcodeService.revokeShortCode('invalid', 'invalid'),
).resolves.toEqualLeft(SHORTCODE_NOT_FOUND);
});
test('should send pubsub message to `shortcode/{uid}/revoked` on successful deletion of Shortcode', async () => {
mockPrisma.shortcode.delete.mockResolvedValueOnce(mockEmbed);
test('should send pubsub message to `shortcode/{uid}/revoked` on successful deletion of shortcode', async () => {
mockPrisma.shortcode.delete.mockResolvedValueOnce(shortCodeWithUser);
const result = await shortcodeService.revokeShortCode(
mockEmbed.id,
mockEmbed.creatorUid,
shortCodeWithUser.id,
shortCodeWithUser.creatorUid,
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`shortcode/${mockEmbed.creatorUid}/revoked`,
`shortcode/${shortCodeWithUser.creatorUid}/revoked`,
{
id: mockEmbed.id,
createdOn: mockEmbed.createdOn,
request: JSON.stringify(mockEmbed.request),
properties: JSON.stringify(mockEmbed.embedProperties),
id: shortCodeWithUser.id,
createdOn: shortCodeWithUser.createdOn,
request: JSON.stringify(shortCodeWithUser.request),
},
);
});
});
describe('deleteUserShortCodes', () => {
test('should successfully delete all users Shortcodes with valid user uid', async () => {
test('should successfully delete all users shortcodes with valid user uid', async () => {
mockPrisma.shortcode.deleteMany.mockResolvedValueOnce({ count: 1 });
const result = await shortcodeService.deleteUserShortCodes(
mockEmbed.creatorUid,
shortCodeWithUser.creatorUid,
);
expect(result).toEqual(1);
});
@@ -394,176 +303,9 @@ describe('ShortcodeService', () => {
mockPrisma.shortcode.deleteMany.mockResolvedValueOnce({ count: 0 });
const result = await shortcodeService.deleteUserShortCodes(
mockEmbed.creatorUid,
shortCodeWithUser.creatorUid,
);
expect(result).toEqual(0);
});
});
describe('updateShortcode', () => {
test('should return SHORTCODE_PROPERTIES_NOT_FOUND error when updatedProps in invalid', async () => {
const result = await shortcodeService.updateEmbedProperties(
mockEmbed.id,
user.uid,
'',
);
expect(result).toEqualLeft(SHORTCODE_PROPERTIES_NOT_FOUND);
});
test('should return SHORTCODE_PROPERTIES_NOT_FOUND error when updatedProps in invalid JSON format', async () => {
const result = await shortcodeService.updateEmbedProperties(
mockEmbed.id,
user.uid,
'{kk',
);
expect(result).toEqualLeft(SHORTCODE_INVALID_PROPERTIES_JSON);
});
test('should return SHORTCODE_NOT_FOUND error when Shortcode ID is invalid', async () => {
mockPrisma.shortcode.update.mockRejectedValue('RecordNotFound');
const result = await shortcodeService.updateEmbedProperties(
'invalidID',
user.uid,
'{}',
);
expect(result).toEqualLeft(SHORTCODE_NOT_FOUND);
});
test('should successfully update a Shortcodes with valid inputs', async () => {
mockPrisma.shortcode.update.mockResolvedValueOnce({
...mockEmbed,
embedProperties: '{"foo":"bar"}',
});
const result = await shortcodeService.updateEmbedProperties(
mockEmbed.id,
user.uid,
'{"foo":"bar"}',
);
expect(result).toEqualRight({
id: mockEmbed.id,
createdOn: mockEmbed.createdOn,
request: JSON.stringify(mockEmbed.request),
properties: JSON.stringify('{"foo":"bar"}'),
});
});
test('should send pubsub message to `shortcode/{uid}/updated` on successful Update of Shortcode', async () => {
mockPrisma.shortcode.update.mockResolvedValueOnce({
...mockEmbed,
embedProperties: '{"foo":"bar"}',
});
const result = await shortcodeService.updateEmbedProperties(
mockEmbed.id,
user.uid,
'{"foo":"bar"}',
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`shortcode/${mockEmbed.creatorUid}/updated`,
{
id: mockEmbed.id,
createdOn: mockEmbed.createdOn,
request: JSON.stringify(mockEmbed.request),
properties: JSON.stringify('{"foo":"bar"}'),
},
);
});
});
describe('deleteShortcode', () => {
test('should return true on successful deletion of Shortcode with valid inputs', async () => {
mockPrisma.shortcode.delete.mockResolvedValueOnce(mockEmbed);
const result = await shortcodeService.deleteShortcode(mockEmbed.id);
expect(result).toEqualRight(true);
});
test('should return SHORTCODE_NOT_FOUND error when Shortcode is invalid', async () => {
mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
expect(shortcodeService.deleteShortcode('invalid')).resolves.toEqualLeft(
SHORTCODE_NOT_FOUND,
);
});
});
describe('fetchAllShortcodes', () => {
test('should return list of Shortcodes with valid inputs and no cursor', async () => {
mockPrisma.shortcode.findMany.mockResolvedValueOnce(
shortcodesWithUserEmail,
);
const result = await shortcodeService.fetchAllShortcodes(
{
cursor: null,
take: 10,
},
user.email,
);
expect(result).toEqual(<ShortcodeWithUserEmail[]>[
{
id: shortcodes[0].id,
request: JSON.stringify(shortcodes[0].request),
properties: JSON.stringify(shortcodes[0].embedProperties),
createdOn: shortcodes[0].createdOn,
creator: {
uid: user.uid,
email: user.email,
},
},
{
id: shortcodes[1].id,
request: JSON.stringify(shortcodes[1].request),
properties: JSON.stringify(shortcodes[1].embedProperties),
createdOn: shortcodes[1].createdOn,
creator: {
uid: user.uid,
email: user.email,
},
},
]);
});
test('should return list of Shortcode with valid inputs and cursor', async () => {
mockPrisma.shortcode.findMany.mockResolvedValue([
shortcodesWithUserEmail[1],
]);
const result = await shortcodeService.fetchAllShortcodes(
{
cursor: 'blablabla',
take: 10,
},
user.email,
);
expect(result).toEqual(<ShortcodeWithUserEmail[]>[
{
id: shortcodes[1].id,
request: JSON.stringify(shortcodes[1].request),
properties: JSON.stringify(shortcodes[1].embedProperties),
createdOn: shortcodes[1].createdOn,
creator: {
uid: user.uid,
email: user.email,
},
},
]);
});
test('should return an empty array for an invalid cursor', async () => {
mockPrisma.shortcode.findMany.mockResolvedValue([]);
const result = await shortcodeService.fetchAllShortcodes(
{
cursor: 'invalidcursor',
take: 10,
},
user.email,
);
expect(result).toHaveLength(0);
});
});
});

View File

@@ -1,16 +1,12 @@
import { Injectable, OnModuleInit } 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 E from 'fp-ts/Either';
import { PrismaService } from 'src/prisma/prisma.service';
import {
SHORTCODE_INVALID_PROPERTIES_JSON,
SHORTCODE_INVALID_REQUEST_JSON,
SHORTCODE_NOT_FOUND,
SHORTCODE_PROPERTIES_NOT_FOUND,
} from 'src/errors';
import { SHORTCODE_INVALID_JSON, SHORTCODE_NOT_FOUND } from 'src/errors';
import { UserDataHandler } from 'src/user/user.data.handler';
import { Shortcode, ShortcodeWithUserEmail } from './shortcode.model';
import { Shortcode } from './shortcode.model';
import { Shortcode as DBShortCode } from '@prisma/client';
import { PubSubService } from 'src/pubsub/pubsub.service';
import { UserService } from 'src/user/user.service';
@@ -50,14 +46,10 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
* @param shortcodeInfo Prisma Shortcode type
* @returns GQL Shortcode
*/
private cast(shortcodeInfo: DBShortCode): Shortcode {
private returnShortCode(shortcodeInfo: DBShortCode): Shortcode {
return <Shortcode>{
id: shortcodeInfo.id,
request: JSON.stringify(shortcodeInfo.request),
properties:
shortcodeInfo.embedProperties != null
? JSON.stringify(shortcodeInfo.embedProperties)
: null,
createdOn: shortcodeInfo.createdOn,
};
}
@@ -102,7 +94,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
const shortcodeInfo = await this.prisma.shortcode.findFirstOrThrow({
where: { id: shortcode },
});
return E.right(this.cast(shortcodeInfo));
return E.right(this.returnShortCode(shortcodeInfo));
} catch (error) {
return E.left(SHORTCODE_NOT_FOUND);
}
@@ -112,22 +104,14 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
* Create a new ShortCode
*
* @param request JSON string of request details
* @param userInfo user UI
* @param properties JSON string of embed properties, if present
* @param userUID user UID, if present
* @returns Either of ShortCode or error
*/
async createShortcode(
request: string,
properties: string | null = null,
userInfo: AuthUser,
) {
const requestData = stringToJson(request);
if (E.isLeft(requestData) || !requestData.right)
return E.left(SHORTCODE_INVALID_REQUEST_JSON);
async createShortcode(request: string, userUID: string | null) {
const shortcodeData = stringToJson(request);
if (E.isLeft(shortcodeData)) return E.left(SHORTCODE_INVALID_JSON);
const parsedProperties = stringToJson(properties);
if (E.isLeft(parsedProperties))
return E.left(SHORTCODE_INVALID_PROPERTIES_JSON);
const user = await this.userService.findUserById(userUID);
const generatedShortCode = await this.generateUniqueShortCodeID();
if (E.isLeft(generatedShortCode)) return E.left(generatedShortCode.left);
@@ -135,9 +119,8 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
const createdShortCode = await this.prisma.shortcode.create({
data: {
id: generatedShortCode.right,
request: requestData.right,
embedProperties: parsedProperties.right ?? undefined,
creatorUid: userInfo.uid,
request: shortcodeData.right,
creatorUid: O.isNone(user) ? null : user.value.uid,
},
});
@@ -145,11 +128,11 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
if (createdShortCode.creatorUid) {
this.pubsub.publish(
`shortcode/${createdShortCode.creatorUid}/created`,
this.cast(createdShortCode),
this.returnShortCode(createdShortCode),
);
}
return E.right(this.cast(createdShortCode));
return E.right(this.returnShortCode(createdShortCode));
}
/**
@@ -173,14 +156,14 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
});
const fetchedShortCodes: Shortcode[] = shortCodes.map((code) =>
this.cast(code),
this.returnShortCode(code),
);
return fetchedShortCodes;
}
/**
* Delete a ShortCode created by User of uid
* Delete a ShortCode
*
* @param shortcode ShortCode
* @param uid User Uid
@@ -199,7 +182,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
this.pubsub.publish(
`shortcode/${deletedShortCodes.creatorUid}/revoked`,
this.cast(deletedShortCodes),
this.returnShortCode(deletedShortCodes),
);
return E.right(true);
@@ -222,118 +205,4 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
return deletedShortCodes.count;
}
/**
* Delete a Shortcode
*
* @param shortcodeID ID of Shortcode being deleted
* @returns Boolean on successful deletion
*/
async deleteShortcode(shortcodeID: string) {
try {
await this.prisma.shortcode.delete({
where: {
id: shortcodeID,
},
});
return E.right(true);
} catch (error) {
return E.left(SHORTCODE_NOT_FOUND);
}
}
/**
* Update a created Shortcode
* @param shortcodeID Shortcode ID
* @param uid User Uid
* @returns Updated Shortcode
*/
async updateEmbedProperties(
shortcodeID: string,
uid: string,
updatedProps: string,
) {
if (!updatedProps) return E.left(SHORTCODE_PROPERTIES_NOT_FOUND);
const parsedProperties = stringToJson(updatedProps);
if (E.isLeft(parsedProperties) || !parsedProperties.right)
return E.left(SHORTCODE_INVALID_PROPERTIES_JSON);
try {
const updatedShortcode = await this.prisma.shortcode.update({
where: {
creator_uid_shortcode_unique: {
creatorUid: uid,
id: shortcodeID,
},
},
data: {
embedProperties: parsedProperties.right,
},
});
this.pubsub.publish(
`shortcode/${updatedShortcode.creatorUid}/updated`,
this.cast(updatedShortcode),
);
return E.right(this.cast(updatedShortcode));
} catch (error) {
return E.left(SHORTCODE_NOT_FOUND);
}
}
/**
* Fetch all created ShortCodes
*
* @param args Pagination arguments
* @param userEmail User email
* @returns ShortcodeWithUserEmail
*/
async fetchAllShortcodes(
args: PaginationArgs,
userEmail: string | null = null,
) {
const shortCodes = await this.prisma.shortcode.findMany({
where: userEmail
? {
User: {
email: userEmail,
},
}
: undefined,
orderBy: {
createdOn: 'desc',
},
skip: args.cursor ? 1 : 0,
take: args.take,
cursor: args.cursor ? { id: args.cursor } : undefined,
include: {
User: true,
},
});
const fetchedShortCodes: ShortcodeWithUserEmail[] = shortCodes.map(
(code) => {
return <ShortcodeWithUserEmail>{
id: code.id,
request: JSON.stringify(code.request),
properties:
code.embedProperties != null
? JSON.stringify(code.embedProperties)
: null,
createdOn: code.createdOn,
creator: code.User
? {
uid: code.User.uid,
email: code.User.email,
}
: null,
};
},
);
return fetchedShortCodes;
}
}

View File

@@ -14,13 +14,6 @@ export class CreateRootTeamCollectionArgs {
@Field({ name: 'title', description: 'Title of the new collection' })
title: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}
@ArgsType()
@@ -33,13 +26,6 @@ export class CreateChildTeamCollectionArgs {
@Field({ name: 'childTitle', description: 'Title of the new collection' })
childTitle: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}
@ArgsType()
@@ -47,14 +33,12 @@ export class RenameTeamCollectionArgs {
@Field(() => ID, {
name: 'collectionID',
description: 'ID of the collection',
deprecationReason: 'Switch to updateTeamCollection mutation instead',
})
collectionID: string;
@Field({
name: 'newTitle',
description: 'The updated title of the collection',
deprecationReason: 'Switch to updateTeamCollection mutation instead',
})
newTitle: string;
}
@@ -114,26 +98,3 @@ export class ReplaceTeamCollectionArgs {
})
parentCollectionID?: string;
}
@ArgsType()
export class UpdateTeamCollectionArgs {
@Field(() => ID, {
name: 'collectionID',
description: 'ID of the collection',
})
collectionID: string;
@Field({
name: 'newTitle',
description: 'The updated title of the collection',
nullable: true,
})
newTitle: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}

View File

@@ -12,17 +12,12 @@ export class TeamCollection {
})
title: string;
@Field({
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
@Field(() => ID, {
description: 'ID of the collection',
nullable: true,
})
parentID: string;
teamID: string;
}
@ObjectType()

View File

@@ -25,7 +25,6 @@ import {
MoveTeamCollectionArgs,
RenameTeamCollectionArgs,
ReplaceTeamCollectionArgs,
UpdateTeamCollectionArgs,
UpdateTeamCollectionOrderArgs,
} from './input-type.args';
import * as E from 'fp-ts/Either';
@@ -142,14 +141,7 @@ export class TeamCollectionResolver {
);
if (E.isLeft(teamCollections)) throwErr(teamCollections.left);
return <TeamCollection>{
id: teamCollections.right.id,
title: teamCollections.right.title,
parentID: teamCollections.right.parentID,
data: !teamCollections.right.data
? null
: JSON.stringify(teamCollections.right.data),
};
return teamCollections.right;
}
// Mutations
@@ -163,7 +155,6 @@ export class TeamCollectionResolver {
const teamCollection = await this.teamCollectionService.createCollection(
args.teamID,
args.title,
args.data,
null,
);
@@ -239,7 +230,6 @@ export class TeamCollectionResolver {
const teamCollection = await this.teamCollectionService.createCollection(
team.right.id,
args.childTitle,
args.data,
args.collectionID,
);
@@ -249,7 +239,6 @@ export class TeamCollectionResolver {
@Mutation(() => TeamCollection, {
description: 'Rename a collection',
deprecationReason: 'Switch to updateTeamCollection mutation instead',
})
@UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
@@ -314,23 +303,6 @@ export class TeamCollectionResolver {
return request.right;
}
@Mutation(() => TeamCollection, {
description: 'Update Team Collection details',
})
@UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
async updateTeamCollection(@Args() args: UpdateTeamCollectionArgs) {
const updatedTeamCollection =
await this.teamCollectionService.updateTeamCollection(
args.collectionID,
args.data,
args.newTitle,
);
if (E.isLeft(updatedTeamCollection)) throwErr(updatedTeamCollection.left);
return updatedTeamCollection.right;
}
// Subscriptions
@Subscription(() => TeamCollection, {

View File

@@ -1,7 +1,6 @@
import { Team, TeamCollection as DBTeamCollection } from '@prisma/client';
import { mockDeep, mockReset } from 'jest-mock-extended';
import { mock, mockDeep, mockReset } from 'jest-mock-extended';
import {
TEAM_COLL_DATA_INVALID,
TEAM_COLL_DEST_SAME,
TEAM_COLL_INVALID_JSON,
TEAM_COLL_IS_PARENT_COLL,
@@ -19,6 +18,8 @@ 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';
const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>();
@@ -53,60 +54,35 @@ const rootTeamCollection: DBTeamCollection = {
id: '123',
orderIndex: 1,
parentID: null,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
};
const rootTeamCollectionsCasted: TeamCollection = {
id: rootTeamCollection.id,
title: rootTeamCollection.title,
parentID: rootTeamCollection.parentID,
data: JSON.stringify(rootTeamCollection.data),
};
const rootTeamCollection_2: DBTeamCollection = {
id: 'erv',
orderIndex: 2,
parentID: null,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
};
const rootTeamCollection_2Casted: TeamCollection = {
id: 'erv',
parentID: null,
data: JSON.stringify(rootTeamCollection_2.data),
title: 'Root Collection 1',
};
const childTeamCollection: DBTeamCollection = {
id: 'rfe',
orderIndex: 1,
parentID: rootTeamCollection.id,
data: {},
title: 'Child Collection 1',
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
};
const childTeamCollectionCasted: TeamCollection = {
id: 'rfe',
parentID: rootTeamCollection.id,
data: JSON.stringify(childTeamCollection.data),
title: 'Child Collection 1',
};
const childTeamCollection_2: DBTeamCollection = {
id: 'bgdz',
orderIndex: 1,
data: {},
parentID: rootTeamCollection_2.id,
title: 'Child Collection 1',
teamID: team.id,
@@ -114,20 +90,11 @@ const childTeamCollection_2: DBTeamCollection = {
updatedOn: currentTime,
};
const childTeamCollection_2Casted: TeamCollection = {
id: 'bgdz',
data: JSON.stringify(childTeamCollection_2.data),
parentID: rootTeamCollection_2.id,
title: 'Child Collection 1',
};
const rootTeamCollectionList: DBTeamCollection[] = [
{
id: 'fdv',
orderIndex: 1,
parentID: null,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -138,8 +105,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 2,
parentID: null,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -149,8 +114,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 3,
parentID: null,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -159,8 +122,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
id: 'bre3',
orderIndex: 4,
parentID: null,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -171,8 +132,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 5,
parentID: null,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -183,8 +142,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
parentID: null,
title: 'Root Collection 1',
teamID: team.id,
data: {},
createdOn: currentTime,
updatedOn: currentTime,
},
@@ -194,8 +151,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
parentID: null,
title: 'Root Collection 1',
teamID: team.id,
data: {},
createdOn: currentTime,
updatedOn: currentTime,
},
@@ -204,7 +159,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 8,
parentID: null,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -214,7 +168,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 9,
parentID: null,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -225,83 +178,17 @@ const rootTeamCollectionList: DBTeamCollection[] = [
parentID: null,
title: 'Root Collection 1',
teamID: team.id,
data: {},
createdOn: currentTime,
updatedOn: currentTime,
},
];
const rootTeamCollectionListCasted: TeamCollection[] = [
{
id: 'fdv',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: 'fbbg',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: 'fgbfg',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: 'bre3',
parentID: null,
data: JSON.stringify(rootTeamCollection.data),
title: 'Root Collection 1',
},
{
id: 'hghgf',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '123',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '54tyh',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '234re',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '34rtg',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '45tgh',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
];
const childTeamCollectionList: DBTeamCollection[] = [
{
id: '123',
orderIndex: 1,
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -311,8 +198,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
orderIndex: 2,
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -322,8 +207,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
orderIndex: 3,
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -332,8 +215,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '567',
orderIndex: 4,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -343,8 +224,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '123',
orderIndex: 5,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -354,8 +233,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '678',
orderIndex: 6,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -365,8 +242,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '789',
orderIndex: 7,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -376,8 +251,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '890',
orderIndex: 8,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -387,7 +260,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '012',
orderIndex: 9,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -397,8 +269,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '0bhu',
orderIndex: 10,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -406,75 +276,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
},
];
const childTeamCollectionListCasted: TeamCollection[] = [
{
id: '123',
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: JSON.stringify({}),
},
{
id: '345',
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: JSON.stringify({}),
},
{
id: '456',
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: JSON.stringify({}),
},
{
id: '567',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '123',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '678',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '789',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '890',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '012',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '0bhu',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
];
beforeEach(() => {
mockReset(mockPrisma);
mockPubSub.publish.mockClear();
@@ -513,7 +314,7 @@ describe('getParentOfCollection', () => {
const result = await teamCollectionService.getParentOfCollection(
childTeamCollection.id,
);
expect(result).toEqual(rootTeamCollectionsCasted);
expect(result).toEqual(rootTeamCollection);
});
test('should return null successfully for a root collection with valid collectionID', async () => {
@@ -549,7 +350,7 @@ describe('getChildrenOfCollection', () => {
null,
10,
);
expect(result).toEqual(childTeamCollectionListCasted);
expect(result).toEqual(childTeamCollectionList);
});
test('should return a list of 3 child collections successfully with cursor being equal to the 7th item in the list', async () => {
@@ -565,9 +366,9 @@ describe('getChildrenOfCollection', () => {
10,
);
expect(result).toEqual([
{ ...childTeamCollectionListCasted[7] },
{ ...childTeamCollectionListCasted[8] },
{ ...childTeamCollectionListCasted[9] },
{ ...childTeamCollectionList[7] },
{ ...childTeamCollectionList[8] },
{ ...childTeamCollectionList[9] },
]);
});
@@ -594,7 +395,7 @@ describe('getTeamRootCollections', () => {
null,
10,
);
expect(result).toEqual(rootTeamCollectionListCasted);
expect(result).toEqual(rootTeamCollectionList);
});
test('should return a list of 3 root collections successfully with cursor being equal to the 7th item in the list', async () => {
@@ -610,9 +411,9 @@ describe('getTeamRootCollections', () => {
10,
);
expect(result).toEqual([
{ ...rootTeamCollectionListCasted[7] },
{ ...rootTeamCollectionListCasted[8] },
{ ...rootTeamCollectionListCasted[9] },
{ ...rootTeamCollectionList[7] },
{ ...rootTeamCollectionList[8] },
{ ...rootTeamCollectionList[9] },
]);
});
@@ -666,7 +467,6 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID,
'ab',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id,
);
expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE);
@@ -681,27 +481,11 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID,
'abcd',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id,
);
expect(result).toEqualLeft(TEAM_NOT_OWNER);
});
test('should throw TEAM_COLL_DATA_INVALID when parent TeamCollection does not belong to the team', async () => {
// isOwnerCheck
mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce(
rootTeamCollection,
);
const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID,
'abcd',
'{',
rootTeamCollection.id,
);
expect(result).toEqualLeft(TEAM_COLL_DATA_INVALID);
});
test('should successfully create a new root TeamCollection with valid inputs', async () => {
// isOwnerCheck
mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce(
@@ -715,10 +499,9 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID,
'abcdefg',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id,
);
expect(result).toEqualRight(rootTeamCollectionsCasted);
expect(result).toEqualRight(rootTeamCollection);
});
test('should successfully create a new child TeamCollection with valid inputs', async () => {
@@ -734,10 +517,9 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection(
childTeamCollection.teamID,
childTeamCollection.title,
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id,
);
expect(result).toEqualRight(childTeamCollectionCasted);
expect(result).toEqualRight(childTeamCollection);
});
test('should send pubsub message to "team_coll/<teamID>/coll_added" if child TeamCollection is created successfully', async () => {
@@ -753,13 +535,11 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection(
childTeamCollection.teamID,
childTeamCollection.title,
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id,
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection.teamID}/coll_added`,
childTeamCollectionCasted,
childTeamCollection,
);
});
@@ -776,13 +556,11 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID,
'abcdefg',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id,
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_added`,
rootTeamCollectionsCasted,
rootTeamCollection,
);
});
});
@@ -812,7 +590,7 @@ describe('renameCollection', () => {
'NewTitle',
);
expect(result).toEqualRight({
...rootTeamCollectionsCasted,
...rootTeamCollection,
title: 'NewTitle',
});
});
@@ -850,7 +628,7 @@ describe('renameCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_updated`,
{
...rootTeamCollectionsCasted,
...rootTeamCollection,
title: 'NewTitle',
},
);
@@ -1057,8 +835,9 @@ describe('moveCollection', () => {
null,
);
expect(result).toEqualRight({
...childTeamCollectionCasted,
...childTeamCollection,
parentID: null,
orderIndex: 2,
});
});
@@ -1114,8 +893,9 @@ describe('moveCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection.teamID}/coll_moved`,
{
...childTeamCollectionCasted,
...childTeamCollection,
parentID: null,
orderIndex: 2,
},
);
});
@@ -1154,8 +934,9 @@ describe('moveCollection', () => {
childTeamCollection_2.id,
);
expect(result).toEqualRight({
...rootTeamCollectionsCasted,
parentID: childTeamCollection_2Casted.id,
...rootTeamCollection,
parentID: childTeamCollection_2.id,
orderIndex: 1,
});
});
@@ -1195,8 +976,9 @@ describe('moveCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection_2.teamID}/coll_moved`,
{
...rootTeamCollectionsCasted,
parentID: childTeamCollection_2Casted.id,
...rootTeamCollection,
parentID: childTeamCollection_2.id,
orderIndex: 1,
},
);
});
@@ -1235,8 +1017,9 @@ describe('moveCollection', () => {
childTeamCollection_2.id,
);
expect(result).toEqualRight({
...childTeamCollectionCasted,
parentID: childTeamCollection_2Casted.id,
...childTeamCollection,
parentID: childTeamCollection_2.id,
orderIndex: 1,
});
});
@@ -1276,8 +1059,9 @@ describe('moveCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection.teamID}/coll_moved`,
{
...childTeamCollectionCasted,
parentID: childTeamCollection_2Casted.id,
...childTeamCollection,
parentID: childTeamCollection_2.id,
orderIndex: 1,
},
);
});
@@ -1373,7 +1157,7 @@ describe('updateCollectionOrder', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollectionList[4].teamID}/coll_order_updated`,
{
collection: rootTeamCollectionListCasted[4],
collection: rootTeamCollectionList[4],
nextCollection: null,
},
);
@@ -1454,8 +1238,8 @@ describe('updateCollectionOrder', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollectionList[2].teamID}/coll_order_updated`,
{
collection: childTeamCollectionListCasted[4],
nextCollection: childTeamCollectionListCasted[2],
collection: childTeamCollectionList[4],
nextCollection: childTeamCollectionList[2],
},
);
});
@@ -1521,7 +1305,7 @@ describe('importCollectionsFromJSON', () => {
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_added`,
rootTeamCollectionsCasted,
rootTeamCollection,
);
});
});
@@ -1640,7 +1424,7 @@ describe('replaceCollectionsWithJSON', () => {
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_added`,
rootTeamCollectionsCasted,
rootTeamCollection,
);
});
});
@@ -1677,64 +1461,4 @@ describe('totalCollectionsInTeam', () => {
});
});
describe('updateTeamCollection', () => {
test('should throw TEAM_COLL_SHORT_TITLE if title is invalid', async () => {
const result = await teamCollectionService.updateTeamCollection(
rootTeamCollection.id,
JSON.stringify(rootTeamCollection.data),
'de',
);
expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE);
});
test('should throw TEAM_COLL_DATA_INVALID is collection data is invalid', async () => {
const result = await teamCollectionService.updateTeamCollection(
rootTeamCollection.id,
'{',
rootTeamCollection.title,
);
expect(result).toEqualLeft(TEAM_COLL_DATA_INVALID);
});
test('should throw TEAM_COLL_NOT_FOUND is collectionID is invalid', async () => {
mockPrisma.teamCollection.update.mockRejectedValueOnce('RecordNotFound');
const result = await teamCollectionService.updateTeamCollection(
'invalid_id',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.title,
);
expect(result).toEqualLeft(TEAM_COLL_NOT_FOUND);
});
test('should successfully update a collection', async () => {
mockPrisma.teamCollection.update.mockResolvedValueOnce(rootTeamCollection);
const result = await teamCollectionService.updateTeamCollection(
rootTeamCollection.id,
JSON.stringify({ foo: 'bar' }),
'new_title',
);
expect(result).toEqualRight({
data: JSON.stringify({ foo: 'bar' }),
title: 'new_title',
...rootTeamCollectionsCasted,
});
});
test('should send pubsub message to "team_coll/<teamID>/coll_updated" if TeamCollection is updated successfully', async () => {
mockPrisma.teamCollection.update.mockResolvedValueOnce(rootTeamCollection);
const result = await teamCollectionService.updateTeamCollection(
rootTeamCollection.id,
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.title,
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_updated`,
rootTeamCollectionsCasted,
);
});
});
//ToDo: write test cases for exportCollectionsToJSON

View File

@@ -13,7 +13,6 @@ import {
TEAM_COLL_IS_PARENT_COLL,
TEAM_COL_SAME_NEXT_COLL,
TEAM_COL_REORDERING_FAILED,
TEAM_COLL_DATA_INVALID,
} from '../errors';
import { PubSubService } from '../pubsub/pubsub.service';
import { isValidLength } from 'src/utils';
@@ -70,7 +69,6 @@ export class TeamCollectionService {
this.generatePrismaQueryObjForFBCollFolder(f, teamID, index + 1),
),
},
data: folder.data ?? undefined,
};
}
@@ -120,7 +118,6 @@ export class TeamCollectionService {
name: collection.right.title,
folders: childrenCollectionObjects,
requests: requests.map((x) => x.request),
data: JSON.stringify(collection.right.data),
};
return E.right(result);
@@ -201,11 +198,8 @@ export class TeamCollectionService {
),
);
teamCollections.forEach((collection) =>
this.pubsub.publish(
`team_coll/${destTeamID}/coll_added`,
this.cast(collection),
),
teamCollections.forEach((x) =>
this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x),
);
return E.right(true);
@@ -274,11 +268,8 @@ export class TeamCollectionService {
),
);
teamCollections.forEach((collections) =>
this.pubsub.publish(
`team_coll/${destTeamID}/coll_added`,
this.cast(collections),
),
teamCollections.forEach((x) =>
this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x),
);
return E.right(true);
@@ -286,17 +277,11 @@ export class TeamCollectionService {
/**
* Typecast a database TeamCollection to a TeamCollection model
*
* @param teamCollection database TeamCollection
* @returns TeamCollection model
*/
private cast(teamCollection: DBTeamCollection): TeamCollection {
return <TeamCollection>{
id: teamCollection.id,
title: teamCollection.title,
parentID: teamCollection.parentID,
data: !teamCollection.data ? null : JSON.stringify(teamCollection.data),
};
return <TeamCollection>{ ...teamCollection };
}
/**
@@ -339,7 +324,7 @@ export class TeamCollectionService {
});
if (!teamCollection) return null;
return !teamCollection.parent ? null : this.cast(teamCollection.parent);
return teamCollection.parent;
}
/**
@@ -350,12 +335,12 @@ export class TeamCollectionService {
* @param take Number of items we want returned
* @returns A list of child collections
*/
async getChildrenOfCollection(
getChildrenOfCollection(
collectionID: string,
cursor: string | null,
take: number,
) {
const res = await this.prisma.teamCollection.findMany({
return this.prisma.teamCollection.findMany({
where: {
parentID: collectionID,
},
@@ -366,12 +351,6 @@ export class TeamCollectionService {
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
});
const childCollections = res.map((teamCollection) =>
this.cast(teamCollection),
);
return childCollections;
}
/**
@@ -387,7 +366,7 @@ export class TeamCollectionService {
cursor: string | null,
take: number,
) {
const res = await this.prisma.teamCollection.findMany({
return this.prisma.teamCollection.findMany({
where: {
teamID,
parentID: null,
@@ -399,12 +378,6 @@ export class TeamCollectionService {
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
});
const teamCollections = res.map((teamCollection) =>
this.cast(teamCollection),
);
return teamCollections;
}
/**
@@ -497,7 +470,6 @@ export class TeamCollectionService {
async createCollection(
teamID: string,
title: string,
data: string | null = null,
parentTeamCollectionID: string | null,
) {
const isTitleValid = isValidLength(title, this.TITLE_LENGTH);
@@ -509,13 +481,6 @@ export class TeamCollectionService {
if (O.isNone(isOwner)) return E.left(TEAM_NOT_OWNER);
}
if (data === '') return E.left(TEAM_COLL_DATA_INVALID);
if (data) {
const jsonReq = stringToJson(data);
if (E.isLeft(jsonReq)) return E.left(TEAM_COLL_DATA_INVALID);
data = jsonReq.right;
}
const isParent = parentTeamCollectionID
? {
connect: {
@@ -533,23 +498,18 @@ export class TeamCollectionService {
},
},
parent: isParent,
data: data ?? undefined,
orderIndex: !parentTeamCollectionID
? (await this.getRootCollectionsCount(teamID)) + 1
: (await this.getChildCollectionsCount(parentTeamCollectionID)) + 1,
},
});
this.pubsub.publish(
`team_coll/${teamID}/coll_added`,
this.cast(teamCollection),
);
this.pubsub.publish(`team_coll/${teamID}/coll_added`, teamCollection);
return E.right(this.cast(teamCollection));
}
/**
* @deprecated Use updateTeamCollection method instead
* Update the title of a TeamCollection
*
* @param collectionID The Collection ID
@@ -572,10 +532,10 @@ export class TeamCollectionService {
this.pubsub.publish(
`team_coll/${updatedTeamCollection.teamID}/coll_updated`,
this.cast(updatedTeamCollection),
updatedTeamCollection,
);
return E.right(this.cast(updatedTeamCollection));
return E.right(updatedTeamCollection);
} catch (error) {
return E.left(TEAM_COLL_NOT_FOUND);
}
@@ -734,8 +694,8 @@ export class TeamCollectionService {
* @returns An Option of boolean, is parent or not
*/
private async isParent(
collection: DBTeamCollection,
destCollection: DBTeamCollection,
collection: TeamCollection,
destCollection: TeamCollection,
): Promise<O.Option<boolean>> {
//* Recursively check if collection is a parent by going up the tree of child-parent collections until we reach a root collection i.e parentID === null
//* Valid condition, isParent returns false
@@ -1011,49 +971,4 @@ export class TeamCollectionService {
const teamCollectionsCount = this.prisma.teamCollection.count();
return teamCollectionsCount;
}
/**
* Update Team Collection details
*
* @param collectionID Collection ID
* @param collectionData new header data in a JSONified string form
* @param newTitle New title of the collection
* @returns Updated TeamCollection
*/
async updateTeamCollection(
collectionID: string,
collectionData: string = null,
newTitle: string = null,
) {
try {
if (newTitle != null) {
const isTitleValid = isValidLength(newTitle, this.TITLE_LENGTH);
if (!isTitleValid) return E.left(TEAM_COLL_SHORT_TITLE);
}
if (collectionData === '') return E.left(TEAM_COLL_DATA_INVALID);
if (collectionData) {
const jsonReq = stringToJson(collectionData);
if (E.isLeft(jsonReq)) return E.left(TEAM_COLL_DATA_INVALID);
collectionData = jsonReq.right;
}
const updatedTeamCollection = await this.prisma.teamCollection.update({
where: { id: collectionID },
data: {
data: collectionData ?? undefined,
title: newTitle ?? undefined,
},
});
this.pubsub.publish(
`team_coll/${updatedTeamCollection.teamID}/coll_updated`,
this.cast(updatedTeamCollection),
);
return E.right(this.cast(updatedTeamCollection));
} catch (e) {
return E.left(TEAM_COLL_NOT_FOUND);
}
}
}

View File

@@ -301,7 +301,7 @@ describe('TeamEnvironmentsService', () => {
describe('createDuplicateEnvironment', () => {
test('should successfully duplicate an existing team environment', async () => {
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
teamEnvironment,
);
@@ -322,9 +322,7 @@ describe('TeamEnvironmentsService', () => {
});
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
mockPrisma.teamEnvironment.findFirstOrThrow.mockRejectedValue(
'NotFoundError',
);
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
const result = await teamEnvironmentsService.createDuplicateEnvironment(
teamEnvironment.id,
@@ -334,7 +332,7 @@ describe('TeamEnvironmentsService', () => {
});
test('should send pubsub message to "team_environment/<teamID>/created" if team environment is updated successfully', async () => {
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
teamEnvironment,
);

View File

@@ -183,10 +183,11 @@ export class TeamEnvironmentsService {
*/
async createDuplicateEnvironment(id: string) {
try {
const environment = await this.prisma.teamEnvironment.findFirstOrThrow({
const environment = await this.prisma.teamEnvironment.findFirst({
where: {
id: id,
},
rejectOnNotFound: true,
});
const result = await this.prisma.teamEnvironment.create({

View File

@@ -42,7 +42,6 @@ const teamCollection: DbTeamCollection = {
id: 'team-coll-1',
parentID: null,
teamID: team.id,
data: {},
title: 'Team Collection 1',
orderIndex: 1,
createdOn: new Date(),

View File

@@ -1,8 +1,6 @@
// This interface defines how data will be received from the app when we are importing Hoppscotch collections
export interface CollectionFolder {
id?: string;
folders: CollectionFolder[];
requests: any[];
name: string;
data?: string;
}

View File

@@ -6,13 +6,6 @@ import { PaginationArgs } from 'src/types/input-types.args';
export class CreateRootUserCollectionArgs {
@Field({ name: 'title', description: 'Title of the new user collection' })
title: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}
@ArgsType()
export class CreateChildUserCollectionArgs {
@@ -24,13 +17,6 @@ export class CreateChildUserCollectionArgs {
description: 'ID of the parent to the new user collection',
})
parentUserCollectionID: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}
@ArgsType()
@@ -109,26 +95,3 @@ export class ImportUserCollectionsFromJSONArgs {
})
parentCollectionID?: string;
}
@ArgsType()
export class UpdateUserCollectionsArgs {
@Field(() => ID, {
name: 'userCollectionID',
description: 'ID of the user collection',
})
userCollectionID: string;
@Field({
name: 'newTitle',
description: 'The updated title of the user collection',
nullable: true,
})
newTitle: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}

View File

@@ -30,7 +30,6 @@ import {
MoveUserCollectionArgs,
RenameUserCollectionsArgs,
UpdateUserCollectionArgs,
UpdateUserCollectionsArgs,
} from './input-type.args';
import { ReqType } from 'src/types/RequestTypes';
import * as E from 'fp-ts/Either';
@@ -143,13 +142,7 @@ export class UserCollectionResolver {
);
if (E.isLeft(userCollection)) throwErr(userCollection.left);
return <UserCollection>{
...userCollection.right,
userID: userCollection.right.userUid,
data: !userCollection.right.data
? null
: JSON.stringify(userCollection.right.data),
};
return userCollection.right;
}
@Query(() => UserCollectionExportJSONData, {
@@ -198,7 +191,6 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection(
user,
args.title,
args.data,
null,
ReqType.REST,
);
@@ -220,7 +212,6 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection(
user,
args.title,
args.data,
null,
ReqType.GQL,
);
@@ -241,7 +232,6 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection(
user,
args.title,
args.data,
args.parentUserCollectionID,
ReqType.GQL,
);
@@ -262,7 +252,6 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection(
user,
args.title,
args.data,
args.parentUserCollectionID,
ReqType.REST,
);
@@ -370,26 +359,6 @@ export class UserCollectionResolver {
return importedCollection.right;
}
@Mutation(() => UserCollection, {
description: 'Update a UserCollection',
})
@UseGuards(GqlAuthGuard)
async updateUserCollection(
@GqlUser() user: AuthUser,
@Args() args: UpdateUserCollectionsArgs,
) {
const updatedUserCollection =
await this.userCollectionService.updateUserCollection(
args.newTitle,
args.data,
args.userCollectionID,
user.uid,
);
if (E.isLeft(updatedUserCollection)) throwErr(updatedUserCollection.left);
return updatedUserCollection.right;
}
// Subscriptions
@Subscription(() => UserCollection, {
description: 'Listen for User Collection Creation',

View File

@@ -12,7 +12,6 @@ import {
USER_NOT_FOUND,
USER_NOT_OWNER,
USER_COLL_INVALID_JSON,
USER_COLL_DATA_INVALID,
} from 'src/errors';
import { PrismaService } from 'src/prisma/prisma.service';
import { AuthUser } from 'src/types/AuthUser';
@@ -44,12 +43,8 @@ export class UserCollectionService {
*/
private cast(collection: UserCollection) {
return <UserCollectionModel>{
id: collection.id,
title: collection.title,
type: collection.type,
parentID: collection.parentID,
...collection,
userID: collection.userUid,
data: !collection.data ? null : JSON.stringify(collection.data),
};
}
@@ -151,7 +146,7 @@ export class UserCollectionService {
},
});
return !parent ? null : this.cast(parent);
return parent;
}
/**
@@ -169,7 +164,7 @@ export class UserCollectionService {
take: number,
type: ReqType,
) {
const res = await this.prisma.userCollection.findMany({
return this.prisma.userCollection.findMany({
where: {
parentID: collectionID,
type: type,
@@ -181,12 +176,6 @@ export class UserCollectionService {
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
});
const childCollections = res.map((childCollection) =>
this.cast(childCollection),
);
return childCollections;
}
/**
@@ -222,20 +211,12 @@ export class UserCollectionService {
async createUserCollection(
user: AuthUser,
title: string,
data: string | null = null,
parentUserCollectionID: string | null,
type: ReqType,
) {
const isTitleValid = isValidLength(title, this.TITLE_LENGTH);
if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE);
if (data === '') return E.left(USER_COLL_DATA_INVALID);
if (data) {
const jsonReq = stringToJson(data);
if (E.isLeft(jsonReq)) return E.left(USER_COLL_DATA_INVALID);
data = jsonReq.right;
}
// If creating a child collection
if (parentUserCollectionID !== null) {
const parentCollection = await this.getUserCollection(
@@ -270,19 +251,15 @@ export class UserCollectionService {
},
},
parent: isParent,
data: data ?? undefined,
orderIndex: !parentUserCollectionID
? (await this.getRootCollectionsCount(user.uid)) + 1
: (await this.getChildCollectionsCount(parentUserCollectionID)) + 1,
},
});
await this.pubsub.publish(
`user_coll/${user.uid}/created`,
this.cast(userCollection),
);
await this.pubsub.publish(`user_coll/${user.uid}/created`, userCollection);
return E.right(this.cast(userCollection));
return E.right(userCollection);
}
/**
@@ -299,7 +276,7 @@ export class UserCollectionService {
take: number,
type: ReqType,
) {
const res = await this.prisma.userCollection.findMany({
return this.prisma.userCollection.findMany({
where: {
userUid: user.uid,
parentID: null,
@@ -312,12 +289,6 @@ export class UserCollectionService {
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
});
const userCollections = res.map((childCollection) =>
this.cast(childCollection),
);
return userCollections;
}
/**
@@ -336,7 +307,7 @@ export class UserCollectionService {
take: number,
type: ReqType,
) {
const res = await this.prisma.userCollection.findMany({
return this.prisma.userCollection.findMany({
where: {
userUid: user.uid,
parentID: userCollectionID,
@@ -346,16 +317,9 @@ export class UserCollectionService {
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
});
const childCollections = res.map((childCollection) =>
this.cast(childCollection),
);
return childCollections;
}
/**
* @deprecated Use updateUserCollection method instead
* Update the title of a UserCollection
*
* @param newTitle The new title of collection
@@ -387,10 +351,10 @@ export class UserCollectionService {
this.pubsub.publish(
`user_coll/${updatedUserCollection.userUid}/updated`,
this.cast(updatedUserCollection),
updatedUserCollection,
);
return E.right(this.cast(updatedUserCollection));
return E.right(updatedUserCollection);
} catch (error) {
return E.left(USER_COLL_NOT_FOUND);
}
@@ -627,10 +591,10 @@ export class UserCollectionService {
this.pubsub.publish(
`user_coll/${collection.right.userUid}/moved`,
this.cast(updatedCollection.right),
updatedCollection.right,
);
return E.right(this.cast(updatedCollection.right));
return E.right(updatedCollection.right);
}
// destCollectionID != null i.e move into another collection
@@ -678,10 +642,10 @@ export class UserCollectionService {
this.pubsub.publish(
`user_coll/${collection.right.userUid}/moved`,
this.cast(updatedCollection.right),
updatedCollection.right,
);
return E.right(this.cast(updatedCollection.right));
return E.right(updatedCollection.right);
}
/**
@@ -882,7 +846,6 @@ export class UserCollectionService {
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
};
}),
data: JSON.stringify(collection.right.data),
};
return E.right(result);
@@ -955,7 +918,6 @@ export class UserCollectionService {
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
};
}),
data: JSON.stringify(parentCollection.right.data),
}),
collectionType: parentCollection.right.type,
});
@@ -1009,7 +971,6 @@ export class UserCollectionService {
this.generatePrismaQueryObj(f, userID, index + 1, reqType),
),
},
data: folder.data ?? undefined,
};
}
@@ -1079,63 +1040,10 @@ export class UserCollectionService {
),
);
userCollections.forEach((collection) =>
this.pubsub.publish(`user_coll/${userID}/created`, this.cast(collection)),
userCollections.forEach((x) =>
this.pubsub.publish(`user_coll/${userID}/created`, x),
);
return E.right(true);
}
/**
* Update a UserCollection
*
* @param newTitle The new title of collection
* @param userCollectionID The Collection Id
* @param userID The User UID
* @returns An Either of the updated UserCollection
*/
async updateUserCollection(
newTitle: string = null,
collectionData: string | null = null,
userCollectionID: string,
userID: string,
) {
if (collectionData === '') return E.left(USER_COLL_DATA_INVALID);
if (collectionData) {
const jsonReq = stringToJson(collectionData);
if (E.isLeft(jsonReq)) return E.left(USER_COLL_DATA_INVALID);
collectionData = jsonReq.right;
}
if (newTitle != null) {
const isTitleValid = isValidLength(newTitle, this.TITLE_LENGTH);
if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE);
}
// Check to see is the collection belongs to the user
const isOwner = await this.isOwnerCheck(userCollectionID, userID);
if (O.isNone(isOwner)) return E.left(USER_NOT_OWNER);
try {
const updatedUserCollection = await this.prisma.userCollection.update({
where: {
id: userCollectionID,
},
data: {
data: collectionData ?? undefined,
title: newTitle ?? undefined,
},
});
this.pubsub.publish(
`user_coll/${updatedUserCollection.userUid}/updated`,
this.cast(updatedUserCollection),
);
return E.right(this.cast(updatedUserCollection));
} catch (error) {
return E.left(USER_COLL_NOT_FOUND);
}
}
}

View File

@@ -13,12 +13,6 @@ export class UserCollection {
})
title: string;
@Field({
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
@Field(() => ReqType, {
description: 'Type of the user collection',
})

View File

@@ -142,15 +142,13 @@ 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,
executedOn: new Date(),
isStarred: false,
});
@@ -160,7 +158,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]),
reqType: ReqType.REST,
executedOn,
executedOn: new Date(),
isStarred: false,
};
@@ -174,15 +172,13 @@ 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,
executedOn: new Date(),
isStarred: false,
});
@@ -192,7 +188,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]),
reqType: ReqType.GQL,
executedOn,
executedOn: new Date(),
isStarred: false,
};
@@ -216,15 +212,13 @@ 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,
executedOn: new Date(),
isStarred: false,
});
@@ -234,7 +228,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]),
reqType: ReqType.GQL,
executedOn,
executedOn: new Date(),
isStarred: false,
};
@@ -251,15 +245,13 @@ 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,
executedOn: new Date(),
isStarred: false,
});
@@ -269,7 +261,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]),
reqType: ReqType.REST,
executedOn,
executedOn: new Date(),
isStarred: false,
};
@@ -331,15 +323,13 @@ describe('UserHistoryService', () => {
).toEqualLeft(USER_HISTORY_NOT_FOUND);
});
test('Should star/unstar a request in the history and publish a updated subscription', async () => {
const executedOn = new Date();
mockPrisma.userHistory.findFirst.mockResolvedValueOnce({
userUid: 'abc',
id: '1',
request: [{}],
responseMetadata: [{}],
reqType: ReqType.REST,
executedOn,
executedOn: new Date(),
isStarred: false,
});
@@ -349,7 +339,7 @@ describe('UserHistoryService', () => {
request: [{}],
responseMetadata: [{}],
reqType: ReqType.REST,
executedOn,
executedOn: new Date(),
isStarred: true,
});
@@ -359,7 +349,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]),
reqType: ReqType.REST,
executedOn,
executedOn: new Date(),
isStarred: true,
};

View File

@@ -147,7 +147,7 @@ module.exports = {
// The glob patterns Jest uses to detect test files
testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
"**/src/__tests__/commands/**/*.*.ts",
"**/src/__tests__/**/*.*.ts",
],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped

View File

@@ -1,6 +1,6 @@
{
"name": "@hoppscotch/cli",
"version": "0.4.0",
"version": "0.3.2",
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
"homepage": "https://hoppscotch.io",
"main": "dist/index.js",
@@ -10,18 +10,14 @@
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=18"
},
"scripts": {
"build": "pnpm exec tsup",
"dev": "pnpm exec tsup --watch",
"debugger": "node debugger.js 9999",
"prepublish": "pnpm exec tsup",
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
"test": "pnpm run build && jest && rm -rf dist",
"do-typecheck": "pnpm exec tsc --noEmit",
"do-test": "pnpm test"
"test": "pnpm run build && jest && rm -rf dist"
},
"keywords": [
"cli",
@@ -42,24 +38,26 @@
"devDependencies": {
"@hoppscotch/data": "workspace:^",
"@hoppscotch/js-sandbox": "workspace:^",
"@relmify/jest-fp-ts": "^2.1.1",
"@swc/core": "^1.3.92",
"@types/jest": "^29.5.5",
"@types/lodash": "^4.14.199",
"@types/qs": "^6.9.8",
"@relmify/jest-fp-ts": "^2.0.2",
"@swc/core": "^1.2.181",
"@types/axios": "^0.14.0",
"@types/chalk": "^2.2.0",
"@types/commander": "^2.12.2",
"@types/jest": "^27.4.1",
"@types/lodash": "^4.14.181",
"@types/qs": "^6.9.7",
"axios": "^0.21.4",
"chalk": "^4.1.2",
"commander": "^11.0.0",
"chalk": "^4.1.1",
"commander": "^8.0.0",
"esm": "^3.2.25",
"fp-ts": "^2.16.1",
"io-ts": "^2.2.20",
"jest": "^29.7.0",
"fp-ts": "^2.12.1",
"io-ts": "^2.2.16",
"jest": "^27.5.1",
"lodash": "^4.17.21",
"prettier": "^3.0.3",
"qs": "^6.11.2",
"ts-jest": "^29.1.1",
"tsup": "^7.2.0",
"typescript": "^5.2.2",
"zod": "^3.22.4"
"prettier": "^2.8.4",
"qs": "^6.10.3",
"ts-jest": "^27.1.4",
"tsup": "^5.12.7",
"typescript": "^4.6.4"
}
}

View File

@@ -28,7 +28,7 @@ describe("Test 'hopp test <file>' command:", () => {
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
});
test("Malformed collection file.", async () => {
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
"malformed-collection2.json"
@@ -106,7 +106,7 @@ describe("Test 'hopp test <file> --env <file>' command:", () => {
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
const { error, stdout } = await execAsync(cmd);
const { error } = await execAsync(cmd);
expect(error).toBeNull();
});
@@ -129,6 +129,7 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
console.log("invalid value thing", out)
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});

View File

@@ -1,6 +1,7 @@
{
"URL": "https://echo.hoppscotch.io",
"HOST": "echo.hoppscotch.io",
"X-COUNTRY": "IN",
"BODY_VALUE": "body_value",
"BODY_KEY": "body_key"
}

View File

@@ -12,7 +12,7 @@
"method": "POST",
"auth": { "authType": "none", "authActive": true },
"preRequestScript": "",
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst X_COUNTRY = pw.env.get(\"X-COUNTRY\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n\t pw.expect(pw.response.body.headers[\"x-country\"]).toBe(X_COUNTRY); \n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
"body": {
"contentType": "application/json",
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"

View File

@@ -14,9 +14,10 @@ import { isHoppCLIError } from "../utils/checks";
export const test = (path: string, options: TestCmdOptions) => async () => {
try {
const delay = options.delay ? parseDelayOption(options.delay) : 0
const envs = options.env ? await parseEnvsData(options.env) : <HoppEnvs>{ global: [], selected: [] }
const envName = options.envName
const envs = options.env ? await parseEnvsData(options.env, envName) : <HoppEnvs>{ global: [], selected: [] }
const collections = await parseCollectionData(path)
const report = await collectionsRunner({collections, envs, delay})
const hasSucceeded = collectionsRunnerResult(report)
collectionsRunnerExit(hasSucceeded)

View File

@@ -48,14 +48,12 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
ERROR_MSG = `Unavailable command: ${error.command}`;
break;
case "MALFORMED_ENV_FILE":
ERROR_MSG = `The environment file is not of the correct format.`;
break;
case "BULK_ENV_FILE":
ERROR_MSG = `CLI doesn't support bulk environments export.`;
break;
case "MALFORMED_COLLECTION":
ERROR_MSG = `${error.path}\n${parseErrorData(error.data)}`;
break;
case "ENVIRONMENT_NAME_NOT_FOUND":
ERROR_MSG = `\n${parseErrorData(error.data)}`;
break;
case "NO_FILE_PATH":
ERROR_MSG = `Please provide a hoppscotch-collection file path.`;
break;
@@ -87,4 +85,4 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
if (!S.isEmpty(ERROR_MSG)) {
console.error(ERROR_CODE, ERROR_MSG);
}
};
};

View File

@@ -50,6 +50,7 @@ program
"path to a hoppscotch collection.json file for CI testing"
)
.option("-e, --env <file_path>", "path to an environment variables json file")
.option("-eN, --envName <environment_name>","Specific Name of the environment")
.option(
"-d, --delay <delay_in_ms>",
"delay in milliseconds(ms) between consecutive requests within a collection"

View File

@@ -33,4 +33,5 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
effectiveFinalHeaders: { key: string; value: string; active: boolean }[];
effectiveFinalParams: { key: string; value: string; active: boolean }[];
effectiveFinalBody: FormData | string | null;
effectiveFinalMaskedURL: string;
}

View File

@@ -1,45 +1,50 @@
import { error } from "../../types/errors";
import {
HoppEnvs,
HoppEnvPair,
HoppEnvKeyPairObject,
HoppEnvExportObject,
HoppBulkEnvExportObject,
} from "../../types/request";
import { HoppEnvs, HoppEnvPair } from "../../types/request";
import { readJsonFile } from "../../utils/mutators";
/**
* Parses env json file for given path and validates the parsed env json object.
* @param path Path of env.json file to be parsed.
* @param envName Name of the environment that should be used. If undefined first environment is used.
* @returns For successful parsing we get HoppEnvs object.
*/
export async function parseEnvsData(path: string) {
const contents = await readJsonFile(path);
const envPairs: Array<HoppEnvPair> = [];
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
const HoppEnvExportObjectResult = HoppEnvExportObject.safeParse(contents);
const HoppBulkEnvExportObjectResult =
HoppBulkEnvExportObject.safeParse(contents);
// CLI doesnt support bulk environments export.
// Hence we check for this case and throw an error if it matches the format.
if (HoppBulkEnvExportObjectResult.success) {
throw error({ code: "BULK_ENV_FILE", path, data: error });
export async function parseEnvsData(path: string, envName: string | undefined) {
const contents = await readJsonFile(path)
if(!(contents && typeof contents === "object" && Array.isArray(contents))) {
throw error({ code: "MALFORMED_ENV_FILE", path, data: null })
}
// Checks if the environment file is of the correct format.
// If it doesnt match either of them, we throw an error.
if (!(HoppEnvKeyPairResult.success || HoppEnvExportObjectResult.success)) {
throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
}
const envPairs: Array<HoppEnvPair> = []
if (HoppEnvKeyPairResult.success) {
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
envPairs.push({ key, value });
const contentEntries = Object.entries(contents)
let environmentFound = false;
for(const [key, obj] of contentEntries) {
if(!(typeof obj === "object" && "name" in obj && "variables" in obj)) {
throw error({ code: "MALFORMED_ENV_FILE", path, data: { value: obj } })
}
} else if (HoppEnvExportObjectResult.success) {
const { key, value } = HoppEnvExportObjectResult.data.variables[0];
envPairs.push({ key, value });
if(envName && envName !== obj.name) {
continue
}
environmentFound = true;
for(const variable of obj.variables) {
if(
!(
typeof variable === "object" &&
"key" in variable &&
"value" in variable &&
"secret" in variable
)
) {
throw error({ code: "MALFORMED_ENV_FILE", path, data: { value: variable } });
}
const { key, value, secret } = variable;
envPairs.push({ key: key, value: value, secret: secret });
}
break
}
return <HoppEnvs>{ global: [], selected: envPairs };
if(envName && !environmentFound) {
throw error({ code: "ENVIRONMENT_NAME_NOT_FOUND", data: envName });
}
return <HoppEnvs>{ global: [], selected: envPairs }
}

View File

@@ -1,6 +1,7 @@
export type TestCmdOptions = {
env: string | undefined;
delay: string | undefined;
envName: string | undefined;
};
export type HOPP_ENV_FILE_EXT = "json";

View File

@@ -15,6 +15,7 @@ type HoppErrors = {
FILE_NOT_FOUND: HoppErrorPath;
UNKNOWN_COMMAND: HoppErrorCmd;
MALFORMED_COLLECTION: HoppErrorPath & HoppErrorData;
ENVIRONMENT_NAME_NOT_FOUND: HoppErrorData;
NO_FILE_PATH: {};
PRE_REQUEST_SCRIPT_ERROR: HoppErrorData;
PARSING_ERROR: HoppErrorData;
@@ -24,7 +25,6 @@ type HoppErrors = {
REQUEST_ERROR: HoppErrorData;
INVALID_ARGUMENT: HoppErrorData;
MALFORMED_ENV_FILE: HoppErrorPath & HoppErrorData;
BULK_ENV_FILE: HoppErrorPath & HoppErrorData;
INVALID_FILE_TYPE: HoppErrorData;
};

View File

@@ -1,30 +1,13 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import { TestReport } from "../interfaces/response";
import { HoppCLIError } from "./errors";
import { z } from "zod";
export type FormDataEntry = {
key: string;
value: string | Blob;
};
export type HoppEnvPair = { key: string; value: string };
export const HoppEnvKeyPairObject = z.record(z.string(), z.string());
// Shape of the single environment export object that is exported from the app.
export const HoppEnvExportObject = z.object({
name: z.string(),
variables: z.array(
z.object({
key: z.string(),
value: z.string(),
})
),
});
// Shape of the bulk environment export object that is exported from the app.
export const HoppBulkEnvExportObject = z.array(HoppEnvExportObject);
export type HoppEnvPair = { key: string; value: string; secret: boolean };
export type HoppEnvs = {
global: HoppEnvPair[];

View File

@@ -1,7 +1,7 @@
import { bold } from "chalk";
import { groupEnd, group, log } from "console";
import { handleError } from "../handlers/error";
import { RequestConfig } from "../interfaces/request";
import { Method } from "axios";
import { RequestRunnerResponse, TestReport } from "../interfaces/response";
import { HoppCLIError } from "../types/errors";
import {
@@ -172,11 +172,12 @@ export const printFailedTestsReport = (
export const printRequestRunner = {
/**
* Request-runner starting message.
* @param requestConfig Provides request's method and url.
* @param requestMethod Provides request's method
* @param maskedURL Provides the URL with secrets masked with asterisks
*/
start: (requestConfig: RequestConfig) => {
const METHOD = BG_INFO(` ${requestConfig.method} `);
const ENDPOINT = requestConfig.url;
start: (requestMethod: Method | undefined, maskedURL: string) => {
const METHOD = BG_INFO(` ${requestMethod} `);
const ENDPOINT = maskedURL;
process.stdout.write(`${METHOD} ${ENDPOINT}`);
},

View File

@@ -6,24 +6,23 @@ import {
parseTemplateString,
parseTemplateStringE,
} from "@hoppscotch/data";
import { runPreRequestScript } from "@hoppscotch/js-sandbox/node";
import * as A from "fp-ts/Array";
import * as E from "fp-ts/Either";
import * as O from "fp-ts/Option";
import * as RA from "fp-ts/ReadonlyArray";
import * as TE from "fp-ts/TaskEither";
import { runPreRequestScript } from "@hoppscotch/js-sandbox";
import { flow, pipe } from "fp-ts/function";
import * as TE from "fp-ts/TaskEither";
import * as E from "fp-ts/Either";
import * as RA from "fp-ts/ReadonlyArray";
import * as A from "fp-ts/Array";
import * as O from "fp-ts/Option";
import * as S from "fp-ts/string";
import qs from "qs";
import { EffectiveHoppRESTRequest } from "../interfaces/request";
import { HoppCLIError, error } from "../types/errors";
import { error, HoppCLIError } from "../types/errors";
import { HoppEnvs } from "../types/request";
import { PreRequestMetrics } from "../types/response";
import { isHoppCLIError } from "./checks";
import { arrayFlatMap, arraySort, tupleToRecord } from "./functions/array";
import { getEffectiveFinalMetaData } from "./getters";
import { tupleToRecord, arraySort, arrayFlatMap } from "./functions/array";
import { toFormData } from "./mutators";
import { getEffectiveFinalMetaData } from "./getters";
import { PreRequestMetrics } from "../types/response";
/**
* Runs pre-request-script runner over given request which extracts set ENVs and
@@ -51,9 +50,9 @@ export const preRequestScriptRunner = (
isHoppCLIError(reason)
? reason
: error({
code: "PRE_REQUEST_SCRIPT_ERROR",
data: reason,
})
code: "PRE_REQUEST_SCRIPT_ERROR",
data: reason,
})
)
);
@@ -152,6 +151,12 @@ export function getEffectiveRESTRequest(
request.endpoint,
envVariables
);
const maskedEnvVariables = setAllEnvironmentValuesToAsterisk(envVariables)
const _effectiveFinalMaskedURL = parseTemplateStringE(
request.endpoint,
maskedEnvVariables)
if (E.isLeft(_effectiveFinalURL)) {
return E.left(
error({
@@ -161,6 +166,7 @@ export function getEffectiveRESTRequest(
);
}
const effectiveFinalURL = _effectiveFinalURL.right;
const effectiveFinalMaskedURL = E.isLeft(_effectiveFinalMaskedURL) ? request.endpoint : _effectiveFinalMaskedURL.right;
return E.right({
...request,
@@ -168,6 +174,7 @@ export function getEffectiveRESTRequest(
effectiveFinalHeaders,
effectiveFinalParams,
effectiveFinalBody,
effectiveFinalMaskedURL,
});
}
@@ -243,15 +250,15 @@ function getFinalBodyFromRequest(
arrayFlatMap((x) =>
x.isFile
? x.value.map((v) => ({
key: parseTemplateString(x.key, envVariables),
value: v as string | Blob,
}))
key: parseTemplateString(x.key, envVariables),
value: v as string | Blob,
}))
: [
{
key: parseTemplateString(x.key, envVariables),
value: parseTemplateString(x.value, envVariables),
},
]
{
key: parseTemplateString(x.key, envVariables),
value: parseTemplateString(x.value, envVariables),
},
]
),
toFormData,
E.right
@@ -288,3 +295,22 @@ export const getPreRequestMetrics = (
hasPreReqErrors ? { failed: 1, passed: 0 } : { failed: 0, passed: 1 },
(scripts) => <PreRequestMetrics>{ scripts, duration }
);
/**
* Mask all environment values with asterisks
* @param variables Environment variable array
* @returns Environment variable array with masked values
*/
const setAllEnvironmentValuesToAsterisk = (
variables: Environment["variables"]
): Environment["variables"] => {
const envVariables: Environment["variables"] = [];
for (const variable of variables) {
let value = variable.value
if (variable.secret) {
value = "******"
}
envVariables.push({ key: variable.key, secret: variable.secret, value: value })
}
return envVariables
}

View File

@@ -220,6 +220,7 @@ export const processRequest =
effectiveFinalHeaders: [],
effectiveFinalParams: [],
effectiveFinalURL: "",
effectiveFinalMaskedURL:"",
};
// Executing pre-request-script
@@ -237,8 +238,8 @@ export const processRequest =
// Creating request-config for request-runner.
const requestConfig = createRequest(effectiveRequest);
printRequestRunner.start(requestConfig);
printRequestRunner.start(requestConfig.method, effectiveRequest.effectiveFinalMaskedURL);
// Default value for request-runner's response.
let _requestRunnerRes: RequestRunnerResponse = {

View File

@@ -1,19 +1,17 @@
import { HoppRESTRequest } from "@hoppscotch/data";
import { TestDescriptor } from "@hoppscotch/js-sandbox";
import { runTestScript } from "@hoppscotch/js-sandbox/node";
import * as A from "fp-ts/Array";
import * as RA from "fp-ts/ReadonlyArray";
import * as T from "fp-ts/Task";
import * as TE from "fp-ts/TaskEither";
import { flow, pipe } from "fp-ts/function";
import { execTestScript, TestDescriptor } from "@hoppscotch/js-sandbox";
import { hrtime } from "process";
import { flow, pipe } from "fp-ts/function";
import * as RA from "fp-ts/ReadonlyArray";
import * as A from "fp-ts/Array";
import * as TE from "fp-ts/TaskEither";
import * as T from "fp-ts/Task";
import {
RequestRunnerResponse,
TestReport,
TestScriptParams,
} from "../interfaces/response";
import { HoppCLIError, error } from "../types/errors";
import { error, HoppCLIError } from "../types/errors";
import { HoppEnvs } from "../types/request";
import { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response";
import { getDurationInSeconds } from "./getters";
@@ -38,7 +36,7 @@ export const testRunner = (
pipe(
TE.of(testScriptData),
TE.chain(({ testScript, response, envs }) =>
runTestScript(testScript, envs, response)
execTestScript(testScript, envs, response)
)
)
),

View File

@@ -57,7 +57,7 @@ module.exports = {
{
name: "localStorage",
message:
"Do not use 'localStorage' directly. Please use the PersistenceService",
"Do not use 'localStorage' directly. Please use localpersistence.ts functions or stores",
},
],
// window.localStorage block
@@ -66,10 +66,8 @@ module.exports = {
{
selector: "CallExpression[callee.object.property.name='localStorage']",
message:
"Do not use 'localStorage' directly. Please use the PersistenceService",
"Do not use 'localStorage' directly. Please use localpersistence.ts functions or stores",
},
],
eqeqeq: 1,
"no-else-return": 1,
},
}

View File

@@ -4,5 +4,5 @@ module.exports = {
singleQuote: false,
printWidth: 80,
useTabs: false,
tabWidth: 2,
tabWidth: 2
}

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width=".88em" height="1em" viewBox="0 0 21 24"><path fill="currentColor" d="M12.731 2.751 17.666 5.6a2.138 2.138 0 1 1 2.07 3.548l-.015.003v5.7a2.14 2.14 0 1 1-2.098 3.502l-.002-.002-4.905 2.832a2.14 2.14 0 1 1-4.079.054l-.004.015-4.941-2.844a2.14 2.14 0 1 1-2.067-3.556l.015-.003V9.15a2.14 2.14 0 1 1 1.58-3.926l-.01-.005c.184.106.342.231.479.376l.001.001 4.938-2.85a2.14 2.14 0 1 1 4.096.021l.004-.015zm-.515.877a.766.766 0 0 1-.057.057l-.001.001 6.461 11.19c.026-.009.056-.016.082-.023V9.146a2.14 2.14 0 0 1-1.555-2.603l-.003.015.019-.072zm-3.015.059-.06-.06-4.946 2.852A2.137 2.137 0 0 1 2.749 9.12l-.015.004-.076.021v5.708l.084.023 6.461-11.19zm2.076.507a2.164 2.164 0 0 1-1.207-.004l.015.004-6.46 11.189c.286.276.496.629.597 1.026l.003.015h12.911c.102-.413.313-.768.599-1.043l.001-.001L11.28 4.194zm.986 16.227 4.917-2.838a1.748 1.748 0 0 1-.038-.142H4.222l-.021.083 4.939 2.852c.39-.403.936-.653 1.54-.653.626 0 1.189.268 1.581.696l.001.002z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width=".88em" height="1em" viewBox="0 0 21 24" class="iconify iconify--fontisto"><path fill="currentColor" d="M12.731 2.751 17.666 5.6a2.138 2.138 0 1 1 2.07 3.548l-.015.003v5.7a2.14 2.14 0 1 1-2.098 3.502l-.002-.002-4.905 2.832a2.14 2.14 0 1 1-4.079.054l-.004.015-4.941-2.844a2.14 2.14 0 1 1-2.067-3.556l.015-.003V9.15a2.14 2.14 0 1 1 1.58-3.926l-.01-.005c.184.106.342.231.479.376l.001.001 4.938-2.85a2.14 2.14 0 1 1 4.096.021l.004-.015zm-.515.877a.766.766 0 0 1-.057.057l-.001.001 6.461 11.19c.026-.009.056-.016.082-.023V9.146a2.14 2.14 0 0 1-1.555-2.603l-.003.015.019-.072zm-3.015.059-.06-.06-4.946 2.852A2.137 2.137 0 0 1 2.749 9.12l-.015.004-.076.021v5.708l.084.023 6.461-11.19zm2.076.507a2.164 2.164 0 0 1-1.207-.004l.015.004-6.46 11.189c.286.276.496.629.597 1.026l.003.015h12.911c.102-.413.313-.768.599-1.043l.001-.001L11.28 4.194zm.986 16.227 4.917-2.838a1.748 1.748 0 0 1-.038-.142H4.222l-.021.083 4.939 2.852c.39-.403.936-.653 1.54-.653.626 0 1.189.268 1.581.696l.001.002z"/></svg>

Before

Width:  |  Height:  |  Size: 1017 B

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M10.133 1h4.409a.5.5 0 0 1 .5.5v4.422c0 .026-.035.033-.045.01l-.048-.112a9.095 9.095 0 0 0-4.825-4.776c-.023-.01-.016-.044.01-.044Zm-8.588.275h-.5v1h.5c7.027 0 12.229 5.199 12.229 12.226v.5h1v-.5c0-7.58-5.65-13.226-13.229-13.226Zm.034 4.22h-.5v1h.5c2.361 0 4.348.837 5.744 2.238 1.395 1.401 2.227 3.395 2.227 5.758v.5h1v-.5c0-2.604-.921-4.859-2.52-6.463-1.596-1.605-3.845-2.532-6.45-2.532Zm-.528 8.996v-4.423c0-.041.033-.074.074-.074a4.923 4.923 0 0 1 4.923 4.922.074.074 0 0 1-.074.074H1.551a.5.5 0 0 1-.5-.5Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 684 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M9.277 2.084a.5.5 0 0 1 .185.607l-2.269 5.5a.5.5 0 0 1-.462.309H3.5a.5.5 0 0 1-.354-.854l5.5-5.5a.5.5 0 0 1 .631-.062ZM4.707 7.5h1.69l1.186-2.875L4.707 7.5Zm2.016 6.416a.5.5 0 0 1-.185-.607l2.269-5.5a.5.5 0 0 1 .462-.309H12.5a.5.5 0 0 1 .354.854l-5.5 5.5a.5.5 0 0 1-.631.062Zm4.57-5.416h-1.69l-1.186 2.875L11.293 8.5Z" clip-rule="evenodd"/><path fill="currentColor" fill-rule="evenodd" d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1 0A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 633 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" d="M1 2h4.257a2.5 2.5 0 0 1 1.768.732L9.293 5 5 9.293 3.732 8.025A2.5 2.5 0 0 1 3 6.257V4H2v2.257a3.5 3.5 0 0 0 1.025 2.475L5 10.707l1.25-1.25 2.396 2.397.708-.708L6.957 8.75 8.75 6.957l2.396 2.397.708-.708L9.457 6.25 10.707 5 7.732 2.025A3.5 3.5 0 0 0 5.257 1H1v1ZM10.646 2.354l2.622 2.62A2.5 2.5 0 0 1 14 6.744V12h1V6.743a3.5 3.5 0 0 0-1.025-2.475l-2.621-2.622-.707.708ZM4.268 13.975l-2.622-2.621.708-.708 2.62 2.622A2.5 2.5 0 0 0 6.744 14H15v1H6.743a3.5 3.5 0 0 1-2.475-1.025Z"/></svg>

Before

Width:  |  Height:  |  Size: 610 B

View File

@@ -1,25 +1,7 @@
/*
* Write hoppscotch-common related custom styles in this file.
* If styles are sharable across all package then write into hoppscotch-ui/assets/scss/styles.scss file.
*/
* {
backface-visibility: hidden;
-moz-backface-visibility: hidden;
-webkit-backface-visibility: hidden;
&::before {
backface-visibility: hidden;
-moz-backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
&::after {
backface-visibility: hidden;
-moz-backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
@apply backface-hidden;
@apply before:backface-hidden;
@apply after:backface-hidden;
@apply selection:bg-accentDark;
@apply selection:text-accentContrast;
@apply overscroll-none;
@@ -33,13 +15,13 @@
::-webkit-scrollbar-track {
@apply bg-transparent;
@apply border-b-0 border-l border-r-0 border-t-0 border-solid border-dividerLight;
@apply border-solid border-l border-dividerLight border-t-0 border-b-0 border-r-0;
}
::-webkit-scrollbar-thumb {
@apply bg-divider bg-clip-content;
@apply rounded-full;
@apply border-4 border-solid border-transparent;
@apply border-solid border-transparent border-4;
@apply hover:bg-dividerDark;
@apply hover:bg-clip-content;
}
@@ -57,7 +39,7 @@ input::placeholder,
textarea::placeholder,
.cm-placeholder {
@apply text-secondary;
@apply opacity-50 #{!important};
@apply opacity-50;
}
input,
@@ -72,11 +54,11 @@ html {
body {
@apply bg-primary;
@apply text-body text-secondary;
@apply text-secondary text-body;
@apply font-medium;
@apply select-none;
@apply overflow-x-hidden;
@apply leading-body #{!important};
@apply leading-body;
animation: fade 300ms forwards;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
@@ -142,8 +124,8 @@ a {
&.link {
@apply items-center;
@apply px-1 py-0.5;
@apply -mx-1 -my-0.5;
@apply py-0.5 px-1;
@apply -my-0.5 -mx-1;
@apply text-accent;
@apply rounded;
@apply hover:text-accentDark;
@@ -155,7 +137,7 @@ a {
.cm-tooltip {
.tippy-box {
@apply shadow-none #{!important};
@apply shadow-none;
@apply fixed;
@apply inline-flex;
@apply -mt-8;
@@ -172,15 +154,15 @@ a {
@apply flex;
@apply text-tiny text-primary;
@apply font-semibold;
@apply px-2 py-1;
@apply py-1 px-2;
@apply truncate;
@apply leading-body;
@apply leading-normal;
@apply items-center;
kbd {
@apply hidden;
@apply font-sans;
background-color: rgba(107, 114, 128, 0.45);
@apply bg-gray-500/45;
@apply text-primaryLight;
@apply rounded-sm;
@apply px-1;
@@ -188,12 +170,6 @@ a {
@apply truncate;
@apply sm:inline-flex;
}
.env-icon {
@apply transition;
@apply inline-flex;
@apply items-center;
}
}
.tippy-svg-arrow {
@@ -219,9 +195,9 @@ a {
@apply max-h-[45vh];
@apply items-stretch;
@apply overflow-y-auto;
@apply text-body text-secondary;
@apply text-secondary text-body;
@apply p-2;
@apply leading-body;
@apply leading-normal;
@apply focus:outline-none;
scroll-behavior: smooth;
@@ -253,12 +229,12 @@ a {
hr {
@apply border-b border-dividerLight;
@apply my-2 #{!important};
@apply my-2;
}
.heading {
@apply font-bold;
@apply text-lg text-secondaryDark;
@apply text-secondaryDark text-lg;
@apply tracking-tight;
}
@@ -267,7 +243,7 @@ hr {
.textarea {
@apply flex;
@apply w-full;
@apply px-4 py-2;
@apply py-2 px-4;
@apply bg-transparent;
@apply rounded;
@apply text-secondaryDark;
@@ -308,7 +284,7 @@ button {
@apply transform;
@apply origin-top-left;
@apply scale-75;
@apply -translate-y-4 translate-x-1;
@apply translate-x-1 -translate-y-4;
}
.floating-input:focus-within ~ label {
@@ -317,7 +293,7 @@ button {
.floating-input ~ .end-actions {
@apply absolute;
@apply right-[.05rem];
@apply right-0.2;
@apply inset-y-0;
@apply flex;
@apply items-center;
@@ -342,28 +318,44 @@ pre.ace_editor {
}
}
.select-wrapper {
@apply flex flex-1;
@apply relative;
@apply after:absolute;
@apply after:flex;
@apply after:inset-y-0;
@apply after:items-center;
@apply after:justify-center;
@apply after:pointer-events-none;
@apply after:font-icon;
@apply after:text-current;
@apply after:right-3;
@apply after:content-["\e5cf"];
@apply after:text-lg;
}
.info-response {
color: var(--status-info-color);
@apply text-pink-500;
}
.success-response {
color: var(--status-success-color);
@apply text-green-500;
}
.redirect-response {
color: var(--status-redirect-color);
.redir-response {
@apply text-yellow-500;
}
.critical-error-response {
color: var(--status-critical-error-color);
.cl-error-response {
@apply text-red-500;
}
.server-error-response {
color: var(--status-server-error-color);
.sv-error-response {
@apply text-red-600;
}
.missing-data-response {
color: var(--status-missing-data-color);
@apply text-secondaryLight;
}
.toasted-container {
@@ -374,7 +366,7 @@ pre.ace_editor {
@apply px-4 py-2;
@apply bg-tooltip;
@apply border-secondaryDark;
@apply text-body text-primary;
@apply text-primary text-body;
@apply justify-between;
@apply shadow-lg;
@apply font-semibold;
@@ -402,7 +394,7 @@ pre.ace_editor {
@apply before:opacity-10;
@apply before:inset-0;
@apply before:transition;
@apply before:content-[''];
@apply before:content-DEFAULT;
@apply hover:no-underline;
@apply hover:before:opacity-20;
}
@@ -436,7 +428,7 @@ pre.ace_editor {
@apply before:opacity-0;
@apply before:z-20;
@apply before:transition;
@apply before:content-[''];
@apply before:content-DEFAULT;
@apply hover:before:opacity-100;
}
@@ -509,16 +501,32 @@ pre.ace_editor {
}
}
.cm-panel.cm-search [name="close"] {
@apply flex;
@apply items-center;
@apply justify-center;
@apply min-h-5;
@apply min-w-5;
@apply bg-primaryDark #{!important};
@apply sticky #{!important};
@apply right-0 #{!important};
@apply ml-auto #{!important};
@apply my-auto #{!important};
@apply rounded #{!important};
@apply outline #{!important};
@apply outline-divider #{!important};
}
.shortcut-key {
@apply inline-flex;
@apply font-sans;
@apply text-tiny;
@apply bg-dividerLight;
@apply bg-divider;
@apply rounded;
@apply ml-2;
@apply px-1;
@apply min-w-[1.25rem];
@apply min-h-[1.25rem];
@apply min-w-5;
@apply min-h-5;
@apply items-center;
@apply justify-center;
@apply border border-dividerDark;

View File

@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,274 @@
@mixin base-theme {
--font-sans: "Inter Variable", sans-serif;
--font-icon: "Material Symbols Rounded Variable";
--font-mono: "Roboto Mono Variable", monospace;
--font-size-body: 0.75rem;
--font-size-tiny: 0.688rem;
--line-height-body: 1rem;
--upper-primary-sticky-fold: 4.125rem;
--upper-secondary-sticky-fold: 6.188rem;
--upper-tertiary-sticky-fold: 8.25rem;
--upper-fourth-sticky-fold: 10.2rem;
--upper-mobile-primary-sticky-fold: 6.625rem;
--upper-mobile-secondary-sticky-fold: 8.688rem;
--upper-mobile-sticky-fold: 10.75rem;
--upper-mobile-tertiary-sticky-fold: 8.25rem;
--lower-primary-sticky-fold: 3rem;
--lower-secondary-sticky-fold: 5.063rem;
--lower-tertiary-sticky-fold: 7.125rem;
--lower-fourth-sticky-fold: 9.188rem;
--sidebar-primary-sticky-fold: 2rem;
}
@mixin dark-theme {
--primary-color: theme("colors.dark.800");
--primary-light-color: theme("colors.dark.600");
--primary-dark-color: theme("colors.neutral.800");
--primary-contrast-color: theme("colors.neutral.900");
--secondary-color: theme("colors.neutral.400");
--secondary-light-color: theme("colors.neutral.500");
--secondary-dark-color: theme("colors.neutral.50");
--divider-color: theme("colors.neutral.800");
--divider-light-color: theme("colors.dark.500");
--divider-dark-color: theme("colors.dark.300");
--error-color: theme("colors.stone.800");
--tooltip-color: theme("colors.neutral.100");
--popover-color: theme("colors.dark.700");
--editor-theme: "merbivore_soft";
}
@mixin light-theme {
--primary-color: theme("colors.white");
--primary-light-color: theme("colors.gray.50");
--primary-dark-color: theme("colors.gray.100");
--primary-contrast-color: theme("colors.light.50");
--secondary-color: theme("colors.gray.500");
--secondary-light-color: theme("colors.gray.400");
--secondary-dark-color: theme("colors.gray.900");
--divider-color: theme("colors.gray.100");
--divider-light-color: theme("colors.gray.100");
--divider-dark-color: theme("colors.gray.300");
--error-color: theme("colors.yellow.100");
--tooltip-color: theme("colors.neutral.800");
--popover-color: theme("colors.white");
--editor-theme: "textmate";
}
@mixin black-theme {
--primary-color: theme("colors.dark.900");
--primary-light-color: theme("colors.neutral.900");
--primary-dark-color: theme("colors.dark.800");
--primary-contrast-color: theme("colors.dark.900");
--secondary-color: theme("colors.neutral.400");
--secondary-light-color: theme("colors.neutral.500");
--secondary-dark-color: theme("colors.neutral.100");
--divider-color: theme("colors.dark.600");
--divider-light-color: theme("colors.dark.800");
--divider-dark-color: theme("colors.dark.200");
--error-color: theme("colors.stone.900");
--tooltip-color: theme("colors.neutral.100");
--popover-color: theme("colors.dark.900");
--editor-theme: "twilight";
}
@mixin dark-editor-theme {
--editor-type-color: theme("colors.purple.400");
--editor-name-color: theme("colors.blue.400");
--editor-operator-color: theme("colors.indigo.400");
--editor-invalid-color: theme("colors.red.400");
--editor-separator-color: theme("colors.gray.400");
--editor-meta-color: theme("colors.gray.400");
--editor-variable-color: theme("colors.green.400");
--editor-link-color: theme("colors.cyan.400");
--editor-process-color: theme("colors.fuchsia.400");
--editor-constant-color: theme("colors.violet.400");
--editor-keyword-color: theme("colors.pink.400");
}
@mixin light-editor-theme {
--editor-type-color: theme("colors.purple.600");
--editor-name-color: theme("colors.red.600");
--editor-operator-color: theme("colors.indigo.600");
--editor-invalid-color: theme("colors.red.600");
--editor-separator-color: theme("colors.gray.600");
--editor-meta-color: theme("colors.gray.600");
--editor-variable-color: theme("colors.green.600");
--editor-link-color: theme("colors.cyan.600");
--editor-process-color: theme("colors.blue.600");
--editor-constant-color: theme("colors.fuchsia.600");
--editor-keyword-color: theme("colors.pink.600");
}
@mixin black-editor-theme {
--editor-type-color: theme("colors.purple.400");
--editor-name-color: theme("colors.fuchsia.400");
--editor-operator-color: theme("colors.indigo.400");
--editor-invalid-color: theme("colors.red.400");
--editor-separator-color: theme("colors.gray.400");
--editor-meta-color: theme("colors.gray.400");
--editor-variable-color: theme("colors.green.400");
--editor-link-color: theme("colors.cyan.400");
--editor-process-color: theme("colors.violet.400");
--editor-constant-color: theme("colors.blue.400");
--editor-keyword-color: theme("colors.pink.400");
}
@mixin green-theme {
--accent-color: theme("colors.green.500");
--accent-light-color: theme("colors.green.400");
--accent-dark-color: theme("colors.green.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.green.200");
--gradient-via-color: theme("colors.green.400");
--gradient-to-color: theme("colors.green.600");
}
@mixin teal-theme {
--accent-color: theme("colors.teal.500");
--accent-light-color: theme("colors.teal.400");
--accent-dark-color: theme("colors.teal.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.teal.200");
--gradient-via-color: theme("colors.teal.400");
--gradient-to-color: theme("colors.teal.600");
}
@mixin blue-theme {
--accent-color: theme("colors.blue.500");
--accent-light-color: theme("colors.blue.400");
--accent-dark-color: theme("colors.blue.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.blue.200");
--gradient-via-color: theme("colors.blue.400");
--gradient-to-color: theme("colors.blue.600");
}
@mixin indigo-theme {
--accent-color: theme("colors.indigo.500");
--accent-light-color: theme("colors.indigo.400");
--accent-dark-color: theme("colors.indigo.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.indigo.200");
--gradient-via-color: theme("colors.indigo.400");
--gradient-to-color: theme("colors.indigo.600");
}
@mixin purple-theme {
--accent-color: theme("colors.purple.500");
--accent-light-color: theme("colors.purple.400");
--accent-dark-color: theme("colors.purple.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.purple.200");
--gradient-via-color: theme("colors.purple.400");
--gradient-to-color: theme("colors.purple.600");
}
@mixin yellow-theme {
--accent-color: theme("colors.yellow.500");
--accent-light-color: theme("colors.yellow.400");
--accent-dark-color: theme("colors.yellow.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.yellow.200");
--gradient-via-color: theme("colors.yellow.400");
--gradient-to-color: theme("colors.yellow.600");
}
@mixin orange-theme {
--accent-color: theme("colors.orange.500");
--accent-light-color: theme("colors.orange.400");
--accent-dark-color: theme("colors.orange.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.orange.200");
--gradient-via-color: theme("colors.orange.400");
--gradient-to-color: theme("colors.orange.600");
}
@mixin red-theme {
--accent-color: theme("colors.red.500");
--accent-light-color: theme("colors.red.400");
--accent-dark-color: theme("colors.red.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.red.200");
--gradient-via-color: theme("colors.red.400");
--gradient-to-color: theme("colors.red.600");
}
@mixin pink-theme {
--accent-color: theme("colors.pink.500");
--accent-light-color: theme("colors.pink.400");
--accent-dark-color: theme("colors.pink.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.pink.200");
--gradient-via-color: theme("colors.pink.400");
--gradient-to-color: theme("colors.pink.600");
}
:root {
@include base-theme;
@include dark-theme;
@include dark-editor-theme;
@include green-theme;
}
:root.light {
@include light-theme;
@include light-editor-theme;
color-scheme: light;
}
:root.dark {
@include dark-theme;
@include dark-editor-theme;
color-scheme: dark;
}
:root.black {
@include black-theme;
@include black-editor-theme;
color-scheme: dark;
}
:root[data-accent="blue"] {
@include blue-theme;
}
:root[data-accent="green"] {
@include green-theme;
}
:root[data-accent="teal"] {
@include teal-theme;
}
:root[data-accent="indigo"] {
@include indigo-theme;
}
:root[data-accent="purple"] {
@include purple-theme;
}
:root[data-accent="orange"] {
@include orange-theme;
}
:root[data-accent="pink"] {
@include pink-theme;
}
:root[data-accent="red"] {
@include red-theme;
}
:root[data-accent="yellow"] {
@include yellow-theme;
}

View File

@@ -1,89 +0,0 @@
@mixin green-theme {
--accent-color: theme("colors.emerald.500");
--accent-light-color: theme("colors.emerald.400");
--accent-dark-color: theme("colors.emerald.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.emerald.400");
--gradient-via-color: theme("colors.emerald.500");
--gradient-to-color: theme("colors.emerald.600");
}
@mixin teal-theme {
--accent-color: theme("colors.teal.500");
--accent-light-color: theme("colors.teal.400");
--accent-dark-color: theme("colors.teal.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.teal.400");
--gradient-via-color: theme("colors.teal.500");
--gradient-to-color: theme("colors.teal.600");
}
@mixin blue-theme {
--accent-color: theme("colors.blue.500");
--accent-light-color: theme("colors.blue.400");
--accent-dark-color: theme("colors.blue.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.blue.400");
--gradient-via-color: theme("colors.blue.500");
--gradient-to-color: theme("colors.blue.600");
}
@mixin indigo-theme {
--accent-color: theme("colors.indigo.500");
--accent-light-color: theme("colors.indigo.400");
--accent-dark-color: theme("colors.indigo.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.indigo.400");
--gradient-via-color: theme("colors.indigo.500");
--gradient-to-color: theme("colors.indigo.600");
}
@mixin purple-theme {
--accent-color: theme("colors.purple.500");
--accent-light-color: theme("colors.purple.400");
--accent-dark-color: theme("colors.purple.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.purple.400");
--gradient-via-color: theme("colors.purple.500");
--gradient-to-color: theme("colors.purple.600");
}
@mixin yellow-theme {
--accent-color: theme("colors.amber.500");
--accent-light-color: theme("colors.amber.400");
--accent-dark-color: theme("colors.amber.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.amber.400");
--gradient-via-color: theme("colors.amber.500");
--gradient-to-color: theme("colors.amber.600");
}
@mixin orange-theme {
--accent-color: theme("colors.orange.500");
--accent-light-color: theme("colors.orange.400");
--accent-dark-color: theme("colors.orange.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.orange.400");
--gradient-via-color: theme("colors.orange.500");
--gradient-to-color: theme("colors.orange.600");
}
@mixin red-theme {
--accent-color: theme("colors.red.500");
--accent-light-color: theme("colors.red.400");
--accent-dark-color: theme("colors.red.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.red.400");
--gradient-via-color: theme("colors.red.500");
--gradient-to-color: theme("colors.red.600");
}
@mixin pink-theme {
--accent-color: theme("colors.pink.500");
--accent-light-color: theme("colors.pink.400");
--accent-dark-color: theme("colors.pink.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.pink.400");
--gradient-via-color: theme("colors.pink.500");
--gradient-to-color: theme("colors.pink.600");
}

View File

@@ -1,140 +0,0 @@
@mixin base-theme {
--font-sans: "Inter Variable", sans-serif;
--font-mono: "Roboto Mono Variable", monospace;
--font-size-body: 0.75rem;
--font-size-tiny: 0.625rem;
--line-height-body: 1rem;
--upper-primary-sticky-fold: 4.125rem;
--upper-secondary-sticky-fold: 6.188rem;
--upper-tertiary-sticky-fold: 8.25rem;
--upper-fourth-sticky-fold: 10.2rem;
--upper-mobile-primary-sticky-fold: 6.75rem;
--upper-mobile-secondary-sticky-fold: 8.813rem;
--upper-mobile-sticky-fold: 10.875rem;
--upper-mobile-tertiary-sticky-fold: 8.25rem;
--lower-primary-sticky-fold: 3rem;
--lower-secondary-sticky-fold: 5.063rem;
--lower-tertiary-sticky-fold: 7.125rem;
--lower-fourth-sticky-fold: 9.188rem;
--sidebar-primary-sticky-fold: 2rem;
}
@mixin light-theme {
--primary-color: theme("colors.white");
--primary-light-color: theme("colors.gray.50");
--primary-dark-color: theme("colors.gray.100");
--primary-contrast-color: #fdfdfd;
--secondary-color: theme("colors.gray.500");
--secondary-light-color: theme("colors.gray.400");
--secondary-dark-color: theme("colors.gray.900");
--divider-color: theme("colors.gray.100");
--divider-light-color: theme("colors.gray.100");
--divider-dark-color: theme("colors.gray.300");
--banner-info-color: theme("colors.stone.100");
--banner-warning-color: theme("colors.yellow.100");
--banner-error-color: theme("colors.red.100");
--tooltip-color: theme("colors.neutral.800");
--popover-color: theme("colors.white");
--method-get-color: theme("colors.green.500");
--method-post-color: theme("colors.amber.500");
--method-put-color: theme("colors.blue.500");
--method-patch-color: theme("colors.purple.500");
--method-delete-color: theme("colors.red.500");
--method-head-color: theme("colors.lime.500");
--method-options-color: theme("colors.pink.500");
--method-default-color: theme("colors.gray.500");
--status-info-color: theme("colors.blue.500");
--status-success-color: theme("colors.green.500");
--status-redirect-color: theme("colors.amber.500");
--status-critical-error-color: theme("colors.red.500");
--status-server-error-color: theme("colors.rose.500");
--status-missing-data-color: theme("colors.slate.500");
--editor-theme: "textmate";
}
@mixin dark-theme {
--primary-color: #181818;
--primary-light-color: #1c1c1e;
--primary-dark-color: theme("colors.neutral.800");
--primary-contrast-color: theme("colors.neutral.900");
--secondary-color: theme("colors.neutral.400");
--secondary-light-color: theme("colors.neutral.500");
--secondary-dark-color: theme("colors.zinc.50");
--divider-color: #1f1f1f;
--divider-light-color: #1f1f1f;
--divider-dark-color: theme("colors.zinc.800");
--banner-info-color: theme("colors.stone.800");
--banner-warning-color: theme("colors.yellow.800");
--banner-error-color: theme("colors.red.800");
--tooltip-color: theme("colors.neutral.100");
--popover-color: #1b1b1b;
--method-get-color: theme("colors.emerald.500");
--method-post-color: theme("colors.yellow.500");
--method-put-color: theme("colors.sky.500");
--method-patch-color: theme("colors.violet.500");
--method-delete-color: theme("colors.rose.500");
--method-head-color: theme("colors.teal.500");
--method-options-color: theme("colors.indigo.500");
--method-default-color: theme("colors.neutral.500");
--status-info-color: theme("colors.blue.500");
--status-success-color: theme("colors.green.500");
--status-redirect-color: theme("colors.amber.500");
--status-critical-error-color: theme("colors.red.500");
--status-server-error-color: theme("colors.rose.500");
--status-missing-data-color: theme("colors.slate.500");
--editor-theme: "merbivore_soft";
}
@mixin black-theme {
--primary-color: #0f0f0f;
--primary-light-color: theme("colors.neutral.900");
--primary-dark-color: #181818;
--primary-contrast-color: #0f0f0f;
--secondary-color: theme("colors.neutral.400");
--secondary-light-color: theme("colors.neutral.500");
--secondary-dark-color: theme("colors.neutral.50");
--divider-color: theme("colors.neutral.900");
--divider-light-color: theme("colors.neutral.900");
--divider-dark-color: theme("colors.zinc.800");
--banner-info-color: theme("colors.stone.900");
--banner-warning-color: theme("colors.yellow.900");
--banner-error-color: theme("colors.red.900");
--tooltip-color: theme("colors.neutral.100");
--popover-color: theme("colors.stone.950");
--method-get-color: theme("colors.emerald.500");
--method-post-color: theme("colors.yellow.500");
--method-put-color: theme("colors.sky.500");
--method-patch-color: theme("colors.violet.500");
--method-delete-color: theme("colors.rose.500");
--method-head-color: theme("colors.teal.500");
--method-options-color: theme("colors.indigo.500");
--method-default-color: theme("colors.zinc.500");
--status-info-color: theme("colors.blue.500");
--status-success-color: theme("colors.green.500");
--status-redirect-color: theme("colors.amber.500");
--status-critical-error-color: theme("colors.red.500");
--status-server-error-color: theme("colors.rose.500");
--status-missing-data-color: theme("colors.slate.500");
--editor-theme: "twilight";
}

View File

@@ -1,41 +0,0 @@
@mixin light-editor-theme {
--editor-type-color: theme("colors.violet.600");
--editor-name-color: theme("colors.red.600");
--editor-operator-color: theme("colors.indigo.600");
--editor-invalid-color: theme("colors.red.600");
--editor-separator-color: theme("colors.gray.600");
--editor-meta-color: theme("colors.gray.600");
--editor-variable-color: theme("colors.emerald.600");
--editor-link-color: theme("colors.cyan.600");
--editor-process-color: theme("colors.blue.600");
--editor-constant-color: theme("colors.fuchsia.600");
--editor-keyword-color: theme("colors.pink.600");
}
@mixin dark-editor-theme {
--editor-type-color: theme("colors.violet.400");
--editor-name-color: theme("colors.blue.400");
--editor-operator-color: theme("colors.indigo.400");
--editor-invalid-color: theme("colors.red.400");
--editor-separator-color: theme("colors.gray.400");
--editor-meta-color: theme("colors.gray.400");
--editor-variable-color: theme("colors.emerald.400");
--editor-link-color: theme("colors.cyan.400");
--editor-process-color: theme("colors.fuchsia.400");
--editor-constant-color: theme("colors.violet.400");
--editor-keyword-color: theme("colors.pink.400");
}
@mixin black-editor-theme {
--editor-type-color: theme("colors.violet.400");
--editor-name-color: theme("colors.fuchsia.400");
--editor-operator-color: theme("colors.indigo.400");
--editor-invalid-color: theme("colors.red.400");
--editor-separator-color: theme("colors.gray.400");
--editor-meta-color: theme("colors.gray.400");
--editor-variable-color: theme("colors.emerald.400");
--editor-link-color: theme("colors.cyan.400");
--editor-process-color: theme("colors.violet.400");
--editor-constant-color: theme("colors.blue.400");
--editor-keyword-color: theme("colors.pink.400");
}

View File

@@ -1,64 +0,0 @@
@import "./base-themes.scss";
@import "./editor-themes.scss";
@import "./accent-themes.scss";
:root {
@include base-theme;
@include dark-theme;
@include green-theme;
@include dark-editor-theme;
}
:root.light {
@include light-theme;
@include light-editor-theme;
color-scheme: light;
}
:root.dark {
@include dark-theme;
@include dark-editor-theme;
color-scheme: dark;
}
:root.black {
@include black-theme;
@include black-editor-theme;
color-scheme: dark;
}
:root[data-accent="blue"] {
@include blue-theme;
}
:root[data-accent="green"] {
@include green-theme;
}
:root[data-accent="teal"] {
@include teal-theme;
}
:root[data-accent="indigo"] {
@include indigo-theme;
}
:root[data-accent="purple"] {
@include purple-theme;
}
:root[data-accent="orange"] {
@include orange-theme;
}
:root[data-accent="pink"] {
@include pink-theme;
}
:root[data-accent="red"] {
@include red-theme;
}
:root[data-accent="yellow"] {
@include yellow-theme;
}

View File

@@ -48,7 +48,8 @@
"turn_off": "Ausschalten",
"turn_on": "Einschalten",
"undo": "Rückgängig machen",
"yes": "Ja"
"yes": "Ja",
"secret": "Als Secret speichern"
},
"add": {
"new": "Neue hinzufügen",
@@ -880,4 +881,4 @@
"team": "Team Workspace",
"title": "Workspaces"
}
}
}

View File

@@ -1,6 +1,5 @@
{
"action": {
"add": "Add",
"autoscroll": "Autoscroll",
"cancel": "Cancel",
"choose_file": "Choose a file",
@@ -11,7 +10,6 @@
"connect": "Connect",
"connecting": "Connecting",
"copy": "Copy",
"create": "Create",
"delete": "Delete",
"disconnect": "Disconnect",
"dismiss": "Dismiss",
@@ -41,7 +39,6 @@
"scroll_to_top": "Scroll to top",
"search": "Search",
"send": "Send",
"share": "Share",
"start": "Start",
"starting": "Starting",
"stop": "Stop",
@@ -51,36 +48,17 @@
"turn_off": "Turn off",
"turn_on": "Turn on",
"undo": "Undo",
"yes": "Yes"
"yes": "Yes",
"secret": "Save as secret"
},
"add": {
"new": "Add new",
"star": "Add star"
},
"cookies": {
"modal": {
"new_domain_name": "New domain name",
"set": "Set a cookie",
"cookie_string": "Cookie string",
"enter_cookie_string": "Enter cookie string",
"cookie_name": "Name",
"cookie_value": "Value",
"cookie_path": "Path",
"cookie_expires": "Expires",
"managed_tab": "Managed",
"raw_tab": "Raw",
"interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.",
"empty_domains": "Domain list is empty",
"empty_domain": "Domain is empty",
"no_cookies_in_domain": "No cookies set for this domain"
}
},
"app": {
"chat_with_us": "Chat with us",
"contact_us": "Contact us",
"cookies": "Cookies",
"copy": "Copy",
"copy_interface_type": "Copy interface type",
"copy_user_id": "Copy User Auth Token",
"developer_option": "Developer options",
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
@@ -96,7 +74,6 @@
"keyboard_shortcuts": "Keyboard shortcuts",
"name": "Hoppscotch",
"new_version_found": "New version found. Refresh to update.",
"open_in_hoppscotch": "Open in Hoppscotch",
"options": "Options",
"proxy_privacy_policy": "Proxy privacy policy",
"reload": "Reload",
@@ -136,34 +113,18 @@
},
"authorization": {
"generate_token": "Generate Token",
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
"include_in_url": "Include in URL",
"learn": "Learn how",
"pass_key_by": "Pass by",
"password": "Password",
"token": "Token",
"type": "Authorization Type",
"username": "Username",
"oauth": {
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed",
"something_went_wrong_on_token_generation": "Something went wrong on token generation",
"redirect_auth_server_returned_error": "Auth Server returned an error state",
"redirect_no_auth_code": "No Authorization Code present in the redirect",
"redirect_invalid_state": "Invalid State value present in the redirect",
"redirect_no_token_endpoint": "No Token Endpoint Defined",
"redirect_no_client_id": "No Client ID defined",
"redirect_no_client_secret": "No Client Secret Defined",
"redirect_no_code_verifier": "No Code Verifier Defined",
"redirect_auth_token_request_failed": "Request to get the auth token failed",
"redirect_auth_token_request_invalid_response": "Invalid Response from the Token Endpoint when requesting for an auth token",
"something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect"
}
"username": "Username"
},
"collection": {
"created": "Collection created",
"different_parent": "Cannot reorder collection with different parent",
"edit": "Edit Collection",
"import_or_create": "Import or create a collection",
"invalid_name": "Please provide a name for the collection",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
@@ -191,7 +152,6 @@
"remove_folder": "Are you sure you want to permanently delete this folder?",
"remove_history": "Are you sure you want to permanently delete all history?",
"remove_request": "Are you sure you want to permanently delete this request?",
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
"remove_team": "Are you sure you want to delete this team?",
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
@@ -233,8 +193,7 @@
"profile": "Login to view your profile",
"protocols": "Protocols are empty",
"schema": "Connect to a GraphQL endpoint to view schema",
"shared_requests_logout": "Login to view your shared requests or create a new one",
"shared_requests": "Shared requests are empty",
"shortcodes": "Shortcodes are empty",
"subscription": "Subscriptions are empty",
"team_name": "Team name empty",
"teams": "You don't belong to any teams",
@@ -251,7 +210,6 @@
"empty_variables": "No variables",
"global": "Global",
"global_variables": "Global variables",
"import_or_create": "Import or create a environment",
"invalid_name": "Please provide a name for the environment",
"list": "Environment variables",
"my_environments": "My Environments",
@@ -274,13 +232,9 @@
"variable": "Variable",
"variable_list": "Variable List"
},
"graphql_collections": {
"title": "GraphQL Collections"
},
"error": {
"browser_support_sse": "This browser doesn't seems to have Server Sent Events support.",
"check_console_details": "Check console log for details.",
"check_how_to_add_origin": "Check how you can add an origin",
"curl_invalid_format": "cURL is not formatted properly",
"danger_zone": "Danger zone",
"delete_account": "Your account is currently an owner in these teams:",
@@ -296,12 +250,9 @@
"json_prettify_invalid_body": "Couldn't prettify an invalid body, solve json syntax errors and try again",
"network_error": "There seems to be a network error. Please try again.",
"network_fail": "Could not send request",
"no_collections_to_export": "No collections to export. Please create a collection to get started.",
"no_duration": "No duration",
"no_environments_to_export": "No environments to export. Please create an environment to get started.",
"no_results_found": "No matches found",
"page_not_found": "This page could not be found",
"please_install_extension": "Please install the extension and add origin to the extension.",
"proxy_error": "Proxy error",
"script_fail": "Could not execute pre-request script",
"something_went_wrong": "Something went wrong",
@@ -312,8 +263,7 @@
"create_secret_gist": "Create secret Gist",
"gist_created": "Gist created",
"require_github": "Login with GitHub to create secret gist",
"title": "Export",
"failed": "Something went wrong while exporting"
"title": "Export"
},
"filter": {
"all": "All",
@@ -350,8 +300,8 @@
"authorization": "The authorization header will be automatically generated when you send the request.",
"generate_documentation_first": "Generate documentation first",
"network_fail": "Unable to reach the API endpoint. Check your network connection or select a different Interceptor and try again.",
"offline": "You're using Hoppscotch offline. Updates will sync when you're online, based on workspace settings.",
"offline_short": "You're using Hoppscotch offline.",
"offline": "You seem to be offline. Data in this workspace might not be up to date.",
"offline_short": "You seem to be offline.",
"post_request_tests": "Test scripts are written in JavaScript, and are run after the response is received.",
"pre_request_script": "Pre-request scripts are written in JavaScript, and are run before the request is sent.",
"script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.",
@@ -380,7 +330,6 @@
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
"from_postman": "Import from Postman",
"from_postman_description": "Import from Postman collection",
"from_file": "Import from File",
"from_url": "Import from URL",
"gist_url": "Enter Gist URL",
"import_from_url_invalid_fetch": "Couldn't get data from the url",
@@ -388,14 +337,7 @@
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
"import_from_url_success": "Collections Imported",
"json_description": "Import collections from a Hoppscotch Collections JSON file",
"title": "Import",
"hoppscotch_environment": "Hoppscotch Environment",
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
"postman_environment": "Postman Environment",
"postman_environment_description": "Import Postman Environment JSON file",
"environments_from_gist": "Import From Gist",
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist"
"title": "Import"
},
"inspections": {
"description": "Inspect possible errors",
@@ -432,9 +374,7 @@
"close_unsaved_tab": "You have unsaved changes",
"collections": "Collections",
"confirm": "Confirm",
"customize_request": "Customize Request",
"edit_request": "Edit Request",
"share_request": "Share Request",
"import_export": "Import / Export"
},
"mqtt": {
@@ -510,14 +450,13 @@
"structured": "Structured",
"text": "Text"
},
"copy_link": "Copy link",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Duration",
"enter_curl": "Enter cURL command",
"generate_code": "Generate code",
"generated_code": "Generated code",
"go_to_authorization_tab": "Go to Authorization tab",
"go_to_body_tab": "Go to Body tab",
"header_list": "Header List",
"invalid_name": "Please provide a name for the request",
"method": "Method",
@@ -542,7 +481,6 @@
"saved": "Request saved",
"share": "Share",
"share_description": "Share Hoppscotch with your friends",
"share_request": "Share Request",
"stop": "Stop",
"title": "Request",
"type": "Request type",
@@ -620,34 +558,16 @@
"use_experimental_url_bar": "Use experimental URL bar with environment highlighting",
"user": "User",
"verified_email": "Verified email",
"additional": "Additional Settings",
"verify_email": "Verify email"
},
"shared_requests": {
"button": "Button",
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
"customize": "Customize",
"creating_widget": "Creating widget",
"copy_html": "Copy HTML",
"copy_link": "Copy Link",
"copy_markdown": "Copy Markdown",
"deleted": "Shared request deleted",
"description": "Select a widget, you can change and customize this later",
"embed": "Embed",
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
"link": "Link",
"link_info": "Create a shareable link to share with anyone on the internet with view access.",
"modified": "Shared request modified",
"not_found": "Shared request not found",
"open_new_tab": "Open in new tab",
"preview": "Preview",
"run_in_hoppscotch": "Run in Hoppscotch",
"theme": {
"dark": "Dark",
"light": "Light",
"system": "System",
"title": "Theme"
}
"shortcodes": {
"actions": "Actions",
"created_on": "Created on",
"deleted": "Shortcode deleted",
"method": "Method",
"not_found": "Shortcode not found",
"short_code": "Short code",
"url": "URL"
},
"shortcut": {
"general": {
@@ -677,6 +597,7 @@
"title": "Others"
},
"request": {
"copy_request_link": "Copy Request Link",
"delete_method": "Select DELETE method",
"get_method": "Select GET method",
"head_method": "Select HEAD method",
@@ -692,7 +613,6 @@
"save_to_collections": "Save to Collections",
"send_request": "Send Request",
"show_code": "Generate code snippet",
"share_request": "Share Request",
"title": "Request"
},
"response": {
@@ -817,7 +737,6 @@
"connection_failed": "Connection failed",
"connection_lost": "Connection lost",
"copied_to_clipboard": "Copied to clipboard",
"copied_interface_to_clipboard": "Copied {language} interface type to clipboard",
"deleted": "Deleted",
"deprecated": "DEPRECATED",
"disabled": "Disabled",
@@ -825,11 +744,9 @@
"disconnected_from": "Disconnected from {name}",
"docs_generated": "Documentation generated",
"download_started": "Download started",
"download_failed": "Download failed",
"enabled": "Enabled",
"file_imported": "File imported",
"finished_in": "Finished in {duration} ms",
"hide": "Hide",
"history_deleted": "History deleted",
"linewrap": "Wrap lines",
"loading": "Loading...",
@@ -840,7 +757,6 @@
"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",
"show": "Show",
"subscribed_failed": "Failed to subscribe to topic: {topic}",
"subscribed_success": "Successfully subscribed to topic: {topic}",
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
@@ -876,7 +792,6 @@
"queries": "Queries",
"query": "Query",
"schema": "Schema",
"shared_requests": "Shared Requests",
"socketio": "Socket.IO",
"sse": "SSE",
"tests": "Tests",
@@ -923,7 +838,7 @@
"new": "New Team",
"new_created": "New team created",
"new_name": "My New Team",
"no_access": "You do not have edit access to this team",
"no_access": "You do not have edit access to these collections",
"no_invite_found": "Invitation not found. Contact your team owner.",
"no_request_found": "Request not found.",
"not_found": "Team not found. Contact your team owner.",

View File

@@ -5,7 +5,7 @@
"choose_file": "選擇一個檔案",
"clear": "清除",
"clear_all": "全部清除",
"clear_history": "清除所有歷史記錄",
"clear_history": "Clear all History",
"close": "關閉",
"connect": "連線",
"connecting": "正在連接",
@@ -79,8 +79,8 @@
"search": "搜尋",
"share": "分享",
"shortcuts": "快捷方式",
"social_description": "在社交媒體上追蹤我們即可在第一時間得知新聞、更新、以及新版本的消息。",
"social_links": "社群連結",
"social_description": "Follow us on social media to stay updated with the latest news, updates and releases.",
"social_links": "Social links",
"spotlight": "聚光燈",
"status": "狀態",
"status_description": "檢查網站狀態",
@@ -135,15 +135,15 @@
"renamed": "集合已重新命名",
"request_in_use": "請求正在使用中",
"save_as": "另存為",
"save_to_collection": "儲存到集合",
"save_to_collection": "Save to Collection",
"select": "選擇一個集合",
"select_location": "選擇位置",
"select_team": "選擇一個團隊",
"team_collections": "團隊集合"
},
"confirm": {
"close_unsaved_tab": "您確定要關閉此分頁嗎?",
"close_unsaved_tabs": "您確定要關閉所有分頁嗎?{count} 個未儲存的分頁將會遺失。",
"close_unsaved_tab": "Are you sure you want to close this tab?",
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
"exit_team": "您確定要離開此團隊嗎?",
"logout": "您確定要登出嗎?",
"remove_collection": "您確定要永久刪除該集合嗎?",
@@ -158,9 +158,9 @@
"sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。"
},
"context_menu": {
"add_parameters": "新增至參數",
"open_request_in_new_tab": "在新分頁開啟請求",
"set_environment_variable": "設為變數"
"add_parameters": "Add to parameters",
"open_request_in_new_tab": "Open request in new tab",
"set_environment_variable": "Set as variable"
},
"count": {
"header": "請求標頭 {count}",
@@ -204,31 +204,31 @@
"create_new": "建立新環境",
"created": "已建立環境",
"deleted": "刪除環境",
"duplicated": "已複製環境",
"duplicated": "Environment duplicated",
"edit": "編輯環境",
"empty_variables": "無變數",
"global": "全域",
"global_variables": "全域變數",
"empty_variables": "No variables",
"global": "Global",
"global_variables": "Global variables",
"invalid_name": "請提供有效的環境名稱",
"list": "環境變數",
"list": "Environment variables",
"my_environments": "我的環境",
"name": "名稱",
"name": "Name",
"nested_overflow": "巢狀環境變數不得大於 10 層",
"new": "建立環境",
"no_active_environment": "無使用中的環境",
"no_active_environment": "No active environment",
"no_environment": "無環境",
"no_environment_description": "未選取任何環境。請選擇要對以下變數進行的動作。",
"quick_peek": "快速預覽環境",
"replace_with_variable": "以變數替代",
"scope": "範圍",
"quick_peek": "Environment Quick Peek",
"replace_with_variable": "Replace with variable",
"scope": "Scope",
"select": "選擇環境",
"set": "設定環境",
"set_as_environment": "設為環境",
"set": "Set environment",
"set_as_environment": "Set as environment",
"team_environments": "團隊環境",
"title": "環境",
"updated": "更新環境",
"value": "數值",
"variable": "變數",
"value": "Value",
"variable": "Variable",
"variable_list": "變數列表"
},
"error": {
@@ -252,7 +252,7 @@
"no_duration": "無持續時間",
"no_results_found": "找不到結果",
"page_not_found": "找不到此頁面",
"proxy_error": "Proxy 錯誤",
"proxy_error": "Proxy error",
"script_fail": "無法執行預請求指令碼",
"something_went_wrong": "發生了一些錯誤",
"test_script_fail": "無法執行測試指令碼"
@@ -278,13 +278,13 @@
"renamed": "資料夾已重新命名"
},
"graphql": {
"connection_switch_confirm": "您要使用最新的 GraphQL 端點連線嗎?",
"connection_switch_new_url": "切換至分頁將斷開使用中的 GraphQL 連線。新的連線網址為 ",
"connection_switch_url": "您已連接至 GraphQL 端點。連線網址為 ",
"connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?",
"connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is",
"connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is",
"mutations": "變體",
"schema": "綱要",
"subscriptions": "訂閱",
"switch_connection": "切換連線"
"switch_connection": "Switch connection"
},
"group": {
"time": "時間",
@@ -339,27 +339,27 @@
"title": "匯入"
},
"inspections": {
"description": "檢查潛在錯誤",
"description": "Inspect possible errors",
"environment": {
"add_environment": "新增至環境",
"not_found": "找不到環境變數 “{environment}”"
"add_environment": "Add to Environment",
"not_found": "Environment variable “{environment}” not found."
},
"header": {
"cookie": "瀏覽器不允許 Hoppscotch 設定 Cookie 標頭。在我們推出 Hoppscotch 桌面版前,請先使用 Authorization 標頭。"
"cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
},
"response": {
"401_error": "請檢查您的授權認證。",
"404_error": "請檢查您的請求網址和方式類型。",
"cors_error": "請檢查您的跨來源資源共用設定。",
"default_error": "請檢查您的請求。",
"network_error": "請檢查您的網路連線。"
"401_error": "Please check your authentication credentials.",
"404_error": "Please check your request URL and method type.",
"cors_error": "Please check your Cross-Origin Resource Sharing configuration.",
"default_error": "Please check your request.",
"network_error": "Please check your network connection."
},
"title": "檢查工具",
"title": "Inspector",
"url": {
"extension_not_installed": "未安裝擴充套件。",
"extension_unknown_origin": "請確認您是否已將 API 端點的來源加入 Hoppscotch 擴充套件的清單。",
"extention_enable_action": "啟用瀏覽器擴充套件",
"extention_not_enabled": "未啟用擴充套件。"
"extension_not_installed": "Extension not installed.",
"extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list.",
"extention_enable_action": "Enable Browser Extension",
"extention_not_enabled": "Extension not enabled."
}
},
"layout": {
@@ -472,7 +472,7 @@
"payload": "負載",
"query": "查詢",
"raw_body": "原始請求本體",
"rename": "重新命名請求",
"rename": "Rename Request",
"renamed": "請求已重新命名",
"run": "執行",
"save": "儲存",
@@ -510,7 +510,7 @@
"accent_color": "強調色",
"account": "帳號",
"account_deleted": "已刪除您的帳號",
"account_description": "自您的帳號設定。",
"account_description": "自定義您的帳號設定。",
"account_email_description": "您的主要電子郵件地址。",
"account_name_description": "這是您的顯示名稱。",
"background": "背景",
@@ -542,7 +542,7 @@
"read_the": "閱讀",
"reset_default": "重置為預設",
"short_codes": "快捷碼",
"short_codes_description": "您建立的快捷碼。",
"short_codes_description": "我們為您打造的快捷碼。",
"sidebar_on_left": "左側邊欄",
"sync": "同步",
"sync_collections": "集合",
@@ -551,9 +551,9 @@
"sync_history": "歷史",
"system_mode": "系統",
"telemetry": "遙測服務",
"telemetry_helps_us": "遙測服務能夠幫助我們進行個化操作,為您提供最佳體驗。",
"telemetry_helps_us": "遙測服務幫助我們進行個化操作,為您提供最佳體驗。",
"theme": "主題",
"theme_description": "自您的應用程式主題。",
"theme_description": "自定義您的應用程式主題。",
"use_experimental_url_bar": "使用帶有環境醒目標示的實驗性網址欄",
"user": "使用者",
"verified_email": "已確認電子郵件地址",
@@ -592,26 +592,26 @@
"title": "導航"
},
"others": {
"prettify": "美化編輯器的內容",
"title": "其他"
"prettify": "Prettify Editor's Content",
"title": "Others"
},
"request": {
"copy_request_link": "複製請求連結",
"delete_method": "選擇 DELETE 方法",
"get_method": "選擇 GET 方法",
"head_method": "選擇 HEAD 方法",
"import_curl": "匯入 cURL",
"import_curl": "Import cURL",
"method": "方法",
"next_method": "選擇下一個方法",
"post_method": "選擇 POST 方法",
"previous_method": "選擇上一個方法",
"put_method": "選擇 PUT 方法",
"rename": "重新命名請求",
"rename": "Rename Request",
"reset_request": "重置請求",
"save_request": "儲存請求",
"save_request": "Save Request",
"save_to_collections": "儲存到集合",
"send_request": "傳送請求",
"show_code": "產生程式碼片段",
"show_code": "Generate code snippet",
"title": "請求"
},
"response": {
@@ -642,82 +642,82 @@
"url": "網址"
},
"spotlight": {
"change_language": "變更語言",
"change_language": "Change Language",
"environments": {
"delete": "刪除目前環境",
"duplicate": "複製目前環境",
"duplicate_global": "複製全域環境",
"edit": "編輯目前環境",
"edit_global": "編輯全域環境",
"new": "建立新環境",
"new_variable": "建立新環境變數",
"title": "環境"
"delete": "Delete current environment",
"duplicate": "Duplicate current environment",
"duplicate_global": "Duplicate global environment",
"edit": "Edit current environment",
"edit_global": "Edit global environment",
"new": "Create new environment",
"new_variable": "Create a new environment variable",
"title": "Environments"
},
"general": {
"chat": "與客服對話",
"help_menu": "幫助與支援",
"open_docs": "閱讀說明文件",
"open_github": "開啟 GitHub 儲存庫",
"open_keybindings": "鍵盤快捷鍵",
"social": "社交",
"title": "一般"
"chat": "Chat with support",
"help_menu": "Help and support",
"open_docs": "Read Documentation",
"open_github": "Open GitHub repository",
"open_keybindings": "Keyboard shortcuts",
"social": "Social",
"title": "General"
},
"graphql": {
"connect": "連接至伺服器",
"disconnect": "斷開與伺服器的連線"
"connect": "Connect to server",
"disconnect": "Disconnect from server"
},
"miscellaneous": {
"invite": "邀請您的朋友使用 Hoppscotch",
"title": "雜項"
"invite": "Invite your friends to Hoppscotch",
"title": "Miscellaneous"
},
"request": {
"save_as_new": "儲存為新請求",
"select_method": "選擇方法",
"switch_to": "切換至",
"tab_authorization": "授權分頁",
"tab_body": "本體分頁",
"tab_headers": "標頭分頁",
"tab_parameters": "參數分頁",
"tab_pre_request_script": "預請求腳本分頁",
"tab_query": "查詢分頁",
"tab_tests": "測試分頁",
"tab_variables": "變數分頁"
"save_as_new": "Save as new request",
"select_method": "Select method",
"switch_to": "Switch to",
"tab_authorization": "Authorization tab",
"tab_body": "Body tab",
"tab_headers": "Headers tab",
"tab_parameters": "Parameters tab",
"tab_pre_request_script": "Pre-request script tab",
"tab_query": "Query tab",
"tab_tests": "Tests tab",
"tab_variables": "Variables tab"
},
"response": {
"copy": "複製回應",
"download": "下載回應",
"title": "回應"
"copy": "Copy response",
"download": "Download response as file",
"title": "Response"
},
"section": {
"interceptor": "攔截器",
"interface": "介面",
"theme": "主題",
"user": "使用者"
"interceptor": "Interceptor",
"interface": "Interface",
"theme": "Theme",
"user": "User"
},
"settings": {
"change_interceptor": "變更攔截器",
"change_language": "變更語言",
"change_interceptor": "Change Interceptor",
"change_language": "Change Language",
"theme": {
"black": "黑色",
"dark": "暗色",
"light": "亮色",
"system": "跟隨系統"
"black": "Black",
"dark": "Dark",
"light": "Light",
"system": "System preference"
}
},
"tab": {
"close_current": "關閉目前分頁",
"close_others": "關閉所有其他分頁",
"duplicate": "複製目前分頁",
"new_tab": "開啟新分頁",
"title": "分頁"
"close_current": "Close current tab",
"close_others": "Close all other tabs",
"duplicate": "Duplicate current tab",
"new_tab": "Open a new tab",
"title": "Tabs"
},
"workspace": {
"delete": "刪除目前團隊",
"edit": "編輯目前團隊",
"invite": "邀請他人加入團隊",
"new": "建立新團隊",
"switch_to_personal": "切換至您的個人工作區",
"title": "團隊"
"delete": "Delete current team",
"edit": "Edit current team",
"invite": "Invite people to team",
"new": "Create new team",
"switch_to_personal": "Switch to your personal workspace",
"title": "Teams"
}
},
"sse": {
@@ -777,11 +777,11 @@
"tab": {
"authorization": "授權",
"body": "請求本體",
"close": "關閉分頁",
"close_others": "關閉其他分頁",
"close": "Close Tab",
"close_others": "Close other Tabs",
"collections": "集合",
"documentation": "幫助文件",
"duplicate": "複製分頁",
"duplicate": "Duplicate Tab",
"environments": "環境",
"headers": "請求標頭",
"history": "歷史記錄",

View File

@@ -1,7 +1,7 @@
{
"name": "@hoppscotch/common",
"private": true,
"version": "2023.8.4-1",
"version": "2023.8.1",
"scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*",
"test": "vitest --run",
@@ -17,46 +17,51 @@
"postinstall": "pnpm run gql-codegen",
"do-test": "pnpm run test",
"do-lint": "pnpm run prod-lint",
"do-typecheck": "node type-check.mjs",
"do-typecheck": "pnpm run lint",
"do-lintfix": "pnpm run lintfix"
},
"dependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"@codemirror/autocomplete": "^6.11.0",
"@codemirror/commands": "^6.3.0",
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/autocomplete": "^6.9.0",
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-javascript": "^6.1.9",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/language": "6.9.2",
"@codemirror/language": "^6.9.0",
"@codemirror/legacy-modes": "^6.3.3",
"@codemirror/lint": "^6.4.2",
"@codemirror/search": "^6.5.4",
"@codemirror/state": "^6.3.1",
"@codemirror/view": "^6.22.0",
"@codemirror/lint": "^6.4.0",
"@codemirror/search": "^6.5.1",
"@codemirror/state": "^6.2.1",
"@codemirror/view": "^6.16.0",
"@fontsource-variable/inter": "^5.0.8",
"@fontsource-variable/material-symbols-rounded": "^5.0.7",
"@fontsource-variable/roboto-mono": "^5.0.9",
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
"@hoppscotch/data": "workspace:^",
"@hoppscotch/js-sandbox": "workspace:^",
"@hoppscotch/ui": "workspace:^",
"@hoppscotch/vue-toasted": "^0.1.0",
"@lezer/highlight": "1.2.0",
"@unhead/vue": "^1.8.8",
"@urql/core": "^4.2.0",
"@lezer/highlight": "^1.1.6",
"@sentry/tracing": "^7.64.0",
"@sentry/vue": "^7.64.0",
"@urql/core": "^4.1.1",
"@urql/devtools": "^2.0.3",
"@urql/exchange-auth": "^2.1.6",
"@urql/exchange-graphcache": "^6.3.3",
"@urql/exchange-graphcache": "^6.3.2",
"@vitejs/plugin-legacy": "^4.1.1",
"@vueuse/core": "^10.6.1",
"acorn-walk": "^8.3.0",
"axios": "^1.6.2",
"@vueuse/core": "^10.3.0",
"@vueuse/head": "^1.3.1",
"acorn-walk": "^8.2.0",
"axios": "^1.4.0",
"buffer": "^6.0.3",
"cookie-es": "^1.0.0",
"dioc": "workspace:^",
"esprima": "^4.0.1",
"events": "^3.3.0",
"fp-ts": "^2.16.1",
"fuse.js": "^6.6.2",
"globalthis": "^1.0.3",
"graphql": "^16.8.1",
"graphql-language-service-interface": "^2.10.2",
"graphql": "^16.8.0",
"graphql-language-service-interface": "^2.9.1",
"graphql-tag": "^2.12.6",
"httpsnippet": "^3.0.1",
"insomnia-importers": "^3.6.0",
@@ -64,18 +69,15 @@
"js-yaml": "^4.1.0",
"jsonpath-plus": "^7.2.0",
"lodash-es": "^4.17.21",
"lossless-json": "^3.0.2",
"minisearch": "^6.3.0",
"lossless-json": "^2.0.11",
"minisearch": "^6.1.0",
"nprogress": "^0.2.0",
"paho-mqtt": "^1.1.0",
"path": "^0.12.7",
"postman-collection": "^4.3.0",
"postman-collection": "^4.2.0",
"process": "^0.11.10",
"qs": "^6.11.2",
"quicktype-core": "^23.0.79",
"rxjs": "^7.8.1",
"set-cookie-parser": "^2.6.0",
"set-cookie-parser-es": "^1.0.5",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"socket.io-client-v3": "npm:socket.io-client@^3.1.3",
"socket.io-client-v4": "npm:socket.io-client@^4.4.1",
@@ -86,21 +88,19 @@
"tern": "^0.24.3",
"timers": "^0.1.1",
"tippy.js": "^6.3.7",
"url": "^0.11.3",
"url": "^0.11.1",
"util": "^0.12.5",
"verzod": "^0.2.0",
"uuid": "^9.0.1",
"vue": "^3.3.8",
"vue-i18n": "^9.7.1",
"vue-pdf-embed": "^1.2.1",
"vue-router": "^4.2.5",
"uuid": "^9.0.0",
"vue": "^3.3.4",
"vue-i18n": "^9.2.2",
"vue-pdf-embed": "^1.1.6",
"vue-router": "^4.2.4",
"vue-tippy": "6.3.1",
"vuedraggable-es": "^4.1.1",
"wonka": "^6.3.4",
"workbox-window": "^7.0.0",
"xml-formatter": "^3.6.0",
"yargs-parser": "^21.1.1",
"zod": "^3.22.4"
"xml-formatter": "^3.5.0",
"yargs-parser": "^21.1.1"
},
"devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
@@ -110,58 +110,53 @@
"@graphql-codegen/typed-document-node": "^5.0.1",
"@graphql-codegen/typescript": "^4.0.1",
"@graphql-codegen/typescript-operations": "^4.0.1",
"@graphql-codegen/typescript-urql-graphcache": "^3.0.0",
"@graphql-codegen/urql-introspection": "^3.0.0",
"@graphql-codegen/typescript-urql-graphcache": "^2.4.5",
"@graphql-codegen/urql-introspection": "^2.2.1",
"@graphql-typed-document-node/core": "^3.2.0",
"@iconify-json/lucide": "^1.1.141",
"@iconify-json/lucide": "^1.1.119",
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
"@relmify/jest-fp-ts": "^2.1.1",
"@rushstack/eslint-patch": "^1.6.0",
"@types/har-format": "^1.2.15",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/lossless-json": "^1.0.4",
"@types/nprogress": "^0.2.3",
"@types/paho-mqtt": "^1.0.10",
"@types/postman-collection": "^3.5.10",
"@types/splitpanes": "^2.2.6",
"@types/uuid": "^9.0.7",
"@types/yargs-parser": "^21.0.3",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"@vitejs/plugin-vue": "^4.5.0",
"@vue/compiler-sfc": "^3.3.8",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/runtime-core": "^3.3.8",
"autoprefixer": "^10.4.14",
"@rushstack/eslint-patch": "^1.3.3",
"@types/har-format": "^1.2.12",
"@types/js-yaml": "^4.0.5",
"@types/lodash-es": "^4.17.8",
"@types/lossless-json": "^1.0.1",
"@types/nprogress": "^0.2.0",
"@types/paho-mqtt": "^1.0.7",
"@types/postman-collection": "^3.5.7",
"@types/splitpanes": "^2.2.1",
"@types/uuid": "^9.0.2",
"@types/yargs-parser": "^21.0.0",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.4.0",
"@vitejs/plugin-vue": "^4.3.1",
"@vue/compiler-sfc": "^3.3.4",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/runtime-core": "^3.3.4",
"cross-env": "^7.0.3",
"dotenv": "^16.3.1",
"eslint": "^8.54.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-vue": "^9.18.1",
"glob": "^10.3.10",
"eslint": "^8.47.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.17.0",
"npm-run-all": "^4.1.5",
"openapi-types": "^12.1.3",
"postcss": "^8.4.23",
"prettier": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7",
"rollup-plugin-polyfill-node": "^0.13.0",
"sass": "^1.69.5",
"tailwindcss": "^3.3.2",
"typescript": "^5.3.2",
"unplugin-fonts": "^1.1.1",
"unplugin-icons": "^0.17.4",
"unplugin-vue-components": "^0.25.2",
"vite": "^4.5.0",
"vite-plugin-checker": "^0.6.2",
"vite-plugin-fonts": "^0.7.0",
"rollup-plugin-polyfill-node": "^0.12.0",
"sass": "^1.66.0",
"typescript": "^5.1.6",
"unplugin-fonts": "^1.0.3",
"unplugin-icons": "^0.16.5",
"unplugin-vue-components": "^0.25.1",
"vite": "^4.4.9",
"vite-plugin-checker": "^0.6.1",
"vite-plugin-html-config": "^1.0.11",
"vite-plugin-inspect": "^0.7.42",
"vite-plugin-inspect": "^0.7.38",
"vite-plugin-pages": "^0.31.0",
"vite-plugin-pages-sitemap": "^1.6.1",
"vite-plugin-pwa": "^0.17.0",
"vite-plugin-pwa": "^0.16.4",
"vite-plugin-vue-layouts": "^0.8.0",
"vitest": "^0.34.6",
"vue-tsc": "^1.8.22"
"vite-plugin-windicss": "^1.9.1",
"vitest": "^0.34.2",
"vue-tsc": "^1.8.8",
"windicss": "^3.5.6"
}
}
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="156" height="32" fill="none"><rect width="156" height="32" fill="#000" rx="4"/><text xmlns="http://www.w3.org/2000/svg" x="50%" y="50%" fill="#fff" dominant-baseline="central" font-family="Helvetica,sans-serif" font-size="12" font-weight="bold" text-anchor="middle" text-rendering="geometricPrecision">▶ Run in Hoppscotch</text></svg>

Before

Width:  |  Height:  |  Size: 386 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="156" height="32" fill="none"><rect width="156" height="32" fill="#fff" rx="4"/><text xmlns="http://www.w3.org/2000/svg" x="50%" y="50%" fill="#000" dominant-baseline="central" font-family="Helvetica,sans-serif" font-size="12" font-weight="bold" text-anchor="middle" text-rendering="geometricPrecision">▶ Run in Hoppscotch</text></svg>

Before

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

After

Width:  |  Height:  |  Size: 926 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 624 B

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Some files were not shown because too many files have changed in this diff Show More