feat: add auto-create collection option to mock server creation (#5637)

Co-authored-by: Anwarul Islam <anwaarulislaam@gmail.com>
This commit is contained in:
Mir Arif Hasan 2025-12-03 23:31:06 +06:00 committed by GitHub
parent cd82eb212d
commit 008335c715
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1342 additions and 143 deletions

View file

@ -892,6 +892,13 @@ export const MOCK_SERVER_NOT_FOUND = 'mock_server/not_found';
*/ */
export const MOCK_SERVER_INVALID_COLLECTION = 'mock_server/invalid_collection'; export const MOCK_SERVER_INVALID_COLLECTION = 'mock_server/invalid_collection';
/**
* Mock server collection creation failed
* (MockServerService)
*/
export const MOCK_SERVER_COLLECTION_CREATION_FAILED =
'mock_server/collection_creation_failed';
/** /**
* Mock server already exists for this collection * Mock server already exists for this collection
* (MockServerService) * (MockServerService)

View file

@ -0,0 +1,781 @@
import { randomUUID } from 'crypto';
const generateRefId = () => `${Date.now().toString(36)}_${randomUUID()}`;
export const mockServerCollRequestExample = (
collectionName: string = 'Hoppscotch API Mock example',
) => {
const baseEnv = '<<mockUrl>>';
return [
{
v: 10,
name: collectionName,
folders: [],
requests: [
{
v: '16',
_ref_id: `req_${generateRefId()}`,
name: 'addPet',
method: 'POST',
endpoint: baseEnv + '/v2/pet',
params: [],
headers: [],
auth: {
authType: 'oauth-2',
authActive: true,
grantTypeInfo: {
authEndpoint: baseEnv + '/oauth/authorize',
clientID: '',
grantType: 'IMPLICIT',
scopes: 'write:pets read:pets',
token: '',
authRequestParams: [],
refreshRequestParams: [],
},
addTo: 'HEADERS',
},
body: {
contentType: 'application/json',
body: '{\n\t"id": 1,\n\t"category": {\n\t\t"id": 1,\n\t\t"name": "string"\n\t},\n\t"name": "doggie",\n\t"photoUrls": [\n\t\t"string"\n\t],\n\t"tags": [],\n\t"status": "available"\n}',
},
preRequestScript: '',
testScript: '',
requestVariables: [],
responses: {
'Invalid input': {
name: 'Invalid input',
status: 'Method Not Allowed',
code: 405,
headers: [
{
key: 'content-type',
value: 'application/json',
description: '',
active: true,
},
],
body: '',
originalRequest: {
v: '6',
name: 'addPet',
auth: {
authType: 'oauth-2',
authActive: true,
grantTypeInfo: {
authEndpoint: baseEnv + '/oauth/authorize',
clientID: '',
grantType: 'IMPLICIT',
scopes: 'write:pets read:pets',
token: '',
authRequestParams: [],
refreshRequestParams: [],
},
addTo: 'HEADERS',
},
body: {
contentType: 'application/json',
body: '{\n\t"id": 1,\n\t"category": {\n\t\t"id": 1,\n\t\t"name": "string"\n\t},\n\t"name": "doggie",\n\t"photoUrls": [\n\t\t"string"\n\t],\n\t"tags": [],\n\t"status": "available"\n}',
},
endpoint: baseEnv + '/v2/pet',
params: [],
headers: [],
method: 'POST',
requestVariables: [],
},
},
},
},
{
v: '16',
_ref_id: `req_${generateRefId()}`,
name: 'updatePet',
method: 'PUT',
endpoint: baseEnv + '/v2/pet',
params: [],
headers: [],
auth: {
authType: 'oauth-2',
authActive: true,
grantTypeInfo: {
authEndpoint: baseEnv + '/oauth/authorize',
clientID: '',
grantType: 'IMPLICIT',
scopes: 'write:pets read:pets',
token: '',
authRequestParams: [],
refreshRequestParams: [],
},
addTo: 'HEADERS',
},
body: {
contentType: 'application/json',
body: '{\n\t"id": 1,\n\t"category": {\n\t\t"id": 1,\n\t\t"name": "string"\n\t},\n\t"name": "doggie",\n\t"photoUrls": [\n\t\t"string"\n\t],\n\t"tags": [],\n\t"status": "available"\n}',
},
preRequestScript: '',
testScript: '',
requestVariables: [],
responses: {
'Invalid ID supplied': {
name: 'Invalid ID supplied',
status: 'Bad Request',
code: 400,
headers: [
{
key: 'content-type',
value: 'application/json',
description: '',
active: true,
},
],
body: '',
originalRequest: {
v: '6',
name: 'updatePet',
auth: {
authType: 'oauth-2',
authActive: true,
grantTypeInfo: {
authEndpoint: baseEnv + '/oauth/authorize',
clientID: '',
grantType: 'IMPLICIT',
scopes: 'write:pets read:pets',
token: '',
authRequestParams: [],
refreshRequestParams: [],
},
addTo: 'HEADERS',
},
body: {
contentType: 'application/json',
body: '{\n\t"id": 1,\n\t"category": {\n\t\t"id": 1,\n\t\t"name": "string"\n\t},\n\t"name": "doggie",\n\t"photoUrls": [\n\t\t"string"\n\t],\n\t"tags": [],\n\t"status": "available"\n}',
},
endpoint: baseEnv + '/v2/pet',
params: [],
headers: [],
method: 'PUT',
requestVariables: [],
},
},
'Pet not found': {
name: 'Pet not found',
status: 'Not Found',
code: 404,
headers: [
{
key: 'content-type',
value: 'application/json',
description: '',
active: true,
},
],
body: '',
originalRequest: {
v: '6',
name: 'updatePet',
auth: {
authType: 'oauth-2',
authActive: true,
grantTypeInfo: {
authEndpoint: baseEnv + '/oauth/authorize',
clientID: '',
grantType: 'IMPLICIT',
scopes: 'write:pets read:pets',
token: '',
authRequestParams: [],
refreshRequestParams: [],
},
addTo: 'HEADERS',
},
body: {
contentType: 'application/json',
body: '{\n\t"id": 1,\n\t"category": {\n\t\t"id": 1,\n\t\t"name": "string"\n\t},\n\t"name": "doggie",\n\t"photoUrls": [\n\t\t"string"\n\t],\n\t"tags": [],\n\t"status": "available"\n}',
},
endpoint: baseEnv + '/v2/pet',
params: [],
headers: [],
method: 'PUT',
requestVariables: [],
},
},
'Validation exception': {
name: 'Validation exception',
status: 'Method Not Allowed',
code: 405,
headers: [
{
key: 'content-type',
value: 'application/json',
description: '',
active: true,
},
],
body: '',
originalRequest: {
v: '6',
name: 'updatePet',
auth: {
authType: 'oauth-2',
authActive: true,
grantTypeInfo: {
authEndpoint: baseEnv + '/oauth/authorize',
clientID: '',
grantType: 'IMPLICIT',
scopes: 'write:pets read:pets',
token: '',
authRequestParams: [],
refreshRequestParams: [],
},
addTo: 'HEADERS',
},
body: {
contentType: 'application/json',
body: '{\n\t"id": 1,\n\t"category": {\n\t\t"id": 1,\n\t\t"name": "string"\n\t},\n\t"name": "doggie",\n\t"photoUrls": [\n\t\t"string"\n\t],\n\t"tags": [],\n\t"status": "available"\n}',
},
endpoint: baseEnv + '/v2/pet',
params: [],
headers: [],
method: 'PUT',
requestVariables: [],
},
},
},
},
{
v: '16',
_ref_id: `req_${generateRefId()}`,
name: 'findPetsByStatus',
method: 'GET',
endpoint: baseEnv + '/v2/pet/findByStatus',
params: [
{
key: 'status',
value: '',
active: true,
description:
'Status values that need to be considered for filter',
},
],
headers: [],
auth: {
authType: 'oauth-2',
authActive: true,
grantTypeInfo: {
authEndpoint: baseEnv + '/oauth/authorize',
clientID: '',
grantType: 'IMPLICIT',
scopes: 'write:pets read:pets',
token: '',
authRequestParams: [],
refreshRequestParams: [],
},
addTo: 'HEADERS',
},
body: {
contentType: null,
body: null,
},
preRequestScript: '',
testScript: '',
requestVariables: [],
responses: {
'successful operation': {
name: 'successful operation',
status: 'OK',
code: 200,
headers: [
{
key: 'content-type',
value: 'application/json',
description: '',
active: true,
},
],
body: '',
originalRequest: {
v: '6',
name: 'findPetsByStatus',
auth: {
authType: 'oauth-2',
authActive: true,
grantTypeInfo: {
authEndpoint: baseEnv + '/oauth/authorize',
clientID: '',
grantType: 'IMPLICIT',
scopes: 'write:pets read:pets',
token: '',
authRequestParams: [],
refreshRequestParams: [],
},
addTo: 'HEADERS',
},
body: {
contentType: null,
body: null,
},
endpoint: 'petstore.swagger.io/v2/pet/findByStatus',
params: [
{
key: 'status',
value: '',
active: true,
description:
'Status values that need to be considered for filter',
},
],
headers: [],
method: 'GET',
requestVariables: [],
},
},
'Invalid status value': {
name: 'Invalid status value',
status: 'Bad Request',
code: 400,
headers: [
{
key: 'content-type',
value: 'application/json',
description: '',
active: true,
},
],
body: '',
originalRequest: {
v: '6',
name: 'findPetsByStatus',
auth: {
authType: 'oauth-2',
authActive: true,
grantTypeInfo: {
authEndpoint: baseEnv + '/oauth/authorize',
clientID: '',
grantType: 'IMPLICIT',
scopes: 'write:pets read:pets',
token: '',
authRequestParams: [],
refreshRequestParams: [],
},
addTo: 'HEADERS',
},
body: {
contentType: null,
body: null,
},
endpoint: 'petstore.swagger.io/v2/pet/findByStatus',
params: [
{
key: 'status',
value: '',
active: true,
description:
'Status values that need to be considered for filter',
},
],
headers: [],
method: 'GET',
requestVariables: [],
},
},
},
},
{
v: '16',
_ref_id: `req_${generateRefId()}`,
name: 'getPetById',
method: 'GET',
endpoint: baseEnv + '/v2/pet/<<petId>>',
params: [],
headers: [],
auth: {
authType: 'api-key',
addTo: 'HEADERS',
authActive: true,
key: 'api_key',
value: '',
},
body: {
contentType: null,
body: null,
},
preRequestScript: '',
testScript: '',
requestVariables: [
{
key: 'petId',
value: '',
active: true,
},
],
responses: {
'successful operation': {
name: 'successful operation',
status: 'OK',
code: 200,
headers: [
{
key: 'content-type',
value: 'application/json',
description: '',
active: true,
},
],
body: '',
originalRequest: {
v: '6',
name: 'getPetById',
auth: {
authType: 'api-key',
addTo: 'HEADERS',
authActive: true,
key: 'api_key',
value: '',
},
body: {
contentType: null,
body: null,
},
endpoint: 'petstore.swagger.io/v2/pet/<<petId>>',
params: [],
headers: [],
method: 'GET',
requestVariables: [
{
key: 'petId',
value: '',
active: true,
},
],
},
},
'Invalid ID supplied': {
name: 'Invalid ID supplied',
status: 'Bad Request',
code: 400,
headers: [
{
key: 'content-type',
value: 'application/json',
description: '',
active: true,
},
],
body: '',
originalRequest: {
v: '6',
name: 'getPetById',
auth: {
authType: 'api-key',
addTo: 'HEADERS',
authActive: true,
key: 'api_key',
value: '',
},
body: {
contentType: null,
body: null,
},
endpoint: 'petstore.swagger.io/v2/pet/<<petId>>',
params: [],
headers: [],
method: 'GET',
requestVariables: [
{
key: 'petId',
value: '',
active: true,
},
],
},
},
'Pet not found': {
name: 'Pet not found',
status: 'Not Found',
code: 404,
headers: [
{
key: 'content-type',
value: 'application/json',
description: '',
active: true,
},
],
body: '',
originalRequest: {
v: '6',
name: 'getPetById',
auth: {
authType: 'api-key',
addTo: 'HEADERS',
authActive: true,
key: 'api_key',
value: '',
},
body: {
contentType: null,
body: null,
},
endpoint: 'petstore.swagger.io/v2/pet/<<petId>>',
params: [],
headers: [],
method: 'GET',
requestVariables: [
{
key: 'petId',
value: '',
active: true,
},
],
},
},
},
},
{
v: '16',
_ref_id: `req_${generateRefId()}`,
name: 'updatePetWithForm',
method: 'POST',
endpoint: baseEnv + '/v2/pet/<<petId>>',
params: [],
headers: [],
auth: {
authType: 'oauth-2',
authActive: true,
grantTypeInfo: {
authEndpoint: baseEnv + '/oauth/authorize',
clientID: '',
grantType: 'IMPLICIT',
scopes: 'write:pets read:pets',
token: '',
authRequestParams: [],
refreshRequestParams: [],
},
addTo: 'HEADERS',
},
body: {
contentType: 'application/x-www-form-urlencoded',
body: 'name: \nstatus: ',
},
preRequestScript: '',
testScript: '',
requestVariables: [
{
key: 'petId',
value: '',
active: true,
},
],
responses: {
'Invalid input': {
name: 'Invalid input',
status: 'Method Not Allowed',
code: 405,
headers: [
{
key: 'content-type',
value: 'application/json',
description: '',
active: true,
},
],
body: '',
originalRequest: {
v: '6',
name: 'updatePetWithForm',
auth: {
authType: 'oauth-2',
authActive: true,
grantTypeInfo: {
authEndpoint: baseEnv + '/oauth/authorize',
clientID: '',
grantType: 'IMPLICIT',
scopes: 'write:pets read:pets',
token: '',
authRequestParams: [],
refreshRequestParams: [],
},
addTo: 'HEADERS',
},
body: {
contentType: 'application/x-www-form-urlencoded',
body: 'name: \nstatus: ',
},
endpoint: 'petstore.swagger.io/v2/pet/<<petId>>',
params: [],
headers: [],
method: 'POST',
requestVariables: [
{
key: 'petId',
value: '',
active: true,
},
],
},
},
},
},
{
v: '16',
_ref_id: `req_${generateRefId()}`,
name: 'deletePet',
method: 'DELETE',
endpoint: baseEnv + '/v2/pet/<<petId>>',
params: [],
headers: [
{
key: 'api_key',
value: '',
active: true,
description: '',
},
],
auth: {
authType: 'oauth-2',
authActive: true,
grantTypeInfo: {
authEndpoint: baseEnv + '/oauth/authorize',
clientID: '',
grantType: 'IMPLICIT',
scopes: 'write:pets read:pets',
token: '',
authRequestParams: [],
refreshRequestParams: [],
},
addTo: 'HEADERS',
},
body: {
contentType: null,
body: null,
},
preRequestScript: '',
testScript: '',
requestVariables: [
{
key: 'petId',
value: '',
active: true,
},
],
responses: {
'Invalid ID supplied': {
name: 'Invalid ID supplied',
status: 'Bad Request',
code: 400,
headers: [
{
key: 'content-type',
value: 'application/json',
description: '',
active: true,
},
],
body: '',
originalRequest: {
v: '6',
name: 'deletePet',
auth: {
authType: 'oauth-2',
authActive: true,
grantTypeInfo: {
authEndpoint: baseEnv + '/oauth/authorize',
clientID: '',
grantType: 'IMPLICIT',
scopes: 'write:pets read:pets',
token: '',
authRequestParams: [],
refreshRequestParams: [],
},
addTo: 'HEADERS',
},
body: {
contentType: null,
body: null,
},
endpoint: 'petstore.swagger.io/v2/pet/<<petId>>',
params: [],
headers: [
{
key: 'api_key',
value: '',
active: true,
description: '',
},
],
method: 'DELETE',
requestVariables: [
{
key: 'petId',
value: '',
active: true,
},
],
},
},
'Pet not found': {
name: 'Pet not found',
status: 'Not Found',
code: 404,
headers: [
{
key: 'content-type',
value: 'application/json',
description: '',
active: true,
},
],
body: '',
originalRequest: {
v: '6',
name: 'deletePet',
auth: {
authType: 'oauth-2',
authActive: true,
grantTypeInfo: {
authEndpoint: baseEnv + '/oauth/authorize',
clientID: '',
grantType: 'IMPLICIT',
scopes: 'write:pets read:pets',
token: '',
authRequestParams: [],
refreshRequestParams: [],
},
addTo: 'HEADERS',
},
body: {
contentType: null,
body: null,
},
endpoint: 'petstore.swagger.io/v2/pet/<<petId>>',
params: [],
headers: [
{
key: 'api_key',
value: '',
active: true,
description: '',
},
],
method: 'DELETE',
requestVariables: [
{
key: 'petId',
value: '',
active: true,
},
],
},
},
},
},
],
data: {
auth: {
authType: 'inherit',
authActive: true,
},
headers: [],
_ref_id: `coll_${generateRefId()}`,
},
},
];
};

View file

@ -118,10 +118,25 @@ export class CreateMockServerInput {
name: string; name: string;
@Field({ @Field({
nullable: true,
description: description:
'ID of the (team or user) collection to associate with the mock server', 'ID of the (team or user) collection to associate with the mock server',
}) })
collectionID: string; collectionID?: string;
@Field({
nullable: true,
description:
'Whether to auto-create a collection for the mock server if collectionID is not provided',
})
autoCreateCollection?: boolean;
@Field({
nullable: true,
description:
'Whether to auto-create request examples in the collection for the mock server',
})
autoCreateRequestExample?: boolean;
@Field(() => WorkspaceType, { @Field(() => WorkspaceType, {
description: 'Type of workspace: USER or TEAM', description: 'Type of workspace: USER or TEAM',

View file

@ -8,9 +8,18 @@ import { TeamModule } from 'src/team/team.module';
import { TeamRequestModule } from 'src/team-request/team-request.module'; import { TeamRequestModule } from 'src/team-request/team-request.module';
import { MockServerController } from './mock-server.controller'; import { MockServerController } from './mock-server.controller';
import { AccessTokenModule } from 'src/access-token/access-token.module'; import { AccessTokenModule } from 'src/access-token/access-token.module';
import { TeamCollectionModule } from 'src/team-collection/team-collection.module';
import { UserCollectionModule } from 'src/user-collection/user-collection.module';
@Module({ @Module({
imports: [PrismaModule, TeamModule, TeamRequestModule, AccessTokenModule], imports: [
PrismaModule,
UserCollectionModule,
TeamModule,
TeamCollectionModule,
TeamRequestModule,
AccessTokenModule,
],
controllers: [MockServerController], controllers: [MockServerController],
providers: [ providers: [
MockServerService, MockServerService,

View file

@ -26,6 +26,8 @@ import { GqlTeamMemberGuard } from 'src/team/guards/gql-team-member.guard';
import { RequiresTeamRole } from 'src/team/decorators/requires-team-role.decorator'; import { RequiresTeamRole } from 'src/team/decorators/requires-team-role.decorator';
import { TeamAccessRole } from 'src/team/team.model'; import { TeamAccessRole } from 'src/team/team.model';
import { throwErr } from 'src/utils'; import { throwErr } from 'src/utils';
import { AuthUser } from 'src/types/AuthUser';
import { INVALID_PARAMS } from 'src/errors';
@Resolver(() => MockServer) @Resolver(() => MockServer)
export class MockServerResolver { export class MockServerResolver {
@ -72,7 +74,7 @@ export class MockServerResolver {
}) })
@UseGuards(GqlAuthGuard) @UseGuards(GqlAuthGuard)
async myMockServers( async myMockServers(
@GqlUser() user: User, @GqlUser() user: AuthUser,
@Args() args: OffsetPaginationArgs, @Args() args: OffsetPaginationArgs,
): Promise<MockServer[]> { ): Promise<MockServer[]> {
return this.mockServerService.getUserMockServers(user.uid, args); return this.mockServerService.getUserMockServers(user.uid, args);
@ -104,7 +106,7 @@ export class MockServerResolver {
}) })
@UseGuards(GqlAuthGuard) @UseGuards(GqlAuthGuard)
async mockServer( async mockServer(
@GqlUser() user: User, @GqlUser() user: AuthUser,
@Args({ @Args({
name: 'id', name: 'id',
type: () => ID, type: () => ID,
@ -124,7 +126,7 @@ export class MockServerResolver {
}) })
@UseGuards(GqlAuthGuard) @UseGuards(GqlAuthGuard)
async mockServerLogs( async mockServerLogs(
@GqlUser() user: User, @GqlUser() user: AuthUser,
@Args({ @Args({
name: 'mockServerID', name: 'mockServerID',
type: () => ID, type: () => ID,
@ -151,8 +153,15 @@ export class MockServerResolver {
@UseGuards(GqlAuthGuard) @UseGuards(GqlAuthGuard)
async createMockServer( async createMockServer(
@Args('input') input: CreateMockServerInput, @Args('input') input: CreateMockServerInput,
@GqlUser() user: User, @GqlUser() user: AuthUser,
): Promise<MockServer> { ): Promise<MockServer> {
if (
(input.collectionID && input.autoCreateCollection) ||
(!input.collectionID && !input.autoCreateCollection)
) {
throwErr(INVALID_PARAMS);
}
const result = await this.mockServerService.createMockServer(user, input); const result = await this.mockServerService.createMockServer(user, input);
if (E.isLeft(result)) throwErr(result.left); if (E.isLeft(result)) throwErr(result.left);
@ -164,7 +173,7 @@ export class MockServerResolver {
}) })
@UseGuards(GqlAuthGuard) @UseGuards(GqlAuthGuard)
async updateMockServer( async updateMockServer(
@GqlUser() user: User, @GqlUser() user: AuthUser,
@Args() args: MockServerMutationArgs, @Args() args: MockServerMutationArgs,
@Args('input') input: UpdateMockServerInput, @Args('input') input: UpdateMockServerInput,
): Promise<MockServer> { ): Promise<MockServer> {
@ -183,7 +192,7 @@ export class MockServerResolver {
}) })
@UseGuards(GqlAuthGuard) @UseGuards(GqlAuthGuard)
async deleteMockServer( async deleteMockServer(
@GqlUser() user: User, @GqlUser() user: AuthUser,
@Args() args: MockServerMutationArgs, @Args() args: MockServerMutationArgs,
): Promise<boolean> { ): Promise<boolean> {
const result = await this.mockServerService.deleteMockServer( const result = await this.mockServerService.deleteMockServer(
@ -200,7 +209,7 @@ export class MockServerResolver {
}) })
@UseGuards(GqlAuthGuard) @UseGuards(GqlAuthGuard)
async deleteMockServerLog( async deleteMockServerLog(
@GqlUser() user: User, @GqlUser() user: AuthUser,
@Args({ @Args({
name: 'logID', name: 'logID',
type: () => ID, type: () => ID,

View file

@ -2,6 +2,8 @@ import { MockServerService } from './mock-server.service';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { MockServerAnalyticsService } from './mock-server-analytics.service'; import { MockServerAnalyticsService } from './mock-server-analytics.service';
import { TeamCollectionService } from '../team-collection/team-collection.service';
import { UserCollectionService } from '../user-collection/user-collection.service';
import { mockDeep, mockReset } from 'jest-mock-extended'; import { mockDeep, mockReset } from 'jest-mock-extended';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import { import {
@ -17,9 +19,9 @@ import {
UserCollection, UserCollection,
TeamCollection, TeamCollection,
UserRequest, UserRequest,
User,
} from 'src/generated/prisma/client'; } from 'src/generated/prisma/client';
import { WorkspaceType } from '../types/WorkspaceTypes'; import { WorkspaceType } from '../types/WorkspaceTypes';
import { User } from '../user/user.model';
import { import {
CreateMockServerInput, CreateMockServerInput,
UpdateMockServerInput, UpdateMockServerInput,
@ -28,17 +30,23 @@ import {
const mockPrisma = mockDeep<PrismaService>(); const mockPrisma = mockDeep<PrismaService>();
const mockAnalyticsService = mockDeep<MockServerAnalyticsService>(); const mockAnalyticsService = mockDeep<MockServerAnalyticsService>();
const mockConfigService = mockDeep<ConfigService>(); const mockConfigService = mockDeep<ConfigService>();
const mockTeamCollectionService = mockDeep<TeamCollectionService>();
const mockUserCollectionService = mockDeep<UserCollectionService>();
const mockServerService = new MockServerService( const mockServerService = new MockServerService(
mockAnalyticsService,
mockPrisma,
mockConfigService, mockConfigService,
mockPrisma,
mockAnalyticsService,
mockTeamCollectionService,
mockUserCollectionService,
); );
beforeEach(() => { beforeEach(() => {
mockReset(mockPrisma); mockReset(mockPrisma);
mockReset(mockAnalyticsService); mockReset(mockAnalyticsService);
mockReset(mockConfigService); mockReset(mockConfigService);
mockReset(mockTeamCollectionService);
mockReset(mockUserCollectionService);
// Default config values // Default config values
mockConfigService.get.mockImplementation((key: string) => { mockConfigService.get.mockImplementation((key: string) => {
@ -57,6 +65,7 @@ const user: User = {
email: 'test@example.com', email: 'test@example.com',
photoURL: null, photoURL: null,
isAdmin: false, isAdmin: false,
refreshToken: null,
currentGQLSession: '{}', currentGQLSession: '{}',
currentRESTSession: '{}', currentRESTSession: '{}',
createdOn: currentTime, createdOn: currentTime,
@ -471,6 +480,282 @@ describe('MockServerService', () => {
expect(result.left).toBe('mock_server/creation_failed'); expect(result.left).toBe('mock_server/creation_failed');
} }
}); });
describe('auto-create collection', () => {
test('should auto-create user collection without request example', async () => {
const autoCreateInput: CreateMockServerInput = {
name: 'Auto Mock Server',
workspaceType: WorkspaceType.USER,
workspaceID: undefined,
delayInMs: 0,
autoCreateCollection: true,
autoCreateRequestExample: false,
};
const createdCollection = { ...userCollection, id: 'new-coll-123' };
mockUserCollectionService.createUserCollection.mockResolvedValue(
E.right(createdCollection as any),
);
mockPrisma.mockServer.create.mockResolvedValue({
...dbMockServer,
collectionID: 'new-coll-123',
});
const result = await mockServerService.createMockServer(
user,
autoCreateInput,
);
expect(E.isRight(result)).toBe(true);
expect(mockUserCollectionService.createUserCollection).toHaveBeenCalledWith(
user,
autoCreateInput.name,
null,
null,
'REST',
);
expect(mockPrisma.mockServer.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
collectionID: 'new-coll-123',
}),
}),
);
});
test('should auto-create user collection with request example', async () => {
const autoCreateInput: CreateMockServerInput = {
name: 'Auto Mock Server',
workspaceType: WorkspaceType.USER,
workspaceID: undefined,
delayInMs: 0,
autoCreateCollection: true,
autoCreateRequestExample: true,
};
mockUserCollectionService.importCollectionsFromJSON.mockResolvedValue(
E.right({
exportedCollection: JSON.stringify([{ id: 'imported-coll-123' }]),
} as any),
);
mockPrisma.mockServer.create.mockResolvedValue({
...dbMockServer,
collectionID: 'imported-coll-123',
});
const result = await mockServerService.createMockServer(
user,
autoCreateInput,
);
expect(E.isRight(result)).toBe(true);
expect(mockUserCollectionService.importCollectionsFromJSON).toHaveBeenCalled();
expect(mockPrisma.mockServer.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
collectionID: 'imported-coll-123',
}),
}),
);
});
test('should auto-create team collection without request example', async () => {
const autoCreateInput: CreateMockServerInput = {
name: 'Team Auto Mock',
workspaceType: WorkspaceType.TEAM,
workspaceID: 'team123',
delayInMs: 0,
autoCreateCollection: true,
autoCreateRequestExample: false,
};
const createdTeamColl = { ...teamCollection, id: 'new-team-coll-123' };
mockPrisma.team.findFirst.mockResolvedValue({ id: 'team123' } as any);
mockTeamCollectionService.createCollection.mockResolvedValue(
E.right(createdTeamColl as any),
);
mockPrisma.mockServer.create.mockResolvedValue({
...dbMockServer,
workspaceType: WorkspaceType.TEAM,
workspaceID: 'team123',
collectionID: 'new-team-coll-123',
});
const result = await mockServerService.createMockServer(
user,
autoCreateInput,
);
expect(E.isRight(result)).toBe(true);
expect(mockTeamCollectionService.createCollection).toHaveBeenCalledWith(
'team123',
autoCreateInput.name,
null,
null,
);
expect(mockPrisma.mockServer.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
collectionID: 'new-team-coll-123',
}),
}),
);
});
test('should auto-create team collection with request example', async () => {
const autoCreateInput: CreateMockServerInput = {
name: 'Team Auto Mock',
workspaceType: WorkspaceType.TEAM,
workspaceID: 'team123',
delayInMs: 0,
autoCreateCollection: true,
autoCreateRequestExample: true,
};
mockPrisma.team.findFirst.mockResolvedValue({ id: 'team123' } as any);
mockTeamCollectionService.importCollectionsFromJSON.mockResolvedValue(
E.right([{ id: 'imported-team-coll-123' }] as any),
);
mockPrisma.mockServer.create.mockResolvedValue({
...dbMockServer,
workspaceType: WorkspaceType.TEAM,
workspaceID: 'team123',
collectionID: 'imported-team-coll-123',
});
const result = await mockServerService.createMockServer(
user,
autoCreateInput,
);
expect(E.isRight(result)).toBe(true);
expect(mockTeamCollectionService.importCollectionsFromJSON).toHaveBeenCalled();
expect(mockPrisma.mockServer.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
collectionID: 'imported-team-coll-123',
}),
}),
);
});
test('should return error when auto-create user collection fails', async () => {
const autoCreateInput: CreateMockServerInput = {
name: 'Auto Mock Server',
workspaceType: WorkspaceType.USER,
workspaceID: undefined,
delayInMs: 0,
autoCreateCollection: true,
autoCreateRequestExample: false,
};
mockUserCollectionService.createUserCollection.mockResolvedValue(
E.left('user_collection/creation_failed'),
);
const result = await mockServerService.createMockServer(
user,
autoCreateInput,
);
expect(E.isLeft(result)).toBe(true);
if (E.isLeft(result)) {
expect(result.left).toBe('user_collection/creation_failed');
}
});
test('should return error when auto-create team collection fails', async () => {
const autoCreateInput: CreateMockServerInput = {
name: 'Team Auto Mock',
workspaceType: WorkspaceType.TEAM,
workspaceID: 'team123',
delayInMs: 0,
autoCreateCollection: true,
autoCreateRequestExample: false,
};
mockPrisma.team.findFirst.mockResolvedValue({ id: 'team123' } as any);
mockTeamCollectionService.createCollection.mockResolvedValue(
E.left('team_coll/short_title'),
);
const result = await mockServerService.createMockServer(
user,
autoCreateInput,
);
expect(E.isLeft(result)).toBe(true);
if (E.isLeft(result)) {
expect(result.left).toBe('team_coll/short_title');
}
});
test('should rollback collection on mock server creation failure', async () => {
const autoCreateInput: CreateMockServerInput = {
name: 'Auto Mock Server',
workspaceType: WorkspaceType.USER,
workspaceID: undefined,
delayInMs: 0,
autoCreateCollection: true,
autoCreateRequestExample: false,
};
const createdCollection = { ...userCollection, id: 'rollback-coll-123' };
mockUserCollectionService.createUserCollection.mockResolvedValue(
E.right(createdCollection as any),
);
mockPrisma.mockServer.create.mockRejectedValue(
new Error('Database error'),
);
mockUserCollectionService.deleteUserCollection.mockResolvedValue(
E.right(true),
);
const result = await mockServerService.createMockServer(
user,
autoCreateInput,
);
expect(E.isLeft(result)).toBe(true);
expect(mockUserCollectionService.deleteUserCollection).toHaveBeenCalledWith(
'rollback-coll-123',
user.uid,
);
});
test('should rollback team collection on mock server creation failure', async () => {
const autoCreateInput: CreateMockServerInput = {
name: 'Team Auto Mock',
workspaceType: WorkspaceType.TEAM,
workspaceID: 'team123',
delayInMs: 0,
autoCreateCollection: true,
autoCreateRequestExample: false,
};
const createdTeamColl = { ...teamCollection, id: 'rollback-team-coll-123' };
mockPrisma.team.findFirst.mockResolvedValue({ id: 'team123' } as any);
mockTeamCollectionService.createCollection.mockResolvedValue(
E.right(createdTeamColl as any),
);
mockPrisma.mockServer.create.mockRejectedValue(
new Error('Database error'),
);
mockTeamCollectionService.deleteCollection.mockResolvedValue(
E.right(true),
);
const result = await mockServerService.createMockServer(
user,
autoCreateInput,
);
expect(E.isLeft(result)).toBe(true);
expect(mockTeamCollectionService.deleteCollection).toHaveBeenCalledWith(
'rollback-team-coll-123',
);
});
});
}); });
describe('updateMockServer', () => { describe('updateMockServer', () => {

View file

@ -8,7 +8,6 @@ import {
MockServerCollection, MockServerCollection,
MockServerLog, MockServerLog,
} from './mock-server.model'; } from './mock-server.model';
import { User } from 'src/user/user.model';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import { import {
MOCK_SERVER_NOT_FOUND, MOCK_SERVER_NOT_FOUND,
@ -19,6 +18,7 @@ import {
MOCK_SERVER_DELETION_FAILED, MOCK_SERVER_DELETION_FAILED,
MOCK_SERVER_LOG_NOT_FOUND, MOCK_SERVER_LOG_NOT_FOUND,
MOCK_SERVER_LOG_DELETION_FAILED, MOCK_SERVER_LOG_DELETION_FAILED,
MOCK_SERVER_COLLECTION_CREATION_FAILED,
} from 'src/errors'; } from 'src/errors';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
import { WorkspaceType } from 'src/types/WorkspaceTypes'; import { WorkspaceType } from 'src/types/WorkspaceTypes';
@ -31,13 +31,20 @@ import { OffsetPaginationArgs } from 'src/types/input-types.args';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { MockServerAnalyticsService } from './mock-server-analytics.service'; import { MockServerAnalyticsService } from './mock-server-analytics.service';
import { PrismaError } from 'src/prisma/prisma-error-codes'; import { PrismaError } from 'src/prisma/prisma-error-codes';
import { TeamCollectionService } from 'src/team-collection/team-collection.service';
import { UserCollectionService } from 'src/user-collection/user-collection.service';
import { ReqType } from 'src/types/RequestTypes';
import { AuthUser } from 'src/types/AuthUser';
import { mockServerCollRequestExample } from './constants/mock-server-coll-request-example';
@Injectable() @Injectable()
export class MockServerService { export class MockServerService {
constructor( constructor(
private readonly mockServerAnalyticsService: MockServerAnalyticsService,
private readonly prisma: PrismaService,
private readonly configService: ConfigService, private readonly configService: ConfigService,
private readonly prisma: PrismaService,
private readonly mockServerAnalyticsService: MockServerAnalyticsService,
private readonly teamCollectionService: TeamCollectionService,
private readonly userCollectionService: UserCollectionService,
) {} ) {}
/** /**
@ -252,7 +259,10 @@ export class MockServerService {
/** /**
* Validate workspace access permission and existence * Validate workspace access permission and existence
*/ */
private async validateWorkspace(user: User, input: CreateMockServerInput) { private async validateWorkspace(
user: AuthUser,
input: CreateMockServerInput,
) {
if (input.workspaceType === WorkspaceType.TEAM) { if (input.workspaceType === WorkspaceType.TEAM) {
if (!input.workspaceID) return E.left(TEAM_INVALID_ID); if (!input.workspaceID) return E.left(TEAM_INVALID_ID);
@ -271,7 +281,12 @@ export class MockServerService {
/** /**
* Validate collection exists and user has access * Validate collection exists and user has access
*/ */
private async validateCollection(user: User, input: CreateMockServerInput) { private async validateCollection(
user: AuthUser,
input: CreateMockServerInput,
) {
if (!input.collectionID) return E.left(MOCK_SERVER_INVALID_COLLECTION);
if (input.workspaceType === WorkspaceType.TEAM) { if (input.workspaceType === WorkspaceType.TEAM) {
const collection = await this.prisma.teamCollection.findUnique({ const collection = await this.prisma.teamCollection.findUnique({
where: { id: input.collectionID, teamID: input.workspaceID }, where: { id: input.collectionID, teamID: input.workspaceID },
@ -291,25 +306,106 @@ export class MockServerService {
return E.left(MOCK_SERVER_INVALID_COLLECTION); return E.left(MOCK_SERVER_INVALID_COLLECTION);
} }
private async createAutoCollection(
user: AuthUser,
input: CreateMockServerInput,
) {
if (input.workspaceType === WorkspaceType.USER) {
if (!input.autoCreateRequestExample) {
// create only a collection
const userColl = await this.userCollectionService.createUserCollection(
user,
input.name,
null,
null,
ReqType.REST,
);
if (E.isLeft(userColl)) return E.left(userColl.left);
return E.right({ id: userColl.right.id });
} else {
// create collection with a request example
const importedUserColl =
await this.userCollectionService.importCollectionsFromJSON(
JSON.stringify(mockServerCollRequestExample(input.name)),
user.uid,
null,
ReqType.REST,
);
if (E.isLeft(importedUserColl)) return E.left(importedUserColl.left);
if (JSON.parse(importedUserColl.right.exportedCollection).length === 0)
return E.left(MOCK_SERVER_COLLECTION_CREATION_FAILED);
return E.right({
id: JSON.parse(importedUserColl.right.exportedCollection)[0].id,
});
}
} else if (input.workspaceType === WorkspaceType.TEAM) {
if (!input.workspaceID) return E.left(TEAM_INVALID_ID);
if (!input.autoCreateRequestExample) {
const teamColl = await this.teamCollectionService.createCollection(
input.workspaceID,
input.name,
null,
null,
);
if (E.isLeft(teamColl)) return E.left(teamColl.left);
return E.right({ id: teamColl.right.id });
} else {
const importedTeamColl =
await this.teamCollectionService.importCollectionsFromJSON(
JSON.stringify(mockServerCollRequestExample(input.name)),
input.workspaceID,
null,
);
if (E.isLeft(importedTeamColl)) return E.left(importedTeamColl.left);
if (importedTeamColl.right.length === 0)
return E.left(MOCK_SERVER_COLLECTION_CREATION_FAILED);
return E.right({
id: importedTeamColl.right[0].id,
});
}
}
return E.left(MOCK_SERVER_COLLECTION_CREATION_FAILED);
}
/** /**
* Create a new mock server * Create a new mock server
*/ */
async createMockServer( async createMockServer(
user: User, user: AuthUser,
input: CreateMockServerInput, input: CreateMockServerInput,
): Promise<E.Either<string, MockServer>> { ): Promise<E.Either<string, MockServer>> {
let collectionID: string | undefined = input.collectionID;
try { try {
// Validate workspace type and ID // Validate workspace type and ID
const workspaceValidation = await this.validateWorkspace(user, input); const workspaceValidation = await this.validateWorkspace(user, input);
if (E.isLeft(workspaceValidation)) { if (E.isLeft(workspaceValidation)) {
return E.left(workspaceValidation.left); return E.left(workspaceValidation.left);
} }
if (!input.autoCreateCollection) {
// Validate collection exists and user has access // Validate collection exists and user has access
const collectionValidation = await this.validateCollection(user, input); const collectionValidation = await this.validateCollection(user, input);
if (E.isLeft(collectionValidation)) { if (E.isLeft(collectionValidation)) {
return E.left(collectionValidation.left); return E.left(collectionValidation.left);
} }
}
// Auto-create collection if needed
if (input.autoCreateCollection) {
const newCollection = await this.createAutoCollection(user, input);
if (E.isLeft(newCollection)) {
return E.left(newCollection.left);
}
collectionID = newCollection.right.id;
}
// Create mock server // Create mock server
const subdomain: string = this.generateMockServerSubdomain(); const subdomain: string = this.generateMockServerSubdomain();
@ -318,7 +414,7 @@ export class MockServerService {
name: input.name, name: input.name,
subdomain, subdomain,
creatorUid: user.uid, creatorUid: user.uid,
collectionID: input.collectionID, collectionID: input.collectionID ?? collectionID,
workspaceType: input.workspaceType, workspaceType: input.workspaceType,
workspaceID: workspaceID:
input.workspaceType === WorkspaceType.TEAM input.workspaceType === WorkspaceType.TEAM
@ -335,9 +431,21 @@ export class MockServerService {
return E.right(this.cast(mockServer)); return E.right(this.cast(mockServer));
} catch (error) { } catch (error) {
if (input.autoCreateCollection && collectionID) {
if (input.workspaceType === WorkspaceType.USER) {
await this.userCollectionService.deleteUserCollection(
collectionID,
user.uid,
);
} else if (input.workspaceType === WorkspaceType.TEAM) {
await this.teamCollectionService.deleteCollection(collectionID);
}
}
if (error.code === PrismaError.UNIQUE_CONSTRAINT_VIOLATION) { if (error.code === PrismaError.UNIQUE_CONSTRAINT_VIOLATION) {
return this.createMockServer(user, input); // Retry on subdomain conflict return this.createMockServer(user, input); // Retry on subdomain conflict
} }
console.error('Error creating mock server:', error); console.error('Error creating mock server:', error);
return E.left(MOCK_SERVER_CREATION_FAILED); return E.left(MOCK_SERVER_CREATION_FAILED);
} }

View file

@ -1,4 +1,5 @@
import { InputType, Field } from '@nestjs/graphql'; import { InputType, Field } from '@nestjs/graphql';
import { IsOptional, Matches } from 'class-validator';
import { WorkspaceType } from 'src/types/WorkspaceTypes'; import { WorkspaceType } from 'src/types/WorkspaceTypes';
@InputType() @InputType()
@ -13,6 +14,10 @@ export class CreatePublishedDocsArgs {
name: 'version', name: 'version',
description: 'Version of the published document', description: 'Version of the published document',
}) })
@Matches(/^[a-zA-Z0-9.-]+$/, {
message:
'Version must only contain alphanumeric characters, dots, and hyphens',
})
version: string; version: string;
@Field({ @Field({
@ -62,6 +67,11 @@ export class UpdatePublishedDocsArgs {
description: 'Version of the published document', description: 'Version of the published document',
nullable: true, nullable: true,
}) })
@IsOptional()
@Matches(/^[a-zA-Z0-9.-]+$/, {
message:
'Version must only contain alphanumeric characters, dots, and hyphens',
})
version?: string; version?: string;
@Field({ @Field({

View file

@ -231,7 +231,7 @@ export class TeamCollectionResolver {
parentCollectionID ?? null, parentCollectionID ?? null,
); );
if (E.isLeft(importedCollection)) throwErr(importedCollection.left); if (E.isLeft(importedCollection)) throwErr(importedCollection.left);
return importedCollection.right; return true;
} }
@Mutation(() => TeamCollection, { @Mutation(() => TeamCollection, {

View file

@ -1397,7 +1397,7 @@ describe('importCollectionsFromJSON', () => {
rootTeamCollection.teamID, rootTeamCollection.teamID,
null, null,
); );
expect(result).toEqualRight(true); expect(result).toEqualRight([rootTeamCollection]);
}); });
test('should successfully create new TeamCollections in a child collection and TeamRequests with valid inputs', async () => { test('should successfully create new TeamCollections in a child collection and TeamRequests with valid inputs', async () => {
@ -1410,7 +1410,7 @@ describe('importCollectionsFromJSON', () => {
rootTeamCollection.teamID, rootTeamCollection.teamID,
rootTeamCollection.id, rootTeamCollection.id,
); );
expect(result).toEqualRight(true); expect(result).toEqualRight([rootTeamCollection]);
}); });
test('should send pubsub message to "team_coll/<teamID>/coll_added" on successful creation from jsonString', async () => { test('should send pubsub message to "team_coll/<teamID>/coll_added" on successful creation from jsonString', async () => {

View file

@ -266,7 +266,7 @@ export class TeamCollectionService {
), ),
); );
return E.right(true); return E.right(teamCollections);
} }
/** /**

View file

@ -1083,6 +1083,8 @@
"environment_variable_added": "Mock URL added to environment", "environment_variable_added": "Mock URL added to environment",
"environment_variable_updated": "Mock URL updated in environment", "environment_variable_updated": "Mock URL updated in environment",
"environment_created_with_variable": "Environment created with mock URL", "environment_created_with_variable": "Environment created with mock URL",
"add_example_request": "Add example request",
"add_example_request_hint": "The collection will be created with a sample request that demonstrates how to use the mock server",
"create_example_collection": "Create example collection", "create_example_collection": "Create example collection",
"create_example_collection_hint": "Create a pet store example collection with sample requests (GET, POST, PUT, DELETE)", "create_example_collection_hint": "Create a pet store example collection with sample requests (GET, POST, PUT, DELETE)",
"creating_example_collection": "Creating example collection...", "creating_example_collection": "Creating example collection...",

View file

@ -186,24 +186,24 @@
</div> </div>
</div> </div>
<!-- Create Example Collection Toggle (only when "new collection" is selected) --> <!-- Auto-create Request Example Toggle (only for new collection mode) -->
<div <div
v-if="collectionSelectionMode === 'new'" v-if="collectionSelectionMode === 'new'"
class="flex flex-col space-y-2" class="flex flex-col space-y-2"
> >
<div class="flex items-center"> <div class="flex items-center">
<HoppSmartToggle <HoppSmartToggle
:on="createExampleCollection" :on="autoCreateRequestExample"
@change="createExampleCollection = !createExampleCollection" @change="autoCreateRequestExample = !autoCreateRequestExample"
> >
{{ t("mock_server.create_example_collection") }} {{ t("mock_server.add_example_request") }}
</HoppSmartToggle> </HoppSmartToggle>
</div> </div>
<div <div
v-if="createExampleCollection" v-if="autoCreateRequestExample"
class="w-full text-xs text-secondaryLight" class="w-full text-xs text-secondaryLight"
> >
{{ t("mock_server.create_example_collection_hint") }} {{ t("mock_server.add_example_request_hint") }}
</div> </div>
</div> </div>
</div> </div>
@ -276,9 +276,7 @@
:loading="loading" :loading="loading"
:disabled=" :disabled="
!mockServerName.trim() || !mockServerName.trim() ||
(!effectiveCollectionID && (!effectiveCollectionID && collectionSelectionMode === 'existing')
collectionSelectionMode === 'existing') ||
(collectionSelectionMode === 'new' && !createExampleCollection)
" "
:icon="IconServer" :icon="IconServer"
@click="handleCreateMockServer" @click="handleCreateMockServer"
@ -300,17 +298,10 @@ import { useReadonlyStream } from "@composables/stream"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { computed, ref, watch } from "vue" import { computed, ref, watch } from "vue"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import * as E from "fp-ts/Either"
import { MockServer } from "~/helpers/backend/graphql" import { MockServer } from "~/helpers/backend/graphql"
import { showCreateMockServerModal$ } from "~/newstore/mockServers" import { showCreateMockServerModal$ } from "~/newstore/mockServers"
import { useMockServer } from "~/composables/useMockServer" import { useMockServer } from "~/composables/useMockServer"
import MockServerCreatedInfo from "~/components/mockServer/MockServerCreatedInfo.vue" import MockServerCreatedInfo from "~/components/mockServer/MockServerCreatedInfo.vue"
import { useService } from "dioc/vue"
import { WorkspaceService } from "~/services/workspace.service"
import {
createMockCollectionForTeam,
createMockCollectionForPersonal,
} from "~/helpers/mockServer/exampleMockCollection"
// Icons // Icons
import IconCheck from "~icons/lucide/check" import IconCheck from "~icons/lucide/check"
@ -330,12 +321,6 @@ const {
toggleMockServer, toggleMockServer,
} = useMockServer() } = useMockServer()
// Services
const workspaceService = useService(WorkspaceService)
// Current workspace
const currentWorkspace = computed(() => workspaceService.currentWorkspace.value)
// Modal state // Modal state
const modalData = useReadonlyStream(showCreateMockServerModal$, { const modalData = useReadonlyStream(showCreateMockServerModal$, {
show: false, show: false,
@ -350,7 +335,7 @@ const createdServer = ref<MockServer | null>(null)
const delayInMsVal = ref<string>("0") const delayInMsVal = ref<string>("0")
const isPublic = ref<boolean>(true) const isPublic = ref<boolean>(true)
const setInEnvironment = ref<boolean>(true) const setInEnvironment = ref<boolean>(true)
const createExampleCollection = ref<boolean>(false) const autoCreateRequestExample = ref<boolean>(true)
const selectedCollectionID = ref("") const selectedCollectionID = ref("")
const selectedCollectionName = ref("") const selectedCollectionName = ref("")
const tippyActions = ref<TippyComponent | null>(null) const tippyActions = ref<TippyComponent | null>(null)
@ -388,6 +373,8 @@ const effectiveCollectionID = computed(() => {
// Get collection name // Get collection name
const collectionName = computed(() => { const collectionName = computed(() => {
if (selectedCollectionName.value) return selectedCollectionName.value if (selectedCollectionName.value) return selectedCollectionName.value
// When creating new collection, use the mock server name as collection name
if (collectionSelectionMode.value === "new") return mockServerName.value
return "Unknown Collection" return "Unknown Collection"
}) })
@ -402,42 +389,6 @@ const selectCollection = (option: any) => {
selectedCollectionName.value = option.label selectedCollectionName.value = option.label
} }
// Function to create an example collection and return its ID and name
const createExampleCollectionAndGetID = async (
collectionName: string
): Promise<{
id: string
name: string
}> => {
const workspaceType = currentWorkspace.value.type
if (workspaceType === "personal") {
// For personal workspace
const result = await createMockCollectionForPersonal(collectionName)
if (E.isLeft(result)) {
throw new Error(result.left)
}
return result.right
} else if (workspaceType === "team" && currentWorkspace.value.teamID) {
// For team workspace
const teamID = currentWorkspace.value.teamID
const result = await createMockCollectionForTeam(teamID, collectionName)
if (E.isLeft(result)) {
throw new Error(result.left)
}
// Wait a bit for the subscription to update
await new Promise((resolve) => setTimeout(resolve, 500))
return result.right
}
throw new Error("Unknown workspace type")
}
// Create new mock server // Create new mock server
const handleCreateMockServer = async () => { const handleCreateMockServer = async () => {
// Validate mock server name first // Validate mock server name first
@ -446,53 +397,28 @@ const handleCreateMockServer = async () => {
return return
} }
// Start loading and show creating message // For existing collection mode, validate that a collection is selected
if (
collectionSelectionMode.value === "existing" &&
!effectiveCollectionID.value
) {
toast.error(t("mock_server.select_collection_error"))
return
}
// Start loading
loading.value = true loading.value = true
// If "new collection" mode is selected, create example collection (if toggle is enabled) // Determine if we should auto-create a collection
let collectionIDToUse = effectiveCollectionID.value const isNewCollectionMode = collectionSelectionMode.value === "new"
if (collectionSelectionMode.value === "new") {
if (createExampleCollection.value) {
try {
// Silently create the collection in the background
const newCollection = await createExampleCollectionAndGetID(
mockServerName.value.trim()
)
// Update the selected collection with the actual created collection's ID and name
collectionIDToUse = newCollection.id
selectedCollectionID.value = newCollection.id
selectedCollectionName.value = newCollection.name
} catch (error) {
console.error("Failed to create collection:", error)
// If collection creation fails, stop the entire process
toast.error(t("mock_server.failed_to_create_mock_server"))
loading.value = false
return
}
} else {
// If new collection mode but example collection is not enabled
toast.error(t("mock_server.enable_example_collection_hint"))
loading.value = false
return
}
}
// Validate collection ID
if (!collectionIDToUse) {
toast.error(t("mock_server.select_collection_error"))
loading.value = false
return
}
// Wait a bit more to ensure collection is fully available in the system
await new Promise((resolve) => setTimeout(resolve, 300))
// Now create the mock server // Now create the mock server
const result = await createMockServer({ const result = await createMockServer({
mockServerName: mockServerName.value, mockServerName: mockServerName.value,
collectionID: collectionIDToUse, collectionID: isNewCollectionMode ? undefined : effectiveCollectionID.value,
autoCreateCollection: isNewCollectionMode ? true : undefined,
autoCreateRequestExample:
isNewCollectionMode && autoCreateRequestExample.value ? true : undefined,
delayInMs: Number(delayInMsVal.value) || 0, delayInMs: Number(delayInMsVal.value) || 0,
isPublic: isPublic.value, isPublic: isPublic.value,
setInEnvironment: setInEnvironment.value, setInEnvironment: setInEnvironment.value,
@ -503,6 +429,12 @@ const handleCreateMockServer = async () => {
if (result.success && result.server) { if (result.success && result.server) {
createdServer.value = result.server createdServer.value = result.server
// Update the selected collection info from the created server
if (result.server.collection) {
selectedCollectionID.value = result.server.collection.id
selectedCollectionName.value = result.server.collection.title
}
} }
} }
@ -539,19 +471,12 @@ watch(show, (newShow) => {
loading.value = false loading.value = false
delayInMsVal.value = "0" delayInMsVal.value = "0"
isPublic.value = true isPublic.value = true
autoCreateRequestExample.value = true
setInEnvironment.value = true setInEnvironment.value = true
createExampleCollection.value = false
selectedCollectionID.value = "" selectedCollectionID.value = ""
selectedCollectionName.value = "" selectedCollectionName.value = ""
createdServer.value = null createdServer.value = null
collectionSelectionMode.value = "existing" collectionSelectionMode.value = "existing"
} }
}) })
// Auto-enable example collection toggle when switching to "new" mode
watch(collectionSelectionMode, (newMode) => {
if (newMode === "new") {
createExampleCollection.value = true
}
})
</script> </script>

View file

@ -27,9 +27,11 @@ import {
addMockServer, addMockServer,
mockServers$, mockServers$,
updateMockServer as updateMockServerInStore, updateMockServer as updateMockServerInStore,
loadMockServers,
} from "~/newstore/mockServers" } from "~/newstore/mockServers"
import { TeamCollectionsService } from "~/services/team-collection.service" import { TeamCollectionsService } from "~/services/team-collection.service"
import { WorkspaceService } from "~/services/workspace.service" import { WorkspaceService } from "~/services/workspace.service"
import { platform } from "~/platform"
export function useMockServer() { export function useMockServer() {
const t = useI18n() const t = useI18n()
@ -62,6 +64,30 @@ export function useMockServer() {
: undefined : undefined
) )
// Function to refetch collections and mock servers
const refetchData = async () => {
try {
// Refetch mock servers
await loadMockServers()
// Refetch collections based on workspace type
if (
currentWorkspace.value.type === "team" &&
currentWorkspace.value.teamID
) {
// For team workspace, reload team collections by re-initializing with the same team ID
teamCollectionsService.changeTeamID(currentWorkspace.value.teamID)
} else {
// For personal workspace, load REST collections only (mock servers are REST-based)
if (platform.sync.collections.loadUserCollections) {
await platform.sync.collections.loadUserCollections("REST")
}
}
} catch (error) {
console.error("Failed to refetch data:", error)
}
}
// Function to add mock URL to environment // Function to add mock URL to environment
const addMockUrlToEnvironment = async ( const addMockUrlToEnvironment = async (
mockUrl: string, mockUrl: string,
@ -190,7 +216,9 @@ export function useMockServer() {
// Create new mock server // Create new mock server
const createMockServer = async (params: { const createMockServer = async (params: {
mockServerName: string mockServerName: string
collectionID: string collectionID?: string
autoCreateCollection?: boolean
autoCreateRequestExample?: boolean
delayInMs: number delayInMs: number
isPublic: boolean isPublic: boolean
setInEnvironment: boolean setInEnvironment: boolean
@ -199,16 +227,24 @@ export function useMockServer() {
const { const {
mockServerName, mockServerName,
collectionID, collectionID,
autoCreateCollection,
autoCreateRequestExample,
delayInMs, delayInMs,
isPublic, isPublic,
setInEnvironment, setInEnvironment,
collectionName, collectionName,
} = params } = params
if (!mockServerName.trim() || !collectionID) { if (!mockServerName.trim()) {
if (!collectionID) { return { success: false, server: null }
toast.error(t("mock_server.select_collection_error"))
} }
// Exactly one of collectionID or autoCreateCollection must be provided (XOR)
if (
(!collectionID && !autoCreateCollection) ||
(collectionID && autoCreateCollection)
) {
toast.error(t("mock_server.select_collection_error"))
return { success: false, server: null } return { success: false, server: null }
} }
@ -225,11 +261,13 @@ export function useMockServer() {
const result = await pipe( const result = await pipe(
createMockServerMutation( createMockServerMutation(
mockServerName.trim(), mockServerName.trim(),
collectionID,
workspaceType, workspaceType,
workspaceID, workspaceID,
delayInMs, delayInMs,
isPublic isPublic,
collectionID,
autoCreateCollection,
autoCreateRequestExample
), ),
TE.match( TE.match(
(error) => { (error) => {
@ -258,6 +296,9 @@ export function useMockServer() {
} }
} }
// Refetch collections and mock servers to get the latest data
await refetchData()
return { success: true, server: result } return { success: true, server: result }
} }

View file

@ -54,11 +54,13 @@ type DeleteMockServerError =
export const createMockServer = ( export const createMockServer = (
name: string, name: string,
collectionID: string,
workspaceType: WorkspaceType = WorkspaceType.User, workspaceType: WorkspaceType = WorkspaceType.User,
workspaceID?: string, workspaceID?: string,
delayInMs: number = 0, delayInMs: number = 0,
isPublic: boolean = true isPublic: boolean = true,
collectionID?: string,
autoCreateCollection?: boolean,
autoCreateRequestExample?: boolean
) => ) =>
TE.tryCatch( TE.tryCatch(
async () => { async () => {
@ -67,6 +69,8 @@ export const createMockServer = (
input: { input: {
name, name,
collectionID, collectionID,
autoCreateCollection,
autoCreateRequestExample,
workspaceType, workspaceType,
workspaceID, workspaceID,
delayInMs, delayInMs,
@ -107,7 +111,7 @@ export const createMockServer = (
return { return {
...data, ...data,
userUid: data.creator?.uid || "", // Legacy field userUid: data.creator?.uid || "", // Legacy field
collectionID: data.collection?.id || collectionID, // Legacy field collectionID: data.collection?.id || collectionID || "", // Legacy field - use response collection ID if available
} as MockServer } as MockServer
}, },
(error) => (error as Error).message as CreateMockServerError (error) => (error as Error).message as CreateMockServerError

View file

@ -4,6 +4,7 @@ import * as E from "fp-ts/Either"
export type CollectionsPlatformDef = { export type CollectionsPlatformDef = {
initCollectionsSync: () => void initCollectionsSync: () => void
loadUserCollections?: (collectionType: "REST" | "GQL") => Promise<void>
importToPersonalWorkspace?: ( importToPersonalWorkspace?: (
collections: HoppCollection[], collections: HoppCollection[],
reqType: ReqType reqType: ReqType

View file

@ -1032,6 +1032,7 @@ import { importToPersonalWorkspace } from "./import"
export const def: CollectionsPlatformDef = { export const def: CollectionsPlatformDef = {
initCollectionsSync, initCollectionsSync,
loadUserCollections,
importToPersonalWorkspace, importToPersonalWorkspace,
} }

View file

@ -1032,6 +1032,7 @@ function setupUserRequestDeletedSubscription() {
export const def: CollectionsPlatformDef = { export const def: CollectionsPlatformDef = {
initCollectionsSync, initCollectionsSync,
loadUserCollections,
importToPersonalWorkspace, importToPersonalWorkspace,
} }