Compare commits
39 Commits
pr/JoelJac
...
feat/reque
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3e941271e | ||
|
|
e7fded89d5 | ||
|
|
40c8d7e4b7 | ||
|
|
1d93d9dabe | ||
|
|
e9f1dc7ba1 | ||
|
|
1e906bbec3 | ||
|
|
7776668e40 | ||
|
|
3d38b7678b | ||
|
|
1ca215d887 | ||
|
|
aec04a13e3 | ||
|
|
9a2058bbec | ||
|
|
1c65147a77 | ||
|
|
207eea538f | ||
|
|
e30b59e1b5 | ||
|
|
9704b3324b | ||
|
|
386bb453d4 | ||
|
|
175641246e | ||
|
|
fb615d2d2b | ||
|
|
7685409ee6 | ||
|
|
e01818444e | ||
|
|
52220d9a2e | ||
|
|
82d687f665 | ||
|
|
d44083a380 | ||
|
|
7589c57e86 | ||
|
|
c8009aec77 | ||
|
|
7114f4fd43 | ||
|
|
ca1a4ec31b | ||
|
|
359633102e | ||
|
|
b278691a9d | ||
|
|
c27e1be230 | ||
|
|
11e8ba7bb3 | ||
|
|
2a0000cfc4 | ||
|
|
0ed063724e | ||
|
|
170ec15821 | ||
|
|
3611cac241 | ||
|
|
919579b1da | ||
|
|
4798d7bbbd | ||
|
|
a0c6b22641 | ||
|
|
de8929ab18 |
@@ -112,17 +112,17 @@ services:
|
|||||||
build:
|
build:
|
||||||
dockerfile: packages/hoppscotch-backend/Dockerfile
|
dockerfile: packages/hoppscotch-backend/Dockerfile
|
||||||
context: .
|
context: .
|
||||||
target: prod
|
target: dev
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
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)
|
# 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
|
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
||||||
- PORT=3000
|
- PORT=3000
|
||||||
volumes:
|
volumes:
|
||||||
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
||||||
# - ./packages/hoppscotch-backend/:/usr/src/app
|
- ./packages/hoppscotch-backend/:/usr/src/app
|
||||||
- /usr/src/app/node_modules/
|
- /usr/src/app/node_modules/
|
||||||
depends_on:
|
depends_on:
|
||||||
hoppscotch-db:
|
hoppscotch-db:
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE
|
||||||
|
"TeamCollection"
|
||||||
|
ADD
|
||||||
|
titleSearch tsvector GENERATED ALWAYS AS (to_tsvector('english', title)) STORED;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE
|
||||||
|
"TeamRequest"
|
||||||
|
ADD
|
||||||
|
titleSearch tsvector GENERATED ALWAYS AS (to_tsvector('english', title)) STORED;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "TeamCollection_textSearch_idx" ON "TeamCollection" USING GIN (titleSearch);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "TeamRequest_textSearch_idx" ON "TeamRequest" USING GIN (titleSearch);
|
||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
EnableAndDisableSSOArgs,
|
EnableAndDisableSSOArgs,
|
||||||
InfraConfigArgs,
|
InfraConfigArgs,
|
||||||
} from 'src/infra-config/input-args';
|
} from 'src/infra-config/input-args';
|
||||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import { ServiceStatus } from 'src/infra-config/helper';
|
import { ServiceStatus } from 'src/infra-config/helper';
|
||||||
|
|
||||||
@UseGuards(GqlThrottlerGuard)
|
@UseGuards(GqlThrottlerGuard)
|
||||||
@@ -274,10 +274,10 @@ export class InfraResolver {
|
|||||||
async infraConfigs(
|
async infraConfigs(
|
||||||
@Args({
|
@Args({
|
||||||
name: 'configNames',
|
name: 'configNames',
|
||||||
type: () => [InfraConfigEnumForClient],
|
type: () => [InfraConfigEnum],
|
||||||
description: 'Configs to fetch',
|
description: 'Configs to fetch',
|
||||||
})
|
})
|
||||||
names: InfraConfigEnumForClient[],
|
names: InfraConfigEnum[],
|
||||||
) {
|
) {
|
||||||
const infraConfigs = await this.infraConfigService.getMany(names);
|
const infraConfigs = await this.infraConfigService.getMany(names);
|
||||||
if (E.isLeft(infraConfigs)) throwErr(infraConfigs.left);
|
if (E.isLeft(infraConfigs)) throwErr(infraConfigs.left);
|
||||||
|
|||||||
@@ -18,12 +18,7 @@ import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
|||||||
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
|
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
|
||||||
import {
|
import { AuthProvider, authCookieHandler, authProviderCheck } from './helper';
|
||||||
AuthProvider,
|
|
||||||
authCookieHandler,
|
|
||||||
authProviderCheck,
|
|
||||||
throwHTTPErr,
|
|
||||||
} from './helper';
|
|
||||||
import { GoogleSSOGuard } from './guards/google-sso.guard';
|
import { GoogleSSOGuard } from './guards/google-sso.guard';
|
||||||
import { GithubSSOGuard } from './guards/github-sso.guard';
|
import { GithubSSOGuard } from './guards/github-sso.guard';
|
||||||
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
||||||
@@ -31,6 +26,7 @@ import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.gua
|
|||||||
import { SkipThrottle } from '@nestjs/throttler';
|
import { SkipThrottle } from '@nestjs/throttler';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
|
||||||
@UseGuards(ThrottlerBehindProxyGuard)
|
@UseGuards(ThrottlerBehindProxyGuard)
|
||||||
@Controller({ path: 'auth', version: '1' })
|
@Controller({ path: 'auth', version: '1' })
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import { GithubStrategy } from './strategies/github.strategy';
|
|||||||
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
||||||
import { AuthProvider, authProviderCheck } from './helper';
|
import { AuthProvider, authProviderCheck } from './helper';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { loadInfraConfiguration } from 'src/infra-config/helper';
|
import {
|
||||||
|
isInfraConfigTablePopulated,
|
||||||
|
loadInfraConfiguration,
|
||||||
|
} from 'src/infra-config/helper';
|
||||||
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -34,6 +37,11 @@ import { InfraConfigModule } from 'src/infra-config/infra-config.module';
|
|||||||
})
|
})
|
||||||
export class AuthModule {
|
export class AuthModule {
|
||||||
static async register() {
|
static async register() {
|
||||||
|
const isInfraConfigPopulated = await isInfraConfigTablePopulated();
|
||||||
|
if (!isInfraConfigPopulated) {
|
||||||
|
return { module: AuthModule };
|
||||||
|
}
|
||||||
|
|
||||||
const env = await loadInfraConfiguration();
|
const env = await loadInfraConfiguration();
|
||||||
const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS;
|
const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS;
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
RefreshTokenPayload,
|
RefreshTokenPayload,
|
||||||
} from 'src/types/AuthTokens';
|
} from 'src/types/AuthTokens';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { AuthError } from 'src/types/AuthError';
|
import { RESTError } from 'src/types/RESTError';
|
||||||
import { AuthUser, IsAdmin } from 'src/types/AuthUser';
|
import { AuthUser, IsAdmin } from 'src/types/AuthUser';
|
||||||
import { VerificationToken } from '@prisma/client';
|
import { VerificationToken } from '@prisma/client';
|
||||||
import { Origin } from './helper';
|
import { Origin } from './helper';
|
||||||
@@ -117,7 +117,7 @@ export class AuthService {
|
|||||||
userUid,
|
userUid,
|
||||||
);
|
);
|
||||||
if (E.isLeft(updatedUser))
|
if (E.isLeft(updatedUser))
|
||||||
return E.left(<AuthError>{
|
return E.left(<RESTError>{
|
||||||
message: updatedUser.left,
|
message: updatedUser.left,
|
||||||
statusCode: HttpStatus.NOT_FOUND,
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
});
|
});
|
||||||
@@ -255,7 +255,7 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
async verifyMagicLinkTokens(
|
async verifyMagicLinkTokens(
|
||||||
magicLinkIDTokens: VerifyMagicDto,
|
magicLinkIDTokens: VerifyMagicDto,
|
||||||
): Promise<E.Right<AuthTokens> | E.Left<AuthError>> {
|
): Promise<E.Right<AuthTokens> | E.Left<RESTError>> {
|
||||||
const passwordlessTokens = await this.validatePasswordlessTokens(
|
const passwordlessTokens = await this.validatePasswordlessTokens(
|
||||||
magicLinkIDTokens,
|
magicLinkIDTokens,
|
||||||
);
|
);
|
||||||
@@ -373,7 +373,7 @@ export class AuthService {
|
|||||||
if (usersCount === 1) {
|
if (usersCount === 1) {
|
||||||
const elevatedUser = await this.usersService.makeAdmin(user.uid);
|
const elevatedUser = await this.usersService.makeAdmin(user.uid);
|
||||||
if (E.isLeft(elevatedUser))
|
if (E.isLeft(elevatedUser))
|
||||||
return E.left(<AuthError>{
|
return E.left(<RESTError>{
|
||||||
message: elevatedUser.left,
|
message: elevatedUser.left,
|
||||||
statusCode: HttpStatus.NOT_FOUND,
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
import { AuthProvider, authProviderCheck } from '../helper';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
import { AuthProvider, authProviderCheck } from '../helper';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
import { AuthProvider, authProviderCheck } from '../helper';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicrosoftSSOGuard
|
export class MicrosoftSSOGuard
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { HttpException, HttpStatus } from '@nestjs/common';
|
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { AuthError } from 'src/types/AuthError';
|
|
||||||
import { AuthTokens } from 'src/types/AuthTokens';
|
import { AuthTokens } from 'src/types/AuthTokens';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import * as cookie from 'cookie';
|
import * as cookie from 'cookie';
|
||||||
@@ -25,15 +24,6 @@ export enum AuthProvider {
|
|||||||
EMAIL = 'EMAIL',
|
EMAIL = 'EMAIL',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This function allows throw to be used as an expression
|
|
||||||
* @param errMessage Message present in the error message
|
|
||||||
*/
|
|
||||||
export function throwHTTPErr(errorData: AuthError): never {
|
|
||||||
const { message, statusCode } = errorData;
|
|
||||||
throw new HttpException(message, statusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets and returns the cookies in the response object on successful authentication
|
* Sets and returns the cookies in the response object on successful authentication
|
||||||
* @param res Express Response Object
|
* @param res Express Response Object
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export class GithubStrategy extends PassportStrategy(Strategy) {
|
|||||||
super({
|
super({
|
||||||
clientID: configService.get('INFRA.GITHUB_CLIENT_ID'),
|
clientID: configService.get('INFRA.GITHUB_CLIENT_ID'),
|
||||||
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
|
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
|
||||||
callbackURL: configService.get('GITHUB_CALLBACK_URL'),
|
callbackURL: configService.get('INFRA.GITHUB_CALLBACK_URL'),
|
||||||
scope: [configService.get('GITHUB_SCOPE')],
|
scope: [configService.get('INFRA.GITHUB_SCOPE')],
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export class GoogleStrategy extends PassportStrategy(Strategy) {
|
|||||||
super({
|
super({
|
||||||
clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'),
|
clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'),
|
||||||
clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'),
|
clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'),
|
||||||
callbackURL: configService.get('GOOGLE_CALLBACK_URL'),
|
callbackURL: configService.get('INFRA.GOOGLE_CALLBACK_URL'),
|
||||||
scope: configService.get('GOOGLE_SCOPE').split(','),
|
scope: configService.get('INFRA.GOOGLE_SCOPE').split(','),
|
||||||
passReqToCallback: true,
|
passReqToCallback: true,
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
|||||||
super({
|
super({
|
||||||
clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'),
|
clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'),
|
||||||
clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'),
|
clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'),
|
||||||
callbackURL: configService.get('MICROSOFT_CALLBACK_URL'),
|
callbackURL: configService.get('INFRA.MICROSOFT_CALLBACK_URL'),
|
||||||
scope: [configService.get('MICROSOFT_SCOPE')],
|
scope: [configService.get('INFRA.MICROSOFT_SCOPE')],
|
||||||
tenant: configService.get('MICROSOFT_TENANT'),
|
tenant: configService.get('INFRA.MICROSOFT_TENANT'),
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,6 +228,12 @@ export const TEAM_COL_NOT_SAME_PARENT =
|
|||||||
export const TEAM_COL_SAME_NEXT_COLL =
|
export const TEAM_COL_SAME_NEXT_COLL =
|
||||||
'team_coll/collection_and_next_collection_are_same';
|
'team_coll/collection_and_next_collection_are_same';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Team Collection search failed
|
||||||
|
* (TeamCollectionService)
|
||||||
|
*/
|
||||||
|
export const TEAM_COL_SEARCH_FAILED = 'team_coll/team_collection_search_failed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Team Collection Re-Ordering Failed
|
* Team Collection Re-Ordering Failed
|
||||||
* (TeamCollectionService)
|
* (TeamCollectionService)
|
||||||
@@ -283,6 +289,13 @@ export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
|
|||||||
export const TEAM_COLL_DATA_INVALID =
|
export const TEAM_COLL_DATA_INVALID =
|
||||||
'team_coll/team_coll_data_invalid' as const;
|
'team_coll/team_coll_data_invalid' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Team Collection parent tree generation failed
|
||||||
|
* (TeamCollectionService)
|
||||||
|
*/
|
||||||
|
export const TEAM_COLL_PARENT_TREE_GEN_FAILED =
|
||||||
|
'team_coll/team_coll_parent_tree_generation_failed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tried to perform an action on a request that doesn't accept their member role level
|
* Tried to perform an action on a request that doesn't accept their member role level
|
||||||
* (GqlRequestTeamMemberGuard)
|
* (GqlRequestTeamMemberGuard)
|
||||||
@@ -308,6 +321,19 @@ export const TEAM_REQ_INVALID_TARGET_COLL_ID =
|
|||||||
*/
|
*/
|
||||||
export const TEAM_REQ_REORDERING_FAILED = 'team_req/reordering_failed' as const;
|
export const TEAM_REQ_REORDERING_FAILED = 'team_req/reordering_failed' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Team Request search failed
|
||||||
|
* (TeamRequestService)
|
||||||
|
*/
|
||||||
|
export const TEAM_REQ_SEARCH_FAILED = 'team_req/team_request_search_failed';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Team Request parent tree generation failed
|
||||||
|
* (TeamRequestService)
|
||||||
|
*/
|
||||||
|
export const TEAM_REQ_PARENT_TREE_GEN_FAILED =
|
||||||
|
'team_req/team_req_parent_tree_generation_failed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No Postmark Sender Email defined
|
* No Postmark Sender Email defined
|
||||||
* (AuthService)
|
* (AuthService)
|
||||||
@@ -705,6 +731,13 @@ export const INFRA_CONFIG_INVALID_INPUT = 'infra_config/invalid_input' as const;
|
|||||||
export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED =
|
export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED =
|
||||||
'infra_config/service_not_configured' as const;
|
'infra_config/service_not_configured' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Infra Config update/fetch operation not allowed
|
||||||
|
* (InfraConfigService)
|
||||||
|
*/
|
||||||
|
export const INFRA_CONFIG_OPERATION_NOT_ALLOWED =
|
||||||
|
'infra_config/operation_not_allowed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error message for when the database table does not exist
|
* Error message for when the database table does not exist
|
||||||
* (InfraConfigService)
|
* (InfraConfigService)
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { AuthProvider } from 'src/auth/helper';
|
import { AuthProvider } from 'src/auth/helper';
|
||||||
import { AUTH_PROVIDER_NOT_CONFIGURED } from 'src/errors';
|
import {
|
||||||
|
AUTH_PROVIDER_NOT_CONFIGURED,
|
||||||
|
DATABASE_TABLE_NOT_EXIST,
|
||||||
|
} from 'src/errors';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
@@ -14,14 +17,21 @@ const AuthProviderConfigurations = {
|
|||||||
[AuthProvider.GOOGLE]: [
|
[AuthProvider.GOOGLE]: [
|
||||||
InfraConfigEnum.GOOGLE_CLIENT_ID,
|
InfraConfigEnum.GOOGLE_CLIENT_ID,
|
||||||
InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
||||||
|
InfraConfigEnum.GOOGLE_CALLBACK_URL,
|
||||||
|
InfraConfigEnum.GOOGLE_SCOPE,
|
||||||
],
|
],
|
||||||
[AuthProvider.GITHUB]: [
|
[AuthProvider.GITHUB]: [
|
||||||
InfraConfigEnum.GITHUB_CLIENT_ID,
|
InfraConfigEnum.GITHUB_CLIENT_ID,
|
||||||
InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
||||||
|
InfraConfigEnum.GITHUB_CALLBACK_URL,
|
||||||
|
InfraConfigEnum.GITHUB_SCOPE,
|
||||||
],
|
],
|
||||||
[AuthProvider.MICROSOFT]: [
|
[AuthProvider.MICROSOFT]: [
|
||||||
InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
||||||
InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
||||||
|
InfraConfigEnum.MICROSOFT_CALLBACK_URL,
|
||||||
|
InfraConfigEnum.MICROSOFT_SCOPE,
|
||||||
|
InfraConfigEnum.MICROSOFT_TENANT,
|
||||||
],
|
],
|
||||||
[AuthProvider.EMAIL]: [
|
[AuthProvider.EMAIL]: [
|
||||||
InfraConfigEnum.MAILER_SMTP_URL,
|
InfraConfigEnum.MAILER_SMTP_URL,
|
||||||
@@ -54,6 +64,125 @@ export async function loadInfraConfiguration() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the default values from .env file and return them as an array
|
||||||
|
* @returns Array of default infra configs
|
||||||
|
*/
|
||||||
|
export async function getDefaultInfraConfigs(): Promise<
|
||||||
|
{ name: InfraConfigEnum; value: string }[]
|
||||||
|
> {
|
||||||
|
const prisma = new PrismaService();
|
||||||
|
|
||||||
|
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
|
||||||
|
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MAILER_SMTP_URL,
|
||||||
|
value: process.env.MAILER_SMTP_URL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
|
||||||
|
value: process.env.MAILER_ADDRESS_FROM,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
|
||||||
|
value: process.env.GOOGLE_CLIENT_ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
||||||
|
value: process.env.GOOGLE_CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GOOGLE_CALLBACK_URL,
|
||||||
|
value: process.env.GOOGLE_CALLBACK_URL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GOOGLE_SCOPE,
|
||||||
|
value: process.env.GOOGLE_SCOPE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GITHUB_CLIENT_ID,
|
||||||
|
value: process.env.GITHUB_CLIENT_ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
||||||
|
value: process.env.GITHUB_CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GITHUB_CALLBACK_URL,
|
||||||
|
value: process.env.GITHUB_CALLBACK_URL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.GITHUB_SCOPE,
|
||||||
|
value: process.env.GITHUB_SCOPE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
||||||
|
value: process.env.MICROSOFT_CLIENT_ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
||||||
|
value: process.env.MICROSOFT_CLIENT_SECRET,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MICROSOFT_CALLBACK_URL,
|
||||||
|
value: process.env.MICROSOFT_CALLBACK_URL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MICROSOFT_SCOPE,
|
||||||
|
value: process.env.MICROSOFT_SCOPE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.MICROSOFT_TENANT,
|
||||||
|
value: process.env.MICROSOFT_TENANT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||||
|
value: getConfiguredSSOProviders(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
||||||
|
value: false.toString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.ANALYTICS_USER_ID,
|
||||||
|
value: generateAnalyticsUserId(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
|
value: (await prisma.infraConfig.count()) === 0 ? 'true' : 'false',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return infraConfigDefaultObjs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify if 'infra_config' table is loaded with all entries
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
export async function isInfraConfigTablePopulated(): Promise<boolean> {
|
||||||
|
const prisma = new PrismaService();
|
||||||
|
try {
|
||||||
|
const dbInfraConfigs = await prisma.infraConfig.findMany();
|
||||||
|
const infraConfigDefaultObjs = await getDefaultInfraConfigs();
|
||||||
|
|
||||||
|
const propsRemainingToInsert = infraConfigDefaultObjs.filter(
|
||||||
|
(p) => !dbInfraConfigs.find((e) => e.name === p.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (propsRemainingToInsert.length > 0) {
|
||||||
|
console.log(
|
||||||
|
'Infra Config table is not populated with all entries. Populating now...',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the app after 5 seconds
|
* Stop the app after 5 seconds
|
||||||
* (Docker will re-start the app)
|
* (Docker will re-start the app)
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { InfraConfigService } from './infra-config.service';
|
|||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
||||||
import { RESTAdminGuard } from 'src/admin/guards/rest-admin.guard';
|
import { RESTAdminGuard } from 'src/admin/guards/rest-admin.guard';
|
||||||
import { throwHTTPErr } from 'src/auth/helper';
|
import { RESTError } from 'src/types/RESTError';
|
||||||
import { AuthError } from 'src/types/AuthError';
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
|
||||||
@UseGuards(ThrottlerBehindProxyGuard)
|
@UseGuards(ThrottlerBehindProxyGuard)
|
||||||
@Controller({ path: 'site', version: '1' })
|
@Controller({ path: 'site', version: '1' })
|
||||||
@@ -17,11 +17,11 @@ export class SiteController {
|
|||||||
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
||||||
async fetchSetupInfo() {
|
async fetchSetupInfo() {
|
||||||
const status = await this.infraConfigService.get(
|
const status = await this.infraConfigService.get(
|
||||||
InfraConfigEnumForClient.IS_FIRST_TIME_INFRA_SETUP,
|
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (E.isLeft(status))
|
if (E.isLeft(status))
|
||||||
throwHTTPErr(<AuthError>{
|
throwHTTPErr(<RESTError>{
|
||||||
message: status.left,
|
message: status.left,
|
||||||
statusCode: HttpStatus.NOT_FOUND,
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
});
|
});
|
||||||
@@ -32,13 +32,13 @@ export class SiteController {
|
|||||||
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
||||||
async setSetupAsComplete() {
|
async setSetupAsComplete() {
|
||||||
const res = await this.infraConfigService.update(
|
const res = await this.infraConfigService.update(
|
||||||
InfraConfigEnumForClient.IS_FIRST_TIME_INFRA_SETUP,
|
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
false.toString(),
|
false.toString(),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (E.isLeft(res))
|
if (E.isLeft(res))
|
||||||
throwHTTPErr(<AuthError>{
|
throwHTTPErr(<RESTError>{
|
||||||
message: res.left,
|
message: res.left,
|
||||||
statusCode: HttpStatus.FORBIDDEN,
|
statusCode: HttpStatus.FORBIDDEN,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||||
import { AuthProvider } from 'src/auth/helper';
|
import { AuthProvider } from 'src/auth/helper';
|
||||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import { ServiceStatus } from './helper';
|
import { ServiceStatus } from './helper';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
@@ -8,7 +8,7 @@ export class InfraConfig {
|
|||||||
@Field({
|
@Field({
|
||||||
description: 'Infra Config Name',
|
description: 'Infra Config Name',
|
||||||
})
|
})
|
||||||
name: InfraConfigEnumForClient;
|
name: InfraConfigEnum;
|
||||||
|
|
||||||
@Field({
|
@Field({
|
||||||
description: 'Infra Config Value',
|
description: 'Infra Config Value',
|
||||||
@@ -16,7 +16,7 @@ export class InfraConfig {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerEnumType(InfraConfigEnumForClient, {
|
registerEnumType(InfraConfigEnum, {
|
||||||
name: 'InfraConfigEnum',
|
name: 'InfraConfigEnum',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { InfraConfigService } from './infra-config.service';
|
import { InfraConfigService } from './infra-config.service';
|
||||||
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import {
|
import {
|
||||||
InfraConfigEnum,
|
INFRA_CONFIG_NOT_FOUND,
|
||||||
InfraConfigEnumForClient,
|
INFRA_CONFIG_OPERATION_NOT_ALLOWED,
|
||||||
} from 'src/types/InfraConfig';
|
INFRA_CONFIG_UPDATE_FAILED,
|
||||||
import { INFRA_CONFIG_NOT_FOUND, INFRA_CONFIG_UPDATE_FAILED } from 'src/errors';
|
} from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as helper from './helper';
|
import * as helper from './helper';
|
||||||
|
import { InfraConfig as dbInfraConfig } from '@prisma/client';
|
||||||
|
import { InfraConfig } from './infra-config.model';
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockConfigService = mockDeep<ConfigService>();
|
const mockConfigService = mockDeep<ConfigService>();
|
||||||
@@ -19,12 +22,82 @@ const infraConfigService = new InfraConfigService(
|
|||||||
mockConfigService,
|
mockConfigService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const INITIALIZED_DATE_CONST = new Date();
|
||||||
|
const dbInfraConfigs: dbInfraConfig[] = [
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
|
||||||
|
value: 'abcdefghijkl',
|
||||||
|
active: true,
|
||||||
|
createdOn: INITIALIZED_DATE_CONST,
|
||||||
|
updatedOn: INITIALIZED_DATE_CONST,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||||
|
value: 'google',
|
||||||
|
active: true,
|
||||||
|
createdOn: INITIALIZED_DATE_CONST,
|
||||||
|
updatedOn: INITIALIZED_DATE_CONST,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const infraConfigs: InfraConfig[] = [
|
||||||
|
{
|
||||||
|
name: dbInfraConfigs[0].name as InfraConfigEnum,
|
||||||
|
value: dbInfraConfigs[0].value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: dbInfraConfigs[1].name as InfraConfigEnum,
|
||||||
|
value: dbInfraConfigs[1].value,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockReset(mockPrisma);
|
mockReset(mockPrisma);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('InfraConfigService', () => {
|
describe('InfraConfigService', () => {
|
||||||
describe('update', () => {
|
describe('update', () => {
|
||||||
|
it('should update the infra config without backend server restart', async () => {
|
||||||
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
|
const value = 'true';
|
||||||
|
|
||||||
|
mockPrisma.infraConfig.update.mockResolvedValueOnce({
|
||||||
|
id: '',
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
active: true,
|
||||||
|
createdOn: new Date(),
|
||||||
|
updatedOn: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(helper, 'stopApp').mockReturnValueOnce();
|
||||||
|
const result = await infraConfigService.update(name, value);
|
||||||
|
|
||||||
|
expect(helper.stopApp).not.toHaveBeenCalled();
|
||||||
|
expect(result).toEqualRight({ name, value });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update the infra config with backend server restart', async () => {
|
||||||
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
|
const value = 'true';
|
||||||
|
|
||||||
|
mockPrisma.infraConfig.update.mockResolvedValueOnce({
|
||||||
|
id: '',
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
active: true,
|
||||||
|
createdOn: new Date(),
|
||||||
|
updatedOn: new Date(),
|
||||||
|
});
|
||||||
|
jest.spyOn(helper, 'stopApp').mockReturnValueOnce();
|
||||||
|
|
||||||
|
const result = await infraConfigService.update(name, value, true);
|
||||||
|
|
||||||
|
expect(helper.stopApp).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result).toEqualRight({ name, value });
|
||||||
|
});
|
||||||
|
|
||||||
it('should update the infra config', async () => {
|
it('should update the infra config', async () => {
|
||||||
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
const value = 'true';
|
const value = 'true';
|
||||||
@@ -71,7 +144,7 @@ describe('InfraConfigService', () => {
|
|||||||
|
|
||||||
describe('get', () => {
|
describe('get', () => {
|
||||||
it('should get the infra config', async () => {
|
it('should get the infra config', async () => {
|
||||||
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
const value = 'true';
|
const value = 'true';
|
||||||
|
|
||||||
mockPrisma.infraConfig.findUniqueOrThrow.mockResolvedValueOnce({
|
mockPrisma.infraConfig.findUniqueOrThrow.mockResolvedValueOnce({
|
||||||
@@ -87,7 +160,7 @@ describe('InfraConfigService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should pass correct params to prisma findUnique', async () => {
|
it('should pass correct params to prisma findUnique', async () => {
|
||||||
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
|
|
||||||
await infraConfigService.get(name);
|
await infraConfigService.get(name);
|
||||||
|
|
||||||
@@ -98,7 +171,7 @@ describe('InfraConfigService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if the infra config does not exist', async () => {
|
it('should throw an error if the infra config does not exist', async () => {
|
||||||
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
|
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
|
||||||
|
|
||||||
mockPrisma.infraConfig.findUniqueOrThrow.mockRejectedValueOnce('null');
|
mockPrisma.infraConfig.findUniqueOrThrow.mockRejectedValueOnce('null');
|
||||||
|
|
||||||
@@ -106,4 +179,45 @@ describe('InfraConfigService', () => {
|
|||||||
expect(result).toEqualLeft(INFRA_CONFIG_NOT_FOUND);
|
expect(result).toEqualLeft(INFRA_CONFIG_NOT_FOUND);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getMany', () => {
|
||||||
|
it('should throw error if any disallowed names are provided', async () => {
|
||||||
|
const disallowedNames = [InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS];
|
||||||
|
const result = await infraConfigService.getMany(disallowedNames);
|
||||||
|
|
||||||
|
expect(result).toEqualLeft(INFRA_CONFIG_OPERATION_NOT_ALLOWED);
|
||||||
|
});
|
||||||
|
it('should resolve right with disallowed names if `checkDisallowed` parameter passed', async () => {
|
||||||
|
const disallowedNames = [InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS];
|
||||||
|
|
||||||
|
const dbInfraConfigResponses = dbInfraConfigs.filter((dbConfig) =>
|
||||||
|
disallowedNames.includes(dbConfig.name as InfraConfigEnum),
|
||||||
|
);
|
||||||
|
mockPrisma.infraConfig.findMany.mockResolvedValueOnce(
|
||||||
|
dbInfraConfigResponses,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await infraConfigService.getMany(disallowedNames, false);
|
||||||
|
|
||||||
|
expect(result).toEqualRight(
|
||||||
|
infraConfigs.filter((i) => disallowedNames.includes(i.name)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return right with infraConfigs if Prisma query succeeds', async () => {
|
||||||
|
const allowedNames = [InfraConfigEnum.GOOGLE_CLIENT_ID];
|
||||||
|
|
||||||
|
const dbInfraConfigResponses = dbInfraConfigs.filter((dbConfig) =>
|
||||||
|
allowedNames.includes(dbConfig.name as InfraConfigEnum),
|
||||||
|
);
|
||||||
|
mockPrisma.infraConfig.findMany.mockResolvedValueOnce(
|
||||||
|
dbInfraConfigResponses,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await infraConfigService.getMany(allowedNames);
|
||||||
|
expect(result).toEqualRight(
|
||||||
|
infraConfigs.filter((i) => allowedNames.includes(i.name)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,28 +3,25 @@ import { InfraConfig } from './infra-config.model';
|
|||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { InfraConfig as DBInfraConfig } from '@prisma/client';
|
import { InfraConfig as DBInfraConfig } from '@prisma/client';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import {
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
InfraConfigEnum,
|
|
||||||
InfraConfigEnumForClient,
|
|
||||||
} from 'src/types/InfraConfig';
|
|
||||||
import {
|
import {
|
||||||
AUTH_PROVIDER_NOT_SPECIFIED,
|
AUTH_PROVIDER_NOT_SPECIFIED,
|
||||||
DATABASE_TABLE_NOT_EXIST,
|
DATABASE_TABLE_NOT_EXIST,
|
||||||
INFRA_CONFIG_INVALID_INPUT,
|
INFRA_CONFIG_INVALID_INPUT,
|
||||||
INFRA_CONFIG_NOT_FOUND,
|
INFRA_CONFIG_NOT_FOUND,
|
||||||
INFRA_CONFIG_NOT_LISTED,
|
|
||||||
INFRA_CONFIG_RESET_FAILED,
|
INFRA_CONFIG_RESET_FAILED,
|
||||||
INFRA_CONFIG_UPDATE_FAILED,
|
INFRA_CONFIG_UPDATE_FAILED,
|
||||||
INFRA_CONFIG_SERVICE_NOT_CONFIGURED,
|
INFRA_CONFIG_SERVICE_NOT_CONFIGURED,
|
||||||
|
INFRA_CONFIG_OPERATION_NOT_ALLOWED,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { throwErr, validateSMTPEmail, validateSMTPUrl } from 'src/utils';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
import {
|
import {
|
||||||
ServiceStatus,
|
throwErr,
|
||||||
generateAnalyticsUserId,
|
validateSMTPEmail,
|
||||||
getConfiguredSSOProviders,
|
validateSMTPUrl,
|
||||||
stopApp,
|
validateUrl,
|
||||||
} from './helper';
|
} from 'src/utils';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { ServiceStatus, getDefaultInfraConfigs, stopApp } from './helper';
|
||||||
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
|
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
|
||||||
import { AuthProvider } from 'src/auth/helper';
|
import { AuthProvider } from 'src/auth/helper';
|
||||||
|
|
||||||
@@ -35,66 +32,22 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onModuleInit() {
|
// Following fields are not updatable by `infraConfigs` Mutation. Use dedicated mutations for these fields instead.
|
||||||
await this.initializeInfraConfigTable();
|
EXCLUDE_FROM_UPDATE_CONFIGS = [
|
||||||
}
|
InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||||
|
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
||||||
async getDefaultInfraConfigs(): Promise<
|
InfraConfigEnum.ANALYTICS_USER_ID,
|
||||||
{ name: InfraConfigEnum; value: string }[]
|
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
> {
|
];
|
||||||
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
|
// Following fields can not be fetched by `infraConfigs` Query. Use dedicated queries for these fields instead.
|
||||||
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
|
EXCLUDE_FROM_FETCH_CONFIGS = [
|
||||||
{
|
InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||||
name: InfraConfigEnum.MAILER_SMTP_URL,
|
InfraConfigEnum.ANALYTICS_USER_ID,
|
||||||
value: process.env.MAILER_SMTP_URL,
|
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
|
|
||||||
value: process.env.MAILER_ADDRESS_FROM,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
|
|
||||||
value: process.env.GOOGLE_CLIENT_ID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GOOGLE_CLIENT_SECRET,
|
|
||||||
value: process.env.GOOGLE_CLIENT_SECRET,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GITHUB_CLIENT_ID,
|
|
||||||
value: process.env.GITHUB_CLIENT_ID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.GITHUB_CLIENT_SECRET,
|
|
||||||
value: process.env.GITHUB_CLIENT_SECRET,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MICROSOFT_CLIENT_ID,
|
|
||||||
value: process.env.MICROSOFT_CLIENT_ID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
|
|
||||||
value: process.env.MICROSOFT_CLIENT_SECRET,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
|
||||||
value: getConfiguredSSOProviders(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
|
|
||||||
value: false.toString(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.ANALYTICS_USER_ID,
|
|
||||||
value: generateAnalyticsUserId(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
|
||||||
value: (await this.prisma.infraConfig.count()) === 0 ? 'true' : 'false',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return infraConfigDefaultObjs;
|
async onModuleInit() {
|
||||||
|
await this.initializeInfraConfigTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,16 +56,8 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
*/
|
*/
|
||||||
async initializeInfraConfigTable() {
|
async initializeInfraConfigTable() {
|
||||||
try {
|
try {
|
||||||
// Get all the 'names' of the properties to be saved in the 'infra_config' table
|
|
||||||
const enumValues = Object.values(InfraConfigEnum);
|
|
||||||
|
|
||||||
// Fetch the default values (value in .env) for configs to be saved in 'infra_config' table
|
// Fetch the default values (value in .env) for configs to be saved in 'infra_config' table
|
||||||
const infraConfigDefaultObjs = await this.getDefaultInfraConfigs();
|
const infraConfigDefaultObjs = await getDefaultInfraConfigs();
|
||||||
|
|
||||||
// Check if all the 'names' are listed in the default values
|
|
||||||
if (enumValues.length !== infraConfigDefaultObjs.length) {
|
|
||||||
throw new Error(INFRA_CONFIG_NOT_LISTED);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eliminate the rows (from 'infraConfigDefaultObjs') that are already present in the database table
|
// Eliminate the rows (from 'infraConfigDefaultObjs') that are already present in the database table
|
||||||
const dbInfraConfigs = await this.prisma.infraConfig.findMany();
|
const dbInfraConfigs = await this.prisma.infraConfig.findMany();
|
||||||
@@ -169,11 +114,7 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
* @param restartEnabled If true, restart the app after updating the InfraConfig
|
* @param restartEnabled If true, restart the app after updating the InfraConfig
|
||||||
* @returns InfraConfig model
|
* @returns InfraConfig model
|
||||||
*/
|
*/
|
||||||
async update(
|
async update(name: InfraConfigEnum, value: string, restartEnabled = false) {
|
||||||
name: InfraConfigEnumForClient | InfraConfigEnum,
|
|
||||||
value: string,
|
|
||||||
restartEnabled = false,
|
|
||||||
) {
|
|
||||||
const isValidate = this.validateEnvValues([{ name, value }]);
|
const isValidate = this.validateEnvValues([{ name, value }]);
|
||||||
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
||||||
|
|
||||||
@@ -197,6 +138,11 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
* @returns InfraConfig model
|
* @returns InfraConfig model
|
||||||
*/
|
*/
|
||||||
async updateMany(infraConfigs: InfraConfigArgs[]) {
|
async updateMany(infraConfigs: InfraConfigArgs[]) {
|
||||||
|
for (let i = 0; i < infraConfigs.length; i++) {
|
||||||
|
if (this.EXCLUDE_FROM_UPDATE_CONFIGS.includes(infraConfigs[i].name))
|
||||||
|
return E.left(INFRA_CONFIG_OPERATION_NOT_ALLOWED);
|
||||||
|
}
|
||||||
|
|
||||||
const isValidate = this.validateEnvValues(infraConfigs);
|
const isValidate = this.validateEnvValues(infraConfigs);
|
||||||
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
||||||
|
|
||||||
@@ -230,12 +176,26 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
) {
|
) {
|
||||||
switch (service) {
|
switch (service) {
|
||||||
case AuthProvider.GOOGLE:
|
case AuthProvider.GOOGLE:
|
||||||
return configMap.GOOGLE_CLIENT_ID && configMap.GOOGLE_CLIENT_SECRET;
|
return (
|
||||||
|
configMap.GOOGLE_CLIENT_ID &&
|
||||||
|
configMap.GOOGLE_CLIENT_SECRET &&
|
||||||
|
configMap.GOOGLE_CALLBACK_URL &&
|
||||||
|
configMap.GOOGLE_SCOPE
|
||||||
|
);
|
||||||
case AuthProvider.GITHUB:
|
case AuthProvider.GITHUB:
|
||||||
return configMap.GITHUB_CLIENT_ID && configMap.GITHUB_CLIENT_SECRET;
|
return (
|
||||||
|
configMap.GITHUB_CLIENT_ID &&
|
||||||
|
configMap.GITHUB_CLIENT_SECRET &&
|
||||||
|
configMap.GITHUB_CALLBACK_URL &&
|
||||||
|
configMap.GITHUB_SCOPE
|
||||||
|
);
|
||||||
case AuthProvider.MICROSOFT:
|
case AuthProvider.MICROSOFT:
|
||||||
return (
|
return (
|
||||||
configMap.MICROSOFT_CLIENT_ID && configMap.MICROSOFT_CLIENT_SECRET
|
configMap.MICROSOFT_CLIENT_ID &&
|
||||||
|
configMap.MICROSOFT_CLIENT_SECRET &&
|
||||||
|
configMap.MICROSOFT_CALLBACK_URL &&
|
||||||
|
configMap.MICROSOFT_SCOPE &&
|
||||||
|
configMap.MICROSOFT_TENANT
|
||||||
);
|
);
|
||||||
case AuthProvider.EMAIL:
|
case AuthProvider.EMAIL:
|
||||||
return configMap.MAILER_SMTP_URL && configMap.MAILER_ADDRESS_FROM;
|
return configMap.MAILER_SMTP_URL && configMap.MAILER_ADDRESS_FROM;
|
||||||
@@ -310,7 +270,7 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
* @param name Name of the InfraConfig
|
* @param name Name of the InfraConfig
|
||||||
* @returns InfraConfig model
|
* @returns InfraConfig model
|
||||||
*/
|
*/
|
||||||
async get(name: InfraConfigEnumForClient) {
|
async get(name: InfraConfigEnum) {
|
||||||
try {
|
try {
|
||||||
const infraConfig = await this.prisma.infraConfig.findUniqueOrThrow({
|
const infraConfig = await this.prisma.infraConfig.findUniqueOrThrow({
|
||||||
where: { name },
|
where: { name },
|
||||||
@@ -327,7 +287,15 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
* @param names Names of the InfraConfigs
|
* @param names Names of the InfraConfigs
|
||||||
* @returns InfraConfig model
|
* @returns InfraConfig model
|
||||||
*/
|
*/
|
||||||
async getMany(names: InfraConfigEnumForClient[]) {
|
async getMany(names: InfraConfigEnum[], checkDisallowedKeys: boolean = true) {
|
||||||
|
if (checkDisallowedKeys) {
|
||||||
|
// Check if the names are allowed to fetch by client
|
||||||
|
for (let i = 0; i < names.length; i++) {
|
||||||
|
if (this.EXCLUDE_FROM_FETCH_CONFIGS.includes(names[i]))
|
||||||
|
return E.left(INFRA_CONFIG_OPERATION_NOT_ALLOWED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const infraConfigs = await this.prisma.infraConfig.findMany({
|
const infraConfigs = await this.prisma.infraConfig.findMany({
|
||||||
where: { name: { in: names } },
|
where: { name: { in: names } },
|
||||||
@@ -354,7 +322,7 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
*/
|
*/
|
||||||
async reset() {
|
async reset() {
|
||||||
try {
|
try {
|
||||||
const infraConfigDefaultObjs = await this.getDefaultInfraConfigs();
|
const infraConfigDefaultObjs = await getDefaultInfraConfigs();
|
||||||
|
|
||||||
await this.prisma.infraConfig.deleteMany({
|
await this.prisma.infraConfig.deleteMany({
|
||||||
where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } },
|
where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } },
|
||||||
@@ -387,36 +355,60 @@ export class InfraConfigService implements OnModuleInit {
|
|||||||
*/
|
*/
|
||||||
validateEnvValues(
|
validateEnvValues(
|
||||||
infraConfigs: {
|
infraConfigs: {
|
||||||
name: InfraConfigEnumForClient | InfraConfigEnum;
|
name: InfraConfigEnum;
|
||||||
value: string;
|
value: string;
|
||||||
}[],
|
}[],
|
||||||
) {
|
) {
|
||||||
for (let i = 0; i < infraConfigs.length; i++) {
|
for (let i = 0; i < infraConfigs.length; i++) {
|
||||||
switch (infraConfigs[i].name) {
|
switch (infraConfigs[i].name) {
|
||||||
case InfraConfigEnumForClient.MAILER_SMTP_URL:
|
case InfraConfigEnum.MAILER_SMTP_URL:
|
||||||
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
|
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
|
||||||
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.MAILER_ADDRESS_FROM:
|
case InfraConfigEnum.MAILER_ADDRESS_FROM:
|
||||||
const isValidEmail = validateSMTPEmail(infraConfigs[i].value);
|
const isValidEmail = validateSMTPEmail(infraConfigs[i].value);
|
||||||
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.GOOGLE_CLIENT_ID:
|
case InfraConfigEnum.GOOGLE_CLIENT_ID:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.GOOGLE_CLIENT_SECRET:
|
case InfraConfigEnum.GOOGLE_CLIENT_SECRET:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.GITHUB_CLIENT_ID:
|
case InfraConfigEnum.GOOGLE_CALLBACK_URL:
|
||||||
|
if (!validateUrl(infraConfigs[i].value))
|
||||||
|
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.GOOGLE_SCOPE:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.GITHUB_CLIENT_SECRET:
|
case InfraConfigEnum.GITHUB_CLIENT_ID:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.MICROSOFT_CLIENT_ID:
|
case InfraConfigEnum.GITHUB_CLIENT_SECRET:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
case InfraConfigEnumForClient.MICROSOFT_CLIENT_SECRET:
|
case InfraConfigEnum.GITHUB_CALLBACK_URL:
|
||||||
|
if (!validateUrl(infraConfigs[i].value))
|
||||||
|
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.GITHUB_SCOPE:
|
||||||
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MICROSOFT_CLIENT_ID:
|
||||||
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MICROSOFT_CLIENT_SECRET:
|
||||||
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MICROSOFT_CALLBACK_URL:
|
||||||
|
if (!validateUrl(infraConfigs[i].value))
|
||||||
|
return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MICROSOFT_SCOPE:
|
||||||
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
|
break;
|
||||||
|
case InfraConfigEnum.MICROSOFT_TENANT:
|
||||||
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql';
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
import { InfraConfigEnum } from 'src/types/InfraConfig';
|
||||||
import { ServiceStatus } from './helper';
|
import { ServiceStatus } from './helper';
|
||||||
import { AuthProvider } from 'src/auth/helper';
|
import { AuthProvider } from 'src/auth/helper';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class InfraConfigArgs {
|
export class InfraConfigArgs {
|
||||||
@Field(() => InfraConfigEnumForClient, {
|
@Field(() => InfraConfigEnum, {
|
||||||
description: 'Infra Config Name',
|
description: 'Infra Config Name',
|
||||||
})
|
})
|
||||||
name: InfraConfigEnumForClient;
|
name: InfraConfigEnum;
|
||||||
|
|
||||||
@Field({
|
@Field({
|
||||||
description: 'Infra Config Value',
|
description: 'Infra Config Value',
|
||||||
|
|||||||
14
packages/hoppscotch-backend/src/team-collection/helper.ts
Normal file
14
packages/hoppscotch-backend/src/team-collection/helper.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// Type of data returned from the query to obtain all search results
|
||||||
|
export type SearchQueryReturnType = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
type: 'collection' | 'request';
|
||||||
|
method?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Type of data returned from the query to obtain all parents
|
||||||
|
export type ParentTreeQueryReturnType = {
|
||||||
|
id: string;
|
||||||
|
parentID: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
|
||||||
|
import { TeamCollectionService } from './team-collection.service';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
|
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
||||||
|
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
||||||
|
import { RequiresTeamRole } from 'src/team/decorators/requires-team-role.decorator';
|
||||||
|
import { TeamMemberRole } from '@prisma/client';
|
||||||
|
import { RESTTeamMemberGuard } from 'src/team/guards/rest-team-member.guard';
|
||||||
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
|
||||||
|
@UseGuards(ThrottlerBehindProxyGuard)
|
||||||
|
@Controller({ path: 'team-collection', version: '1' })
|
||||||
|
export class TeamCollectionController {
|
||||||
|
constructor(private readonly teamCollectionService: TeamCollectionService) {}
|
||||||
|
|
||||||
|
@Get('search/:teamID/:searchQuery')
|
||||||
|
@RequiresTeamRole(
|
||||||
|
TeamMemberRole.VIEWER,
|
||||||
|
TeamMemberRole.EDITOR,
|
||||||
|
TeamMemberRole.OWNER,
|
||||||
|
)
|
||||||
|
@UseGuards(JwtAuthGuard, RESTTeamMemberGuard)
|
||||||
|
async searchByTitle(
|
||||||
|
@Param('searchQuery') searchQuery: string,
|
||||||
|
@Param('teamID') teamID: string,
|
||||||
|
@Query('take') take: string,
|
||||||
|
@Query('skip') skip: string,
|
||||||
|
) {
|
||||||
|
const res = await this.teamCollectionService.searchByTitle(
|
||||||
|
searchQuery,
|
||||||
|
teamID,
|
||||||
|
parseInt(take),
|
||||||
|
parseInt(skip),
|
||||||
|
);
|
||||||
|
if (E.isLeft(res)) throwHTTPErr(res.left);
|
||||||
|
return res.right;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { GqlCollectionTeamMemberGuard } from './guards/gql-collection-team-membe
|
|||||||
import { TeamModule } from '../team/team.module';
|
import { TeamModule } from '../team/team.module';
|
||||||
import { UserModule } from '../user/user.module';
|
import { UserModule } from '../user/user.module';
|
||||||
import { PubSubModule } from '../pubsub/pubsub.module';
|
import { PubSubModule } from '../pubsub/pubsub.module';
|
||||||
|
import { TeamCollectionController } from './team-collection.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, TeamModule, UserModule, PubSubModule],
|
imports: [PrismaModule, TeamModule, UserModule, PubSubModule],
|
||||||
@@ -15,5 +16,6 @@ import { PubSubModule } from '../pubsub/pubsub.module';
|
|||||||
GqlCollectionTeamMemberGuard,
|
GqlCollectionTeamMemberGuard,
|
||||||
],
|
],
|
||||||
exports: [TeamCollectionService, GqlCollectionTeamMemberGuard],
|
exports: [TeamCollectionService, GqlCollectionTeamMemberGuard],
|
||||||
|
controllers: [TeamCollectionController],
|
||||||
})
|
})
|
||||||
export class TeamCollectionModule {}
|
export class TeamCollectionModule {}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { HttpStatus, Injectable } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { TeamCollection } from './team-collection.model';
|
import { TeamCollection } from './team-collection.model';
|
||||||
import {
|
import {
|
||||||
@@ -14,6 +14,10 @@ import {
|
|||||||
TEAM_COL_SAME_NEXT_COLL,
|
TEAM_COL_SAME_NEXT_COLL,
|
||||||
TEAM_COL_REORDERING_FAILED,
|
TEAM_COL_REORDERING_FAILED,
|
||||||
TEAM_COLL_DATA_INVALID,
|
TEAM_COLL_DATA_INVALID,
|
||||||
|
TEAM_REQ_SEARCH_FAILED,
|
||||||
|
TEAM_COL_SEARCH_FAILED,
|
||||||
|
TEAM_REQ_PARENT_TREE_GEN_FAILED,
|
||||||
|
TEAM_COLL_PARENT_TREE_GEN_FAILED,
|
||||||
} from '../errors';
|
} from '../errors';
|
||||||
import { PubSubService } from '../pubsub/pubsub.service';
|
import { PubSubService } from '../pubsub/pubsub.service';
|
||||||
import { isValidLength } from 'src/utils';
|
import { isValidLength } from 'src/utils';
|
||||||
@@ -22,6 +26,9 @@ import * as O from 'fp-ts/Option';
|
|||||||
import { Prisma, TeamCollection as DBTeamCollection } from '@prisma/client';
|
import { Prisma, TeamCollection as DBTeamCollection } from '@prisma/client';
|
||||||
import { CollectionFolder } from 'src/types/CollectionFolder';
|
import { CollectionFolder } from 'src/types/CollectionFolder';
|
||||||
import { stringToJson } from 'src/utils';
|
import { stringToJson } from 'src/utils';
|
||||||
|
import { CollectionSearchNode } from 'src/types/CollectionSearchNode';
|
||||||
|
import { ParentTreeQueryReturnType, SearchQueryReturnType } from './helper';
|
||||||
|
import { RESTError } from 'src/types/RESTError';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamCollectionService {
|
export class TeamCollectionService {
|
||||||
@@ -1056,4 +1063,266 @@ export class TeamCollectionService {
|
|||||||
return E.left(TEAM_COLL_NOT_FOUND);
|
return E.left(TEAM_COLL_NOT_FOUND);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for TeamCollections and TeamRequests by title
|
||||||
|
*
|
||||||
|
* @param searchQuery The search query
|
||||||
|
* @param teamID The Team ID
|
||||||
|
* @param take Number of items we want returned
|
||||||
|
* @param skip Number of items we want to skip
|
||||||
|
* @returns An Either of the search results
|
||||||
|
*/
|
||||||
|
async searchByTitle(
|
||||||
|
searchQuery: string,
|
||||||
|
teamID: string,
|
||||||
|
take = 10,
|
||||||
|
skip = 0,
|
||||||
|
) {
|
||||||
|
// Fetch all collections and requests that match the search query
|
||||||
|
const searchResults: SearchQueryReturnType[] = [];
|
||||||
|
|
||||||
|
const matchedCollections = await this.searchCollections(
|
||||||
|
searchQuery,
|
||||||
|
teamID,
|
||||||
|
take,
|
||||||
|
skip,
|
||||||
|
);
|
||||||
|
if (E.isLeft(matchedCollections))
|
||||||
|
return E.left(<RESTError>{
|
||||||
|
message: matchedCollections.left,
|
||||||
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
|
});
|
||||||
|
searchResults.push(...matchedCollections.right);
|
||||||
|
|
||||||
|
const matchedRequests = await this.searchRequests(
|
||||||
|
searchQuery,
|
||||||
|
teamID,
|
||||||
|
take,
|
||||||
|
skip,
|
||||||
|
);
|
||||||
|
if (E.isLeft(matchedRequests))
|
||||||
|
return E.left(<RESTError>{
|
||||||
|
message: matchedRequests.left,
|
||||||
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
|
});
|
||||||
|
searchResults.push(...matchedRequests.right);
|
||||||
|
|
||||||
|
// Generate the parent tree for searchResults
|
||||||
|
const searchResultsWithTree: CollectionSearchNode[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < searchResults.length; i++) {
|
||||||
|
const fetchedParentTree = await this.fetchParentTree(searchResults[i]);
|
||||||
|
if (E.isLeft(fetchedParentTree))
|
||||||
|
return E.left(<RESTError>{
|
||||||
|
message: fetchedParentTree.left,
|
||||||
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
|
});
|
||||||
|
searchResultsWithTree.push({
|
||||||
|
type: searchResults[i].type,
|
||||||
|
title: searchResults[i].title,
|
||||||
|
method: searchResults[i].method,
|
||||||
|
id: searchResults[i].id,
|
||||||
|
path: !fetchedParentTree
|
||||||
|
? []
|
||||||
|
: ([fetchedParentTree.right] as CollectionSearchNode[]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return E.right({ data: searchResultsWithTree });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for TeamCollections by title
|
||||||
|
*
|
||||||
|
* @param searchQuery The search query
|
||||||
|
* @param teamID The Team ID
|
||||||
|
* @param take Number of items we want returned
|
||||||
|
* @param skip Number of items we want to skip
|
||||||
|
* @returns An Either of the search results
|
||||||
|
*/
|
||||||
|
private async searchCollections(
|
||||||
|
searchQuery: string,
|
||||||
|
teamID: string,
|
||||||
|
take: number,
|
||||||
|
skip: number,
|
||||||
|
) {
|
||||||
|
const query = Prisma.sql`
|
||||||
|
select id,title,'collection' AS type
|
||||||
|
from "TeamCollection"
|
||||||
|
where "TeamCollection"."teamID"=${teamID}
|
||||||
|
and titlesearch @@ to_tsquery(${searchQuery})
|
||||||
|
order by ts_rank(titlesearch,to_tsquery(${searchQuery}))
|
||||||
|
limit ${take}
|
||||||
|
OFFSET ${skip === 0 ? 0 : (skip - 1) * take};
|
||||||
|
`;
|
||||||
|
try {
|
||||||
|
const res = await this.prisma.$queryRaw<SearchQueryReturnType[]>(query);
|
||||||
|
return E.right(res);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(TEAM_COL_SEARCH_FAILED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for TeamRequests by title
|
||||||
|
*
|
||||||
|
* @param searchQuery The search query
|
||||||
|
* @param teamID The Team ID
|
||||||
|
* @param take Number of items we want returned
|
||||||
|
* @param skip Number of items we want to skip
|
||||||
|
* @returns An Either of the search results
|
||||||
|
*/
|
||||||
|
private async searchRequests(
|
||||||
|
searchQuery: string,
|
||||||
|
teamID: string,
|
||||||
|
take: number,
|
||||||
|
skip: number,
|
||||||
|
) {
|
||||||
|
const query = Prisma.sql`
|
||||||
|
select id,title,request->>'method' as method,'request' AS type
|
||||||
|
from "TeamRequest"
|
||||||
|
where "TeamRequest"."teamID"=${teamID}
|
||||||
|
and titlesearch @@ to_tsquery(${searchQuery})
|
||||||
|
order by ts_rank(titlesearch,to_tsquery(${searchQuery}))
|
||||||
|
limit ${take}
|
||||||
|
OFFSET ${skip === 0 ? 0 : (skip - 1) * take};
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await this.prisma.$queryRaw<SearchQueryReturnType[]>(query);
|
||||||
|
return E.right(res);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(TEAM_REQ_SEARCH_FAILED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the parent tree of a search result
|
||||||
|
*
|
||||||
|
* @param searchResult The search result for which we want to generate the parent tree
|
||||||
|
* @returns The parent tree of the search result
|
||||||
|
*/
|
||||||
|
private async fetchParentTree(searchResult: SearchQueryReturnType) {
|
||||||
|
return searchResult.type === 'collection'
|
||||||
|
? await this.fetchCollectionParentTree(searchResult.id)
|
||||||
|
: await this.fetchRequestParentTree(searchResult.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the parent tree of a collection
|
||||||
|
*
|
||||||
|
* @param id The ID of the collection
|
||||||
|
* @returns The parent tree of the collection
|
||||||
|
*/
|
||||||
|
private async fetchCollectionParentTree(id: string) {
|
||||||
|
try {
|
||||||
|
const query = Prisma.sql`
|
||||||
|
WITH RECURSIVE collection_tree AS (
|
||||||
|
SELECT tc.id, tc."parentID", tc.title
|
||||||
|
FROM "TeamCollection" AS tc
|
||||||
|
JOIN "TeamCollection" AS tr ON tc.id = tr."parentID"
|
||||||
|
WHERE tr.id = ${id}
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT parent.id, parent."parentID", parent.title
|
||||||
|
FROM "TeamCollection" AS parent
|
||||||
|
JOIN collection_tree AS ct ON parent.id = ct."parentID"
|
||||||
|
)
|
||||||
|
SELECT * FROM collection_tree;
|
||||||
|
`;
|
||||||
|
const res = await this.prisma.$queryRaw<ParentTreeQueryReturnType[]>(
|
||||||
|
query,
|
||||||
|
);
|
||||||
|
|
||||||
|
const collectionParentTree = this.generateParentTree(res);
|
||||||
|
return E.right(collectionParentTree);
|
||||||
|
} catch (error) {
|
||||||
|
E.left(TEAM_COLL_PARENT_TREE_GEN_FAILED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the parent tree from the collections
|
||||||
|
*
|
||||||
|
* @param parentCollections The parent collections
|
||||||
|
* @returns The parent tree of the parent collections
|
||||||
|
*/
|
||||||
|
private generateParentTree(parentCollections: ParentTreeQueryReturnType[]) {
|
||||||
|
function findChildren(id) {
|
||||||
|
const collection = parentCollections.filter((item) => item.id === id)[0];
|
||||||
|
if (collection.parentID == null) {
|
||||||
|
return {
|
||||||
|
id: collection.id,
|
||||||
|
title: collection.title,
|
||||||
|
type: 'collection',
|
||||||
|
path: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = {
|
||||||
|
id: collection.id,
|
||||||
|
title: collection.title,
|
||||||
|
type: 'collection',
|
||||||
|
path: findChildren(collection.parentID),
|
||||||
|
};
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parentCollections.length > 0) {
|
||||||
|
if (parentCollections[0].parentID == null) {
|
||||||
|
return {
|
||||||
|
id: parentCollections[0].id,
|
||||||
|
title: parentCollections[0].title,
|
||||||
|
type: 'collection',
|
||||||
|
path: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: parentCollections[0].id,
|
||||||
|
title: parentCollections[0].title,
|
||||||
|
type: 'collection',
|
||||||
|
path: findChildren(parentCollections[0].parentID),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the parent tree of a request
|
||||||
|
*
|
||||||
|
* @param id The ID of the request
|
||||||
|
* @returns The parent tree of the request
|
||||||
|
*/
|
||||||
|
private async fetchRequestParentTree(id: string) {
|
||||||
|
try {
|
||||||
|
const query = Prisma.sql`
|
||||||
|
WITH RECURSIVE request_collection_tree AS (
|
||||||
|
SELECT tc.id, tc."parentID", tc.title
|
||||||
|
FROM "TeamCollection" AS tc
|
||||||
|
JOIN "TeamRequest" AS tr ON tc.id = tr."collectionID"
|
||||||
|
WHERE tr.id = ${id}
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT parent.id, parent."parentID", parent.title
|
||||||
|
FROM "TeamCollection" AS parent
|
||||||
|
JOIN request_collection_tree AS ct ON parent.id = ct."parentID"
|
||||||
|
)
|
||||||
|
SELECT * FROM request_collection_tree;
|
||||||
|
|
||||||
|
`;
|
||||||
|
const res = await this.prisma.$queryRaw<ParentTreeQueryReturnType[]>(
|
||||||
|
query,
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestParentTree = this.generateParentTree(res);
|
||||||
|
return E.right(requestParentTree);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(TEAM_REQ_PARENT_TREE_GEN_FAILED);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { TeamService } from '../../team/team.service';
|
||||||
|
import { TeamMemberRole } from '../../team/team.model';
|
||||||
|
import {
|
||||||
|
BUG_TEAM_NO_REQUIRE_TEAM_ROLE,
|
||||||
|
BUG_AUTH_NO_USER_CTX,
|
||||||
|
BUG_TEAM_NO_TEAM_ID,
|
||||||
|
TEAM_MEMBER_NOT_FOUND,
|
||||||
|
TEAM_NOT_REQUIRED_ROLE,
|
||||||
|
} from 'src/errors';
|
||||||
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RESTTeamMemberGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private readonly reflector: Reflector,
|
||||||
|
private readonly teamService: TeamService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
const requireRoles = this.reflector.get<TeamMemberRole[]>(
|
||||||
|
'requiresTeamRole',
|
||||||
|
context.getHandler(),
|
||||||
|
);
|
||||||
|
if (!requireRoles)
|
||||||
|
throwHTTPErr({ message: BUG_TEAM_NO_REQUIRE_TEAM_ROLE, statusCode: 400 });
|
||||||
|
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
const { user } = request;
|
||||||
|
if (user == undefined)
|
||||||
|
throwHTTPErr({ message: BUG_AUTH_NO_USER_CTX, statusCode: 400 });
|
||||||
|
|
||||||
|
const teamID = request.params.teamID;
|
||||||
|
if (!teamID)
|
||||||
|
throwHTTPErr({ message: BUG_TEAM_NO_TEAM_ID, statusCode: 400 });
|
||||||
|
|
||||||
|
const teamMember = await this.teamService.getTeamMember(teamID, user.uid);
|
||||||
|
if (!teamMember)
|
||||||
|
throwHTTPErr({ message: TEAM_MEMBER_NOT_FOUND, statusCode: 404 });
|
||||||
|
|
||||||
|
if (requireRoles.includes(teamMember.role)) return true;
|
||||||
|
|
||||||
|
throwHTTPErr({ message: TEAM_NOT_REQUIRED_ROLE, statusCode: 403 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Response type of results from the search query
|
||||||
|
export type CollectionSearchNode = {
|
||||||
|
/** Encodes the hierarchy of where the node is **/
|
||||||
|
path: CollectionSearchNode[];
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
type: 'request';
|
||||||
|
title: string;
|
||||||
|
method: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'collection';
|
||||||
|
title: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -4,12 +4,19 @@ export enum InfraConfigEnum {
|
|||||||
|
|
||||||
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
|
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
|
||||||
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
||||||
|
GOOGLE_CALLBACK_URL = 'GOOGLE_CALLBACK_URL',
|
||||||
|
GOOGLE_SCOPE = 'GOOGLE_SCOPE',
|
||||||
|
|
||||||
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
|
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
|
||||||
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
|
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
|
||||||
|
GITHUB_CALLBACK_URL = 'GITHUB_CALLBACK_URL',
|
||||||
|
GITHUB_SCOPE = 'GITHUB_SCOPE',
|
||||||
|
|
||||||
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
||||||
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
||||||
|
MICROSOFT_CALLBACK_URL = 'MICROSOFT_CALLBACK_URL',
|
||||||
|
MICROSOFT_SCOPE = 'MICROSOFT_SCOPE',
|
||||||
|
MICROSOFT_TENANT = 'MICROSOFT_TENANT',
|
||||||
|
|
||||||
VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS',
|
VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS',
|
||||||
|
|
||||||
@@ -17,20 +24,3 @@ export enum InfraConfigEnum {
|
|||||||
ANALYTICS_USER_ID = 'ANALYTICS_USER_ID',
|
ANALYTICS_USER_ID = 'ANALYTICS_USER_ID',
|
||||||
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum InfraConfigEnumForClient {
|
|
||||||
MAILER_SMTP_URL = 'MAILER_SMTP_URL',
|
|
||||||
MAILER_ADDRESS_FROM = 'MAILER_ADDRESS_FROM',
|
|
||||||
|
|
||||||
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
|
|
||||||
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
|
|
||||||
|
|
||||||
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
|
|
||||||
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
|
|
||||||
|
|
||||||
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
|
||||||
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
|
||||||
|
|
||||||
ALLOW_ANALYTICS_COLLECTION = 'ALLOW_ANALYTICS_COLLECTION',
|
|
||||||
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { HttpStatus } from '@nestjs/common';
|
import { HttpStatus } from '@nestjs/common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
** Custom interface to handle errors specific to Auth module
|
** Custom interface to handle errors for REST modules such as Auth, Admin modules
|
||||||
** Since its REST we need to return the HTTP status code along with the error message
|
** Since its REST we need to return the HTTP status code along with the error message
|
||||||
*/
|
*/
|
||||||
export type AuthError = {
|
export type RESTError = {
|
||||||
message: string;
|
message: string;
|
||||||
statusCode: HttpStatus;
|
statusCode: HttpStatus;
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ExecutionContext } from '@nestjs/common';
|
import { ExecutionContext, HttpException } from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
import { pipe } from 'fp-ts/lib/function';
|
import { pipe } from 'fp-ts/lib/function';
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
JSON_INVALID,
|
JSON_INVALID,
|
||||||
} from './errors';
|
} from './errors';
|
||||||
import { AuthProvider } from './auth/helper';
|
import { AuthProvider } from './auth/helper';
|
||||||
|
import { RESTError } from './types/RESTError';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A workaround to throw an exception in an expression.
|
* A workaround to throw an exception in an expression.
|
||||||
@@ -27,6 +28,15 @@ export function throwErr(errMessage: string): never {
|
|||||||
throw new Error(errMessage);
|
throw new Error(errMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function allows throw to be used as an expression
|
||||||
|
* @param errMessage Message present in the error message
|
||||||
|
*/
|
||||||
|
export function throwHTTPErr(errorData: RESTError): never {
|
||||||
|
const { message, statusCode } = errorData;
|
||||||
|
throw new HttpException(message, statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prints the given value to log and returns the same value.
|
* Prints the given value to log and returns the same value.
|
||||||
* Used for debugging functional pipelines.
|
* Used for debugging functional pipelines.
|
||||||
@@ -173,6 +183,16 @@ export const validateSMTPUrl = (url: string) => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks to see if the URL is valid or not
|
||||||
|
* @param url The URL to validate
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
export const validateUrl = (url: string) => {
|
||||||
|
const urlRegex = /^(http|https):\/\/[^ "]+$/;
|
||||||
|
return urlRegex.test(url);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* String to JSON parser
|
* String to JSON parser
|
||||||
* @param {str} str The string to parse
|
* @param {str} str The string to parse
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestD",
|
"name": "RequestD",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -40,7 +40,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -52,7 +53,7 @@
|
|||||||
],
|
],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestC",
|
"name": "RequestC",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -67,7 +68,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": {
|
"auth": {
|
||||||
@@ -88,7 +90,7 @@
|
|||||||
],
|
],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestB",
|
"name": "RequestB",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -104,6 +106,7 @@
|
|||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
},
|
},
|
||||||
|
"requestVariables": [],
|
||||||
"id": "clpttpdq00003qp16kut6doqv"
|
"id": "clpttpdq00003qp16kut6doqv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -116,7 +119,7 @@
|
|||||||
],
|
],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestA",
|
"name": "RequestA",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -132,6 +135,7 @@
|
|||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
},
|
},
|
||||||
|
"requestVariables": [],
|
||||||
"id": "clpttpdq00003qp16kut6doqv"
|
"id": "clpttpdq00003qp16kut6doqv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -158,7 +162,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestB",
|
"name": "RequestB",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -174,6 +178,7 @@
|
|||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
},
|
},
|
||||||
|
"requestVariables": [],
|
||||||
"id": "clpttpdq00003qp16kut6doqv"
|
"id": "clpttpdq00003qp16kut6doqv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -186,7 +191,7 @@
|
|||||||
],
|
],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"name": "RequestA",
|
"name": "RequestA",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -202,6 +207,7 @@
|
|||||||
"contentType": null,
|
"contentType": null,
|
||||||
"body": null
|
"body": null
|
||||||
},
|
},
|
||||||
|
"requestVariables": [],
|
||||||
"id": "clpttpdq00003qp16kut6doqv"
|
"id": "clpttpdq00003qp16kut6doqv"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "<<URL>>",
|
"endpoint": "<<URL>>",
|
||||||
"name": "test1",
|
"name": "test1",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -16,7 +16,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
|
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||||
"name": "",
|
"name": "",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -23,10 +23,11 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.dio/<<HEADERS_TYPE2>>",
|
"endpoint": "https://echo.hoppscotch.dio/<<HEADERS_TYPE2>>",
|
||||||
"name": "success",
|
"name": "success",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -44,7 +45,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests":
|
"requests":
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||||
"name": "fail",
|
"name": "fail",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -22,10 +22,11 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
||||||
"name": "success",
|
"name": "success",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -43,7 +44,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests":
|
"requests":
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||||
"name": "fail",
|
"name": "fail",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -22,7 +22,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
|
||||||
"name": "",
|
"name": "",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -23,10 +23,11 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
|
||||||
"name": "success",
|
"name": "success",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -44,7 +45,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"auth": { "authType": "none", "authActive": true },
|
"auth": { "authType": "none", "authActive": true },
|
||||||
"body": { "body": null, "contentType": null },
|
"body": { "body": null, "contentType": null },
|
||||||
"name": "sample-req",
|
"name": "sample-req",
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"testScript": "pw.expect(pw.env.get(\"variable\")).toBe(\"value\")",
|
"testScript": "pw.expect(pw.env.get(\"variable\")).toBe(\"value\")",
|
||||||
"preRequestScript": "pw.env.set(\"variable\", \"value\");"
|
"preRequestScript": "pw.env.set(\"variable\", \"value\");",
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": { "authType": "inherit", "authActive": true },
|
"auth": { "authType": "inherit", "authActive": true },
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"name": "test-request",
|
"name": "test-request",
|
||||||
"endpoint": "https://echo.hoppscotch.io",
|
"endpoint": "https://echo.hoppscotch.io",
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
@@ -19,7 +19,8 @@
|
|||||||
"body": "{\n \"firstName\": \"<<firstName>>\",\n \"lastName\": \"<<lastName>>\",\n \"greetText\": \"<<salutation>>, <<fullName>>\",\n \"fullName\": \"<<fullName>>\",\n \"id\": \"<<id>>\"\n}"
|
"body": "{\n \"firstName\": \"<<firstName>>\",\n \"lastName\": \"<<lastName>>\",\n \"greetText\": \"<<salutation>>, <<fullName>>\",\n \"fullName\": \"<<fullName>>\",\n \"id\": \"<<id>>\"\n}"
|
||||||
},
|
},
|
||||||
"preRequestScript": "",
|
"preRequestScript": "",
|
||||||
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully resolves environments recursively\", ()=> {\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n});\n\npw.test(\"Successfully resolves environments referenced in the request body\", () => {\n const expectedId = \"7\"\n const expectedFirstName = \"John\"\n const expectedLastName = \"Doe\"\n const expectedFullName = `${expectedFirstName} ${expectedLastName}`\n const expectedGreetText = `Hello, ${expectedFullName}`\n\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n\n const { id, firstName, lastName, fullName, greetText } = JSON.parse(pw.response.body.data)\n\n pw.expect(id).toBe(expectedId)\n pw.expect(expectedFirstName).toBe(firstName)\n pw.expect(expectedLastName).toBe(lastName)\n pw.expect(fullName).toBe(expectedFullName)\n pw.expect(greetText).toBe(expectedGreetText)\n});"
|
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully resolves environments recursively\", ()=> {\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n});\n\npw.test(\"Successfully resolves environments referenced in the request body\", () => {\n const expectedId = \"7\"\n const expectedFirstName = \"John\"\n const expectedLastName = \"Doe\"\n const expectedFullName = `${expectedFirstName} ${expectedLastName}`\n const expectedGreetText = `Hello, ${expectedFullName}`\n\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n\n const { id, firstName, lastName, fullName, greetText } = JSON.parse(pw.response.body.data)\n\n pw.expect(id).toBe(expectedId)\n pw.expect(expectedFirstName).toBe(firstName)\n pw.expect(expectedLastName).toBe(lastName)\n pw.expect(fullName).toBe(expectedFullName)\n pw.expect(greetText).toBe(expectedGreetText)\n});",
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": {
|
"auth": {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"auth": { "authType": "none", "authActive": true },
|
"auth": { "authType": "none", "authActive": true },
|
||||||
"body": { "body": null, "contentType": null },
|
"body": { "body": null, "contentType": null },
|
||||||
"name": "test-secret-headers",
|
"name": "test-secret-headers",
|
||||||
@@ -17,12 +17,13 @@
|
|||||||
"active": true
|
"active": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestVariables": [],
|
||||||
"endpoint": "<<baseURL>>/headers",
|
"endpoint": "<<baseURL>>/headers",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.get(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.get(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.get(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.get(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})",
|
||||||
"preRequestScript": "const secretHeaderValueFromPreReqScript = pw.env.get(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
"preRequestScript": "const secretHeaderValueFromPreReqScript = pw.env.get(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"auth": { "authType": "none", "authActive": true },
|
"auth": { "authType": "none", "authActive": true },
|
||||||
"body": {
|
"body": {
|
||||||
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
|
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
|
||||||
@@ -32,12 +33,13 @@
|
|||||||
"method": "POST",
|
"method": "POST",
|
||||||
"params": [],
|
"params": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
|
"requestVariables": [],
|
||||||
"endpoint": "<<baseURL>>/post",
|
"endpoint": "<<baseURL>>/post",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
|
||||||
"preRequestScript": "const secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
|
"preRequestScript": "const secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"auth": { "authType": "none", "authActive": true },
|
"auth": { "authType": "none", "authActive": true },
|
||||||
"body": { "body": null, "contentType": null },
|
"body": { "body": null, "contentType": null },
|
||||||
"name": "test-secret-query-params",
|
"name": "test-secret-query-params",
|
||||||
@@ -50,12 +52,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
|
"requestVariables": [],
|
||||||
"endpoint": "<<baseURL>>/get",
|
"endpoint": "<<baseURL>>/get",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
|
||||||
"preRequestScript": "const secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
|
"preRequestScript": "const secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "basic",
|
"authType": "basic",
|
||||||
"password": "<<secretBasicAuthPassword>>",
|
"password": "<<secretBasicAuthPassword>>",
|
||||||
@@ -67,12 +70,13 @@
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
|
"requestVariables": [],
|
||||||
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
|
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
|
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
|
||||||
"preRequestScript": ""
|
"preRequestScript": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"auth": {
|
"auth": {
|
||||||
"token": "<<secretBearerToken>>",
|
"token": "<<secretBearerToken>>",
|
||||||
"authType": "bearer",
|
"authType": "bearer",
|
||||||
@@ -85,18 +89,20 @@
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
|
"requestVariables": [],
|
||||||
"endpoint": "<<baseURL>>/bearer",
|
"endpoint": "<<baseURL>>/bearer",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.get(\"secretBearerToken\")\n const preReqSecretBearerToken = pw.env.get(\"preReqSecretBearerToken\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.get(\"secretBearerToken\")\n const preReqSecretBearerToken = pw.env.get(\"preReqSecretBearerToken\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
|
||||||
"preRequestScript": "const secretBearerToken = pw.env.get(\"secretBearerToken\")\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)"
|
"preRequestScript": "const secretBearerToken = pw.env.get(\"secretBearerToken\")\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"auth": { "authType": "none", "authActive": true },
|
"auth": { "authType": "none", "authActive": true },
|
||||||
"body": { "body": null, "contentType": null },
|
"body": { "body": null, "contentType": null },
|
||||||
"name": "test-secret-fallback",
|
"name": "test-secret-fallback",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
|
"requestVariables": [],
|
||||||
"endpoint": "<<baseURL>>",
|
"endpoint": "<<baseURL>>",
|
||||||
"testScript": "pw.test(\"Returns an empty string if the value for a secret environment variable is not found in the system environment\", () => {\n pw.expect(pw.env.get(\"nonExistentValueInSystemEnv\")).toBe(\"\")\n})",
|
"testScript": "pw.test(\"Returns an empty string if the value for a secret environment variable is not found in the system environment\", () => {\n pw.expect(pw.env.get(\"nonExistentValueInSystemEnv\")).toBe(\"\")\n})",
|
||||||
"preRequestScript": ""
|
"preRequestScript": ""
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "none",
|
"authType": "none",
|
||||||
"authActive": true
|
"authActive": true
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"name": "test-secret-headers",
|
"name": "test-secret-headers",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
|
"requestVariables": [],
|
||||||
"headers": [
|
"headers": [
|
||||||
{
|
{
|
||||||
"key": "Secret-Header-Key",
|
"key": "Secret-Header-Key",
|
||||||
@@ -28,7 +29,7 @@
|
|||||||
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "none",
|
"authType": "none",
|
||||||
"authActive": true
|
"authActive": true
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
"name": "test-secret-headers-overrides",
|
"name": "test-secret-headers-overrides",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
|
"requestVariables": [],
|
||||||
"headers": [
|
"headers": [
|
||||||
{
|
{
|
||||||
"key": "Secret-Header-Key",
|
"key": "Secret-Header-Key",
|
||||||
@@ -52,7 +54,7 @@
|
|||||||
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value-overriden\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value-overriden\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "none",
|
"authType": "none",
|
||||||
"authActive": true
|
"authActive": true
|
||||||
@@ -64,13 +66,14 @@
|
|||||||
"name": "test-secret-body",
|
"name": "test-secret-body",
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"params": [],
|
"params": [],
|
||||||
|
"requestVariables": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "<<baseURL>>/post",
|
"endpoint": "<<baseURL>>/post",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
|
||||||
"preRequestScript": "const secretBodyValue = pw.env.get(\"secretBodyValue\")\n\nif (!secretBodyValue) { \n pw.env.set(\"secretBodyValue\", \"secret-body-value\")\n}\n\nconst secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
|
"preRequestScript": "const secretBodyValue = pw.env.get(\"secretBodyValue\")\n\nif (!secretBodyValue) { \n pw.env.set(\"secretBodyValue\", \"secret-body-value\")\n}\n\nconst secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "none",
|
"authType": "none",
|
||||||
"authActive": true
|
"authActive": true
|
||||||
@@ -88,13 +91,14 @@
|
|||||||
"active": true
|
"active": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestVariables": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "<<baseURL>>/get",
|
"endpoint": "<<baseURL>>/get",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
|
||||||
"preRequestScript": "const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n\nif (!secretQueryParamValue) {\n pw.env.set(\"secretQueryParamValue\", \"secret-query-param-value\")\n}\n\nconst secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
|
"preRequestScript": "const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n\nif (!secretQueryParamValue) {\n pw.env.set(\"secretQueryParamValue\", \"secret-query-param-value\")\n}\n\nconst secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"auth": {
|
"auth": {
|
||||||
"authType": "basic",
|
"authType": "basic",
|
||||||
"password": "<<secretBasicAuthPassword>>",
|
"password": "<<secretBasicAuthPassword>>",
|
||||||
@@ -108,13 +112,14 @@
|
|||||||
"name": "test-secret-basic-auth",
|
"name": "test-secret-basic-auth",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
|
"requestVariables": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
|
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
|
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
|
||||||
"preRequestScript": "let secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n\nlet secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\nif (!secretBasicAuthUsername) {\n pw.env.set(\"secretBasicAuthUsername\", \"test-user\")\n}\n\nif (!secretBasicAuthPassword) {\n pw.env.set(\"secretBasicAuthPassword\", \"test-pass\")\n}"
|
"preRequestScript": "let secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n\nlet secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\nif (!secretBasicAuthUsername) {\n pw.env.set(\"secretBasicAuthUsername\", \"test-user\")\n}\n\nif (!secretBasicAuthPassword) {\n pw.env.set(\"secretBasicAuthPassword\", \"test-pass\")\n}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"auth": {
|
"auth": {
|
||||||
"token": "<<secretBearerToken>>",
|
"token": "<<secretBearerToken>>",
|
||||||
"authType": "bearer",
|
"authType": "bearer",
|
||||||
@@ -129,6 +134,7 @@
|
|||||||
"name": "test-secret-bearer-auth",
|
"name": "test-secret-bearer-auth",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"params": [],
|
"params": [],
|
||||||
|
"requestVariables": [],
|
||||||
"headers": [],
|
"headers": [],
|
||||||
"endpoint": "<<baseURL>>/bearer",
|
"endpoint": "<<baseURL>>/bearer",
|
||||||
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n const preReqSecretBearerToken = pw.env.resolve(\"<<preReqSecretBearerToken>>\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
|
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n const preReqSecretBearerToken = pw.env.resolve(\"<<preReqSecretBearerToken>>\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"folders": [],
|
"folders": [],
|
||||||
"requests": [
|
"requests": [
|
||||||
{
|
{
|
||||||
"v": "1",
|
"v": "2",
|
||||||
"endpoint": "https://httpbin.org/post",
|
"endpoint": "https://httpbin.org/post",
|
||||||
"name": "req",
|
"name": "req",
|
||||||
"params": [],
|
"params": [],
|
||||||
@@ -22,7 +22,8 @@
|
|||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n \"key\": \"<<customBodyValue>>\"\n}"
|
"body": "{\n \"key\": \"<<customBodyValue>>\"\n}"
|
||||||
}
|
},
|
||||||
|
"requestVariables": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"auth": { "authType": "inherit", "authActive": false },
|
"auth": { "authType": "inherit", "authActive": false },
|
||||||
|
|||||||
@@ -563,12 +563,22 @@ details[open] summary .indicator {
|
|||||||
.env-highlight {
|
.env-highlight {
|
||||||
@apply text-accentContrast;
|
@apply text-accentContrast;
|
||||||
|
|
||||||
&.env-found {
|
&.request-variable-highlight {
|
||||||
@apply bg-accentDark;
|
@apply bg-amber-500;
|
||||||
@apply hover:bg-accent;
|
@apply hover:bg-amber-600;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.env-not-found {
|
&.environment-variable-highlight {
|
||||||
|
@apply bg-green-500;
|
||||||
|
@apply hover:bg-green-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.global-variable-highlight {
|
||||||
|
@apply bg-blue-500;
|
||||||
|
@apply hover:bg-blue-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.environment-not-found-highlight {
|
||||||
@apply bg-red-500;
|
@apply bg-red-500;
|
||||||
@apply hover:bg-red-600;
|
@apply hover:bg-red-600;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,6 +239,7 @@
|
|||||||
"pending_invites": "There are no pending invites for this team",
|
"pending_invites": "There are no pending invites for this team",
|
||||||
"profile": "Login to view your profile",
|
"profile": "Login to view your profile",
|
||||||
"protocols": "Protocols are empty",
|
"protocols": "Protocols are empty",
|
||||||
|
"request_variables": "This request does not have any request variables",
|
||||||
"schema": "Connect to a GraphQL endpoint to view schema",
|
"schema": "Connect to a GraphQL endpoint to view schema",
|
||||||
"secret_environments": "Secrets are not synced to Hoppscotch",
|
"secret_environments": "Secrets are not synced to Hoppscotch",
|
||||||
"shared_requests": "Shared requests are empty",
|
"shared_requests": "Shared requests are empty",
|
||||||
@@ -315,7 +316,8 @@
|
|||||||
"proxy_error": "Proxy error",
|
"proxy_error": "Proxy error",
|
||||||
"script_fail": "Could not execute pre-request script",
|
"script_fail": "Could not execute pre-request script",
|
||||||
"something_went_wrong": "Something went wrong",
|
"something_went_wrong": "Something went wrong",
|
||||||
"test_script_fail": "Could not execute post-request script"
|
"test_script_fail": "Could not execute post-request script",
|
||||||
|
"reading_files": "Error while reading one or more files."
|
||||||
},
|
},
|
||||||
"export": {
|
"export": {
|
||||||
"as_json": "Export as JSON",
|
"as_json": "Export as JSON",
|
||||||
@@ -413,7 +415,10 @@
|
|||||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||||
"postman_environment": "Postman Environment",
|
"postman_environment": "Postman Environment",
|
||||||
"postman_environment_description": "Import Postman Environment from a JSON file",
|
"postman_environment_description": "Import Postman Environment from a JSON file",
|
||||||
"title": "Import"
|
"title": "Import",
|
||||||
|
"file_size_limit_exceeded_warning_multiple_files": "Chosen files exceed the recommended limit of 10MB. Only the first {files} selected will be imported",
|
||||||
|
"file_size_limit_exceeded_warning_single_file": "The currently chosen file exceeds the recommended limit of 10MB. Please select another file.",
|
||||||
|
"success": "Successfully imported"
|
||||||
},
|
},
|
||||||
"inspections": {
|
"inspections": {
|
||||||
"description": "Inspect possible errors",
|
"description": "Inspect possible errors",
|
||||||
@@ -556,6 +561,7 @@
|
|||||||
"raw_body": "Raw Request Body",
|
"raw_body": "Raw Request Body",
|
||||||
"rename": "Rename Request",
|
"rename": "Rename Request",
|
||||||
"renamed": "Request renamed",
|
"renamed": "Request renamed",
|
||||||
|
"request_variables": "Request variables",
|
||||||
"run": "Run",
|
"run": "Run",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"save_as": "Save as",
|
"save_as": "Save as",
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ const HoppOpenAPIImporter: ImporterOrExporter = {
|
|||||||
step: UrlSource({
|
step: UrlSource({
|
||||||
caption: "import.from_url",
|
caption: "import.from_url",
|
||||||
onImportFromURL: async (content) => {
|
onImportFromURL: async (content) => {
|
||||||
const res = await hoppOpenAPIImporter(content)()
|
const res = await hoppOpenAPIImporter([content])()
|
||||||
|
|
||||||
if (E.isRight(res)) {
|
if (E.isRight(res)) {
|
||||||
handleImportToStore(res.right)
|
handleImportToStore(res.right)
|
||||||
|
|||||||
@@ -694,7 +694,7 @@ class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
|
|||||||
let target = collections[indexPaths.shift() as number]
|
let target = collections[indexPaths.shift() as number]
|
||||||
|
|
||||||
while (indexPaths.length > 0)
|
while (indexPaths.length > 0)
|
||||||
target = target.folders[indexPaths.shift() as number]
|
target = target?.folders[indexPaths.shift() as number]
|
||||||
|
|
||||||
return target !== undefined ? target : null
|
return target !== undefined ? target : null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,9 +32,13 @@
|
|||||||
{{ tab.document.request.method }}
|
{{ tab.document.request.method }}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
class="flex items-center flex-1 flex-shrink-0 min-w-0 px-4 py-2 truncate rounded-r"
|
class="flex items-center flex-1 flex-shrink-0 min-w-0 truncate rounded-r"
|
||||||
>
|
>
|
||||||
{{ tab.document.request.endpoint }}
|
<SmartEnvInput
|
||||||
|
v-model="tab.document.request.endpoint"
|
||||||
|
:readonly="true"
|
||||||
|
:envs="tabRequestVariables"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex mt-2 space-x-2 sm:mt-0">
|
<div class="flex mt-2 space-x-2 sm:mt-0">
|
||||||
@@ -69,6 +73,7 @@
|
|||||||
v-model="tab.document.request"
|
v-model="tab.document.request"
|
||||||
v-model:option-tab="selectedOptionTab"
|
v-model:option-tab="selectedOptionTab"
|
||||||
:properties="properties"
|
:properties="properties"
|
||||||
|
:envs="tabRequestVariables"
|
||||||
/>
|
/>
|
||||||
<HttpResponse
|
<HttpResponse
|
||||||
v-if="tab.document.response"
|
v-if="tab.document.response"
|
||||||
@@ -117,6 +122,15 @@ const sharedRequestURL = computed(() => {
|
|||||||
return `${shortcodeBaseURL}/r/${props.sharedRequestID}`
|
return `${shortcodeBaseURL}/r/${props.sharedRequestID}`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const tabRequestVariables = computed(() => {
|
||||||
|
return tab.value.document.request.requestVariables.map(({ key, value }) => ({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
secret: false,
|
||||||
|
sourceEnv: "RequestVariable",
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
const { subscribeToStream } = useStreamSubscriber()
|
const { subscribeToStream } = useStreamSubscriber()
|
||||||
|
|
||||||
const newSendRequest = async () => {
|
const newSendRequest = async () => {
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ const PostmanEnvironmentsImport: ImporterOrExporter = {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
handleImportToStore([res.right])
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logEvent({
|
||||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||||
@@ -166,19 +166,14 @@ const insomniaEnvironmentsImport: ImporterOrExporter = {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalEnvIndex = res.right.findIndex(
|
const globalEnvs = res.right.filter(
|
||||||
(env) => env.name === "Base Environment"
|
(env) => env.name === "Base Environment"
|
||||||
)
|
)
|
||||||
|
const otherEnvs = res.right.filter(
|
||||||
|
(env) => env.name !== "Base Environment"
|
||||||
|
)
|
||||||
|
|
||||||
const globalEnv =
|
handleImportToStore(otherEnvs, globalEnvs)
|
||||||
globalEnvIndex !== -1 ? res.right[globalEnvIndex] : undefined
|
|
||||||
|
|
||||||
// remove the global env from the environments array to prevent it from being imported twice
|
|
||||||
if (globalEnvIndex !== -1) {
|
|
||||||
res.right.splice(globalEnvIndex, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleImportToStore(res.right, globalEnv)
|
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logEvent({
|
||||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||||
@@ -340,14 +335,14 @@ const showImportFailedError = () => {
|
|||||||
|
|
||||||
const handleImportToStore = async (
|
const handleImportToStore = async (
|
||||||
environments: Environment[],
|
environments: Environment[],
|
||||||
globalEnv?: NonSecretEnvironment
|
globalEnvs: NonSecretEnvironment[] = []
|
||||||
) => {
|
) => {
|
||||||
// if there's a global env, add them to the store
|
// Add global envs to the store
|
||||||
if (globalEnv) {
|
globalEnvs.forEach(({ variables }) => {
|
||||||
globalEnv.variables.forEach(({ key, value, secret }) =>
|
variables.forEach(({ key, value, secret }) => {
|
||||||
addGlobalEnvVariable({ key, value, secret })
|
addGlobalEnvVariable({ key, value, secret })
|
||||||
)
|
})
|
||||||
}
|
})
|
||||||
|
|
||||||
if (props.environmentType === "MY_ENV") {
|
if (props.environmentType === "MY_ENV") {
|
||||||
appendEnvironments(environments)
|
appendEnvironments(environments)
|
||||||
|
|||||||
@@ -150,7 +150,7 @@
|
|||||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||||
<div class="w-2/3 border-r border-dividerLight">
|
<div class="w-2/3 border-r border-dividerLight">
|
||||||
<div v-if="auth.authType === 'basic'">
|
<div v-if="auth.authType === 'basic'">
|
||||||
<HttpAuthorizationBasic v-model="auth" />
|
<HttpAuthorizationBasic v-model="auth" :envs="envs" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="auth.authType === 'inherit'" class="p-4">
|
<div v-if="auth.authType === 'inherit'" class="p-4">
|
||||||
<span v-if="inheritedProperties?.auth">
|
<span v-if="inheritedProperties?.auth">
|
||||||
@@ -169,17 +169,26 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="auth.authType === 'bearer'">
|
<div v-if="auth.authType === 'bearer'">
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput v-model="auth.token" placeholder="Token" />
|
<SmartEnvInput
|
||||||
|
v-model="auth.token"
|
||||||
|
placeholder="Token"
|
||||||
|
:auto-complete-env="true"
|
||||||
|
:envs="envs"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="auth.authType === 'oauth-2'">
|
<div v-if="auth.authType === 'oauth-2'">
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput v-model="auth.token" placeholder="Token" />
|
<SmartEnvInput
|
||||||
|
v-model="auth.token"
|
||||||
|
placeholder="Token"
|
||||||
|
:envs="envs"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<HttpOAuth2Authorization v-model="auth" />
|
<HttpOAuth2Authorization v-model="auth" :envs="envs" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="auth.authType === 'api-key'">
|
<div v-if="auth.authType === 'api-key'">
|
||||||
<HttpAuthorizationApiKey v-model="auth" />
|
<HttpAuthorizationApiKey v-model="auth" :envs="envs" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -215,6 +224,7 @@ import { useColorMode } from "@composables/theming"
|
|||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
import { onMounted } from "vue"
|
import { onMounted } from "vue"
|
||||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||||
|
import { AggregateEnvironment } from "~/newstore/environments"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -225,6 +235,7 @@ const props = defineProps<{
|
|||||||
isCollectionProperty?: boolean
|
isCollectionProperty?: boolean
|
||||||
isRootCollection?: boolean
|
isRootCollection?: boolean
|
||||||
inheritedProperties?: HoppInheritedProperty
|
inheritedProperties?: HoppInheritedProperty
|
||||||
|
envs?: AggregateEnvironment[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -100,10 +100,12 @@
|
|||||||
<HttpBodyParameters
|
<HttpBodyParameters
|
||||||
v-if="body.contentType === 'multipart/form-data'"
|
v-if="body.contentType === 'multipart/form-data'"
|
||||||
v-model="body"
|
v-model="body"
|
||||||
|
:envs="envs"
|
||||||
/>
|
/>
|
||||||
<HttpURLEncodedParams
|
<HttpURLEncodedParams
|
||||||
v-else-if="body.contentType === 'application/x-www-form-urlencoded'"
|
v-else-if="body.contentType === 'application/x-www-form-urlencoded'"
|
||||||
v-model="body"
|
v-model="body"
|
||||||
|
:envs="envs"
|
||||||
/>
|
/>
|
||||||
<HttpRawBody v-else-if="body.contentType !== null" v-model="body" />
|
<HttpRawBody v-else-if="body.contentType !== null" v-model="body" />
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
@@ -141,6 +143,7 @@ import IconExternalLink from "~icons/lucide/external-link"
|
|||||||
import IconInfo from "~icons/lucide/info"
|
import IconInfo from "~icons/lucide/info"
|
||||||
import IconRefreshCW from "~icons/lucide/refresh-cw"
|
import IconRefreshCW from "~icons/lucide/refresh-cw"
|
||||||
import { RESTOptionTabs } from "./RequestOptions.vue"
|
import { RESTOptionTabs } from "./RequestOptions.vue"
|
||||||
|
import { AggregateEnvironment } from "~/newstore/environments"
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
@@ -148,6 +151,7 @@ const t = useI18n()
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
body: HoppRESTReqBody
|
body: HoppRESTReqBody
|
||||||
headers: HoppRESTHeader[]
|
headers: HoppRESTHeader[]
|
||||||
|
envs?: AggregateEnvironment[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -65,6 +65,8 @@
|
|||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="entry.key"
|
v-model="entry.key"
|
||||||
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
|
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
|
||||||
|
:auto-complete-env="true"
|
||||||
|
:envs="envs"
|
||||||
@change="
|
@change="
|
||||||
updateBodyParam(index, {
|
updateBodyParam(index, {
|
||||||
key: $event,
|
key: $event,
|
||||||
@@ -87,6 +89,8 @@
|
|||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="entry.value"
|
v-model="entry.value"
|
||||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||||
|
:auto-complete-env="true"
|
||||||
|
:envs="envs"
|
||||||
@change="
|
@change="
|
||||||
updateBodyParam(index, {
|
updateBodyParam(index, {
|
||||||
key: entry.key,
|
key: entry.key,
|
||||||
@@ -190,11 +194,13 @@ import { useI18n } from "@composables/i18n"
|
|||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
|
import { AggregateEnvironment } from "~/newstore/environments"
|
||||||
|
|
||||||
type Body = HoppRESTReqBody & { contentType: "multipart/form-data" }
|
type Body = HoppRESTReqBody & { contentType: "multipart/form-data" }
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: Body
|
modelValue: Body
|
||||||
|
envs: AggregateEnvironment[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -188,11 +188,25 @@ const copyCodeIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
|||||||
|
|
||||||
const requestCode = computed(() => {
|
const requestCode = computed(() => {
|
||||||
const aggregateEnvs = getAggregateEnvs()
|
const aggregateEnvs = getAggregateEnvs()
|
||||||
|
const requestVariables = request.value.requestVariables.map(
|
||||||
|
(requestVariable) => {
|
||||||
|
if (requestVariable.active)
|
||||||
|
return {
|
||||||
|
key: requestVariable.key,
|
||||||
|
value: requestVariable.value,
|
||||||
|
secret: false,
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
)
|
||||||
const env: Environment = {
|
const env: Environment = {
|
||||||
v: 1,
|
v: 1,
|
||||||
id: "env",
|
id: "env",
|
||||||
name: "Env",
|
name: "Env",
|
||||||
variables: aggregateEnvs,
|
variables: [
|
||||||
|
...(requestVariables as Environment["variables"]),
|
||||||
|
...aggregateEnvs,
|
||||||
|
],
|
||||||
}
|
}
|
||||||
const effectiveRequest = getEffectiveRESTRequest(request.value, env)
|
const effectiveRequest = getEffectiveRESTRequest(request.value, env)
|
||||||
|
|
||||||
@@ -212,6 +226,12 @@ const requestCode = computed(() => {
|
|||||||
active: true,
|
active: true,
|
||||||
})),
|
})),
|
||||||
endpoint: effectiveRequest.effectiveFinalURL,
|
endpoint: effectiveRequest.effectiveFinalURL,
|
||||||
|
requestVariables: effectiveRequest.effectiveFinalRequestVariables.map(
|
||||||
|
(requestVariable) => ({
|
||||||
|
...requestVariable,
|
||||||
|
active: true,
|
||||||
|
})
|
||||||
|
),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
@click="clearContent()"
|
@click="clearContent()"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-if="bulkMode"
|
v-if="bulkHeaders"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': WRAP_LINES }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
@@ -92,6 +92,8 @@
|
|||||||
:auto-complete-source="commonHeaders"
|
:auto-complete-source="commonHeaders"
|
||||||
:env-index="index"
|
:env-index="index"
|
||||||
:inspection-results="getInspectorResult(headerKeyResults, index)"
|
:inspection-results="getInspectorResult(headerKeyResults, index)"
|
||||||
|
:auto-complete-env="true"
|
||||||
|
:envs="envs"
|
||||||
@change="
|
@change="
|
||||||
updateHeader(index, {
|
updateHeader(index, {
|
||||||
id: header.id,
|
id: header.id,
|
||||||
@@ -108,6 +110,8 @@
|
|||||||
getInspectorResult(headerValueResults, index)
|
getInspectorResult(headerValueResults, index)
|
||||||
"
|
"
|
||||||
:env-index="index"
|
:env-index="index"
|
||||||
|
:auto-complete-env="true"
|
||||||
|
:envs="envs"
|
||||||
@change="
|
@change="
|
||||||
updateHeader(index, {
|
updateHeader(index, {
|
||||||
id: header.id,
|
id: header.id,
|
||||||
@@ -329,7 +333,11 @@ import {
|
|||||||
getComputedHeaders,
|
getComputedHeaders,
|
||||||
getComputedAuthHeaders,
|
getComputedAuthHeaders,
|
||||||
} from "~/helpers/utils/EffectiveURL"
|
} from "~/helpers/utils/EffectiveURL"
|
||||||
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
|
import {
|
||||||
|
AggregateEnvironment,
|
||||||
|
aggregateEnvs$,
|
||||||
|
getAggregateEnvs,
|
||||||
|
} from "~/newstore/environments"
|
||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
import { InspectionService, InspectorResult } from "~/services/inspection"
|
import { InspectionService, InspectorResult } from "~/services/inspection"
|
||||||
@@ -359,6 +367,7 @@ const props = defineProps<{
|
|||||||
modelValue: HoppRESTRequest
|
modelValue: HoppRESTRequest
|
||||||
isCollectionProperty?: boolean
|
isCollectionProperty?: boolean
|
||||||
inheritedProperties?: HoppInheritedProperty
|
inheritedProperties?: HoppInheritedProperty
|
||||||
|
envs?: AggregateEnvironment[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -6,31 +6,52 @@
|
|||||||
:styles="
|
:styles="
|
||||||
hasAccessTokenOrAuthURL ? 'pointer-events-none opacity-70' : ''
|
hasAccessTokenOrAuthURL ? 'pointer-events-none opacity-70' : ''
|
||||||
"
|
"
|
||||||
|
:auto-complete-env="true"
|
||||||
placeholder="OpenID Connect Discovery URL"
|
placeholder="OpenID Connect Discovery URL"
|
||||||
|
:envs="envs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="authURL"
|
v-model="authURL"
|
||||||
placeholder="Authorization URL"
|
placeholder="Authorization URL"
|
||||||
|
:auto-complete-env="true"
|
||||||
:styles="hasOIDCURL ? 'pointer-events-none opacity-70' : ''"
|
:styles="hasOIDCURL ? 'pointer-events-none opacity-70' : ''"
|
||||||
></SmartEnvInput>
|
:envs="envs"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="accessTokenURL"
|
v-model="accessTokenURL"
|
||||||
placeholder="Access Token URL"
|
placeholder="Access Token URL"
|
||||||
|
:auto-complete-env="true"
|
||||||
:styles="hasOIDCURL ? 'pointer-events-none opacity-70' : ''"
|
:styles="hasOIDCURL ? 'pointer-events-none opacity-70' : ''"
|
||||||
|
:envs="envs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput v-model="clientID" placeholder="Client ID" />
|
<SmartEnvInput
|
||||||
|
v-model="clientID"
|
||||||
|
:auto-complete-env="true"
|
||||||
|
placeholder="Client ID"
|
||||||
|
:envs="envs"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput v-model="clientSecret" placeholder="Client Secret" />
|
<SmartEnvInput
|
||||||
|
v-model="clientSecret"
|
||||||
|
:auto-complete-env="true"
|
||||||
|
placeholder="Client Secret"
|
||||||
|
:envs="envs"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput v-model="scope" placeholder="Scope" />
|
<SmartEnvInput
|
||||||
|
v-model="scope"
|
||||||
|
:auto-complete-env="true"
|
||||||
|
placeholder="Scope"
|
||||||
|
:envs="envs"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
@@ -62,6 +83,7 @@ const toast = useToast()
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: HoppRESTAuthOAuth2 | HoppGQLAuthOAuth2
|
modelValue: HoppRESTAuthOAuth2 | HoppGQLAuthOAuth2
|
||||||
|
envs?: AggregateEnvironment[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -87,6 +87,8 @@
|
|||||||
:inspection-results="
|
:inspection-results="
|
||||||
getInspectorResult(parameterKeyResults, index)
|
getInspectorResult(parameterKeyResults, index)
|
||||||
"
|
"
|
||||||
|
:auto-complete-env="true"
|
||||||
|
:envs="envs"
|
||||||
@change="
|
@change="
|
||||||
updateParam(index, {
|
updateParam(index, {
|
||||||
id: param.id,
|
id: param.id,
|
||||||
@@ -102,6 +104,8 @@
|
|||||||
:inspection-results="
|
:inspection-results="
|
||||||
getInspectorResult(parameterValueResults, index)
|
getInspectorResult(parameterValueResults, index)
|
||||||
"
|
"
|
||||||
|
:auto-complete-env="true"
|
||||||
|
:envs="envs"
|
||||||
@change="
|
@change="
|
||||||
updateParam(index, {
|
updateParam(index, {
|
||||||
id: param.id,
|
id: param.id,
|
||||||
@@ -209,6 +213,7 @@ import { InspectionService, InspectorResult } from "~/services/inspection"
|
|||||||
import { RESTTabService } from "~/services/tab/rest"
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
import { useNestedSetting } from "~/composables/settings"
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
import { toggleNestedSetting } from "~/newstore/settings"
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
import { AggregateEnvironment } from "~/newstore/environments"
|
||||||
|
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
@@ -242,6 +247,7 @@ useCodemirror(
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: HoppRESTParam[]
|
modelValue: HoppRESTParam[]
|
||||||
|
envs?: AggregateEnvironment[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
v-model="tab.document.request.endpoint"
|
v-model="tab.document.request.endpoint"
|
||||||
:placeholder="`${t('request.url')}`"
|
:placeholder="`${t('request.url')}`"
|
||||||
:auto-complete-source="userHistories"
|
:auto-complete-source="userHistories"
|
||||||
|
:auto-complete-env="true"
|
||||||
:inspection-results="tabResults"
|
:inspection-results="tabResults"
|
||||||
@paste="onPasteUrl($event)"
|
@paste="onPasteUrl($event)"
|
||||||
@enter="newSendRequest"
|
@enter="newSendRequest"
|
||||||
|
|||||||
@@ -5,48 +5,51 @@
|
|||||||
render-inactive-tabs
|
render-inactive-tabs
|
||||||
>
|
>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
v-if="properties ? properties.includes('params') : true"
|
v-if="properties?.includes('params') ?? true"
|
||||||
:id="'params'"
|
:id="'params'"
|
||||||
:label="`${t('tab.parameters')}`"
|
:label="`${t('tab.parameters')}`"
|
||||||
:info="`${newActiveParamsCount$}`"
|
:info="`${newActiveParamsCount}`"
|
||||||
>
|
>
|
||||||
<HttpParameters v-model="request.params" />
|
<HttpParameters v-model="request.params" :envs="envs" />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
v-if="properties ? properties.includes('bodyParams') : true"
|
v-if="properties?.includes('bodyParams') ?? true"
|
||||||
:id="'bodyParams'"
|
:id="'bodyParams'"
|
||||||
:label="`${t('tab.body')}`"
|
:label="`${t('tab.body')}`"
|
||||||
>
|
>
|
||||||
<HttpBody
|
<HttpBody
|
||||||
v-model:headers="request.headers"
|
v-model:headers="request.headers"
|
||||||
v-model:body="request.body"
|
v-model:body="request.body"
|
||||||
|
:envs="envs"
|
||||||
@change-tab="changeOptionTab"
|
@change-tab="changeOptionTab"
|
||||||
/>
|
/>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
v-if="properties ? properties.includes('headers') : true"
|
v-if="properties?.includes('headers') ?? true"
|
||||||
:id="'headers'"
|
:id="'headers'"
|
||||||
:label="`${t('tab.headers')}`"
|
:label="`${t('tab.headers')}`"
|
||||||
:info="`${newActiveHeadersCount$}`"
|
:info="`${newActiveHeadersCount}`"
|
||||||
>
|
>
|
||||||
<HttpHeaders
|
<HttpHeaders
|
||||||
v-model="request"
|
v-model="request"
|
||||||
:inherited-properties="inheritedProperties"
|
:inherited-properties="inheritedProperties"
|
||||||
|
:envs="envs"
|
||||||
@change-tab="changeOptionTab"
|
@change-tab="changeOptionTab"
|
||||||
/>
|
/>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
v-if="properties ? properties.includes('authorization') : true"
|
v-if="properties?.includes('authorization') ?? true"
|
||||||
:id="'authorization'"
|
:id="'authorization'"
|
||||||
:label="`${t('tab.authorization')}`"
|
:label="`${t('tab.authorization')}`"
|
||||||
>
|
>
|
||||||
<HttpAuthorization
|
<HttpAuthorization
|
||||||
v-model="request.auth"
|
v-model="request.auth"
|
||||||
:inherited-properties="inheritedProperties"
|
:inherited-properties="inheritedProperties"
|
||||||
|
:envs="envs"
|
||||||
/>
|
/>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
v-if="properties ? properties.includes('preRequestScript') : true"
|
v-if="properties?.includes('preRequestScript') ?? true"
|
||||||
:id="'preRequestScript'"
|
:id="'preRequestScript'"
|
||||||
:label="`${t('tab.pre_request_script')}`"
|
:label="`${t('tab.pre_request_script')}`"
|
||||||
:indicator="
|
:indicator="
|
||||||
@@ -58,7 +61,7 @@
|
|||||||
<HttpPreRequestScript v-model="request.preRequestScript" />
|
<HttpPreRequestScript v-model="request.preRequestScript" />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
v-if="properties ? properties.includes('tests') : true"
|
v-if="properties?.includes('tests') ?? true"
|
||||||
:id="'tests'"
|
:id="'tests'"
|
||||||
:label="`${t('tab.tests')}`"
|
:label="`${t('tab.tests')}`"
|
||||||
:indicator="
|
:indicator="
|
||||||
@@ -67,6 +70,15 @@
|
|||||||
>
|
>
|
||||||
<HttpTests v-model="request.testScript" />
|
<HttpTests v-model="request.testScript" />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
|
<HoppSmartTab
|
||||||
|
v-if="properties?.includes('requestVariables') ?? true"
|
||||||
|
:id="'requestVariables'"
|
||||||
|
:label="`${t('tab.variables')}`"
|
||||||
|
:info="`${newActiveRequestVariablesCount}`"
|
||||||
|
:align-last="true"
|
||||||
|
>
|
||||||
|
<HttpRequestVariables v-model="request.requestVariables" />
|
||||||
|
</HoppSmartTab>
|
||||||
</HoppSmartTabs>
|
</HoppSmartTabs>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -77,6 +89,7 @@ import { useVModel } from "@vueuse/core"
|
|||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
|
||||||
|
import { AggregateEnvironment } from "~/newstore/environments"
|
||||||
|
|
||||||
const VALID_OPTION_TABS = [
|
const VALID_OPTION_TABS = [
|
||||||
"params",
|
"params",
|
||||||
@@ -85,6 +98,7 @@ const VALID_OPTION_TABS = [
|
|||||||
"authorization",
|
"authorization",
|
||||||
"preRequestScript",
|
"preRequestScript",
|
||||||
"tests",
|
"tests",
|
||||||
|
"requestVariables",
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type RESTOptionTabs = (typeof VALID_OPTION_TABS)[number]
|
export type RESTOptionTabs = (typeof VALID_OPTION_TABS)[number]
|
||||||
@@ -98,6 +112,7 @@ const props = withDefaults(
|
|||||||
optionTab: RESTOptionTabs
|
optionTab: RESTOptionTabs
|
||||||
properties?: string[]
|
properties?: string[]
|
||||||
inheritedProperties?: HoppInheritedProperty
|
inheritedProperties?: HoppInheritedProperty
|
||||||
|
envs?: AggregateEnvironment[]
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
optionTab: "params",
|
optionTab: "params",
|
||||||
@@ -116,22 +131,27 @@ const changeOptionTab = (e: RESTOptionTabs) => {
|
|||||||
selectedOptionTab.value = e
|
selectedOptionTab.value = e
|
||||||
}
|
}
|
||||||
|
|
||||||
const newActiveParamsCount$ = computed(() => {
|
const newActiveParamsCount = computed(() => {
|
||||||
const e = request.value.params.filter(
|
const count = request.value.params.filter(
|
||||||
(x) => x.active && (x.key !== "" || x.value !== "")
|
(x) => x.active && (x.key || x.value)
|
||||||
).length
|
).length
|
||||||
|
|
||||||
if (e === 0) return null
|
return count ? count : null
|
||||||
return `${e}`
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const newActiveHeadersCount$ = computed(() => {
|
const newActiveHeadersCount = computed(() => {
|
||||||
const e = request.value.headers.filter(
|
const count = request.value.headers.filter(
|
||||||
(x) => x.active && (x.key !== "" || x.value !== "")
|
(x) => x.active && (x.key || x.value)
|
||||||
).length
|
).length
|
||||||
|
|
||||||
if (e === 0) return null
|
return count ? count : null
|
||||||
return `${e}`
|
})
|
||||||
|
|
||||||
|
const newActiveRequestVariablesCount = computed(() => {
|
||||||
|
const count = request.value.requestVariables.filter(
|
||||||
|
(x) => x.active && (x.key || x.value)
|
||||||
|
).length
|
||||||
|
return count ? count : null
|
||||||
})
|
})
|
||||||
|
|
||||||
defineActionHandler("request.open-tab", ({ tab }) => {
|
defineActionHandler("request.open-tab", ({ tab }) => {
|
||||||
|
|||||||
@@ -0,0 +1,412 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-1 flex-col">
|
||||||
|
<div
|
||||||
|
class="sticky top-upperMobileSecondaryStickyFold z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4 sm:top-upperSecondaryStickyFold"
|
||||||
|
>
|
||||||
|
<label class="truncate font-semibold text-secondaryLight">
|
||||||
|
{{ t("request.request_variables") }}
|
||||||
|
</label>
|
||||||
|
<div class="flex">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
to="https://docs.hoppscotch.io/documentation/features/rest-api-testing"
|
||||||
|
blank
|
||||||
|
:title="t('app.wiki')"
|
||||||
|
:icon="IconHelpCircle"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.clear_all')"
|
||||||
|
:icon="IconTrash2"
|
||||||
|
@click="clearContent()"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-if="bulkVariables"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('state.linewrap')"
|
||||||
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
|
:icon="IconWrapText"
|
||||||
|
@click.prevent="
|
||||||
|
toggleNestedSetting('WRAP_LINES', 'httpRequestVariables')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('state.bulk_mode')"
|
||||||
|
:icon="IconEdit"
|
||||||
|
:class="{ '!text-accent': bulkMode }"
|
||||||
|
@click="bulkMode = !bulkMode"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('add.new')"
|
||||||
|
:icon="IconPlus"
|
||||||
|
:disabled="bulkMode"
|
||||||
|
@click="addVariable"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="bulkMode" class="h-full relative">
|
||||||
|
<div ref="bulkEditor" class="absolute inset-0"></div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<draggable
|
||||||
|
v-model="workingRequestVariables"
|
||||||
|
item-key="id"
|
||||||
|
animation="250"
|
||||||
|
handle=".draggable-handle"
|
||||||
|
draggable=".draggable-content"
|
||||||
|
ghost-class="cursor-move"
|
||||||
|
chosen-class="bg-primaryLight"
|
||||||
|
drag-class="cursor-grabbing"
|
||||||
|
>
|
||||||
|
<template #item="{ element: variable, index }">
|
||||||
|
<div
|
||||||
|
class="draggable-content group flex divide-x divide-dividerLight border-b border-dividerLight"
|
||||||
|
>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{
|
||||||
|
theme: 'tooltip',
|
||||||
|
delay: [500, 20],
|
||||||
|
content:
|
||||||
|
index !== workingRequestVariables?.length - 1
|
||||||
|
? t('action.drag_to_reorder')
|
||||||
|
: null,
|
||||||
|
}"
|
||||||
|
:icon="IconGripVertical"
|
||||||
|
class="opacity-0"
|
||||||
|
:class="{
|
||||||
|
'draggable-handle cursor-grab group-hover:opacity-100':
|
||||||
|
index !== workingRequestVariables?.length - 1,
|
||||||
|
}"
|
||||||
|
tabindex="-1"
|
||||||
|
/>
|
||||||
|
<SmartEnvInput
|
||||||
|
v-model="variable.key"
|
||||||
|
:placeholder="`${t('count.variable', { count: index + 1 })}`"
|
||||||
|
@change="
|
||||||
|
updateVariable(index, {
|
||||||
|
id: variable.id,
|
||||||
|
key: $event,
|
||||||
|
value: variable.value,
|
||||||
|
active: variable.active,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<SmartEnvInput
|
||||||
|
v-model="variable.value"
|
||||||
|
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||||
|
@change="
|
||||||
|
updateVariable(index, {
|
||||||
|
id: variable.id,
|
||||||
|
key: variable.key,
|
||||||
|
value: $event,
|
||||||
|
active: variable.active,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="
|
||||||
|
variable.hasOwnProperty('active')
|
||||||
|
? variable.active
|
||||||
|
? t('action.turn_off')
|
||||||
|
: t('action.turn_on')
|
||||||
|
: t('action.turn_off')
|
||||||
|
"
|
||||||
|
:icon="
|
||||||
|
variable.hasOwnProperty('active')
|
||||||
|
? variable.active
|
||||||
|
? IconCheckCircle
|
||||||
|
: IconCircle
|
||||||
|
: IconCheckCircle
|
||||||
|
"
|
||||||
|
color="green"
|
||||||
|
@click="
|
||||||
|
updateVariable(index, {
|
||||||
|
id: variable.id,
|
||||||
|
key: variable.key,
|
||||||
|
value: variable.value,
|
||||||
|
active: variable.hasOwnProperty('active')
|
||||||
|
? !variable.active
|
||||||
|
: false,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.remove')"
|
||||||
|
:icon="IconTrash"
|
||||||
|
color="red"
|
||||||
|
@click="deleteVariable(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
<HoppSmartPlaceholder
|
||||||
|
v-if="workingRequestVariables.length === 0"
|
||||||
|
:src="`/images/states/${colorMode.value}/add_files.svg`"
|
||||||
|
:alt="`${t('empty.request_variables')}`"
|
||||||
|
:text="t('empty.request_variables')"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="`${t('add.new')}`"
|
||||||
|
:icon="IconPlus"
|
||||||
|
filled
|
||||||
|
@click="addVariable"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</HoppSmartPlaceholder>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { throwError } from "@functional/error"
|
||||||
|
import {
|
||||||
|
HoppRESTRequestVariable,
|
||||||
|
RawKeyValueEntry,
|
||||||
|
parseRawKeyValueEntriesE,
|
||||||
|
rawKeyValueEntriesToString,
|
||||||
|
} from "@hoppscotch/data"
|
||||||
|
import { useVModel } from "@vueuse/core"
|
||||||
|
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 { flow, pipe } from "fp-ts/function"
|
||||||
|
import { cloneDeep, isEqual } from "lodash-es"
|
||||||
|
import { reactive, ref, watch } from "vue"
|
||||||
|
import draggable from "vuedraggable-es"
|
||||||
|
|
||||||
|
import { useCodemirror } from "~/composables/codemirror"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
|
import { useColorMode } from "~/composables/theming"
|
||||||
|
import { useToast } from "~/composables/toast"
|
||||||
|
import linter from "~/helpers/editor/linting/rawKeyValue"
|
||||||
|
import { objRemoveKey } from "~/helpers/functional/object"
|
||||||
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||||
|
import IconCircle from "~icons/lucide/circle"
|
||||||
|
import IconEdit from "~icons/lucide/edit"
|
||||||
|
import IconGripVertical from "~icons/lucide/grip-vertical"
|
||||||
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
|
import IconPlus from "~icons/lucide/plus"
|
||||||
|
import IconTrash from "~icons/lucide/trash"
|
||||||
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
|
import IconWrapText from "~icons/lucide/wrap-text"
|
||||||
|
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const bulkMode = ref(false)
|
||||||
|
const bulkEditor = ref<any | null>(null)
|
||||||
|
const bulkVariables = ref("")
|
||||||
|
|
||||||
|
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpRequestVariables")
|
||||||
|
|
||||||
|
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: HoppRESTRequestVariable[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", value: Array<HoppRESTRequestVariable>): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// The functional requestVariable list (the requestVariable actually applied to the session)
|
||||||
|
const requestVariables = useVModel(props, "modelValue", emit)
|
||||||
|
|
||||||
|
useCodemirror(
|
||||||
|
bulkEditor,
|
||||||
|
bulkVariables,
|
||||||
|
reactive({
|
||||||
|
extendedEditorConfig: {
|
||||||
|
mode: "text/x-yaml",
|
||||||
|
placeholder: `${t("state.bulk_mode_placeholder")}`,
|
||||||
|
lineWrapping: WRAP_LINES,
|
||||||
|
},
|
||||||
|
linter,
|
||||||
|
completer: null,
|
||||||
|
environmentHighlights: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const idTicker = ref(0)
|
||||||
|
|
||||||
|
const workingRequestVariables = ref<
|
||||||
|
Array<HoppRESTRequestVariable & { id: number }>
|
||||||
|
>([
|
||||||
|
{
|
||||||
|
id: idTicker.value++,
|
||||||
|
key: "",
|
||||||
|
value: "",
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// Sync logic between params and working/bulk params
|
||||||
|
watch(
|
||||||
|
requestVariables,
|
||||||
|
(newRequestVariableList) => {
|
||||||
|
// Sync should overwrite working params
|
||||||
|
const filteredWorkingRequestVariables: HoppRESTRequestVariable[] = pipe(
|
||||||
|
workingRequestVariables.value,
|
||||||
|
A.filterMap(
|
||||||
|
flow(
|
||||||
|
O.fromPredicate((e) => e.key !== ""),
|
||||||
|
O.map(objRemoveKey("id"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const filteredBulkRequestVariables = pipe(
|
||||||
|
parseRawKeyValueEntriesE(bulkVariables.value),
|
||||||
|
E.map(
|
||||||
|
flow(
|
||||||
|
RA.filter((e) => e.key !== ""),
|
||||||
|
RA.toArray
|
||||||
|
)
|
||||||
|
),
|
||||||
|
E.getOrElse(() => [] as RawKeyValueEntry[])
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isEqual(newRequestVariableList, filteredWorkingRequestVariables)) {
|
||||||
|
workingRequestVariables.value = pipe(
|
||||||
|
newRequestVariableList,
|
||||||
|
A.map((x) => ({ id: idTicker.value++, ...x }))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEqual(newRequestVariableList, filteredBulkRequestVariables)) {
|
||||||
|
bulkVariables.value = rawKeyValueEntriesToString(newRequestVariableList)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(workingRequestVariables, (newWorkingRequestVariables) => {
|
||||||
|
const fixedRequestVariables = pipe(
|
||||||
|
newWorkingRequestVariables,
|
||||||
|
A.filterMap(
|
||||||
|
flow(
|
||||||
|
O.fromPredicate((e) => e.key !== ""),
|
||||||
|
O.map(objRemoveKey("id"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isEqual(requestVariables.value, fixedRequestVariables)) {
|
||||||
|
requestVariables.value = cloneDeep(fixedRequestVariables)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(bulkVariables, (newBulkParams) => {
|
||||||
|
const filteredBulkRequestVariables = pipe(
|
||||||
|
parseRawKeyValueEntriesE(newBulkParams),
|
||||||
|
E.map(
|
||||||
|
flow(
|
||||||
|
RA.filter((e) => e.key !== ""),
|
||||||
|
RA.toArray
|
||||||
|
)
|
||||||
|
),
|
||||||
|
E.getOrElse(() => [] as RawKeyValueEntry[])
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isEqual(requestVariables.value, filteredBulkRequestVariables)) {
|
||||||
|
requestVariables.value = filteredBulkRequestVariables
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Rule: Working Request variable always have last element is always an empty param
|
||||||
|
watch(workingRequestVariables, (variableList) => {
|
||||||
|
if (
|
||||||
|
variableList.length > 0 &&
|
||||||
|
variableList[variableList.length - 1].key !== ""
|
||||||
|
) {
|
||||||
|
workingRequestVariables.value.push({
|
||||||
|
id: idTicker.value++,
|
||||||
|
key: "",
|
||||||
|
value: "",
|
||||||
|
active: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const addVariable = () => {
|
||||||
|
workingRequestVariables.value.push({
|
||||||
|
id: idTicker.value++,
|
||||||
|
key: "",
|
||||||
|
value: "",
|
||||||
|
active: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateVariable = (index: number, variable: any & { id: number }) => {
|
||||||
|
workingRequestVariables.value = workingRequestVariables.value.map((h, i) =>
|
||||||
|
i === index ? variable : h
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteVariable = (index: number) => {
|
||||||
|
const requestVariablesBeforeDeletion = cloneDeep(
|
||||||
|
workingRequestVariables.value
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
requestVariablesBeforeDeletion.length > 0 &&
|
||||||
|
index === requestVariablesBeforeDeletion.length - 1
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (deletionToast.value) {
|
||||||
|
deletionToast.value.goAway(0)
|
||||||
|
deletionToast.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
deletionToast.value = toast.success(`${t("state.deleted")}`, {
|
||||||
|
action: [
|
||||||
|
{
|
||||||
|
text: `${t("action.undo")}`,
|
||||||
|
onClick: (_, toastObject) => {
|
||||||
|
workingRequestVariables.value = requestVariablesBeforeDeletion
|
||||||
|
toastObject.goAway(0)
|
||||||
|
deletionToast.value = null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
onComplete: () => {
|
||||||
|
deletionToast.value = null
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
workingRequestVariables.value = pipe(
|
||||||
|
workingRequestVariables.value,
|
||||||
|
A.deleteAt(index),
|
||||||
|
O.getOrElseW(() =>
|
||||||
|
throwError("Working Request Variable Deletion Out of Bounds")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearContent = () => {
|
||||||
|
// set params list to the initial state
|
||||||
|
workingRequestVariables.value = [
|
||||||
|
{
|
||||||
|
id: idTicker.value++,
|
||||||
|
key: "",
|
||||||
|
value: "",
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
bulkVariables.value = ""
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
@click="clearContent()"
|
@click="clearContent()"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-if="bulkMode"
|
v-if="bulkUrlEncodedParams"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('state.linewrap')"
|
:title="t('state.linewrap')"
|
||||||
:class="{ '!text-accent': WRAP_LINES }"
|
:class="{ '!text-accent': WRAP_LINES }"
|
||||||
@@ -84,6 +84,8 @@
|
|||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="param.key"
|
v-model="param.key"
|
||||||
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
|
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
|
||||||
|
:auto-complete-env="true"
|
||||||
|
:envs="envs"
|
||||||
@change="
|
@change="
|
||||||
updateUrlEncodedParam(index, {
|
updateUrlEncodedParam(index, {
|
||||||
id: param.id,
|
id: param.id,
|
||||||
@@ -96,6 +98,8 @@
|
|||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="param.value"
|
v-model="param.value"
|
||||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||||
|
:auto-complete-env="true"
|
||||||
|
:envs="envs"
|
||||||
@change="
|
@change="
|
||||||
updateUrlEncodedParam(index, {
|
updateUrlEncodedParam(index, {
|
||||||
id: param.id,
|
id: param.id,
|
||||||
@@ -200,6 +204,7 @@ import { throwError } from "~/helpers/functional/error"
|
|||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
import { useNestedSetting } from "~/composables/settings"
|
import { useNestedSetting } from "~/composables/settings"
|
||||||
import { toggleNestedSetting } from "~/newstore/settings"
|
import { toggleNestedSetting } from "~/newstore/settings"
|
||||||
|
import { AggregateEnvironment } from "~/newstore/environments"
|
||||||
|
|
||||||
type Body = HoppRESTReqBody & {
|
type Body = HoppRESTReqBody & {
|
||||||
contentType: "application/x-www-form-urlencoded"
|
contentType: "application/x-www-form-urlencoded"
|
||||||
@@ -207,6 +212,7 @@ type Body = HoppRESTReqBody & {
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: Body
|
modelValue: Body
|
||||||
|
envs: AggregateEnvironment[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput v-model="auth.key" placeholder="Key" />
|
<SmartEnvInput
|
||||||
|
v-model="auth.key"
|
||||||
|
:auto-complete-env="true"
|
||||||
|
placeholder="Key"
|
||||||
|
:envs="envs"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput v-model="auth.value" placeholder="Value" />
|
<SmartEnvInput
|
||||||
|
v-model="auth.value"
|
||||||
|
:auto-complete-env="true"
|
||||||
|
placeholder="Value"
|
||||||
|
:envs="envs"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center border-b border-dividerLight">
|
<div class="flex items-center border-b border-dividerLight">
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
@@ -65,11 +75,13 @@ import { useI18n } from "@composables/i18n"
|
|||||||
import { HoppRESTAuthAPIKey } from "@hoppscotch/data"
|
import { HoppRESTAuthAPIKey } from "@hoppscotch/data"
|
||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
import { ref } from "vue"
|
import { ref } from "vue"
|
||||||
|
import { AggregateEnvironment } from "~/newstore/environments"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: HoppRESTAuthAPIKey
|
modelValue: HoppRESTAuthAPIKey
|
||||||
|
envs?: AggregateEnvironment[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -3,12 +3,16 @@
|
|||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="auth.username"
|
v-model="auth.username"
|
||||||
:placeholder="t('authorization.username')"
|
:placeholder="t('authorization.username')"
|
||||||
|
:auto-complete-env="true"
|
||||||
|
:envs="envs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 border-b border-dividerLight">
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="auth.password"
|
v-model="auth.password"
|
||||||
:placeholder="t('authorization.password')"
|
:placeholder="t('authorization.password')"
|
||||||
|
:auto-complete-env="true"
|
||||||
|
:envs="envs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -17,11 +21,13 @@
|
|||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { HoppRESTAuthBasic } from "@hoppscotch/data"
|
import { HoppRESTAuthBasic } from "@hoppscotch/data"
|
||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
|
import { AggregateEnvironment } from "~/newstore/environments"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: HoppRESTAuthBasic
|
modelValue: HoppRESTAuthBasic
|
||||||
|
envs?: AggregateEnvironment[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
{{ t(`${caption}`) }}
|
{{ t(`${caption}`) }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex flex-col ml-10 border border-dashed rounded border-dividerDark"
|
class="flex flex-col ml-10 border border-dashed rounded border-dividerDark"
|
||||||
>
|
>
|
||||||
@@ -23,15 +24,30 @@
|
|||||||
type="file"
|
type="file"
|
||||||
class="p-4 cursor-pointer transition file:transition file:cursor-pointer text-secondary hover:text-secondaryDark file:mr-2 file:py-2 file:px-4 file:rounded file:border-0 file:text-secondary hover:file:text-secondaryDark file:bg-primaryLight hover:file:bg-primaryDark"
|
class="p-4 cursor-pointer transition file:transition file:cursor-pointer text-secondary hover:text-secondaryDark file:mr-2 file:py-2 file:px-4 file:rounded file:border-0 file:text-secondary hover:file:text-secondaryDark file:bg-primaryLight hover:file:bg-primaryDark"
|
||||||
:accept="acceptedFileTypes"
|
:accept="acceptedFileTypes"
|
||||||
|
multiple
|
||||||
@change="onFileChange"
|
@change="onFileChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p v-if="showFileSizeLimitExceededWarning" class="text-red-500 ml-10">
|
||||||
|
<template v-if="importFilesCount">
|
||||||
|
{{
|
||||||
|
t("import.file_size_limit_exceeded_warning_multiple_files", {
|
||||||
|
files:
|
||||||
|
importFilesCount === 1 ? "file" : `${importFilesCount} files`,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
{{ t("import.file_size_limit_exceeded_warning_single_file") }}
|
||||||
|
</template>
|
||||||
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<HoppButtonPrimary
|
<HoppButtonPrimary
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:label="t('import.title')"
|
:label="t('import.title')"
|
||||||
:disabled="!hasFile"
|
:disabled="!hasFile || showFileSizeLimitExceededWarning"
|
||||||
@click="emit('importFromFile', fileContent)"
|
@click="emit('importFromFile', fileContent)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,16 +67,30 @@ defineProps<{
|
|||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
|
const ALLOWED_FILE_SIZE_LIMIT = 10 // 10 MB
|
||||||
|
|
||||||
|
const importFilesCount = ref(0)
|
||||||
|
|
||||||
const hasFile = ref(false)
|
const hasFile = ref(false)
|
||||||
const fileContent = ref("")
|
const showFileSizeLimitExceededWarning = ref(false)
|
||||||
|
const fileContent = ref<string[]>([])
|
||||||
|
|
||||||
const inputChooseFileToImportFrom = ref<HTMLInputElement | any>()
|
const inputChooseFileToImportFrom = ref<HTMLInputElement | any>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "importFromFile", content: string): void
|
(e: "importFromFile", content: string[]): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const onFileChange = () => {
|
const onFileChange = async () => {
|
||||||
|
// Reset the state on entering the handler to avoid any stale state
|
||||||
|
if (showFileSizeLimitExceededWarning.value) {
|
||||||
|
showFileSizeLimitExceededWarning.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importFilesCount.value) {
|
||||||
|
importFilesCount.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
const inputFileToImport = inputChooseFileToImportFrom.value
|
const inputFileToImport = inputChooseFileToImportFrom.value
|
||||||
|
|
||||||
if (!inputFileToImport) {
|
if (!inputFileToImport) {
|
||||||
@@ -69,27 +99,52 @@ const onFileChange = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!inputFileToImport.files || inputFileToImport.files.length === 0) {
|
if (!inputFileToImport.files || inputFileToImport.files.length === 0) {
|
||||||
inputChooseFileToImportFrom.value[0].value = ""
|
inputChooseFileToImportFrom.value = ""
|
||||||
hasFile.value = false
|
hasFile.value = false
|
||||||
toast.show(t("action.choose_file").toString())
|
toast.show(t("action.choose_file").toString())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const readerPromises: Promise<string | null>[] = []
|
||||||
|
|
||||||
|
let totalFileSize = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < inputFileToImport.files.length; i++) {
|
||||||
|
const file = inputFileToImport.files[i]
|
||||||
|
|
||||||
|
totalFileSize += file.size / 1024 / 1024
|
||||||
|
|
||||||
|
if (totalFileSize > ALLOWED_FILE_SIZE_LIMIT) {
|
||||||
|
showFileSizeLimitExceededWarning.value = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
|
|
||||||
reader.onload = ({ target }) => {
|
readerPromises.push(
|
||||||
const content = target!.result as string | null
|
new Promise((resolve, reject) => {
|
||||||
if (!content) {
|
reader.onload = () => resolve(reader.result as string | null)
|
||||||
hasFile.value = false
|
reader.onerror = reject
|
||||||
toast.show(t("action.choose_file").toString())
|
reader.readAsText(file)
|
||||||
return
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileContent.value = content
|
importFilesCount.value = readerPromises.length
|
||||||
|
|
||||||
hasFile.value = !!content?.length
|
const results = await Promise.allSettled(readerPromises)
|
||||||
|
|
||||||
|
const contentsArr = results
|
||||||
|
.filter((result) => result.status === "fulfilled")
|
||||||
|
.map((result) => (result as { value: string | null }).value)
|
||||||
|
.filter(Boolean) as string[]
|
||||||
|
|
||||||
|
const errors = results.filter((result) => result.status === "rejected")
|
||||||
|
if (errors.length) {
|
||||||
|
toast.error(t("error.reading_files"))
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.readAsText(inputFileToImport.files[0])
|
fileContent.value = contentsArr
|
||||||
|
hasFile.value = contentsArr.length > 0
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col space-y-2 py-4">
|
<div class="flex flex-col space-y-2 py-4">
|
||||||
<span>
|
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
|
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
|
||||||
blank
|
blank
|
||||||
@@ -31,8 +30,6 @@
|
|||||||
:active-info-icon="hasChromeExtInstalled"
|
:active-info-icon="hasChromeExtInstalled"
|
||||||
outline
|
outline
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
|
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
|
||||||
blank
|
blank
|
||||||
@@ -42,7 +39,6 @@
|
|||||||
:active-info-icon="hasFirefoxExtInstalled"
|
:active-info-icon="hasFirefoxExtInstalled"
|
||||||
outline
|
outline
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-4 py-4">
|
<div class="space-y-4 py-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
|||||||
@@ -290,7 +290,12 @@ const widgets: Widget[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
type EmbedTabs = "params" | "bodyParams" | "headers" | "authorization"
|
type EmbedTabs =
|
||||||
|
| "params"
|
||||||
|
| "bodyParams"
|
||||||
|
| "headers"
|
||||||
|
| "authorization"
|
||||||
|
| "requestVariables"
|
||||||
|
|
||||||
type EmbedOption = {
|
type EmbedOption = {
|
||||||
selectedTab: EmbedTabs
|
selectedTab: EmbedTabs
|
||||||
|
|||||||
@@ -56,7 +56,12 @@ import { useI18n } from "~/composables/i18n"
|
|||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
type EmbedTabs = "params" | "bodyParams" | "headers" | "authorization"
|
type EmbedTabs =
|
||||||
|
| "params"
|
||||||
|
| "bodyParams"
|
||||||
|
| "headers"
|
||||||
|
| "authorization"
|
||||||
|
| "requestVariables"
|
||||||
|
|
||||||
type EmbedOption = {
|
type EmbedOption = {
|
||||||
selectedTab: EmbedTabs
|
selectedTab: EmbedTabs
|
||||||
|
|||||||
@@ -173,6 +173,11 @@ const embedOptions = ref<EmbedOption>({
|
|||||||
label: t("tab.authorization"),
|
label: t("tab.authorization"),
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: "requestVariables",
|
||||||
|
label: t("tab.variables"),
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
theme: "system",
|
theme: "system",
|
||||||
})
|
})
|
||||||
@@ -223,7 +228,12 @@ const currentUser = useReadonlyStream(
|
|||||||
|
|
||||||
const step = ref(1)
|
const step = ref(1)
|
||||||
|
|
||||||
type EmbedTabs = "params" | "bodyParams" | "headers" | "authorization"
|
type EmbedTabs =
|
||||||
|
| "params"
|
||||||
|
| "bodyParams"
|
||||||
|
| "headers"
|
||||||
|
| "authorization"
|
||||||
|
| "requestVariables"
|
||||||
|
|
||||||
type EmbedOption = {
|
type EmbedOption = {
|
||||||
selectedTab: EmbedTabs
|
selectedTab: EmbedTabs
|
||||||
@@ -369,6 +379,11 @@ const displayCustomizeRequestModal = (
|
|||||||
label: t("tab.authorization"),
|
label: t("tab.authorization"),
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: "requestVariables",
|
||||||
|
label: t("tab.variables"),
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
theme: "system",
|
theme: "system",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex"
|
class="flex overflow-x-scroll"
|
||||||
:class="[{ 'border-b pt-2 ': !noActiveTab }, embedColours]"
|
:class="[{ 'border-b pt-2 ': !noActiveTab }, embedColours]"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -57,7 +57,12 @@ import { computed } from "vue"
|
|||||||
|
|
||||||
import { useI18n } from "~/composables/i18n"
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
|
||||||
type Tabs = "params" | "bodyParams" | "headers" | "authorization"
|
type Tabs =
|
||||||
|
| "params"
|
||||||
|
| "bodyParams"
|
||||||
|
| "headers"
|
||||||
|
| "authorization"
|
||||||
|
| "requestVariables"
|
||||||
|
|
||||||
type EmbedOption = {
|
type EmbedOption = {
|
||||||
selectedTab: Tabs
|
selectedTab: Tabs
|
||||||
|
|||||||
@@ -79,29 +79,27 @@ import { history, historyKeymap } from "@codemirror/commands"
|
|||||||
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
|
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
|
||||||
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
|
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import {
|
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
|
||||||
AggregateEnvironment,
|
|
||||||
aggregateEnvsWithSecrets$,
|
|
||||||
} from "~/newstore/environments"
|
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { onClickOutside, useDebounceFn } from "@vueuse/core"
|
import { onClickOutside, useDebounceFn } from "@vueuse/core"
|
||||||
import { InspectorResult } from "~/services/inspection"
|
import { InspectorResult } from "~/services/inspection"
|
||||||
import { invokeAction } from "~/helpers/actions"
|
import { invokeAction } from "~/helpers/actions"
|
||||||
import { Environment } from "@hoppscotch/data"
|
|
||||||
import { useI18n } from "~/composables/i18n"
|
import { useI18n } from "~/composables/i18n"
|
||||||
import IconEye from "~icons/lucide/eye"
|
import IconEye from "~icons/lucide/eye"
|
||||||
import IconEyeoff from "~icons/lucide/eye-off"
|
import IconEyeoff from "~icons/lucide/eye-off"
|
||||||
|
import { CompletionContext, autocompletion } from "@codemirror/autocomplete"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
|
import { syntaxTree } from "@codemirror/language"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
type Env = Environment["variables"][number] & { source: string }
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
modelValue?: string
|
modelValue?: string
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
styles?: string
|
styles?: string
|
||||||
envs?: Env[] | null
|
envs?: AggregateEnvironment[] | null
|
||||||
focus?: boolean
|
focus?: boolean
|
||||||
selectTextOnMount?: boolean
|
selectTextOnMount?: boolean
|
||||||
environmentHighlights?: boolean
|
environmentHighlights?: boolean
|
||||||
@@ -110,6 +108,7 @@ const props = withDefaults(
|
|||||||
inspectionResults?: InspectorResult[] | undefined
|
inspectionResults?: InspectorResult[] | undefined
|
||||||
contextMenuEnabled?: boolean
|
contextMenuEnabled?: boolean
|
||||||
secret?: boolean
|
secret?: boolean
|
||||||
|
autoCompleteEnv?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
modelValue: "",
|
modelValue: "",
|
||||||
@@ -124,6 +123,7 @@ const props = withDefaults(
|
|||||||
inspectionResults: undefined,
|
inspectionResults: undefined,
|
||||||
contextMenuEnabled: true,
|
contextMenuEnabled: true,
|
||||||
secret: false,
|
secret: false,
|
||||||
|
autoCompleteEnvSource: false,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -353,29 +353,62 @@ watch(
|
|||||||
let clipboardEv: ClipboardEvent | null = null
|
let clipboardEv: ClipboardEvent | null = null
|
||||||
let pastedValue: string | null = null
|
let pastedValue: string | null = null
|
||||||
|
|
||||||
const aggregateEnvs = useReadonlyStream(aggregateEnvsWithSecrets$, []) as Ref<
|
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, []) as Ref<
|
||||||
AggregateEnvironment[]
|
AggregateEnvironment[]
|
||||||
>
|
>
|
||||||
|
|
||||||
|
const tabs = useService(RESTTabService)
|
||||||
|
|
||||||
const envVars = computed(() => {
|
const envVars = computed(() => {
|
||||||
return props.envs
|
if (props.envs) {
|
||||||
? props.envs.map((x) => {
|
return props.envs.map((x) => {
|
||||||
if (x.secret) {
|
const { key, secret } = x
|
||||||
|
const value = secret ? "********" : x.value
|
||||||
|
const sourceEnv = "sourceEnv" in x ? x.sourceEnv : null
|
||||||
return {
|
return {
|
||||||
key: x.key,
|
key,
|
||||||
sourceEnv: "source" in x ? x.source : null,
|
value,
|
||||||
value: "********",
|
sourceEnv,
|
||||||
}
|
secret,
|
||||||
}
|
|
||||||
return {
|
|
||||||
key: x.key,
|
|
||||||
value: x.value,
|
|
||||||
sourceEnv: "source" in x ? x.source : null,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
: aggregateEnvs.value
|
}
|
||||||
|
return [
|
||||||
|
...tabs.currentActiveTab.value.document.request.requestVariables.map(
|
||||||
|
({ active, key, value }) =>
|
||||||
|
active
|
||||||
|
? {
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
sourceEnv: "RequestVariable",
|
||||||
|
secret: false,
|
||||||
|
}
|
||||||
|
: ({} as AggregateEnvironment)
|
||||||
|
),
|
||||||
|
...aggregateEnvs.value,
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function envAutoCompletion(context: CompletionContext) {
|
||||||
|
const options = (envVars.value ?? [])
|
||||||
|
.map((env) => ({
|
||||||
|
label: env?.key ? `<<${env.key}>>` : "",
|
||||||
|
info: env?.value ?? "",
|
||||||
|
apply: env?.key ? `<<${env.key}>>` : "",
|
||||||
|
}))
|
||||||
|
.filter((x) => x)
|
||||||
|
|
||||||
|
const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1)
|
||||||
|
const textBefore = context.state.sliceDoc(nodeBefore.from, context.pos)
|
||||||
|
const tagBefore = /<<\w*$/.exec(textBefore)
|
||||||
|
if (!tagBefore && !context.explicit) return null
|
||||||
|
return {
|
||||||
|
from: tagBefore ? nodeBefore.from + tagBefore.index : context.pos,
|
||||||
|
options: options,
|
||||||
|
validFor: /^(<<\w*)?$/,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
|
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
|
||||||
|
|
||||||
function handleTextSelection() {
|
function handleTextSelection() {
|
||||||
@@ -469,6 +502,12 @@ const getExtensions = (readonly: boolean): Extension => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
props.autoCompleteEnv
|
||||||
|
? autocompletion({
|
||||||
|
activateOnTyping: true,
|
||||||
|
override: [envAutoCompletion],
|
||||||
|
})
|
||||||
|
: [],
|
||||||
ViewPlugin.fromClass(
|
ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
update(update: ViewUpdate) {
|
update(update: ViewUpdate) {
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ export function useCodemirror(
|
|||||||
if (from === to) return
|
if (from === to) return
|
||||||
const text = view.value?.state.doc.sliceString(from, to)
|
const text = view.value?.state.doc.sliceString(from, to)
|
||||||
const { top, left } = view.value?.coordsAtPos(from)
|
const { top, left } = view.value?.coordsAtPos(from)
|
||||||
if (text) {
|
if (text?.trim()) {
|
||||||
invokeAction("contextmenu.open", {
|
invokeAction("contextmenu.open", {
|
||||||
position: {
|
position: {
|
||||||
top,
|
top,
|
||||||
|
|||||||
@@ -1,293 +0,0 @@
|
|||||||
import {
|
|
||||||
FormDataKeyValue,
|
|
||||||
HoppRESTAuth,
|
|
||||||
HoppRESTHeader,
|
|
||||||
HoppRESTParam,
|
|
||||||
HoppRESTReqBody,
|
|
||||||
HoppRESTRequest,
|
|
||||||
RESTReqSchemaVersion,
|
|
||||||
ValidContentTypes,
|
|
||||||
} from "@hoppscotch/data"
|
|
||||||
import { BehaviorSubject, combineLatest, map } from "rxjs"
|
|
||||||
import { applyBodyTransition } from "~/helpers/rules/BodyTransition"
|
|
||||||
import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
|
||||||
|
|
||||||
export class RESTRequest {
|
|
||||||
public v$ = new BehaviorSubject<typeof RESTReqSchemaVersion>(
|
|
||||||
RESTReqSchemaVersion
|
|
||||||
)
|
|
||||||
public name$ = new BehaviorSubject("Untitled")
|
|
||||||
public endpoint$ = new BehaviorSubject("https://echo.hoppscotch.io/")
|
|
||||||
public params$ = new BehaviorSubject<HoppRESTParam[]>([])
|
|
||||||
public headers$ = new BehaviorSubject<HoppRESTHeader[]>([])
|
|
||||||
public method$ = new BehaviorSubject("GET")
|
|
||||||
public auth$ = new BehaviorSubject<HoppRESTAuth>({
|
|
||||||
authType: "none",
|
|
||||||
authActive: true,
|
|
||||||
})
|
|
||||||
public preRequestScript$ = new BehaviorSubject("")
|
|
||||||
public testScript$ = new BehaviorSubject("")
|
|
||||||
public body$ = new BehaviorSubject<HoppRESTReqBody>({
|
|
||||||
contentType: null,
|
|
||||||
body: null,
|
|
||||||
})
|
|
||||||
|
|
||||||
public response$ = new BehaviorSubject<HoppRESTResponse | null>(null)
|
|
||||||
|
|
||||||
get request$() {
|
|
||||||
// any of above changes construct requests
|
|
||||||
return combineLatest([
|
|
||||||
this.v$,
|
|
||||||
this.name$,
|
|
||||||
this.endpoint$,
|
|
||||||
this.params$,
|
|
||||||
this.headers$,
|
|
||||||
this.method$,
|
|
||||||
this.auth$,
|
|
||||||
this.preRequestScript$,
|
|
||||||
this.testScript$,
|
|
||||||
this.body$,
|
|
||||||
]).pipe(
|
|
||||||
map(
|
|
||||||
([
|
|
||||||
v,
|
|
||||||
name,
|
|
||||||
endpoint,
|
|
||||||
params,
|
|
||||||
headers,
|
|
||||||
method,
|
|
||||||
auth,
|
|
||||||
preRequestScript,
|
|
||||||
testScript,
|
|
||||||
body,
|
|
||||||
]) => ({
|
|
||||||
v,
|
|
||||||
name,
|
|
||||||
endpoint,
|
|
||||||
params,
|
|
||||||
headers,
|
|
||||||
method,
|
|
||||||
auth,
|
|
||||||
preRequestScript,
|
|
||||||
testScript,
|
|
||||||
body,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
get contentType$() {
|
|
||||||
return this.body$.pipe(map((body) => body.contentType))
|
|
||||||
}
|
|
||||||
|
|
||||||
get bodyContent$() {
|
|
||||||
return this.body$.pipe(map((body) => body.body))
|
|
||||||
}
|
|
||||||
|
|
||||||
get headersCount$() {
|
|
||||||
return this.headers$.pipe(
|
|
||||||
map(
|
|
||||||
(params) =>
|
|
||||||
params.filter((x) => x.active && (x.key !== "" || x.value !== ""))
|
|
||||||
.length
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
get paramsCount$() {
|
|
||||||
return this.params$.pipe(
|
|
||||||
map(
|
|
||||||
(params) =>
|
|
||||||
params.filter((x) => x.active && (x.key !== "" || x.value !== ""))
|
|
||||||
.length
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
setName(name: string) {
|
|
||||||
this.name$.next(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
setEndpoint(newURL: string) {
|
|
||||||
this.endpoint$.next(newURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
setMethod(newMethod: string) {
|
|
||||||
this.method$.next(newMethod)
|
|
||||||
}
|
|
||||||
|
|
||||||
setParams(entries: HoppRESTParam[]) {
|
|
||||||
this.params$.next(entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
addParam(newParam: HoppRESTParam) {
|
|
||||||
const newParams = this.params$.value.concat(newParam)
|
|
||||||
this.params$.next(newParams)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateParam(index: number, updatedParam: HoppRESTParam) {
|
|
||||||
const newParams = this.params$.value.map((param, i) =>
|
|
||||||
i === index ? updatedParam : param
|
|
||||||
)
|
|
||||||
this.params$.next(newParams)
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteParam(index: number) {
|
|
||||||
const newParams = this.params$.value.filter((_, i) => i !== index)
|
|
||||||
this.params$.next(newParams)
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteAllParams() {
|
|
||||||
this.params$.next([])
|
|
||||||
}
|
|
||||||
|
|
||||||
setHeaders(entries: HoppRESTHeader[]) {
|
|
||||||
this.headers$.next(entries)
|
|
||||||
}
|
|
||||||
|
|
||||||
addHeader(newHeader: HoppRESTHeader) {
|
|
||||||
const newHeaders = this.headers$.value.concat(newHeader)
|
|
||||||
this.headers$.next(newHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateHeader(index: number, updatedHeader: HoppRESTHeader) {
|
|
||||||
const newHeaders = this.headers$.value.map((header, i) =>
|
|
||||||
i === index ? updatedHeader : header
|
|
||||||
)
|
|
||||||
this.headers$.next(newHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteHeader(index: number) {
|
|
||||||
const newHeaders = this.headers$.value.filter((_, i) => i !== index)
|
|
||||||
this.headers$.next(newHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteAllHeaders() {
|
|
||||||
this.headers$.next([])
|
|
||||||
}
|
|
||||||
|
|
||||||
setContentType(newContentType: ValidContentTypes | null) {
|
|
||||||
// TODO: persist body evenafter switching content typees
|
|
||||||
this.body$.next(applyBodyTransition(this.body$.value, newContentType))
|
|
||||||
}
|
|
||||||
|
|
||||||
setBody(newBody: string | FormDataKeyValue[] | null) {
|
|
||||||
const body = { ...this.body$.value }
|
|
||||||
body.body = newBody
|
|
||||||
this.body$.next({ ...body })
|
|
||||||
}
|
|
||||||
|
|
||||||
addFormDataEntry(entry: FormDataKeyValue) {
|
|
||||||
if (this.body$.value.contentType !== "multipart/form-data") return {}
|
|
||||||
const body: HoppRESTReqBody = {
|
|
||||||
contentType: "multipart/form-data",
|
|
||||||
body: [...this.body$.value.body, entry],
|
|
||||||
}
|
|
||||||
this.body$.next(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteFormDataEntry(index: number) {
|
|
||||||
// Only perform update if the current content-type is formdata
|
|
||||||
if (this.body$.value.contentType !== "multipart/form-data") return {}
|
|
||||||
|
|
||||||
const body: HoppRESTReqBody = {
|
|
||||||
contentType: "multipart/form-data",
|
|
||||||
body: [...this.body$.value.body.filter((_, i) => i !== index)],
|
|
||||||
}
|
|
||||||
|
|
||||||
this.body$.next(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFormDataEntry(index: number, entry: FormDataKeyValue) {
|
|
||||||
// Only perform update if the current content-type is formdata
|
|
||||||
if (this.body$.value.contentType !== "multipart/form-data") return {}
|
|
||||||
|
|
||||||
const body: HoppRESTReqBody = {
|
|
||||||
contentType: "multipart/form-data",
|
|
||||||
body: [
|
|
||||||
...this.body$.value.body.map((oldEntry, i) =>
|
|
||||||
i === index ? entry : oldEntry
|
|
||||||
),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
this.body$.next(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteAllFormDataEntries() {
|
|
||||||
// Only perform update if the current content-type is formdata
|
|
||||||
if (this.body$.value.contentType !== "multipart/form-data") return {}
|
|
||||||
|
|
||||||
const body: HoppRESTReqBody = {
|
|
||||||
contentType: "multipart/form-data",
|
|
||||||
body: [],
|
|
||||||
}
|
|
||||||
this.body$.next(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
setRequestBody(newBody: HoppRESTReqBody) {
|
|
||||||
this.body$.next(newBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
setAuth(newAuth: HoppRESTAuth) {
|
|
||||||
this.auth$.next(newAuth)
|
|
||||||
}
|
|
||||||
|
|
||||||
setPreRequestScript(newScript: string) {
|
|
||||||
this.preRequestScript$.next(newScript)
|
|
||||||
}
|
|
||||||
|
|
||||||
setTestScript(newScript: string) {
|
|
||||||
this.testScript$.next(newScript)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateResponse(response: HoppRESTResponse | null) {
|
|
||||||
this.response$.next(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
setRequest(request: HoppRESTRequest) {
|
|
||||||
this.v$.next(RESTReqSchemaVersion)
|
|
||||||
this.name$.next(request.name)
|
|
||||||
this.endpoint$.next(request.endpoint)
|
|
||||||
this.params$.next(request.params)
|
|
||||||
this.headers$.next(request.headers)
|
|
||||||
this.method$.next(request.method)
|
|
||||||
this.auth$.next(request.auth)
|
|
||||||
this.preRequestScript$.next(request.preRequestScript)
|
|
||||||
this.testScript$.next(request.testScript)
|
|
||||||
this.body$.next(request.body)
|
|
||||||
}
|
|
||||||
|
|
||||||
getRequest() {
|
|
||||||
return {
|
|
||||||
v: this.v$.value,
|
|
||||||
name: this.name$.value,
|
|
||||||
endpoint: this.endpoint$.value,
|
|
||||||
params: this.params$.value,
|
|
||||||
headers: this.headers$.value,
|
|
||||||
method: this.method$.value,
|
|
||||||
auth: this.auth$.value,
|
|
||||||
preRequestScript: this.preRequestScript$.value,
|
|
||||||
testScript: this.testScript$.value,
|
|
||||||
body: this.body$.value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetRequest() {
|
|
||||||
this.v$.next(RESTReqSchemaVersion)
|
|
||||||
this.name$.next("")
|
|
||||||
this.endpoint$.next("")
|
|
||||||
this.params$.next([])
|
|
||||||
this.headers$.next([])
|
|
||||||
this.method$.next("GET")
|
|
||||||
this.auth$.next({
|
|
||||||
authType: "none",
|
|
||||||
authActive: false,
|
|
||||||
})
|
|
||||||
this.preRequestScript$.next("")
|
|
||||||
this.testScript$.next("")
|
|
||||||
this.body$.next({
|
|
||||||
contentType: null,
|
|
||||||
body: null,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
import { Environment } from "@hoppscotch/data"
|
import {
|
||||||
|
Environment,
|
||||||
|
HoppRESTHeaders,
|
||||||
|
HoppRESTRequestVariable,
|
||||||
|
} from "@hoppscotch/data"
|
||||||
import { SandboxTestResult, TestDescriptor } from "@hoppscotch/js-sandbox"
|
import { SandboxTestResult, TestDescriptor } from "@hoppscotch/js-sandbox"
|
||||||
import { runTestScript } from "@hoppscotch/js-sandbox/web"
|
import { runTestScript } from "@hoppscotch/js-sandbox/web"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
@@ -65,10 +69,17 @@ const getTestableBody = (
|
|||||||
return x
|
return x
|
||||||
}
|
}
|
||||||
|
|
||||||
const combineEnvVariables = (envs: {
|
const combineEnvVariables = (variables: {
|
||||||
global: Environment["variables"]
|
environments: {
|
||||||
selected: Environment["variables"]
|
selected: Environment["variables"]
|
||||||
}) => [...envs.selected, ...envs.global]
|
global: Environment["variables"]
|
||||||
|
}
|
||||||
|
requestVariables: Environment["variables"]
|
||||||
|
}) => [
|
||||||
|
...variables.requestVariables,
|
||||||
|
...variables.environments.selected,
|
||||||
|
...variables.environments.global,
|
||||||
|
]
|
||||||
|
|
||||||
export const executedResponses$ = new Subject<
|
export const executedResponses$ = new Subject<
|
||||||
HoppRESTResponse & { type: "success" | "fail " }
|
HoppRESTResponse & { type: "success" | "fail " }
|
||||||
@@ -122,6 +133,38 @@ const updateEnvironmentsWithSecret = (
|
|||||||
return updatedEnv
|
return updatedEnv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the environment list to a list with unique keys with value
|
||||||
|
* @param envs The environment list to be transformed
|
||||||
|
* @returns The transformed environment list with keys with value
|
||||||
|
*/
|
||||||
|
const filterNonEmptyEnvironmentVariables = (
|
||||||
|
envs: Environment["variables"]
|
||||||
|
): Environment["variables"] => {
|
||||||
|
const envsMap = new Map<string, Environment["variables"][number]>()
|
||||||
|
|
||||||
|
envs.forEach((env) => {
|
||||||
|
if (env.secret) {
|
||||||
|
envsMap.set(env.key, env)
|
||||||
|
} else if (envsMap.has(env.key)) {
|
||||||
|
const existingEnv = envsMap.get(env.key)
|
||||||
|
|
||||||
|
if (
|
||||||
|
existingEnv &&
|
||||||
|
"value" in existingEnv &&
|
||||||
|
existingEnv.value === "" &&
|
||||||
|
env.value !== ""
|
||||||
|
) {
|
||||||
|
envsMap.set(env.key, env)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
envsMap.set(env.key, env)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(envsMap.values())
|
||||||
|
}
|
||||||
|
|
||||||
export function runRESTRequest$(
|
export function runRESTRequest$(
|
||||||
tab: Ref<HoppTab<HoppRESTDocument>>
|
tab: Ref<HoppTab<HoppRESTDocument>>
|
||||||
): [
|
): [
|
||||||
@@ -175,15 +218,40 @@ export function runRESTRequest$(
|
|||||||
requestHeaders = [...tab.value.document.request.headers]
|
requestHeaders = [...tab.value.document.request.headers]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const finalRequestVariables =
|
||||||
|
tab.value.document.request.requestVariables.map(
|
||||||
|
(v: HoppRESTRequestVariable) => {
|
||||||
|
if (v.active) {
|
||||||
|
return {
|
||||||
|
key: v.key,
|
||||||
|
value: v.value,
|
||||||
|
secret: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const finalRequest = {
|
const finalRequest = {
|
||||||
...tab.value.document.request,
|
...tab.value.document.request,
|
||||||
auth: requestAuth ?? { authType: "none", authActive: false },
|
auth: requestAuth ?? { authType: "none", authActive: false },
|
||||||
headers: requestHeaders,
|
headers: requestHeaders as HoppRESTHeaders,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const finalEnvs = {
|
||||||
|
requestVariables: finalRequestVariables as Environment["variables"],
|
||||||
|
environments: envs.right,
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalEnvsWithNonEmptyValues = filterNonEmptyEnvironmentVariables(
|
||||||
|
combineEnvVariables(finalEnvs)
|
||||||
|
)
|
||||||
|
|
||||||
const effectiveRequest = getEffectiveRESTRequest(finalRequest, {
|
const effectiveRequest = getEffectiveRESTRequest(finalRequest, {
|
||||||
|
id: "env-id",
|
||||||
|
v: 1,
|
||||||
name: "Env",
|
name: "Env",
|
||||||
variables: combineEnvVariables(envs.right),
|
variables: finalEnvsWithNonEmptyValues,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [stream, cancelRun] = createRESTNetworkRequestStream(effectiveRequest)
|
const [stream, cancelRun] = createRESTNetworkRequestStream(effectiveRequest)
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const samples = [
|
|||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -141,6 +142,7 @@ const samples = [
|
|||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -158,6 +160,7 @@ const samples = [
|
|||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -181,6 +184,7 @@ const samples = [
|
|||||||
],
|
],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -209,6 +213,7 @@ const samples = [
|
|||||||
],
|
],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -239,6 +244,7 @@ const samples = [
|
|||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -268,6 +274,7 @@ const samples = [
|
|||||||
],
|
],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -291,6 +298,7 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -312,6 +320,7 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -335,6 +344,7 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -366,6 +376,7 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -430,6 +441,7 @@ const samples = [
|
|||||||
],
|
],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -458,6 +470,7 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -515,6 +528,7 @@ const samples = [
|
|||||||
],
|
],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -560,6 +574,7 @@ const samples = [
|
|||||||
],
|
],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -607,6 +622,7 @@ const samples = [
|
|||||||
],
|
],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -621,6 +637,7 @@ const samples = [
|
|||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -645,6 +662,7 @@ const samples = [
|
|||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -662,6 +680,7 @@ const samples = [
|
|||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -685,6 +704,7 @@ const samples = [
|
|||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -702,6 +722,7 @@ const samples = [
|
|||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -725,6 +746,7 @@ const samples = [
|
|||||||
],
|
],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -747,6 +769,7 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -767,6 +790,7 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -788,6 +812,7 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -808,6 +833,7 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -839,6 +865,7 @@ const samples = [
|
|||||||
headers: [],
|
headers: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
requestVariables: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -181,5 +181,6 @@ export const parseCurlCommand = (curlCommand: string) => {
|
|||||||
testScript: defaultRESTReq.testScript,
|
testScript: defaultRESTReq.testScript,
|
||||||
auth,
|
auth,
|
||||||
body: finalBody,
|
body: finalBody,
|
||||||
|
requestVariables: defaultRESTReq.requestVariables,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { StreamSubscriberFunc } from "@composables/stream"
|
|||||||
import {
|
import {
|
||||||
AggregateEnvironment,
|
AggregateEnvironment,
|
||||||
aggregateEnvsWithSecrets$,
|
aggregateEnvsWithSecrets$,
|
||||||
getAggregateEnvsWithSecrets,
|
getAggregateEnvs,
|
||||||
getCurrentEnvironment,
|
getCurrentEnvironment,
|
||||||
getSelectedEnvironmentType,
|
getSelectedEnvironmentType,
|
||||||
} from "~/newstore/environments"
|
} from "~/newstore/environments"
|
||||||
@@ -23,15 +23,45 @@ import IconUsers from "~icons/lucide/users?raw"
|
|||||||
import IconEdit from "~icons/lucide/edit?raw"
|
import IconEdit from "~icons/lucide/edit?raw"
|
||||||
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||||
import { getService } from "~/modules/dioc"
|
import { getService } from "~/modules/dioc"
|
||||||
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
|
|
||||||
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
|
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
|
||||||
|
|
||||||
const HOPP_ENV_HIGHLIGHT =
|
const HOPP_ENV_HIGHLIGHT =
|
||||||
"cursor-help transition rounded px-1 focus:outline-none mx-0.5 env-highlight"
|
"cursor-help transition rounded px-1 focus:outline-none mx-0.5 env-highlight"
|
||||||
const HOPP_ENV_HIGHLIGHT_FOUND = "env-found"
|
|
||||||
const HOPP_ENV_HIGHLIGHT_NOT_FOUND = "env-not-found"
|
const HOPP_REQUEST_VARIABLE_HIGHLIGHT = "request-variable-highlight"
|
||||||
|
const HOPP_ENVIRONMENT_HIGHLIGHT = "environment-variable-highlight"
|
||||||
|
const HOPP_GLOBAL_ENVIRONMENT_HIGHLIGHT = "global-variable-highlight"
|
||||||
|
const HOPP_ENV_HIGHLIGHT_NOT_FOUND = "environment-not-found-highlight"
|
||||||
|
|
||||||
const secretEnvironmentService = getService(SecretEnvironmentService)
|
const secretEnvironmentService = getService(SecretEnvironmentService)
|
||||||
|
const restTabs = getService(RESTTabService)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the environment list to a list with unique keys with value
|
||||||
|
* @param envs The environment list to be transformed
|
||||||
|
* @returns The transformed environment list with keys with value
|
||||||
|
*/
|
||||||
|
const filterNonEmptyEnvironmentVariables = (
|
||||||
|
envs: AggregateEnvironment[]
|
||||||
|
): AggregateEnvironment[] => {
|
||||||
|
const envsMap = new Map<string, AggregateEnvironment>()
|
||||||
|
|
||||||
|
envs.forEach((env) => {
|
||||||
|
if (envsMap.has(env.key)) {
|
||||||
|
const existingEnv = envsMap.get(env.key)
|
||||||
|
|
||||||
|
if (existingEnv?.value === "" && env.value !== "") {
|
||||||
|
envsMap.set(env.key, env)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
envsMap.set(env.key, env)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(envsMap.values())
|
||||||
|
}
|
||||||
|
|
||||||
const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
||||||
hoverTooltip(
|
hoverTooltip(
|
||||||
@@ -67,7 +97,12 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
|||||||
|
|
||||||
const parsedEnvKey = text.slice(start - from, end - from)
|
const parsedEnvKey = text.slice(start - from, end - from)
|
||||||
|
|
||||||
const tooltipEnv = aggregateEnvs.find((env) => env.key === parsedEnvKey)
|
const envsWithNoEmptyValues =
|
||||||
|
filterNonEmptyEnvironmentVariables(aggregateEnvs)
|
||||||
|
|
||||||
|
const tooltipEnv = envsWithNoEmptyValues.find(
|
||||||
|
(env) => env.key === parsedEnvKey
|
||||||
|
)
|
||||||
|
|
||||||
const envName = tooltipEnv?.sourceEnv ?? "Choose an Environment"
|
const envName = tooltipEnv?.sourceEnv ?? "Choose an Environment"
|
||||||
|
|
||||||
@@ -95,7 +130,12 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
|||||||
|
|
||||||
const result = parseTemplateStringE(envValue, aggregateEnvs)
|
const result = parseTemplateStringE(envValue, aggregateEnvs)
|
||||||
|
|
||||||
const finalEnv = E.isLeft(result) ? "error" : result.right
|
let finalEnv = E.isLeft(result) ? "error" : result.right
|
||||||
|
|
||||||
|
// If the request variable has an secret variable
|
||||||
|
// parseTemplateStringE is passed the secret value which has value undefined
|
||||||
|
// So, we need to check if the result is undefined and then set the finalEnv to ******
|
||||||
|
if (finalEnv === "undefined") finalEnv = "******"
|
||||||
|
|
||||||
const selectedEnvType = getSelectedEnvironmentType()
|
const selectedEnvType = getSelectedEnvironmentType()
|
||||||
|
|
||||||
@@ -123,11 +163,16 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
|||||||
invokeActionType = "modals.my.environment.edit"
|
invokeActionType = "modals.my.environment.edit"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tooltipEnv?.sourceEnv === "RequestVariable") {
|
||||||
|
restTabs.currentActiveTab.value.document.optionTabPreference =
|
||||||
|
"requestVariables"
|
||||||
|
} else {
|
||||||
invokeAction(invokeActionType, {
|
invokeAction(invokeActionType, {
|
||||||
envName: tooltipEnv?.sourceEnv !== "Global" ? envName : "Global",
|
envName: tooltipEnv?.sourceEnv === "Global" ? "Global" : envName,
|
||||||
variableName: parsedEnvKey,
|
variableName: parsedEnvKey,
|
||||||
isSecret: tooltipEnv?.secret,
|
isSecret: tooltipEnv?.secret,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
editIcon.innerHTML = `<span class="inline-flex items-center justify-center my-1">${IconEdit}</span>`
|
editIcon.innerHTML = `<span class="inline-flex items-center justify-center my-1">${IconEdit}</span>`
|
||||||
tooltip.appendChild(editIcon)
|
tooltip.appendChild(editIcon)
|
||||||
@@ -165,11 +210,16 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
function checkEnv(env: string, aggregateEnvs: AggregateEnvironment[]) {
|
function checkEnv(env: string, aggregateEnvs: AggregateEnvironment[]) {
|
||||||
const className = aggregateEnvs.find(
|
let className = HOPP_ENV_HIGHLIGHT_NOT_FOUND
|
||||||
|
|
||||||
|
const envSource = aggregateEnvs.find(
|
||||||
(k: { key: string }) => k.key === env.slice(2, -2)
|
(k: { key: string }) => k.key === env.slice(2, -2)
|
||||||
)
|
)?.sourceEnv
|
||||||
? HOPP_ENV_HIGHLIGHT_FOUND
|
|
||||||
: HOPP_ENV_HIGHLIGHT_NOT_FOUND
|
if (envSource === "RequestVariable")
|
||||||
|
className = HOPP_REQUEST_VARIABLE_HIGHLIGHT
|
||||||
|
else if (envSource === "Global") className = HOPP_GLOBAL_ENVIRONMENT_HIGHLIGHT
|
||||||
|
else if (envSource !== undefined) className = HOPP_ENVIRONMENT_HIGHLIGHT
|
||||||
|
|
||||||
return Decoration.mark({
|
return Decoration.mark({
|
||||||
class: `${HOPP_ENV_HIGHLIGHT} ${className}`,
|
class: `${HOPP_ENV_HIGHLIGHT} ${className}`,
|
||||||
@@ -185,7 +235,10 @@ const getMatchDecorator = (aggregateEnvs: AggregateEnvironment[]) =>
|
|||||||
export const environmentHighlightStyle = (
|
export const environmentHighlightStyle = (
|
||||||
aggregateEnvs: AggregateEnvironment[]
|
aggregateEnvs: AggregateEnvironment[]
|
||||||
) => {
|
) => {
|
||||||
const decorator = getMatchDecorator(aggregateEnvs)
|
const envsWithNoEmptyValues =
|
||||||
|
filterNonEmptyEnvironmentVariables(aggregateEnvs)
|
||||||
|
|
||||||
|
const decorator = getMatchDecorator(envsWithNoEmptyValues)
|
||||||
|
|
||||||
return ViewPlugin.define(
|
return ViewPlugin.define(
|
||||||
(view) => ({
|
(view) => ({
|
||||||
@@ -209,10 +262,45 @@ export class HoppEnvironmentPlugin {
|
|||||||
subscribeToStream: StreamSubscriberFunc,
|
subscribeToStream: StreamSubscriberFunc,
|
||||||
private editorView: Ref<EditorView | undefined>
|
private editorView: Ref<EditorView | undefined>
|
||||||
) {
|
) {
|
||||||
this.envs = getAggregateEnvsWithSecrets()
|
const aggregateEnvs = getAggregateEnvs()
|
||||||
|
const currentTab = restTabs.currentActiveTab.value
|
||||||
|
|
||||||
|
watch(
|
||||||
|
currentTab.document.request,
|
||||||
|
(reqVariables) => {
|
||||||
|
this.envs = [
|
||||||
|
...reqVariables.requestVariables.map(({ key, value }) => ({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
sourceEnv: "RequestVariable",
|
||||||
|
secret: false,
|
||||||
|
})),
|
||||||
|
...aggregateEnvs,
|
||||||
|
]
|
||||||
|
|
||||||
|
this.editorView.value?.dispatch({
|
||||||
|
effects: this.compartment.reconfigure([
|
||||||
|
cursorTooltipField(this.envs),
|
||||||
|
environmentHighlightStyle(this.envs),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
subscribeToStream(aggregateEnvsWithSecrets$, (envs) => {
|
subscribeToStream(aggregateEnvsWithSecrets$, (envs) => {
|
||||||
this.envs = envs
|
this.envs = [
|
||||||
|
...currentTab.document.request.requestVariables.map(
|
||||||
|
({ key, value }) => ({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
sourceEnv: "RequestVariable",
|
||||||
|
secret: false,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
...envs,
|
||||||
|
]
|
||||||
|
|
||||||
this.editorView.value?.dispatch({
|
this.editorView.value?.dispatch({
|
||||||
effects: this.compartment.reconfigure([
|
effects: this.compartment.reconfigure([
|
||||||
cursorTooltipField(this.envs),
|
cursorTooltipField(this.envs),
|
||||||
@@ -251,7 +339,7 @@ export class HoppReactiveEnvPlugin {
|
|||||||
]),
|
]),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true, deep: true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { pipe, flow } from "fp-ts/function"
|
|||||||
import * as TE from "fp-ts/TaskEither"
|
import * as TE from "fp-ts/TaskEither"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
import * as RA from "fp-ts/ReadonlyArray"
|
import * as RA from "fp-ts/ReadonlyArray"
|
||||||
|
import * as A from "fp-ts/Array"
|
||||||
import { translateToNewRESTCollection, HoppCollection } from "@hoppscotch/data"
|
import { translateToNewRESTCollection, HoppCollection } from "@hoppscotch/data"
|
||||||
import { isPlainObject as _isPlainObject } from "lodash-es"
|
import { isPlainObject as _isPlainObject } from "lodash-es"
|
||||||
|
|
||||||
@@ -9,11 +10,13 @@ import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
|||||||
import { safeParseJSON } from "~/helpers/functional/json"
|
import { safeParseJSON } from "~/helpers/functional/json"
|
||||||
import { translateToNewGQLCollection } from "@hoppscotch/data"
|
import { translateToNewGQLCollection } from "@hoppscotch/data"
|
||||||
|
|
||||||
export const hoppRESTImporter = (content: string) =>
|
export const hoppRESTImporter = (content: string[]) =>
|
||||||
pipe(
|
pipe(
|
||||||
safeParseJSON(content),
|
content,
|
||||||
|
A.traverse(O.Applicative)((str) => safeParseJSON(str, true)),
|
||||||
O.chain(
|
O.chain(
|
||||||
flow(
|
flow(
|
||||||
|
A.flatten,
|
||||||
makeCollectionsArray,
|
makeCollectionsArray,
|
||||||
RA.map(validateCollection),
|
RA.map(validateCollection),
|
||||||
O.sequenceArray,
|
O.sequenceArray,
|
||||||
|
|||||||
@@ -8,17 +8,35 @@ import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
|||||||
import { Environment } from "@hoppscotch/data"
|
import { Environment } from "@hoppscotch/data"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
export const hoppEnvImporter = (content: string) => {
|
export const hoppEnvImporter = (contents: string[]) => {
|
||||||
const parsedContent = safeParseJSON(content, true)
|
const parsedContents = contents.map((str) => safeParseJSON(str, true))
|
||||||
|
|
||||||
// parse json from the environments string
|
if (parsedContents.some((parsed) => O.isNone(parsed))) {
|
||||||
if (O.isNone(parsedContent)) {
|
|
||||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parsedValues = parsedContents.flatMap((content) => {
|
||||||
|
const unwrappedContent = O.toNullable(content) as Environment[] | null
|
||||||
|
|
||||||
|
if (unwrappedContent) {
|
||||||
|
return unwrappedContent.map((contentEntry) => {
|
||||||
|
return {
|
||||||
|
...contentEntry,
|
||||||
|
variables: contentEntry.variables?.map((valueEntry) => ({
|
||||||
|
...valueEntry,
|
||||||
|
...("value" in valueEntry
|
||||||
|
? { value: String(valueEntry.value) }
|
||||||
|
: {}),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
const validationResult = z
|
const validationResult = z
|
||||||
.array(entityReference(Environment))
|
.array(entityReference(Environment))
|
||||||
.safeParse(parsedContent.value)
|
.safeParse(parsedValues)
|
||||||
|
|
||||||
if (!validationResult.success) {
|
if (!validationResult.success) {
|
||||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import * as E from "fp-ts/Either"
|
|||||||
|
|
||||||
// TODO: add zod validation
|
// TODO: add zod validation
|
||||||
export const hoppGqlCollectionsImporter = (
|
export const hoppGqlCollectionsImporter = (
|
||||||
content: string
|
contents: string[]
|
||||||
): E.Either<"INVALID_JSON", HoppCollection[]> => {
|
): E.Either<"INVALID_JSON", HoppCollection[]> => {
|
||||||
return E.tryCatch(
|
return E.tryCatch(
|
||||||
() => JSON.parse(content) as HoppCollection[],
|
() => contents.flatMap((content) => JSON.parse(content)),
|
||||||
() => "INVALID_JSON"
|
() => "INVALID_JSON"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { v4 as uuidv4 } from "uuid"
|
|||||||
export function FileSource(metadata: {
|
export function FileSource(metadata: {
|
||||||
acceptedFileTypes: string
|
acceptedFileTypes: string
|
||||||
caption: string
|
caption: string
|
||||||
onImportFromFile: (content: string) => any | Promise<any>
|
onImportFromFile: (content: string[]) => any | Promise<any>
|
||||||
}) {
|
}) {
|
||||||
const stepID = uuidv4()
|
const stepID = uuidv4()
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import { v4 as uuidv4 } from "uuid"
|
|||||||
export function GistSource(metadata: {
|
export function GistSource(metadata: {
|
||||||
caption: string
|
caption: string
|
||||||
onImportFromGist: (
|
onImportFromGist: (
|
||||||
importResult: E.Either<string, string>
|
importResult: E.Either<string, string[]>
|
||||||
) => any | Promise<any>
|
) => any | Promise<any>
|
||||||
}) {
|
}) {
|
||||||
const stepID = uuidv4()
|
const stepID = uuidv4()
|
||||||
|
|
||||||
return defineStep(stepID, UrlImport, () => ({
|
return defineStep(stepID, UrlImport, () => ({
|
||||||
caption: metadata.caption,
|
caption: metadata.caption,
|
||||||
onImportFromURL: (gistResponse) => {
|
onImportFromURL: (gistResponse: Record<string, unknown>) => {
|
||||||
const fileSchema = z.object({
|
const fileSchema = z.object({
|
||||||
files: z.record(z.object({ content: z.string() })),
|
files: z.record(z.object({ content: z.string() })),
|
||||||
})
|
})
|
||||||
@@ -29,9 +29,11 @@ export function GistSource(metadata: {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = Object.values(parseResult.data.files)[0].content
|
const contents = Object.values(parseResult.data.files).map(
|
||||||
|
({ content }) => content
|
||||||
|
)
|
||||||
|
|
||||||
metadata.onImportFromGist(E.right(content))
|
metadata.onImportFromGist(E.right(contents))
|
||||||
},
|
},
|
||||||
fetchLogic: fetchGistFromUrl,
|
fetchLogic: fetchGistFromUrl,
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
import { convert, ImportRequest } from "insomnia-importers"
|
|
||||||
import { pipe } from "fp-ts/function"
|
|
||||||
import {
|
import {
|
||||||
|
HoppCollection,
|
||||||
HoppRESTAuth,
|
HoppRESTAuth,
|
||||||
HoppRESTHeader,
|
HoppRESTHeader,
|
||||||
HoppRESTParam,
|
HoppRESTParam,
|
||||||
HoppRESTReqBody,
|
HoppRESTReqBody,
|
||||||
HoppRESTRequest,
|
HoppRESTRequest,
|
||||||
knownContentTypes,
|
knownContentTypes,
|
||||||
makeRESTRequest,
|
|
||||||
HoppCollection,
|
|
||||||
makeCollection,
|
makeCollection,
|
||||||
|
makeRESTRequest,
|
||||||
|
HoppRESTRequestVariable,
|
||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
|
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
import * as TO from "fp-ts/TaskOption"
|
|
||||||
import * as TE from "fp-ts/TaskEither"
|
import * as TE from "fp-ts/TaskEither"
|
||||||
|
import * as TO from "fp-ts/TaskOption"
|
||||||
|
import { pipe } from "fp-ts/function"
|
||||||
|
import { ImportRequest, convert } from "insomnia-importers"
|
||||||
|
|
||||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||||
import { replaceInsomniaTemplating } from "./insomniaEnv"
|
import { replaceInsomniaTemplating } from "./insomniaEnv"
|
||||||
|
|
||||||
@@ -26,14 +29,27 @@ type UnwrapPromise<T extends Promise<any>> = T extends Promise<infer Y>
|
|||||||
type InsomniaDoc = UnwrapPromise<ReturnType<typeof convert>>
|
type InsomniaDoc = UnwrapPromise<ReturnType<typeof convert>>
|
||||||
type InsomniaResource = ImportRequest
|
type InsomniaResource = ImportRequest
|
||||||
|
|
||||||
|
// insomnia-importers v3.6.0 doesn't provide a type for path parameters and they have deprecated the library
|
||||||
|
type InsomniaPathParameter = {
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
type InsomniaFolderResource = ImportRequest & { _type: "request_group" }
|
type InsomniaFolderResource = ImportRequest & { _type: "request_group" }
|
||||||
type InsomniaRequestResource = ImportRequest & { _type: "request" }
|
type InsomniaRequestResource = ImportRequest & {
|
||||||
|
_type: "request"
|
||||||
|
} & {
|
||||||
|
pathParameters?: InsomniaPathParameter[]
|
||||||
|
}
|
||||||
|
|
||||||
const parseInsomniaDoc = (content: string) =>
|
const parseInsomniaDoc = (content: string) =>
|
||||||
TO.tryCatch(() => convert(content))
|
TO.tryCatch(() => convert(content))
|
||||||
|
|
||||||
|
const replacePathVarTemplating = (expression: string) =>
|
||||||
|
expression.replaceAll(/:([^/]+)/g, "<<$1>>")
|
||||||
|
|
||||||
const replaceVarTemplating = (expression: string) =>
|
const replaceVarTemplating = (expression: string) =>
|
||||||
replaceInsomniaTemplating(expression)
|
pipe(expression, replacePathVarTemplating, replaceInsomniaTemplating)
|
||||||
|
|
||||||
const getFoldersIn = (
|
const getFoldersIn = (
|
||||||
folder: InsomniaFolderResource | null,
|
folder: InsomniaFolderResource | null,
|
||||||
@@ -175,6 +191,15 @@ const getHoppReqParams = (req: InsomniaRequestResource): HoppRESTParam[] =>
|
|||||||
active: !(param.disabled ?? false),
|
active: !(param.disabled ?? false),
|
||||||
})) ?? []
|
})) ?? []
|
||||||
|
|
||||||
|
const getHoppReqVariables = (
|
||||||
|
req: InsomniaRequestResource
|
||||||
|
): HoppRESTRequestVariable[] =>
|
||||||
|
req.pathParameters?.map((variable) => ({
|
||||||
|
key: replaceVarTemplating(variable.name),
|
||||||
|
value: replaceVarTemplating(variable.value ?? ""),
|
||||||
|
active: true,
|
||||||
|
})) ?? []
|
||||||
|
|
||||||
const getHoppRequest = (req: InsomniaRequestResource): HoppRESTRequest =>
|
const getHoppRequest = (req: InsomniaRequestResource): HoppRESTRequest =>
|
||||||
makeRESTRequest({
|
makeRESTRequest({
|
||||||
name: req.name ?? "Untitled Request",
|
name: req.name ?? "Untitled Request",
|
||||||
@@ -187,6 +212,8 @@ const getHoppRequest = (req: InsomniaRequestResource): HoppRESTRequest =>
|
|||||||
|
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
|
||||||
|
requestVariables: getHoppReqVariables(req),
|
||||||
})
|
})
|
||||||
|
|
||||||
const getHoppFolder = (
|
const getHoppFolder = (
|
||||||
@@ -203,15 +230,18 @@ const getHoppFolder = (
|
|||||||
headers: [],
|
headers: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
const getHoppCollections = (doc: InsomniaDoc) =>
|
const getHoppCollections = (docs: InsomniaDoc[]) => {
|
||||||
getFoldersIn(null, doc.data.resources).map((f) =>
|
return docs.flatMap((doc) => {
|
||||||
|
return getFoldersIn(null, doc.data.resources).map((f) =>
|
||||||
getHoppFolder(f, doc.data.resources)
|
getHoppFolder(f, doc.data.resources)
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const hoppInsomniaImporter = (fileContent: string) =>
|
export const hoppInsomniaImporter = (fileContents: string[]) =>
|
||||||
pipe(
|
pipe(
|
||||||
fileContent,
|
fileContents,
|
||||||
parseInsomniaDoc,
|
A.traverse(TO.ApplicativeSeq)(parseInsomniaDoc),
|
||||||
TO.map(getHoppCollections),
|
TO.map(getHoppCollections),
|
||||||
TE.fromTaskOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
TE.fromTaskOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,22 +29,24 @@ export const replaceInsomniaTemplating = (expression: string) => {
|
|||||||
return expression.replaceAll(regex, "<<$1>>")
|
return expression.replaceAll(regex, "<<$1>>")
|
||||||
}
|
}
|
||||||
|
|
||||||
export const insomniaEnvImporter = (content: string) => {
|
export const insomniaEnvImporter = (contents: string[]) => {
|
||||||
const parsedContent = safeParseJSONOrYAML(content)
|
const parsedContents = contents.map((str) => safeParseJSONOrYAML(str))
|
||||||
|
if (parsedContents.some((parsed) => O.isNone(parsed))) {
|
||||||
if (O.isNone(parsedContent)) {
|
|
||||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||||
}
|
}
|
||||||
|
|
||||||
const validationResult = insomniaResourcesSchema.safeParse(
|
const parsedValues = parsedContents.map((parsed) => O.toNullable(parsed))
|
||||||
parsedContent.value
|
|
||||||
)
|
const validationResult = z
|
||||||
|
.array(insomniaResourcesSchema)
|
||||||
|
.safeParse(parsedValues)
|
||||||
|
|
||||||
if (!validationResult.success) {
|
if (!validationResult.success) {
|
||||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||||
}
|
}
|
||||||
|
|
||||||
const insomniaEnvs = validationResult.data.resources
|
const insomniaEnvs = validationResult.data.flatMap(({ resources }) => {
|
||||||
|
return resources
|
||||||
.filter((resource) => resource._type === "environment")
|
.filter((resource) => resource._type === "environment")
|
||||||
.map((envResource) => {
|
.map((envResource) => {
|
||||||
const envResourceData = envResource.data as Record<string, unknown>
|
const envResourceData = envResource.data as Record<string, unknown>
|
||||||
@@ -56,6 +58,7 @@ export const insomniaEnvImporter = (content: string) => {
|
|||||||
|
|
||||||
return { ...envResource, data: stringifiedData }
|
return { ...envResource, data: stringifiedData }
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const environments: NonSecretEnvironment[] = []
|
const environments: NonSecretEnvironment[] = []
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
makeRESTRequest,
|
makeRESTRequest,
|
||||||
HoppCollection,
|
HoppCollection,
|
||||||
makeCollection,
|
makeCollection,
|
||||||
|
HoppRESTRequestVariable,
|
||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
import { pipe, flow } from "fp-ts/function"
|
import { pipe, flow } from "fp-ts/function"
|
||||||
import * as A from "fp-ts/Array"
|
import * as A from "fp-ts/Array"
|
||||||
@@ -82,6 +83,27 @@ const parseOpenAPIParams = (params: OpenAPIParamsType[]): HoppRESTParam[] =>
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const parseOpenAPIVariables = (
|
||||||
|
variables: OpenAPIParamsType[]
|
||||||
|
): HoppRESTRequestVariable[] =>
|
||||||
|
pipe(
|
||||||
|
variables,
|
||||||
|
|
||||||
|
A.filterMap(
|
||||||
|
flow(
|
||||||
|
O.fromPredicate((param) => param.in === "path"),
|
||||||
|
O.map(
|
||||||
|
(param) =>
|
||||||
|
<HoppRESTRequestVariable>{
|
||||||
|
key: param.name,
|
||||||
|
value: "", // TODO: Can we do anything more ? (parse default values maybe)
|
||||||
|
active: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const parseOpenAPIHeaders = (params: OpenAPIParamsType[]): HoppRESTHeader[] =>
|
const parseOpenAPIHeaders = (params: OpenAPIParamsType[]): HoppRESTHeader[] =>
|
||||||
pipe(
|
pipe(
|
||||||
params,
|
params,
|
||||||
@@ -577,6 +599,10 @@ const convertPathToHoppReqs = (
|
|||||||
|
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
|
|
||||||
|
requestVariables: parseOpenAPIVariables(
|
||||||
|
(info.parameters as OpenAPIParamsType[] | undefined) ?? []
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -584,24 +610,28 @@ const convertPathToHoppReqs = (
|
|||||||
RA.toArray
|
RA.toArray
|
||||||
)
|
)
|
||||||
|
|
||||||
const convertOpenApiDocToHopp = (
|
const convertOpenApiDocsToHopp = (
|
||||||
doc: OpenAPI.Document
|
docs: OpenAPI.Document[]
|
||||||
): TE.TaskEither<never, HoppCollection[]> => {
|
): TE.TaskEither<never, HoppCollection[]> => {
|
||||||
|
const collections = docs.map((doc) => {
|
||||||
const name = doc.info.title
|
const name = doc.info.title
|
||||||
|
|
||||||
const paths = Object.entries(doc.paths ?? {})
|
const paths = Object.entries(doc.paths ?? {})
|
||||||
.map(([pathName, pathObj]) => convertPathToHoppReqs(doc, pathName, pathObj))
|
.map(([pathName, pathObj]) =>
|
||||||
|
convertPathToHoppReqs(doc, pathName, pathObj)
|
||||||
|
)
|
||||||
.flat()
|
.flat()
|
||||||
|
|
||||||
return TE.of([
|
return makeCollection({
|
||||||
makeCollection({
|
|
||||||
name,
|
name,
|
||||||
folders: [],
|
folders: [],
|
||||||
requests: paths,
|
requests: paths,
|
||||||
auth: { authType: "inherit", authActive: true },
|
auth: { authType: "inherit", authActive: true },
|
||||||
headers: [],
|
headers: [],
|
||||||
}),
|
})
|
||||||
])
|
})
|
||||||
|
|
||||||
|
return TE.of(collections)
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseOpenAPIDocContent = (str: string) =>
|
const parseOpenAPIDocContent = (str: string) =>
|
||||||
@@ -614,29 +644,49 @@ const parseOpenAPIDocContent = (str: string) =>
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
export const hoppOpenAPIImporter = (fileContent: string) =>
|
export const hoppOpenAPIImporter = (fileContents: string[]) =>
|
||||||
pipe(
|
pipe(
|
||||||
// See if we can parse JSON properly
|
// See if we can parse JSON properly
|
||||||
fileContent,
|
fileContents,
|
||||||
parseOpenAPIDocContent,
|
A.traverse(O.Applicative)(parseOpenAPIDocContent),
|
||||||
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT),
|
TE.fromOption(() => {
|
||||||
|
return IMPORTER_INVALID_FILE_FORMAT
|
||||||
|
}),
|
||||||
// Try validating, else the importer is invalid file format
|
// Try validating, else the importer is invalid file format
|
||||||
TE.chainW((obj) =>
|
TE.chainW((docArr) => {
|
||||||
pipe(
|
return pipe(
|
||||||
TE.tryCatch(
|
TE.tryCatch(
|
||||||
() => SwaggerParser.validate(obj),
|
async () => {
|
||||||
|
const resultDoc = []
|
||||||
|
|
||||||
|
for (const docObj of docArr) {
|
||||||
|
const validatedDoc = await SwaggerParser.validate(docObj)
|
||||||
|
resultDoc.push(validatedDoc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultDoc
|
||||||
|
},
|
||||||
() => IMPORTER_INVALID_FILE_FORMAT
|
() => IMPORTER_INVALID_FILE_FORMAT
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
}),
|
||||||
// Deference the references
|
// Deference the references
|
||||||
TE.chainW((obj) =>
|
TE.chainW((docArr) =>
|
||||||
pipe(
|
pipe(
|
||||||
TE.tryCatch(
|
TE.tryCatch(
|
||||||
() => SwaggerParser.dereference(obj),
|
async () => {
|
||||||
|
const resultDoc = []
|
||||||
|
|
||||||
|
for (const docObj of docArr) {
|
||||||
|
const validatedDoc = await SwaggerParser.dereference(docObj)
|
||||||
|
resultDoc.push(validatedDoc)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultDoc
|
||||||
|
},
|
||||||
() => OPENAPI_DEREF_ERROR
|
() => OPENAPI_DEREF_ERROR
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
TE.chainW(convertOpenApiDocToHopp)
|
TE.chainW(convertOpenApiDocsToHopp)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
QueryParam,
|
QueryParam,
|
||||||
RequestAuthDefinition,
|
RequestAuthDefinition,
|
||||||
VariableDefinition,
|
VariableDefinition,
|
||||||
|
Variable,
|
||||||
} from "postman-collection"
|
} from "postman-collection"
|
||||||
import {
|
import {
|
||||||
HoppRESTAuth,
|
HoppRESTAuth,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
ValidContentTypes,
|
ValidContentTypes,
|
||||||
knownContentTypes,
|
knownContentTypes,
|
||||||
FormDataKeyValue,
|
FormDataKeyValue,
|
||||||
|
HoppRESTRequestVariable,
|
||||||
} from "@hoppscotch/data"
|
} from "@hoppscotch/data"
|
||||||
import { pipe, flow } from "fp-ts/function"
|
import { pipe, flow } from "fp-ts/function"
|
||||||
import * as S from "fp-ts/string"
|
import * as S from "fp-ts/string"
|
||||||
@@ -55,7 +57,11 @@ const readPMCollection = (def: string) =>
|
|||||||
pipe(
|
pipe(
|
||||||
def,
|
def,
|
||||||
safeParseJSON,
|
safeParseJSON,
|
||||||
O.chain((data) => O.tryCatch(() => new PMCollection(data)))
|
O.chain((data) =>
|
||||||
|
O.tryCatch(() => {
|
||||||
|
return new PMCollection(data)
|
||||||
|
})
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const getHoppReqHeaders = (item: Item): HoppRESTHeader[] =>
|
const getHoppReqHeaders = (item: Item): HoppRESTHeader[] =>
|
||||||
@@ -87,6 +93,25 @@ const getHoppReqParams = (item: Item): HoppRESTParam[] => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getHoppReqVariables = (item: Item) => {
|
||||||
|
return pipe(
|
||||||
|
item.request.url.variables.all(),
|
||||||
|
A.filter(
|
||||||
|
(variable): variable is Variable =>
|
||||||
|
variable.key !== undefined &&
|
||||||
|
variable.key !== null &&
|
||||||
|
variable.key.length > 0
|
||||||
|
),
|
||||||
|
A.map((variable) => {
|
||||||
|
return <HoppRESTRequestVariable>{
|
||||||
|
key: replacePMVarTemplating(variable.key ?? ""),
|
||||||
|
value: replacePMVarTemplating(variable.value ?? ""),
|
||||||
|
active: !variable.disabled,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
type PMRequestAuthDef<
|
type PMRequestAuthDef<
|
||||||
AuthType extends
|
AuthType extends
|
||||||
RequestAuthDefinition["type"] = RequestAuthDefinition["type"],
|
RequestAuthDefinition["type"] = RequestAuthDefinition["type"],
|
||||||
@@ -276,6 +301,7 @@ const getHoppRequest = (item: Item): HoppRESTRequest => {
|
|||||||
params: getHoppReqParams(item),
|
params: getHoppReqParams(item),
|
||||||
auth: getHoppReqAuth(item),
|
auth: getHoppReqAuth(item),
|
||||||
body: getHoppReqBody(item),
|
body: getHoppReqBody(item),
|
||||||
|
requestVariables: getHoppReqVariables(item),
|
||||||
|
|
||||||
// TODO: Decide about this
|
// TODO: Decide about this
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
@@ -296,15 +322,17 @@ const getHoppFolder = (ig: ItemGroup<Item>): HoppCollection =>
|
|||||||
headers: [],
|
headers: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getHoppCollection = (coll: PMCollection) => getHoppFolder(coll)
|
export const getHoppCollections = (collections: PMCollection[]) => {
|
||||||
|
return collections.map(getHoppFolder)
|
||||||
|
}
|
||||||
|
|
||||||
export const hoppPostmanImporter = (fileContent: string) =>
|
export const hoppPostmanImporter = (fileContents: string[]) =>
|
||||||
pipe(
|
pipe(
|
||||||
// Try reading
|
// Try reading
|
||||||
fileContent,
|
fileContents,
|
||||||
readPMCollection,
|
A.traverse(O.Applicative)(readPMCollection),
|
||||||
|
|
||||||
O.map(flow(getHoppCollection, A.of)),
|
O.map(flow(getHoppCollections)),
|
||||||
|
|
||||||
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import * as TE from "fp-ts/TaskEither"
|
|
||||||
import * as O from "fp-ts/Option"
|
|
||||||
|
|
||||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
|
||||||
import { safeParseJSON } from "~/helpers/functional/json"
|
|
||||||
|
|
||||||
import { z } from "zod"
|
|
||||||
import { Environment } from "@hoppscotch/data"
|
import { Environment } from "@hoppscotch/data"
|
||||||
|
import * as O from "fp-ts/Option"
|
||||||
|
import * as TE from "fp-ts/TaskEither"
|
||||||
import { uniqueId } from "lodash-es"
|
import { uniqueId } from "lodash-es"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { safeParseJSON } from "~/helpers/functional/json"
|
||||||
|
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||||
|
|
||||||
const postmanEnvSchema = z.object({
|
const postmanEnvSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
@@ -18,32 +17,44 @@ const postmanEnvSchema = z.object({
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const postmanEnvImporter = (content: string) => {
|
type PostmanEnv = z.infer<typeof postmanEnvSchema>
|
||||||
const parsedContent = safeParseJSON(content)
|
|
||||||
|
|
||||||
// parse json from the environments string
|
export const postmanEnvImporter = (contents: string[]) => {
|
||||||
if (O.isNone(parsedContent)) {
|
const parsedContents = contents.map((str) => safeParseJSON(str, true))
|
||||||
|
if (parsedContents.some((parsed) => O.isNone(parsed))) {
|
||||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||||
}
|
}
|
||||||
|
|
||||||
const validationResult = postmanEnvSchema.safeParse(parsedContent.value)
|
const parsedValues = parsedContents.flatMap((parsed) => {
|
||||||
|
const unwrappedEntry = O.toNullable(parsed) as PostmanEnv[] | null
|
||||||
|
|
||||||
|
if (unwrappedEntry) {
|
||||||
|
return unwrappedEntry.map((entry) => ({
|
||||||
|
...entry,
|
||||||
|
values: entry.values?.map((valueEntry) => ({
|
||||||
|
...valueEntry,
|
||||||
|
value: String(valueEntry.value),
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
const validationResult = z.array(postmanEnvSchema).safeParse(parsedValues)
|
||||||
|
|
||||||
if (!validationResult.success) {
|
if (!validationResult.success) {
|
||||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||||
}
|
}
|
||||||
|
|
||||||
const postmanEnv = validationResult.data
|
// Convert `values` to `variables` to match the format expected by the system
|
||||||
|
const environments: Environment[] = validationResult.data.map(
|
||||||
const environment: Environment = {
|
({ name, values }) => ({
|
||||||
id: uniqueId(),
|
id: uniqueId(),
|
||||||
v: 1,
|
v: 1,
|
||||||
name: postmanEnv.name,
|
name,
|
||||||
variables: [],
|
variables: values.map((entires) => ({ ...entires, secret: false })),
|
||||||
}
|
})
|
||||||
|
|
||||||
postmanEnv.values.forEach(({ key, value }) =>
|
|
||||||
environment.variables.push({ key, value, secret: false })
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return TE.right(environment)
|
return TE.right(environments)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
|||||||
const secretEnvironmentService = getService(SecretEnvironmentService)
|
const secretEnvironmentService = getService(SecretEnvironmentService)
|
||||||
|
|
||||||
const unsecretEnvironments = (
|
const unsecretEnvironments = (
|
||||||
global: Environment["variables"],
|
selected: Environment,
|
||||||
selected: Environment
|
global: Environment["variables"]
|
||||||
) => {
|
) => {
|
||||||
const resolvedGlobalWithSecrets = global.map((globalVar, index) => {
|
const resolvedGlobalWithSecrets = global.map((globalVar, index) => {
|
||||||
const secretVar = secretEnvironmentService.getSecretEnvironmentVariable(
|
const secretVar = secretEnvironmentService.getSecretEnvironmentVariable(
|
||||||
@@ -65,8 +65,8 @@ const unsecretEnvironments = (
|
|||||||
|
|
||||||
export const getCombinedEnvVariables = () => {
|
export const getCombinedEnvVariables = () => {
|
||||||
const reformedVars = unsecretEnvironments(
|
const reformedVars = unsecretEnvironments(
|
||||||
getGlobalVariables(),
|
getCurrentEnvironment(),
|
||||||
getCurrentEnvironment()
|
getGlobalVariables()
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
global: cloneDeep(reformedVars.global),
|
global: cloneDeep(reformedVars.global),
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ export const getDefaultRESTRequest = (): HoppRESTRequest => ({
|
|||||||
contentType: null,
|
contentType: null,
|
||||||
body: null,
|
body: null,
|
||||||
},
|
},
|
||||||
|
requestVariables: [],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
import { arrayFlatMap, arraySort } from "../functional/array"
|
import { arrayFlatMap, arraySort } from "../functional/array"
|
||||||
import { toFormData } from "../functional/formData"
|
import { toFormData } from "../functional/formData"
|
||||||
import { tupleWithSameKeysToRecord } from "../functional/record"
|
import { tupleWithSameKeysToRecord } from "../functional/record"
|
||||||
import { getGlobalVariables } from "~/newstore/environments"
|
|
||||||
|
|
||||||
export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
||||||
/**
|
/**
|
||||||
@@ -34,6 +33,7 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
|||||||
effectiveFinalHeaders: { key: string; value: string }[]
|
effectiveFinalHeaders: { key: string; value: string }[]
|
||||||
effectiveFinalParams: { key: string; value: string }[]
|
effectiveFinalParams: { key: string; value: string }[]
|
||||||
effectiveFinalBody: FormData | string | null
|
effectiveFinalBody: FormData | string | null
|
||||||
|
effectiveFinalRequestVariables: { key: string; value: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -313,38 +313,53 @@ export function getEffectiveRESTRequest(
|
|||||||
request: HoppRESTRequest,
|
request: HoppRESTRequest,
|
||||||
environment: Environment
|
environment: Environment
|
||||||
): EffectiveHoppRESTRequest {
|
): EffectiveHoppRESTRequest {
|
||||||
const envVariables = [...environment.variables, ...getGlobalVariables()]
|
|
||||||
|
|
||||||
const effectiveFinalHeaders = pipe(
|
const effectiveFinalHeaders = pipe(
|
||||||
getComputedHeaders(request, envVariables).map((h) => h.header),
|
getComputedHeaders(request, environment.variables).map((h) => h.header),
|
||||||
A.concat(request.headers),
|
A.concat(request.headers),
|
||||||
A.filter((x) => x.active && x.key !== ""),
|
A.filter((x) => x.active && x.key !== ""),
|
||||||
A.map((x) => ({
|
A.map((x) => ({
|
||||||
active: true,
|
active: true,
|
||||||
key: parseTemplateString(x.key, envVariables),
|
key: parseTemplateString(x.key, environment.variables),
|
||||||
value: parseTemplateString(x.value, envVariables),
|
value: parseTemplateString(x.value, environment.variables),
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
const effectiveFinalParams = pipe(
|
const effectiveFinalParams = pipe(
|
||||||
getComputedParams(request, envVariables).map((p) => p.param),
|
getComputedParams(request, environment.variables).map((p) => p.param),
|
||||||
A.concat(request.params),
|
A.concat(request.params),
|
||||||
A.filter((x) => x.active && x.key !== ""),
|
A.filter((x) => x.active && x.key !== ""),
|
||||||
A.map((x) => ({
|
A.map((x) => ({
|
||||||
active: true,
|
active: true,
|
||||||
key: parseTemplateString(x.key, envVariables),
|
key: parseTemplateString(x.key, environment.variables),
|
||||||
value: parseTemplateString(x.value, envVariables),
|
value: parseTemplateString(x.value, environment.variables),
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
const effectiveFinalBody = getFinalBodyFromRequest(request, envVariables)
|
const effectiveFinalRequestVariables = pipe(
|
||||||
|
request.requestVariables,
|
||||||
|
A.filter((x) => x.active && x.key !== ""),
|
||||||
|
A.map((x) => ({
|
||||||
|
active: true,
|
||||||
|
key: parseTemplateString(x.key, environment.variables),
|
||||||
|
value: parseTemplateString(x.value, environment.variables),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const effectiveFinalBody = getFinalBodyFromRequest(
|
||||||
|
request,
|
||||||
|
environment.variables
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...request,
|
...request,
|
||||||
effectiveFinalURL: parseTemplateString(request.endpoint, envVariables),
|
effectiveFinalURL: parseTemplateString(
|
||||||
|
request.endpoint,
|
||||||
|
environment.variables
|
||||||
|
),
|
||||||
effectiveFinalHeaders,
|
effectiveFinalHeaders,
|
||||||
effectiveFinalParams,
|
effectiveFinalParams,
|
||||||
effectiveFinalBody,
|
effectiveFinalBody,
|
||||||
|
effectiveFinalRequestVariables,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function navigateToFolderWithIndexPath(
|
|||||||
|
|
||||||
let target = collections[indexPaths.shift() as number]
|
let target = collections[indexPaths.shift() as number]
|
||||||
|
|
||||||
while (indexPaths.length > 0)
|
while (indexPaths.length > 0 && target)
|
||||||
target = target.folders[indexPaths.shift() as number]
|
target = target.folders[indexPaths.shift() as number]
|
||||||
|
|
||||||
return target !== undefined ? target : null
|
return target !== undefined ? target : null
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ executedResponses$.subscribe((res) => {
|
|||||||
params: res.req.params,
|
params: res.req.params,
|
||||||
preRequestScript: res.req.preRequestScript,
|
preRequestScript: res.req.preRequestScript,
|
||||||
testScript: res.req.testScript,
|
testScript: res.req.testScript,
|
||||||
|
requestVariables: res.req.requestVariables,
|
||||||
v: res.req.v,
|
v: res.req.v,
|
||||||
},
|
},
|
||||||
responseMeta: {
|
responseMeta: {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export type SettingsDef = {
|
|||||||
httpUrlEncoded: boolean
|
httpUrlEncoded: boolean
|
||||||
httpPreRequest: boolean
|
httpPreRequest: boolean
|
||||||
httpTest: boolean
|
httpTest: boolean
|
||||||
|
httpRequestVariables: boolean
|
||||||
graphqlQuery: boolean
|
graphqlQuery: boolean
|
||||||
graphqlResponseBody: boolean
|
graphqlResponseBody: boolean
|
||||||
graphqlHeaders: boolean
|
graphqlHeaders: boolean
|
||||||
@@ -79,6 +80,7 @@ export const getDefaultSettings = (): SettingsDef => ({
|
|||||||
httpUrlEncoded: true,
|
httpUrlEncoded: true,
|
||||||
httpPreRequest: true,
|
httpPreRequest: true,
|
||||||
httpTest: true,
|
httpTest: true,
|
||||||
|
httpRequestVariables: true,
|
||||||
graphqlQuery: true,
|
graphqlQuery: true,
|
||||||
graphqlResponseBody: true,
|
graphqlResponseBody: true,
|
||||||
graphqlHeaders: false,
|
graphqlHeaders: false,
|
||||||
|
|||||||
@@ -95,31 +95,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onMounted, onBeforeUnmount } from "vue"
|
import { ref, onMounted } from "vue"
|
||||||
import { safelyExtractRESTRequest } from "@hoppscotch/data"
|
import { safelyExtractRESTRequest } from "@hoppscotch/data"
|
||||||
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
|
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
|
||||||
import { useRoute } from "vue-router"
|
import { useRoute } from "vue-router"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||||
import { onLoggedIn } from "~/composables/auth"
|
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import {
|
|
||||||
audit,
|
|
||||||
BehaviorSubject,
|
|
||||||
combineLatest,
|
|
||||||
EMPTY,
|
|
||||||
from,
|
|
||||||
map,
|
|
||||||
Subscription,
|
|
||||||
} from "rxjs"
|
|
||||||
import { useToast } from "~/composables/toast"
|
|
||||||
import { watchDebounced } from "@vueuse/core"
|
|
||||||
import { useReadonlyStream } from "~/composables/stream"
|
import { useReadonlyStream } from "~/composables/stream"
|
||||||
import {
|
|
||||||
changeCurrentSyncStatus,
|
|
||||||
currentSyncingStatus$,
|
|
||||||
} from "~/newstore/syncing"
|
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
import { InspectionService } from "~/services/inspection"
|
import { InspectionService } from "~/services/inspection"
|
||||||
import { HeaderInspectorService } from "~/services/inspection/inspectors/header.inspector"
|
import { HeaderInspectorService } from "~/services/inspection/inspectors/header.inspector"
|
||||||
@@ -127,7 +111,7 @@ import { EnvironmentInspectorService } from "~/services/inspection/inspectors/en
|
|||||||
import { ResponseInspectorService } from "~/services/inspection/inspectors/response.inspector"
|
import { ResponseInspectorService } from "~/services/inspection/inspectors/response.inspector"
|
||||||
import { cloneDeep } from "lodash-es"
|
import { cloneDeep } from "lodash-es"
|
||||||
import { RESTTabService } from "~/services/tab/rest"
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
import { HoppTab, PersistableTabState } from "~/services/tab"
|
import { HoppTab } from "~/services/tab"
|
||||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||||
|
|
||||||
const savingRequest = ref(false)
|
const savingRequest = ref(false)
|
||||||
@@ -140,7 +124,6 @@ const exceptedTabID = ref<string | null>(null)
|
|||||||
const renameTabID = ref<string | null>(null)
|
const renameTabID = ref<string | null>(null)
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
const tabs = useService(RESTTabService)
|
const tabs = useService(RESTTabService)
|
||||||
|
|
||||||
@@ -171,12 +154,6 @@ const contextMenu = ref<PopupDetails>({
|
|||||||
|
|
||||||
const activeTabs = tabs.getActiveTabs()
|
const activeTabs = tabs.getActiveTabs()
|
||||||
|
|
||||||
const confirmSync = useReadonlyStream(currentSyncingStatus$, {
|
|
||||||
isInitialSync: false,
|
|
||||||
shouldSync: true,
|
|
||||||
})
|
|
||||||
const tabStateForSync = ref<PersistableTabState<HoppRESTDocument> | null>(null)
|
|
||||||
|
|
||||||
function bindRequestToURLParams() {
|
function bindRequestToURLParams() {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
// Get URL parameters and set that as the request
|
// Get URL parameters and set that as the request
|
||||||
@@ -327,111 +304,6 @@ const shareTabRequest = (tabID: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncTabState = () => {
|
|
||||||
if (tabStateForSync.value)
|
|
||||||
tabs.loadTabsFromPersistedState(tabStateForSync.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs sync of the REST Tab session with Firestore.
|
|
||||||
*
|
|
||||||
* @returns A subscription to the sync observable stream.
|
|
||||||
* Unsubscribe to stop syncing.
|
|
||||||
*/
|
|
||||||
function startTabStateSync(): Subscription {
|
|
||||||
const currentUser$ = platform.auth.getCurrentUserStream()
|
|
||||||
const tabState$ =
|
|
||||||
new BehaviorSubject<PersistableTabState<HoppRESTDocument> | null>(null)
|
|
||||||
|
|
||||||
watchDebounced(
|
|
||||||
tabs.persistableTabState,
|
|
||||||
(state) => {
|
|
||||||
tabState$.next(state)
|
|
||||||
},
|
|
||||||
{ debounce: 500, deep: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
const sub = combineLatest([currentUser$, tabState$])
|
|
||||||
.pipe(
|
|
||||||
map(([user, tabState]) =>
|
|
||||||
user && tabState
|
|
||||||
? from(platform.sync.tabState.writeCurrentTabState(user, tabState))
|
|
||||||
: EMPTY
|
|
||||||
),
|
|
||||||
audit((x) => x)
|
|
||||||
)
|
|
||||||
.subscribe(() => {
|
|
||||||
// NOTE: This subscription should be kept
|
|
||||||
})
|
|
||||||
|
|
||||||
return sub
|
|
||||||
}
|
|
||||||
|
|
||||||
const showSyncToast = () => {
|
|
||||||
toast.show(t("confirm.sync"), {
|
|
||||||
duration: 0,
|
|
||||||
action: [
|
|
||||||
{
|
|
||||||
text: `${t("action.yes")}`,
|
|
||||||
onClick: (_, toastObject) => {
|
|
||||||
syncTabState()
|
|
||||||
changeCurrentSyncStatus({
|
|
||||||
isInitialSync: true,
|
|
||||||
shouldSync: true,
|
|
||||||
})
|
|
||||||
toastObject.goAway(0)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: `${t("action.no")}`,
|
|
||||||
onClick: (_, toastObject) => {
|
|
||||||
changeCurrentSyncStatus({
|
|
||||||
isInitialSync: true,
|
|
||||||
shouldSync: false,
|
|
||||||
})
|
|
||||||
toastObject.goAway(0)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupTabStateSync() {
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
// Subscription to request sync
|
|
||||||
let sub: Subscription | null = null
|
|
||||||
|
|
||||||
// Load request on login resolve and start sync
|
|
||||||
onLoggedIn(async () => {
|
|
||||||
if (
|
|
||||||
Object.keys(route.query).length === 0 &&
|
|
||||||
!(route.query.code || route.query.error)
|
|
||||||
) {
|
|
||||||
const tabStateFromSync =
|
|
||||||
await platform.sync.tabState.loadTabStateFromSync()
|
|
||||||
|
|
||||||
if (tabStateFromSync && !confirmSync.value.isInitialSync) {
|
|
||||||
tabStateForSync.value = tabStateFromSync
|
|
||||||
showSyncToast()
|
|
||||||
// Have to set isInitialSync to true here because the toast is shown
|
|
||||||
// and the user does not click on any of the actions
|
|
||||||
changeCurrentSyncStatus({
|
|
||||||
isInitialSync: true,
|
|
||||||
shouldSync: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sub = startTabStateSync()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Stop subscription to stop syncing
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
sub?.unsubscribe()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
defineActionHandler("contextmenu.open", ({ position, text }) => {
|
defineActionHandler("contextmenu.open", ({ position, text }) => {
|
||||||
if (text) {
|
if (text) {
|
||||||
contextMenu.value = {
|
contextMenu.value = {
|
||||||
@@ -448,7 +320,6 @@ defineActionHandler("contextmenu.open", ({ position, text }) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
setupTabStateSync()
|
|
||||||
bindRequestToURLParams()
|
bindRequestToURLParams()
|
||||||
|
|
||||||
defineActionHandler("rest.request.open", ({ doc }) => {
|
defineActionHandler("rest.request.open", ({ doc }) => {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { EnvironmentsPlatformDef } from "./environments"
|
|||||||
import { CollectionsPlatformDef } from "./collections"
|
import { CollectionsPlatformDef } from "./collections"
|
||||||
import { SettingsPlatformDef } from "./settings"
|
import { SettingsPlatformDef } from "./settings"
|
||||||
import { HistoryPlatformDef } from "./history"
|
import { HistoryPlatformDef } from "./history"
|
||||||
import { TabStatePlatformDef } from "./tab"
|
|
||||||
import { AnalyticsPlatformDef } from "./analytics"
|
import { AnalyticsPlatformDef } from "./analytics"
|
||||||
import { InterceptorsPlatformDef } from "./interceptors"
|
import { InterceptorsPlatformDef } from "./interceptors"
|
||||||
import { HoppModule } from "~/modules"
|
import { HoppModule } from "~/modules"
|
||||||
@@ -25,7 +24,6 @@ export type PlatformDef = {
|
|||||||
collections: CollectionsPlatformDef
|
collections: CollectionsPlatformDef
|
||||||
settings: SettingsPlatformDef
|
settings: SettingsPlatformDef
|
||||||
history: HistoryPlatformDef
|
history: HistoryPlatformDef
|
||||||
tabState: TabStatePlatformDef
|
|
||||||
}
|
}
|
||||||
interceptors: InterceptorsPlatformDef
|
interceptors: InterceptorsPlatformDef
|
||||||
additionalInspectors?: InspectorsPlatformDef
|
additionalInspectors?: InspectorsPlatformDef
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { PersistableTabState } from "~/services/tab"
|
|
||||||
import { HoppUser } from "./auth"
|
|
||||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
|
||||||
|
|
||||||
export type TabStatePlatformDef = {
|
|
||||||
loadTabStateFromSync: () => Promise<PersistableTabState<HoppRESTDocument> | null>
|
|
||||||
writeCurrentTabState: (
|
|
||||||
user: HoppUser,
|
|
||||||
persistableTabState: PersistableTabState<HoppRESTDocument>
|
|
||||||
) => Promise<void>
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,7 @@ import { Ref, markRaw } from "vue"
|
|||||||
import IconPlusCircle from "~icons/lucide/plus-circle"
|
import IconPlusCircle from "~icons/lucide/plus-circle"
|
||||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import {
|
import {
|
||||||
|
AggregateEnvironment,
|
||||||
aggregateEnvsWithSecrets$,
|
aggregateEnvsWithSecrets$,
|
||||||
getCurrentEnvironment,
|
getCurrentEnvironment,
|
||||||
getSelectedEnvironmentType,
|
getSelectedEnvironmentType,
|
||||||
@@ -18,6 +19,7 @@ import { invokeAction } from "~/helpers/actions"
|
|||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
import { useStreamStatic } from "~/composables/stream"
|
import { useStreamStatic } from "~/composables/stream"
|
||||||
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||||
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
|
|
||||||
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
|
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
|
||||||
|
|
||||||
@@ -41,6 +43,7 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
|
|
||||||
private readonly inspection = this.bind(InspectionService)
|
private readonly inspection = this.bind(InspectionService)
|
||||||
private readonly secretEnvs = this.bind(SecretEnvironmentService)
|
private readonly secretEnvs = this.bind(SecretEnvironmentService)
|
||||||
|
private readonly restTabs = this.bind(RESTTabService)
|
||||||
|
|
||||||
private aggregateEnvsWithSecrets = useStreamStatic(
|
private aggregateEnvsWithSecrets = useStreamStatic(
|
||||||
aggregateEnvsWithSecrets$,
|
aggregateEnvsWithSecrets$,
|
||||||
@@ -68,7 +71,14 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
) => {
|
) => {
|
||||||
const newErrors: InspectorResult[] = []
|
const newErrors: InspectorResult[] = []
|
||||||
|
|
||||||
const envKeys = this.aggregateEnvsWithSecrets.value.map((e) => e.key)
|
const currentTab = this.restTabs.currentActiveTab.value
|
||||||
|
|
||||||
|
const environmentVariables = [
|
||||||
|
...currentTab.document.request.requestVariables,
|
||||||
|
...this.aggregateEnvsWithSecrets.value,
|
||||||
|
]
|
||||||
|
|
||||||
|
const envKeys = environmentVariables.map((e) => e.key)
|
||||||
|
|
||||||
target.forEach((element, index) => {
|
target.forEach((element, index) => {
|
||||||
if (isENVInString(element)) {
|
if (isENVInString(element)) {
|
||||||
@@ -124,6 +134,31 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
return newErrors
|
return newErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the environment list to a list with unique keys with value
|
||||||
|
* @param envs The environment list to be transformed
|
||||||
|
* @returns The transformed environment list with keys with value
|
||||||
|
*/
|
||||||
|
private filterNonEmptyEnvironmentVariables = (
|
||||||
|
envs: AggregateEnvironment[]
|
||||||
|
): AggregateEnvironment[] => {
|
||||||
|
const envsMap = new Map<string, AggregateEnvironment>()
|
||||||
|
|
||||||
|
envs.forEach((env) => {
|
||||||
|
if (envsMap.has(env.key)) {
|
||||||
|
const existingEnv = envsMap.get(env.key)
|
||||||
|
|
||||||
|
if (existingEnv?.value === "" && env.value !== "") {
|
||||||
|
envsMap.set(env.key, env)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
envsMap.set(env.key, env)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Array.from(envsMap.values())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the environment variables in the target array are empty
|
* Checks if the environment variables in the target array are empty
|
||||||
* @param target The target array to validate
|
* @param target The target array to validate
|
||||||
@@ -145,7 +180,19 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
const formattedExEnv = exEnv.slice(2, -2)
|
const formattedExEnv = exEnv.slice(2, -2)
|
||||||
const currentSelectedEnvironment = getCurrentEnvironment()
|
const currentSelectedEnvironment = getCurrentEnvironment()
|
||||||
|
|
||||||
this.aggregateEnvsWithSecrets.value.forEach((env) => {
|
const currentTab = this.restTabs.currentActiveTab.value
|
||||||
|
|
||||||
|
const environmentVariables =
|
||||||
|
this.filterNonEmptyEnvironmentVariables([
|
||||||
|
...currentTab.document.request.requestVariables.map((env) => ({
|
||||||
|
...env,
|
||||||
|
secret: false,
|
||||||
|
sourceEnv: "RequestVariable",
|
||||||
|
})),
|
||||||
|
...this.aggregateEnvsWithSecrets.value,
|
||||||
|
])
|
||||||
|
|
||||||
|
environmentVariables.forEach((env) => {
|
||||||
const hasSecretEnv = this.secretEnvs.hasSecretValue(
|
const hasSecretEnv = this.secretEnvs.hasSecretValue(
|
||||||
env.sourceEnv !== "Global"
|
env.sourceEnv !== "Global"
|
||||||
? currentSelectedEnvironment.id
|
? currentSelectedEnvironment.id
|
||||||
@@ -199,14 +246,19 @@ export class EnvironmentInspectorService extends Service implements Inspector {
|
|||||||
"inspections.environment.add_environment_value"
|
"inspections.environment.add_environment_value"
|
||||||
),
|
),
|
||||||
apply: () => {
|
apply: () => {
|
||||||
|
if (env.sourceEnv === "RequestVariable") {
|
||||||
|
currentTab.document.optionTabPreference =
|
||||||
|
"requestVariables"
|
||||||
|
} else {
|
||||||
invokeAction(invokeActionType, {
|
invokeAction(invokeActionType, {
|
||||||
envName:
|
envName:
|
||||||
env.sourceEnv !== "Global"
|
env.sourceEnv === "Global"
|
||||||
? currentSelectedEnvironment.name
|
? "Global"
|
||||||
: "Global",
|
: currentSelectedEnvironment.name,
|
||||||
variableName: formattedExEnv,
|
variableName: formattedExEnv,
|
||||||
isSecret: env.secret,
|
isSecret: env.secret,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
severity: 2,
|
severity: 2,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
|
|||||||
folders: [],
|
folders: [],
|
||||||
requests: [
|
requests: [
|
||||||
{
|
{
|
||||||
v: "1",
|
v: "2",
|
||||||
endpoint: "https://echo.hoppscotch.io",
|
endpoint: "https://echo.hoppscotch.io",
|
||||||
name: "Echo test",
|
name: "Echo test",
|
||||||
params: [],
|
params: [],
|
||||||
@@ -35,6 +35,7 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
|
|||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
body: { contentType: null, body: null },
|
body: { contentType: null, body: null },
|
||||||
|
requestVariables: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
auth: { authType: "none", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
@@ -136,7 +137,8 @@ export const REST_HISTORY_MOCK: RESTHistoryEntry[] = [
|
|||||||
params: [],
|
params: [],
|
||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
v: "1",
|
requestVariables: [],
|
||||||
|
v: "2",
|
||||||
},
|
},
|
||||||
responseMeta: { duration: 807, statusCode: 200 },
|
responseMeta: { duration: 807, statusCode: 200 },
|
||||||
star: false,
|
star: false,
|
||||||
@@ -192,7 +194,7 @@ export const REST_TAB_STATE_MOCK: PersistableTabState<HoppRESTDocument> = {
|
|||||||
tabID: "e6e8d800-caa8-44a2-a6a6-b4765a3167aa",
|
tabID: "e6e8d800-caa8-44a2-a6a6-b4765a3167aa",
|
||||||
doc: {
|
doc: {
|
||||||
request: {
|
request: {
|
||||||
v: "1",
|
v: "2",
|
||||||
endpoint: "https://echo.hoppscotch.io",
|
endpoint: "https://echo.hoppscotch.io",
|
||||||
name: "Echo test",
|
name: "Echo test",
|
||||||
params: [],
|
params: [],
|
||||||
@@ -202,6 +204,7 @@ export const REST_TAB_STATE_MOCK: PersistableTabState<HoppRESTDocument> = {
|
|||||||
preRequestScript: "",
|
preRequestScript: "",
|
||||||
testScript: "",
|
testScript: "",
|
||||||
body: { contentType: null, body: null },
|
body: { contentType: null, body: null },
|
||||||
|
requestVariables: [],
|
||||||
},
|
},
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
saveContext: {
|
saveContext: {
|
||||||
|
|||||||
@@ -48,21 +48,22 @@ const SettingsDefSchema = z.object({
|
|||||||
|
|
||||||
WRAP_LINES: z.optional(
|
WRAP_LINES: z.optional(
|
||||||
z.object({
|
z.object({
|
||||||
httpRequestBody: z.boolean(),
|
httpRequestBody: z.boolean().catch(true),
|
||||||
httpResponseBody: z.boolean(),
|
httpResponseBody: z.boolean().catch(true),
|
||||||
httpHeaders: z.boolean(),
|
httpHeaders: z.boolean().catch(true),
|
||||||
httpParams: z.boolean(),
|
httpParams: z.boolean().catch(true),
|
||||||
httpUrlEncoded: z.boolean(),
|
httpUrlEncoded: z.boolean().catch(true),
|
||||||
httpPreRequest: z.boolean(),
|
httpPreRequest: z.boolean().catch(true),
|
||||||
httpTest: z.boolean(),
|
httpTest: z.boolean().catch(true),
|
||||||
graphqlQuery: z.boolean(),
|
httpRequestVariables: z.boolean().catch(true),
|
||||||
graphqlResponseBody: z.boolean(),
|
graphqlQuery: z.boolean().catch(true),
|
||||||
graphqlHeaders: z.boolean(),
|
graphqlResponseBody: z.boolean().catch(true),
|
||||||
graphqlVariables: z.boolean(),
|
graphqlHeaders: z.boolean().catch(false),
|
||||||
graphqlSchema: z.boolean(),
|
graphqlVariables: z.boolean().catch(false),
|
||||||
importCurl: z.boolean(),
|
graphqlSchema: z.boolean().catch(true),
|
||||||
codeGen: z.boolean(),
|
importCurl: z.boolean().catch(true),
|
||||||
cookie: z.boolean(),
|
codeGen: z.boolean().catch(true),
|
||||||
|
cookie: z.boolean().catch(true),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
@@ -514,6 +515,7 @@ const validRestOperations = [
|
|||||||
"authorization",
|
"authorization",
|
||||||
"preRequestScript",
|
"preRequestScript",
|
||||||
"tests",
|
"tests",
|
||||||
|
"requestVariables",
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export const REST_TAB_STATE_SCHEMA = z
|
export const REST_TAB_STATE_SCHEMA = z
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as S from "fp-ts/string"
|
|||||||
import cloneDeep from "lodash/cloneDeep"
|
import cloneDeep from "lodash/cloneDeep"
|
||||||
import V0_VERSION from "./v/0"
|
import V0_VERSION from "./v/0"
|
||||||
import V1_VERSION from "./v/1"
|
import V1_VERSION from "./v/1"
|
||||||
|
import V2_VERSION from "./v/2"
|
||||||
import { createVersionedEntity, InferredEntity } from "verzod"
|
import { createVersionedEntity, InferredEntity } from "verzod"
|
||||||
import { lodashIsEqualEq, mapThenEq, undefinedEq } from "../utils/eq"
|
import { lodashIsEqualEq, mapThenEq, undefinedEq } from "../utils/eq"
|
||||||
import {
|
import {
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
HoppRESTHeaders,
|
HoppRESTHeaders,
|
||||||
HoppRESTParams,
|
HoppRESTParams,
|
||||||
} from "./v/1"
|
} from "./v/1"
|
||||||
|
import { HoppRESTRequestVariables } from "./v/2"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
export * from "./content-types"
|
export * from "./content-types"
|
||||||
@@ -28,16 +30,19 @@ export {
|
|||||||
HoppRESTHeaders,
|
HoppRESTHeaders,
|
||||||
} from "./v/1"
|
} from "./v/1"
|
||||||
|
|
||||||
|
export { HoppRESTRequestVariables } from "./v/2"
|
||||||
|
|
||||||
const versionedObject = z.object({
|
const versionedObject = z.object({
|
||||||
// v is a stringified number
|
// v is a stringified number
|
||||||
v: z.string().regex(/^\d+$/).transform(Number),
|
v: z.string().regex(/^\d+$/).transform(Number),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const HoppRESTRequest = createVersionedEntity({
|
export const HoppRESTRequest = createVersionedEntity({
|
||||||
latestVersion: 1,
|
latestVersion: 2,
|
||||||
versionMap: {
|
versionMap: {
|
||||||
0: V0_VERSION,
|
0: V0_VERSION,
|
||||||
1: V1_VERSION,
|
1: V1_VERSION,
|
||||||
|
2: V2_VERSION,
|
||||||
},
|
},
|
||||||
getVersion(data) {
|
getVersion(data) {
|
||||||
// For V1 onwards we have the v string storing the number
|
// For V1 onwards we have the v string storing the number
|
||||||
@@ -73,12 +78,18 @@ const HoppRESTRequestEq = Eq.struct<HoppRESTRequest>({
|
|||||||
name: S.Eq,
|
name: S.Eq,
|
||||||
preRequestScript: S.Eq,
|
preRequestScript: S.Eq,
|
||||||
testScript: S.Eq,
|
testScript: S.Eq,
|
||||||
|
requestVariables: mapThenEq(
|
||||||
|
(arr) => arr.filter((v: any) => v.key !== "" && v.value !== ""),
|
||||||
|
lodashIsEqualEq
|
||||||
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const RESTReqSchemaVersion = "1"
|
export const RESTReqSchemaVersion = "2"
|
||||||
|
|
||||||
export type HoppRESTParam = HoppRESTRequest["params"][number]
|
export type HoppRESTParam = HoppRESTRequest["params"][number]
|
||||||
export type HoppRESTHeader = HoppRESTRequest["headers"][number]
|
export type HoppRESTHeader = HoppRESTRequest["headers"][number]
|
||||||
|
export type HoppRESTRequestVariable =
|
||||||
|
HoppRESTRequest["requestVariables"][number]
|
||||||
|
|
||||||
export const isEqualHoppRESTRequest = HoppRESTRequestEq.equals
|
export const isEqualHoppRESTRequest = HoppRESTRequestEq.equals
|
||||||
|
|
||||||
@@ -144,6 +155,14 @@ export function safelyExtractRESTRequest(
|
|||||||
req.headers = result.data
|
req.headers = result.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("requestVariables" in x) {
|
||||||
|
const result = HoppRESTRequestVariables.safeParse(x.requestVariables)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
req.requestVariables = result.data
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return req
|
return req
|
||||||
@@ -160,7 +179,7 @@ export function makeRESTRequest(
|
|||||||
|
|
||||||
export function getDefaultRESTRequest(): HoppRESTRequest {
|
export function getDefaultRESTRequest(): HoppRESTRequest {
|
||||||
return {
|
return {
|
||||||
v: "1",
|
v: "2",
|
||||||
endpoint: "https://echo.hoppscotch.io",
|
endpoint: "https://echo.hoppscotch.io",
|
||||||
name: "Untitled",
|
name: "Untitled",
|
||||||
params: [],
|
params: [],
|
||||||
@@ -176,6 +195,7 @@ export function getDefaultRESTRequest(): HoppRESTRequest {
|
|||||||
contentType: null,
|
contentType: null,
|
||||||
body: null,
|
body: null,
|
||||||
},
|
},
|
||||||
|
requestVariables: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export const HoppRESTHeaders = z.array(
|
|||||||
|
|
||||||
export type HoppRESTHeaders = z.infer<typeof HoppRESTHeaders>
|
export type HoppRESTHeaders = z.infer<typeof HoppRESTHeaders>
|
||||||
|
|
||||||
const V1_SCHEMA = z.object({
|
export const V1_SCHEMA = z.object({
|
||||||
v: z.literal("1"),
|
v: z.literal("1"),
|
||||||
id: z.optional(z.string()), // Firebase Firestore ID
|
id: z.optional(z.string()), // Firebase Firestore ID
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ const V1_SCHEMA = z.object({
|
|||||||
body: HoppRESTReqBody,
|
body: HoppRESTReqBody,
|
||||||
})
|
})
|
||||||
|
|
||||||
function parseRequestBody(
|
export function parseRequestBody(
|
||||||
x: z.infer<typeof V0_SCHEMA>
|
x: z.infer<typeof V0_SCHEMA>
|
||||||
): z.infer<typeof V1_SCHEMA>["body"] {
|
): z.infer<typeof V1_SCHEMA>["body"] {
|
||||||
return {
|
return {
|
||||||
|
|||||||
50
packages/hoppscotch-data/src/rest/v/2.ts
Normal file
50
packages/hoppscotch-data/src/rest/v/2.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { defineVersion } from "verzod"
|
||||||
|
import { z } from "zod"
|
||||||
|
import {
|
||||||
|
HoppRESTAuth,
|
||||||
|
HoppRESTHeaders,
|
||||||
|
HoppRESTParams,
|
||||||
|
HoppRESTReqBody,
|
||||||
|
} from "./1"
|
||||||
|
import { V1_SCHEMA } from "./1"
|
||||||
|
|
||||||
|
export const HoppRESTRequestVariables = z.array(
|
||||||
|
z.object({
|
||||||
|
key: z.string().catch(""),
|
||||||
|
value: z.string().catch(""),
|
||||||
|
active: z.boolean().catch(true),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export type HoppRESTRequestVariables = z.infer<typeof HoppRESTRequestVariables>
|
||||||
|
|
||||||
|
const V2_SCHEMA = z.object({
|
||||||
|
v: z.literal("2"),
|
||||||
|
id: z.optional(z.string()), // Firebase Firestore ID
|
||||||
|
|
||||||
|
name: z.string(),
|
||||||
|
method: z.string(),
|
||||||
|
endpoint: z.string(),
|
||||||
|
params: HoppRESTParams,
|
||||||
|
headers: HoppRESTHeaders,
|
||||||
|
preRequestScript: z.string().catch(""),
|
||||||
|
testScript: z.string().catch(""),
|
||||||
|
|
||||||
|
auth: HoppRESTAuth,
|
||||||
|
|
||||||
|
body: HoppRESTReqBody,
|
||||||
|
|
||||||
|
requestVariables: HoppRESTRequestVariables,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default defineVersion({
|
||||||
|
initial: false,
|
||||||
|
schema: V2_SCHEMA,
|
||||||
|
up(old: z.infer<typeof V1_SCHEMA>) {
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
v: "2",
|
||||||
|
requestVariables: [],
|
||||||
|
} as z.infer<typeof V2_SCHEMA>
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
mutation UpdateUserSession(
|
|
||||||
$currentSession: String!
|
|
||||||
$sessionType: SessionType!
|
|
||||||
) {
|
|
||||||
updateUserSessions(
|
|
||||||
currentSession: $currentSession
|
|
||||||
sessionType: $sessionType
|
|
||||||
) {
|
|
||||||
currentRESTSession
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
query GetCurrentRESTSession {
|
|
||||||
me {
|
|
||||||
currentRESTSession
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ import { def as environmentsDef } from "./platform/environments/environments.pla
|
|||||||
import { def as collectionsDef } from "./platform/collections/collections.platform"
|
import { def as collectionsDef } from "./platform/collections/collections.platform"
|
||||||
import { def as settingsDef } from "./platform/settings/settings.platform"
|
import { def as settingsDef } from "./platform/settings/settings.platform"
|
||||||
import { def as historyDef } from "./platform/history/history.platform"
|
import { def as historyDef } from "./platform/history/history.platform"
|
||||||
import { def as tabStateDef } from "./platform/tabState/tabState.platform"
|
|
||||||
import { proxyInterceptor } from "@hoppscotch/common/platform/std/interceptors/proxy"
|
import { proxyInterceptor } from "@hoppscotch/common/platform/std/interceptors/proxy"
|
||||||
import { ExtensionInspectorService } from "@hoppscotch/common/platform/std/inspections/extension.inspector"
|
import { ExtensionInspectorService } from "@hoppscotch/common/platform/std/inspections/extension.inspector"
|
||||||
import { NativeInterceptorService } from "./platform/interceptors/native"
|
import { NativeInterceptorService } from "./platform/interceptors/native"
|
||||||
@@ -46,7 +45,6 @@ const headerPaddingTop = ref("0px")
|
|||||||
collections: collectionsDef,
|
collections: collectionsDef,
|
||||||
settings: settingsDef,
|
settings: settingsDef,
|
||||||
history: historyDef,
|
history: historyDef,
|
||||||
tabState: tabStateDef,
|
|
||||||
},
|
},
|
||||||
interceptors: {
|
interceptors: {
|
||||||
default: "native",
|
default: "native",
|
||||||
@@ -97,17 +95,18 @@ const headerPaddingTop = ref("0px")
|
|||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
|
||||||
function isTextInput(target: EventTarget | null) {
|
function isTextInput(target: EventTarget | null) {
|
||||||
if (target instanceof HTMLInputElement) {
|
if (target instanceof HTMLInputElement) {
|
||||||
return target.type === 'text'
|
return (
|
||||||
|| target.type === 'email'
|
target.type === "text" ||
|
||||||
|| target.type === 'password'
|
target.type === "email" ||
|
||||||
|| target.type === 'number'
|
target.type === "password" ||
|
||||||
|| target.type === 'search'
|
target.type === "number" ||
|
||||||
|| target.type === 'tel'
|
target.type === "search" ||
|
||||||
|| target.type === 'url'
|
target.type === "tel" ||
|
||||||
|| target.type === 'textarea'
|
target.type === "url" ||
|
||||||
|
target.type === "textarea"
|
||||||
|
)
|
||||||
} else if (target instanceof HTMLTextAreaElement) {
|
} else if (target instanceof HTMLTextAreaElement) {
|
||||||
return true
|
return true
|
||||||
} else if (target instanceof HTMLElement && target.isContentEditable) {
|
} else if (target instanceof HTMLElement && target.isContentEditable) {
|
||||||
@@ -117,8 +116,12 @@ function isTextInput(target: EventTarget | null) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('keydown',function(e){
|
window.addEventListener(
|
||||||
|
"keydown",
|
||||||
|
function (e) {
|
||||||
if (e.key === "Backspace" && !isTextInput(e.target)) {
|
if (e.key === "Backspace" && !isTextInput(e.target)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
},true);
|
},
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user