From cd6dae598184664f6d5132ed94e2b5420dec98e4 Mon Sep 17 00:00:00 2001 From: Parth Bhardwaj Date: Tue, 24 Feb 2026 11:54:20 +0530 Subject: [PATCH 1/3] ENGG-5253: Add support for examples in local workspaces --- .../local-sync/fs-manager.rpc-service.ts | 14 + src/renderer/actions/local-sync/fs-manager.ts | 274 ++++++++++++++++-- src/renderer/actions/local-sync/fs-utils.ts | 100 ++++--- src/renderer/actions/local-sync/schemas.ts | 18 ++ src/renderer/actions/local-sync/types.ts | 22 +- 5 files changed, 355 insertions(+), 73 deletions(-) diff --git a/src/renderer/actions/local-sync/fs-manager.rpc-service.ts b/src/renderer/actions/local-sync/fs-manager.rpc-service.ts index 560520c4..cde5b276 100644 --- a/src/renderer/actions/local-sync/fs-manager.rpc-service.ts +++ b/src/renderer/actions/local-sync/fs-manager.rpc-service.ts @@ -150,6 +150,20 @@ export class FsManagerRPCService extends RPCServiceOverIPC { "createCollectionFromCompleteRecord", this.fsManager.createCollectionFromCompleteRecord.bind(this.fsManager) ); + this.exposeMethodOverIPC( + "createExampleRequest", + this.fsManager.createExampleRequest.bind(this.fsManager) + ); + + this.exposeMethodOverIPC( + "updateExampleRequest", + this.fsManager.updateExampleRequest.bind(this.fsManager) + ); + + this.exposeMethodOverIPC( + "deleteExampleRequest", + this.fsManager.deleteExampleRequest.bind(this.fsManager) + ); // hack await waitForInit(); diff --git a/src/renderer/actions/local-sync/fs-manager.ts b/src/renderer/actions/local-sync/fs-manager.ts index 22671f1d..8de81c30 100644 --- a/src/renderer/actions/local-sync/fs-manager.ts +++ b/src/renderer/actions/local-sync/fs-manager.ts @@ -28,6 +28,7 @@ import { getFileNameFromPath, getIfFileExists, getParentFolderPath, + parseExampleContentIntoRecord, parseFile, parseFileRaw, parseFileResultToApi, @@ -52,6 +53,7 @@ import { ApiEntryType, RequestContentType, ApiMethods, + ExampleRecord, } from "./schemas"; import { API, @@ -62,6 +64,7 @@ import { EnvironmentVariableValue, ErrorCode, ErroredRecord, + ExampleAPI, FileResource, FileSystemResult, FileTypeEnum, @@ -76,6 +79,7 @@ import { ReadmeRecordFileType, } from "./file-types/file-types"; import { isEmpty } from "lodash"; +import { v4 as uuidv4 } from "uuid"; import { HandleError } from "./decorators/handle-error.decorator"; import { FsIgnoreManager } from "./fsIgnore-manager"; import { fileIndex } from "./file-index"; @@ -678,38 +682,83 @@ export class FsManager { const erroredRecords: ErroredRecord[] = []; // eslint-disable-next-line for (const resource of resourceContainerResult.content) { - const entityParsingResult: FileSystemResult | undefined = - await (async () => { - if (resource.type === "folder") { - return parseFolderToCollection(this.rootPath, resource).then( - (result) => - mapSuccessfulFsResult( - result, - (successfulResult) => successfulResult.content - ) - ); - } - return parseFileToApi(this.rootPath, resource).then((result) => - mapSuccessfulFsResult( - result, - (successfulResult) => successfulResult.content - ) - ); - })(); - - if (entityParsingResult?.type === "error") { - erroredRecords.push({ - name: getFileNameFromPath(entityParsingResult.error.path), - path: entityParsingResult.error.path, - error: entityParsingResult.error.message, - type: entityParsingResult.error.fileType, + if (resource.type === "folder") { + const entityParsingResult = await parseFolderToCollection( + this.rootPath, + resource + ).then((result) => + mapSuccessfulFsResult( + result, + (successfulResult) => successfulResult.content + ) + ); + + if (entityParsingResult?.type === "error") { + erroredRecords.push({ + name: getFileNameFromPath(entityParsingResult.error.path), + path: entityParsingResult.error.path, + error: entityParsingResult.error.message, + type: entityParsingResult.error.fileType, + }); + // eslint-disable-next-line + continue; + } + + if (entityParsingResult) { + entities.push(entityParsingResult.content); + } + } else { + const fileResult = await parseFile({ + resource, + fileType: new ApiRecordFileType(), }); - // eslint-disable-next-line - continue; - } - if (entityParsingResult) { - entities.push(entityParsingResult.content); + if (fileResult.type === "error") { + erroredRecords.push({ + name: getFileNameFromPath(fileResult.error.path), + path: fileResult.error.path, + error: fileResult.error.message, + type: fileResult.error.fileType, + }); + // eslint-disable-next-line + continue; + } + + const apiResult = parseFileResultToApi( + this.rootPath, + resource, + fileResult + ); + + if (apiResult.type === "error") { + erroredRecords.push({ + name: getFileNameFromPath(apiResult.error.path), + path: apiResult.error.path, + error: apiResult.error.message, + type: apiResult.error.fileType, + }); + // eslint-disable-next-line + continue; + } + + entities.push(apiResult.content); + + const { content: record } = fileResult; + if (record.examples) { + const parentRequestId = getIdFromPath(resource.path); + for (const [exampleId, exampleContent] of Object.entries( + record.examples + )) { + const exampleResult = parseExampleContentIntoRecord( + exampleContent, + exampleId, + parentRequestId + ); + if (exampleResult.type === "success") { + entities.push(exampleResult.content); + } + } + } } } @@ -1568,4 +1617,169 @@ export class FsManager { return parseFolderToCollection(this.rootPath, collectionFolder); } + + @HandleError + async deleteExampleRequest( + parentRequestId: string, + exampleId: string + ): Promise> { + const requestFileResource = this.createResource({ + id: parentRequestId, + type: "file", + }); + + const fileResult = await parseFile({ + resource: requestFileResource, + fileType: new ApiRecordFileType(), + }); + + if (fileResult.type === "error") { + return fileResult; + } + + const { content } = fileResult; + + if (!content.examples || !(exampleId in content.examples)) { + return { + type: "error", + error: { + code: ErrorCode.UNKNOWN, + message: `Example with id ${exampleId} not found in request ${parentRequestId}`, + path: requestFileResource.path, + fileType: FileTypeEnum.UNKNOWN, + }, + }; + } + + delete content.examples[exampleId]; + + if (Object.keys(content.examples).length === 0) { + delete content.examples; + } + + const writeResult = await writeContent( + requestFileResource, + content, + new ApiRecordFileType() + ); + + if (writeResult.type === "error") { + return writeResult; + } + + return { + type: "success", + }; + } + + @HandleError + async createExampleRequest( + parentRequestId: string, + example: Static + ): Promise> { + const requestFileResource = this.createResource({ + id: parentRequestId, + type: "file", + }); + + const fileResult = await parseFile({ + resource: requestFileResource, + fileType: new ApiRecordFileType(), + }); + + if (fileResult.type === "error") { + return fileResult; + } + + const { content } = fileResult; + + const newExampleId = uuidv4(); + const newExampleRequest: Static = { + name: example.name, + rank: example.rank, + request: example.request, + response: example.response, + }; + + if (!content.examples) { + content.examples = {}; + } + content.examples[newExampleId] = newExampleRequest; + + const writeResult = await writeContent( + requestFileResource, + content, + new ApiRecordFileType() + ); + + if (writeResult.type === "error") { + return writeResult; + } + + return parseExampleContentIntoRecord( + newExampleRequest, + newExampleId, + parentRequestId + ); + } + + @HandleError + async updateExampleRequest( + parentRequestId: string, + exampleId: string, + example: Static + ): Promise> { + const requestFileResource = this.createResource({ + id: parentRequestId, + type: "file", + }); + + const fileResult = await parseFile({ + resource: requestFileResource, + fileType: new ApiRecordFileType(), + }); + + if (fileResult.type === "error") { + return fileResult; + } + + const { content } = fileResult; + + if (!content.examples || !(exampleId in content.examples)) { + return { + type: "error", + error: { + code: ErrorCode.UNKNOWN, + message: `Example with id ${exampleId} not found in request ${parentRequestId}`, + path: requestFileResource.path, + fileType: FileTypeEnum.UNKNOWN, + }, + }; + } + + const updatedExample: Static = { + name: example.name, + rank: example.rank, + request: example.request, + response: example.response, + }; + + content.examples[exampleId] = updatedExample; + + const writeResult = await writeContent( + requestFileResource, + content, + new ApiRecordFileType() + ); + + if (writeResult.type === "error") { + return writeResult; + } + + return parseExampleContentIntoRecord( + updatedExample, + exampleId, + parentRequestId + ); + } } diff --git a/src/renderer/actions/local-sync/fs-utils.ts b/src/renderer/actions/local-sync/fs-utils.ts index 90b5c658..9f191ce8 100644 --- a/src/renderer/actions/local-sync/fs-utils.ts +++ b/src/renderer/actions/local-sync/fs-utils.ts @@ -6,6 +6,7 @@ import { Environment, EnvironmentVariableValue, ErrorCode, + ExampleAPI, FileResource, FileSystemResult, FileTypeEnum, @@ -48,6 +49,7 @@ import { GlobalConfig, ApiRecord, AuthType, + ExampleRecord, } from "./schemas"; import { Stats } from "node:fs"; import { FileType } from "./file-types/file-type.interface"; @@ -84,8 +86,6 @@ export async function getFsResourceStats( } } - - export async function getIfFolderExists(resource: FolderResource) { const statsResult = await getFsResourceStats(resource); const doesFolderExist = @@ -109,12 +109,13 @@ async function hasWorkspaceConfigInAncestors( while (true) { const configPath = appendPath(currentDir, CONFIG_FILE); - const doesConfigFileExist = await getIfFileExists(createFsResource({ - rootPath: currentDir, - path: configPath, - type: "file", - })); - + const doesConfigFileExist = await getIfFileExists( + createFsResource({ + rootPath: currentDir, + path: configPath, + type: "file", + }) + ); if (doesConfigFileExist) { return true; @@ -131,7 +132,9 @@ async function hasWorkspaceConfigInAncestors( return false; } -export async function checkIsWorkspacePathAvailable(workspacePath: string): Promise { +export async function checkIsWorkspacePathAvailable( + workspacePath: string +): Promise { const sanitizedWorkspacePath = sanitizePath(workspacePath); const hasWorkspaceConfig = await hasWorkspaceConfigInAncestors( sanitizedWorkspacePath @@ -535,7 +538,9 @@ async function getWorkspaceIdFromConfig( * For example, on Windows the path is passed with backslashes, while on Unix the path is passed with forward slashes, comparing them directly will fail. */ const workspace = config.workspaces.find( - (ws) => ws.path.replace(/[/\\]/g, '/') === workspaceFolderPath?.replace(/[/\\]/g, '/') + (ws) => + ws.path.replace(/[/\\]/g, "/") === + workspaceFolderPath?.replace(/[/\\]/g, "/") ); if (!workspace) { return null; @@ -543,7 +548,6 @@ async function getWorkspaceIdFromConfig( return workspace.id; } - export async function createGlobalConfigFolder(): Promise< FileSystemResult<{ resource: FolderResource }> > { @@ -567,7 +571,6 @@ export async function createGlobalConfigFolder(): Promise< } } - export async function addWorkspaceToGlobalConfig(params: { name: string; path: string; @@ -608,7 +611,6 @@ export async function addWorkspaceToGlobalConfig(params: { workspaces: [newWorkspace], }; - const writeResult = await writeToGlobalConfig(config); if (writeResult.type === "error") { return writeResult; @@ -628,8 +630,8 @@ export async function addWorkspaceToGlobalConfig(params: { name, id: workspaceId, path: workspacePath, - } - } + }, + }; } const readResult = await parseFile({ @@ -739,7 +741,6 @@ export async function createWorkspaceFolder( }); } - export async function createDefaultWorkspace(): Promise< FileSystemResult<{ name: string; id: string; path: string }> > { @@ -749,23 +750,26 @@ export async function createDefaultWorkspace(): Promise< LOCAL_WORKSPACES_DIRECTORY_NAME ); try { - const rqDirectoryExists = await getIfFolderExists(createFsResource({ - rootPath: rqDirectoryPath, - path: rqDirectoryPath, - type: "folder", - })); - + const rqDirectoryExists = await getIfFolderExists( + createFsResource({ + rootPath: rqDirectoryPath, + path: rqDirectoryPath, + type: "folder", + }) + ); const workspaceFolderPath = appendPath( rqDirectoryPath, DEFAULT_WORKSPACE_NAME ); - const doesWorkspaceFolderExists = await getIfFolderExists(createFsResource({ - rootPath: rqDirectoryPath, - path: workspaceFolderPath, - type: "folder", - })); + const doesWorkspaceFolderExists = await getIfFolderExists( + createFsResource({ + rootPath: rqDirectoryPath, + path: workspaceFolderPath, + type: "folder", + }) + ); if (doesWorkspaceFolderExists) { const workspaceId = await getWorkspaceIdFromConfig(workspaceFolderPath); @@ -831,11 +835,7 @@ export async function createDefaultWorkspace(): Promise< return createWorkspaceFolder(DEFAULT_WORKSPACE_NAME, rqDirectoryPath); } catch (err: any) { - return createFileSystemError( - err, - rqDirectoryPath, - FileTypeEnum.UNKNOWN - ); + return createFileSystemError(err, rqDirectoryPath, FileTypeEnum.UNKNOWN); } } @@ -918,14 +918,14 @@ export async function migrateGlobalConfig(oldConfig: any) { type WorkspaceValidationResult = | { - valid: true; - ws: Static["workspaces"][number]; - } + valid: true; + ws: Static["workspaces"][number]; + } | { - valid: false; - ws: Static["workspaces"][number]; - error: { message: string }; - }; + valid: false; + ws: Static["workspaces"][number]; + error: { message: string }; + }; async function validateWorkspace( ws: Static["workspaces"][number] @@ -1416,3 +1416,25 @@ export function getFileNameFromPath(filePath: string) { const parts = filePath.split("/"); return parts[parts.length - 1]; } + +export const parseExampleContentIntoRecord = ( + content: Static, + id: string, + parentRequestId: string +): FileSystemResult => { + return { + type: "success", + content: { + id, + type: "example_api", + collectionId: null, + parentRequestId, + data: { + rank: content.rank, + name: content.name, + request: content.request, + response: content.response ?? null, + }, + }, + }; +}; diff --git a/src/renderer/actions/local-sync/schemas.ts b/src/renderer/actions/local-sync/schemas.ts index 31b85c06..a4a99938 100644 --- a/src/renderer/actions/local-sync/schemas.ts +++ b/src/renderer/actions/local-sync/schemas.ts @@ -135,6 +135,16 @@ export const BaseRequest = Type.Object({ ), }); +export const ResponseObject = Type.Object({ + body: Type.String(), + headers: Type.Array(KeyValuePair), + method: Type.Enum(ApiMethods), + status: Type.Number(), + statusText: Type.Optional(Type.String()), + time: Type.Optional(Type.Number()), + redirectedUrl: Type.Optional(Type.String()), +}); + export const PathVariable = Type.Object({ id: Type.Number(), key: Type.String(), @@ -171,10 +181,18 @@ export const GraphQLRequest = Type.Intersect([ export const ApiRequest = Type.Union([HttpRequest, GraphQLRequest]); +export const ExampleRecord = Type.Object({ + rank: Type.Optional(Type.String()), + name: Type.String(), + request: ApiRequest, + response: Type.Union([ResponseObject, Type.Null()]), +}); + export const ApiRecord = Type.Object({ name: Type.String(), rank: Type.Optional(Type.String()), request: ApiRequest, + examples: Type.Optional(Type.Record(Type.String(), ExampleRecord)), }); export const Variables = Type.Record( diff --git a/src/renderer/actions/local-sync/types.ts b/src/renderer/actions/local-sync/types.ts index 859ef634..5c0f96fb 100644 --- a/src/renderer/actions/local-sync/types.ts +++ b/src/renderer/actions/local-sync/types.ts @@ -6,6 +6,7 @@ import { EnvironmentVariableType, ApiEntryType, ApiRequest, + ResponseObject, } from "./schemas"; import { type FsService } from "./fs/fs.service"; @@ -52,9 +53,9 @@ export type ContentParseError = { message: string }; export type ContentParseResult = | ContentfulSuccess | { - type: "error"; - error: ContentParseError; - }; + type: "error"; + error: ContentParseError; + }; export type FolderResource = { type: "folder"; @@ -89,6 +90,19 @@ export type API = { }; }; +export type ExampleAPI = { + id: string; + type: "example_api"; + collectionId: null; + parentRequestId: string; + data: { + name: string; + rank?: string; + request: Static; + response: Static | null; + }; +}; + type VariableValueType = string | number | boolean; export type EnvironmentVariableValue = { @@ -116,7 +130,7 @@ export type Environment = { variables?: Variable; }; -export type APIEntity = Collection | API | Environment; +export type APIEntity = Collection | API | Environment | ExampleAPI; export type CollectionRecord = { type: "collection"; From 2bd9b2fcae2141d62266c886f4abc00aa17abd74 Mon Sep 17 00:00:00 2001 From: Parth Bhardwaj Date: Fri, 27 Feb 2026 14:39:35 +0530 Subject: [PATCH 2/3] address comments --- src/renderer/actions/local-sync/fs-manager.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/renderer/actions/local-sync/fs-manager.ts b/src/renderer/actions/local-sync/fs-manager.ts index 8de81c30..abd18c80 100644 --- a/src/renderer/actions/local-sync/fs-manager.ts +++ b/src/renderer/actions/local-sync/fs-manager.ts @@ -1643,10 +1643,10 @@ export class FsManager { return { type: "error", error: { - code: ErrorCode.UNKNOWN, + code: ErrorCode.NotFound, message: `Example with id ${exampleId} not found in request ${parentRequestId}`, path: requestFileResource.path, - fileType: FileTypeEnum.UNKNOWN, + fileType: FileTypeEnum.API, }, }; } @@ -1749,10 +1749,10 @@ export class FsManager { return { type: "error", error: { - code: ErrorCode.UNKNOWN, + code: ErrorCode.NotFound, message: `Example with id ${exampleId} not found in request ${parentRequestId}`, path: requestFileResource.path, - fileType: FileTypeEnum.UNKNOWN, + fileType: FileTypeEnum.API, }, }; } From e8843aa25e97d5b10a3d745d9081781ea7152e5b Mon Sep 17 00:00:00 2001 From: Parth Bhardwaj Date: Thu, 5 Mar 2026 12:12:07 +0530 Subject: [PATCH 3/3] fix --- src/renderer/actions/local-sync/schemas.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/renderer/actions/local-sync/schemas.ts b/src/renderer/actions/local-sync/schemas.ts index a4a99938..667f4a3e 100644 --- a/src/renderer/actions/local-sync/schemas.ts +++ b/src/renderer/actions/local-sync/schemas.ts @@ -62,6 +62,16 @@ const KeyValuePair = Type.Object({ dataType: Type.Optional(Type.Enum(KeyValueDataType)), }); +const ResponseHeader = Type.Object({ + key: Type.String(), + value: Type.String(), + id: Type.Optional(Type.Number()), + isEnabled: Type.Optional(Type.Boolean()), + type: Type.Optional(Type.String()), + description: Type.Optional(Type.String()), + dataType: Type.Optional(Type.Enum(KeyValueDataType)), +}); + const formData = Type.Object({ id: Type.Number(), key: Type.String(), @@ -137,8 +147,8 @@ export const BaseRequest = Type.Object({ export const ResponseObject = Type.Object({ body: Type.String(), - headers: Type.Array(KeyValuePair), - method: Type.Enum(ApiMethods), + headers: Type.Array(ResponseHeader), + method: Type.Optional(Type.Enum(ApiMethods)), status: Type.Number(), statusText: Type.Optional(Type.String()), time: Type.Optional(Type.Number()),