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';
/**
* 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
* (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;
@Field({
nullable: true,
description:
'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, {
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 { MockServerController } from './mock-server.controller';
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({
imports: [PrismaModule, TeamModule, TeamRequestModule, AccessTokenModule],
imports: [
PrismaModule,
UserCollectionModule,
TeamModule,
TeamCollectionModule,
TeamRequestModule,
AccessTokenModule,
],
controllers: [MockServerController],
providers: [
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 { TeamAccessRole } from 'src/team/team.model';
import { throwErr } from 'src/utils';
import { AuthUser } from 'src/types/AuthUser';
import { INVALID_PARAMS } from 'src/errors';
@Resolver(() => MockServer)
export class MockServerResolver {
@ -72,7 +74,7 @@ export class MockServerResolver {
})
@UseGuards(GqlAuthGuard)
async myMockServers(
@GqlUser() user: User,
@GqlUser() user: AuthUser,
@Args() args: OffsetPaginationArgs,
): Promise<MockServer[]> {
return this.mockServerService.getUserMockServers(user.uid, args);
@ -104,7 +106,7 @@ export class MockServerResolver {
})
@UseGuards(GqlAuthGuard)
async mockServer(
@GqlUser() user: User,
@GqlUser() user: AuthUser,
@Args({
name: 'id',
type: () => ID,
@ -124,7 +126,7 @@ export class MockServerResolver {
})
@UseGuards(GqlAuthGuard)
async mockServerLogs(
@GqlUser() user: User,
@GqlUser() user: AuthUser,
@Args({
name: 'mockServerID',
type: () => ID,
@ -151,8 +153,15 @@ export class MockServerResolver {
@UseGuards(GqlAuthGuard)
async createMockServer(
@Args('input') input: CreateMockServerInput,
@GqlUser() user: User,
@GqlUser() user: AuthUser,
): Promise<MockServer> {
if (
(input.collectionID && input.autoCreateCollection) ||
(!input.collectionID && !input.autoCreateCollection)
) {
throwErr(INVALID_PARAMS);
}
const result = await this.mockServerService.createMockServer(user, input);
if (E.isLeft(result)) throwErr(result.left);
@ -164,7 +173,7 @@ export class MockServerResolver {
})
@UseGuards(GqlAuthGuard)
async updateMockServer(
@GqlUser() user: User,
@GqlUser() user: AuthUser,
@Args() args: MockServerMutationArgs,
@Args('input') input: UpdateMockServerInput,
): Promise<MockServer> {
@ -183,7 +192,7 @@ export class MockServerResolver {
})
@UseGuards(GqlAuthGuard)
async deleteMockServer(
@GqlUser() user: User,
@GqlUser() user: AuthUser,
@Args() args: MockServerMutationArgs,
): Promise<boolean> {
const result = await this.mockServerService.deleteMockServer(
@ -200,7 +209,7 @@ export class MockServerResolver {
})
@UseGuards(GqlAuthGuard)
async deleteMockServerLog(
@GqlUser() user: User,
@GqlUser() user: AuthUser,
@Args({
name: 'logID',
type: () => ID,

View file

@ -2,6 +2,8 @@ import { MockServerService } from './mock-server.service';
import { PrismaService } from '../prisma/prisma.service';
import { ConfigService } from '@nestjs/config';
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 * as E from 'fp-ts/Either';
import {
@ -17,9 +19,9 @@ import {
UserCollection,
TeamCollection,
UserRequest,
User,
} from 'src/generated/prisma/client';
import { WorkspaceType } from '../types/WorkspaceTypes';
import { User } from '../user/user.model';
import {
CreateMockServerInput,
UpdateMockServerInput,
@ -28,17 +30,23 @@ import {
const mockPrisma = mockDeep<PrismaService>();
const mockAnalyticsService = mockDeep<MockServerAnalyticsService>();
const mockConfigService = mockDeep<ConfigService>();
const mockTeamCollectionService = mockDeep<TeamCollectionService>();
const mockUserCollectionService = mockDeep<UserCollectionService>();
const mockServerService = new MockServerService(
mockAnalyticsService,
mockPrisma,
mockConfigService,
mockPrisma,
mockAnalyticsService,
mockTeamCollectionService,
mockUserCollectionService,
);
beforeEach(() => {
mockReset(mockPrisma);
mockReset(mockAnalyticsService);
mockReset(mockConfigService);
mockReset(mockTeamCollectionService);
mockReset(mockUserCollectionService);
// Default config values
mockConfigService.get.mockImplementation((key: string) => {
@ -57,6 +65,7 @@ const user: User = {
email: 'test@example.com',
photoURL: null,
isAdmin: false,
refreshToken: null,
currentGQLSession: '{}',
currentRESTSession: '{}',
createdOn: currentTime,
@ -471,6 +480,282 @@ describe('MockServerService', () => {
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', () => {

View file

@ -8,7 +8,6 @@ import {
MockServerCollection,
MockServerLog,
} from './mock-server.model';
import { User } from 'src/user/user.model';
import * as E from 'fp-ts/Either';
import {
MOCK_SERVER_NOT_FOUND,
@ -19,6 +18,7 @@ import {
MOCK_SERVER_DELETION_FAILED,
MOCK_SERVER_LOG_NOT_FOUND,
MOCK_SERVER_LOG_DELETION_FAILED,
MOCK_SERVER_COLLECTION_CREATION_FAILED,
} from 'src/errors';
import { randomBytes } from 'crypto';
import { WorkspaceType } from 'src/types/WorkspaceTypes';
@ -31,13 +31,20 @@ import { OffsetPaginationArgs } from 'src/types/input-types.args';
import { ConfigService } from '@nestjs/config';
import { MockServerAnalyticsService } from './mock-server-analytics.service';
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()
export class MockServerService {
constructor(
private readonly mockServerAnalyticsService: MockServerAnalyticsService,
private readonly prisma: PrismaService,
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
*/
private async validateWorkspace(user: User, input: CreateMockServerInput) {
private async validateWorkspace(
user: AuthUser,
input: CreateMockServerInput,
) {
if (input.workspaceType === WorkspaceType.TEAM) {
if (!input.workspaceID) return E.left(TEAM_INVALID_ID);
@ -271,7 +281,12 @@ export class MockServerService {
/**
* 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) {
const collection = await this.prisma.teamCollection.findUnique({
where: { id: input.collectionID, teamID: input.workspaceID },
@ -291,24 +306,105 @@ export class MockServerService {
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
*/
async createMockServer(
user: User,
user: AuthUser,
input: CreateMockServerInput,
): Promise<E.Either<string, MockServer>> {
let collectionID: string | undefined = input.collectionID;
try {
// Validate workspace type and ID
const workspaceValidation = await this.validateWorkspace(user, input);
if (E.isLeft(workspaceValidation)) {
return E.left(workspaceValidation.left);
}
// Validate collection exists and user has access
const collectionValidation = await this.validateCollection(user, input);
if (E.isLeft(collectionValidation)) {
return E.left(collectionValidation.left);
if (!input.autoCreateCollection) {
// Validate collection exists and user has access
const collectionValidation = await this.validateCollection(user, input);
if (E.isLeft(collectionValidation)) {
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
@ -318,7 +414,7 @@ export class MockServerService {
name: input.name,
subdomain,
creatorUid: user.uid,
collectionID: input.collectionID,
collectionID: input.collectionID ?? collectionID,
workspaceType: input.workspaceType,
workspaceID:
input.workspaceType === WorkspaceType.TEAM
@ -335,9 +431,21 @@ export class MockServerService {
return E.right(this.cast(mockServer));
} 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) {
return this.createMockServer(user, input); // Retry on subdomain conflict
}
console.error('Error creating mock server:', error);
return E.left(MOCK_SERVER_CREATION_FAILED);
}

View file

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

View file

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

View file

@ -1397,7 +1397,7 @@ describe('importCollectionsFromJSON', () => {
rootTeamCollection.teamID,
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 () => {
@ -1410,7 +1410,7 @@ describe('importCollectionsFromJSON', () => {
rootTeamCollection.teamID,
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 () => {

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_updated": "Mock URL updated in environment",
"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_hint": "Create a pet store example collection with sample requests (GET, POST, PUT, DELETE)",
"creating_example_collection": "Creating example collection...",

View file

@ -186,24 +186,24 @@
</div>
</div>
<!-- Create Example Collection Toggle (only when "new collection" is selected) -->
<!-- Auto-create Request Example Toggle (only for new collection mode) -->
<div
v-if="collectionSelectionMode === 'new'"
class="flex flex-col space-y-2"
>
<div class="flex items-center">
<HoppSmartToggle
:on="createExampleCollection"
@change="createExampleCollection = !createExampleCollection"
:on="autoCreateRequestExample"
@change="autoCreateRequestExample = !autoCreateRequestExample"
>
{{ t("mock_server.create_example_collection") }}
{{ t("mock_server.add_example_request") }}
</HoppSmartToggle>
</div>
<div
v-if="createExampleCollection"
v-if="autoCreateRequestExample"
class="w-full text-xs text-secondaryLight"
>
{{ t("mock_server.create_example_collection_hint") }}
{{ t("mock_server.add_example_request_hint") }}
</div>
</div>
</div>
@ -276,9 +276,7 @@
:loading="loading"
:disabled="
!mockServerName.trim() ||
(!effectiveCollectionID &&
collectionSelectionMode === 'existing') ||
(collectionSelectionMode === 'new' && !createExampleCollection)
(!effectiveCollectionID && collectionSelectionMode === 'existing')
"
:icon="IconServer"
@click="handleCreateMockServer"
@ -300,17 +298,10 @@ import { useReadonlyStream } from "@composables/stream"
import { useToast } from "@composables/toast"
import { computed, ref, watch } from "vue"
import { TippyComponent } from "vue-tippy"
import * as E from "fp-ts/Either"
import { MockServer } from "~/helpers/backend/graphql"
import { showCreateMockServerModal$ } from "~/newstore/mockServers"
import { useMockServer } from "~/composables/useMockServer"
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
import IconCheck from "~icons/lucide/check"
@ -330,12 +321,6 @@ const {
toggleMockServer,
} = useMockServer()
// Services
const workspaceService = useService(WorkspaceService)
// Current workspace
const currentWorkspace = computed(() => workspaceService.currentWorkspace.value)
// Modal state
const modalData = useReadonlyStream(showCreateMockServerModal$, {
show: false,
@ -350,7 +335,7 @@ const createdServer = ref<MockServer | null>(null)
const delayInMsVal = ref<string>("0")
const isPublic = ref<boolean>(true)
const setInEnvironment = ref<boolean>(true)
const createExampleCollection = ref<boolean>(false)
const autoCreateRequestExample = ref<boolean>(true)
const selectedCollectionID = ref("")
const selectedCollectionName = ref("")
const tippyActions = ref<TippyComponent | null>(null)
@ -388,6 +373,8 @@ const effectiveCollectionID = computed(() => {
// Get collection name
const collectionName = computed(() => {
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"
})
@ -402,42 +389,6 @@ const selectCollection = (option: any) => {
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
const handleCreateMockServer = async () => {
// Validate mock server name first
@ -446,53 +397,28 @@ const handleCreateMockServer = async () => {
return
}
// Start loading and show creating message
loading.value = true
// If "new collection" mode is selected, create example collection (if toggle is enabled)
let collectionIDToUse = effectiveCollectionID.value
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) {
// For existing collection mode, validate that a collection is selected
if (
collectionSelectionMode.value === "existing" &&
!effectiveCollectionID.value
) {
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))
// Start loading
loading.value = true
// Determine if we should auto-create a collection
const isNewCollectionMode = collectionSelectionMode.value === "new"
// Now create the mock server
const result = await createMockServer({
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,
isPublic: isPublic.value,
setInEnvironment: setInEnvironment.value,
@ -503,6 +429,12 @@ const handleCreateMockServer = async () => {
if (result.success && 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
delayInMsVal.value = "0"
isPublic.value = true
autoCreateRequestExample.value = true
setInEnvironment.value = true
createExampleCollection.value = false
selectedCollectionID.value = ""
selectedCollectionName.value = ""
createdServer.value = null
collectionSelectionMode.value = "existing"
}
})
// Auto-enable example collection toggle when switching to "new" mode
watch(collectionSelectionMode, (newMode) => {
if (newMode === "new") {
createExampleCollection.value = true
}
})
</script>

View file

@ -27,9 +27,11 @@ import {
addMockServer,
mockServers$,
updateMockServer as updateMockServerInStore,
loadMockServers,
} from "~/newstore/mockServers"
import { TeamCollectionsService } from "~/services/team-collection.service"
import { WorkspaceService } from "~/services/workspace.service"
import { platform } from "~/platform"
export function useMockServer() {
const t = useI18n()
@ -62,6 +64,30 @@ export function useMockServer() {
: 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
const addMockUrlToEnvironment = async (
mockUrl: string,
@ -190,7 +216,9 @@ export function useMockServer() {
// Create new mock server
const createMockServer = async (params: {
mockServerName: string
collectionID: string
collectionID?: string
autoCreateCollection?: boolean
autoCreateRequestExample?: boolean
delayInMs: number
isPublic: boolean
setInEnvironment: boolean
@ -199,16 +227,24 @@ export function useMockServer() {
const {
mockServerName,
collectionID,
autoCreateCollection,
autoCreateRequestExample,
delayInMs,
isPublic,
setInEnvironment,
collectionName,
} = params
if (!mockServerName.trim() || !collectionID) {
if (!collectionID) {
toast.error(t("mock_server.select_collection_error"))
}
if (!mockServerName.trim()) {
return { success: false, server: null }
}
// 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 }
}
@ -225,11 +261,13 @@ export function useMockServer() {
const result = await pipe(
createMockServerMutation(
mockServerName.trim(),
collectionID,
workspaceType,
workspaceID,
delayInMs,
isPublic
isPublic,
collectionID,
autoCreateCollection,
autoCreateRequestExample
),
TE.match(
(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 }
}

View file

@ -54,11 +54,13 @@ type DeleteMockServerError =
export const createMockServer = (
name: string,
collectionID: string,
workspaceType: WorkspaceType = WorkspaceType.User,
workspaceID?: string,
delayInMs: number = 0,
isPublic: boolean = true
isPublic: boolean = true,
collectionID?: string,
autoCreateCollection?: boolean,
autoCreateRequestExample?: boolean
) =>
TE.tryCatch(
async () => {
@ -67,6 +69,8 @@ export const createMockServer = (
input: {
name,
collectionID,
autoCreateCollection,
autoCreateRequestExample,
workspaceType,
workspaceID,
delayInMs,
@ -107,7 +111,7 @@ export const createMockServer = (
return {
...data,
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
},
(error) => (error as Error).message as CreateMockServerError

View file

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

View file

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

View file

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