From c00cbf6cb2eaef5bcd8476502149133c2f076c25 Mon Sep 17 00:00:00 2001 From: naman-bruno Date: Wed, 10 Dec 2025 19:38:47 +0530 Subject: [PATCH] workspace schema update (#6374) --- packages/bruno-electron/src/app/apiSpecs.js | 39 +++-- .../src/app/workspace-watcher.js | 45 ++--- packages/bruno-electron/src/ipc/apiSpec.js | 33 ++-- packages/bruno-electron/src/ipc/workspace.js | 14 +- .../src/store/default-workspace.js | 23 +-- .../src/store/workspace-environments.js | 7 +- .../src/utils/workspace-config.js | 157 +++++++++++++----- 7 files changed, 185 insertions(+), 133 deletions(-) diff --git a/packages/bruno-electron/src/app/apiSpecs.js b/packages/bruno-electron/src/app/apiSpecs.js index 1e5c538dd..7bd68ba18 100644 --- a/packages/bruno-electron/src/app/apiSpecs.js +++ b/packages/bruno-electron/src/app/apiSpecs.js @@ -1,6 +1,17 @@ const { dialog, ipcMain } = require('electron'); -const { isDirectory, normalizeAndResolvePath } = require('../utils/filesystem'); +const { normalizeAndResolvePath } = require('../utils/filesystem'); const { generateUidBasedOnHash } = require('../utils/common'); +const { generateYamlContent } = require('../utils/workspace-config'); + +const normalizeWorkspaceConfig = (config) => { + return { + ...config, + name: config.info?.name, + type: config.info?.type, + collections: config.collections || [], + apiSpecs: config.specs || [] + }; +}; const openApiSpecDialog = async (win, watcher, options = {}) => { const { filePaths } = await dialog.showOpenDialog(win, { @@ -32,7 +43,7 @@ const openApiSpec = async (win, watcher, apiSpecPath, options = {}) => { const yamlContent = fs.readFileSync(workspaceFilePath, 'utf8'); const workspaceConfig = yaml.load(yamlContent); - workspaceConfig.apiSpecs = workspaceConfig.apiSpecs || []; + const specs = workspaceConfig.specs || []; let relativePath = apiSpecPath; try { @@ -44,32 +55,28 @@ const openApiSpec = async (win, watcher, apiSpecPath, options = {}) => { console.log('Using absolute path for API spec:', error.message); } - const apiSpecName = path.basename(apiSpecPath, path.extname(apiSpecPath)); - const apiSpecEntry = { - name: apiSpecName, + const specName = path.basename(apiSpecPath, path.extname(apiSpecPath)); + const specEntry = { + name: specName, path: relativePath }; - const existingApiSpec = workspaceConfig.apiSpecs.find((a) => { + const existingSpec = specs.find((a) => { const existingPath = path.isAbsolute(a.path) ? a.path : path.resolve(options.workspacePath, a.path); - return existingPath === apiSpecPath || a.name === apiSpecName; + return existingPath === apiSpecPath || a.name === specName; }); - if (!existingApiSpec) { - workspaceConfig.apiSpecs.push(apiSpecEntry); + if (!existingSpec) { + workspaceConfig.specs = [...specs, specEntry]; - const updatedYamlContent = yaml.dump(workspaceConfig, { - indent: 2, - lineWidth: -1, - noRefs: true - }); + const updatedYamlContent = generateYamlContent(workspaceConfig); fs.writeFileSync(workspaceFilePath, updatedYamlContent); - // Notify frontend that workspace config was updated + const normalizedConfig = normalizeWorkspaceConfig(workspaceConfig); const workspaceUid = generateUidBasedOnHash(options.workspacePath); - win.webContents.send('main:workspace-config-updated', options.workspacePath, workspaceUid, workspaceConfig); + win.webContents.send('main:workspace-config-updated', options.workspacePath, workspaceUid, normalizedConfig); } } } diff --git a/packages/bruno-electron/src/app/workspace-watcher.js b/packages/bruno-electron/src/app/workspace-watcher.js index de42adec7..b63ca0d7d 100644 --- a/packages/bruno-electron/src/app/workspace-watcher.js +++ b/packages/bruno-electron/src/app/workspace-watcher.js @@ -10,17 +10,21 @@ const { decryptStringSafe } = require('../utils/encryption'); const environmentSecretsStore = new EnvironmentSecretsStore(); -/** - * Check if environment has secret variables - */ const envHasSecrets = (environment) => { const secrets = _.filter(environment.variables, (v) => v.secret === true); return secrets && secrets.length > 0; }; -/** - * Handle workspace.yml file changes - */ +const normalizeWorkspaceConfig = (config) => { + return { + ...config, + name: config.info?.name, + type: config.info?.type, + collections: config.collections || [], + apiSpecs: config.specs || [] + }; +}; + const handleWorkspaceFileChange = (win, workspacePath) => { try { const workspaceFilePath = path.join(workspacePath, 'workspace.yml'); @@ -30,9 +34,11 @@ const handleWorkspaceFileChange = (win, workspacePath) => { } const yamlContent = fs.readFileSync(workspaceFilePath, 'utf8'); - const workspaceConfig = yaml.load(yamlContent); + const rawConfig = yaml.load(yamlContent); + const workspaceConfig = normalizeWorkspaceConfig(rawConfig); - if (workspaceConfig.type !== 'workspace') { + const type = workspaceConfig.info?.type || workspaceConfig.type; + if (type !== 'workspace') { return; } @@ -44,9 +50,6 @@ const handleWorkspaceFileChange = (win, workspacePath) => { } }; -/** - * Parse global environment file and handle secrets - */ const parseGlobalEnvironmentFile = async (pathname, workspacePath, workspaceUid) => { const basename = path.basename(pathname); const environmentName = basename.slice(0, -'.yml'.length); @@ -64,14 +67,12 @@ const parseGlobalEnvironmentFile = async (pathname, workspacePath, workspaceUid) file.data.name = environmentName; file.data.uid = generateUidBasedOnHash(pathname); - // Ensure all variables have UIDs _.each(_.get(file, 'data.variables', []), (variable) => { if (!variable.uid) { variable.uid = uuid(); } }); - // Decrypt secrets if present if (envHasSecrets(file.data)) { const envSecrets = environmentSecretsStore.getEnvSecrets(workspacePath, file.data); _.each(envSecrets, (secret) => { @@ -86,9 +87,6 @@ const parseGlobalEnvironmentFile = async (pathname, workspacePath, workspaceUid) return file; }; -/** - * Handle global environment file add - */ const handleGlobalEnvironmentFileAdd = async (win, pathname, workspacePath, workspaceUid) => { try { const file = await parseGlobalEnvironmentFile(pathname, workspacePath, workspaceUid); @@ -98,9 +96,6 @@ const handleGlobalEnvironmentFileAdd = async (win, pathname, workspacePath, work } }; -/** - * Handle global environment file change - */ const handleGlobalEnvironmentFileChange = async (win, pathname, workspacePath, workspaceUid) => { try { const file = await parseGlobalEnvironmentFile(pathname, workspacePath, workspaceUid); @@ -110,9 +105,6 @@ const handleGlobalEnvironmentFileChange = async (win, pathname, workspacePath, w } }; -/** - * Handle global environment file unlink - */ const handleGlobalEnvironmentFileUnlink = async (win, pathname, workspaceUid) => { try { const environmentUid = generateUidBasedOnHash(pathname); @@ -122,10 +114,6 @@ const handleGlobalEnvironmentFileUnlink = async (win, pathname, workspaceUid) => } }; -/** - * Workspace Watcher - * Watches workspace files for changes and notifies the renderer - */ class WorkspaceWatcher { constructor() { this.watchers = {}; @@ -137,7 +125,6 @@ class WorkspaceWatcher { const environmentsDir = path.join(workspacePath, 'environments'); const workspaceUid = generateUidBasedOnHash(workspacePath); - // Close existing watchers if any if (this.watchers[workspacePath]) { this.watchers[workspacePath].close(); } @@ -147,12 +134,10 @@ class WorkspaceWatcher { const self = this; setTimeout(() => { - // Guard against window being destroyed during delay if (win.isDestroyed()) { return; } - // Watch workspace.yml file const watcher = chokidar.watch(workspaceFilePath, { ignoreInitial: false, persistent: true, @@ -168,7 +153,6 @@ class WorkspaceWatcher { self.watchers[workspacePath] = watcher; - // Watch global environment files (.yml) if (fs.existsSync(environmentsDir)) { const envWatcher = chokidar.watch(path.join(environmentsDir, `*.yml`), { ignoreInitial: true, @@ -194,7 +178,6 @@ class WorkspaceWatcher { self.environmentWatchers[workspacePath] = envWatcher; } else { - // Watch for environments directory creation const dirWatcher = chokidar.watch(environmentsDir, { ignoreInitial: false, persistent: true, diff --git a/packages/bruno-electron/src/ipc/apiSpec.js b/packages/bruno-electron/src/ipc/apiSpec.js index 174a67666..39a28a7e0 100644 --- a/packages/bruno-electron/src/ipc/apiSpec.js +++ b/packages/bruno-electron/src/ipc/apiSpec.js @@ -2,8 +2,10 @@ const { ipcMain } = require('electron'); const { openApiSpecDialog, openApiSpec } = require('../app/apiSpecs'); const { writeFile } = require('../utils/filesystem'); const { removeApiSpecUid } = require('../cache/apiSpecUids'); +const { generateYamlContent } = require('../utils/workspace-config'); const path = require('path'); const fs = require('fs'); +const yaml = require('js-yaml'); const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedApiSpecs) => { ipcMain.handle('renderer:open-api-spec', (event, workspacePath = null) => { @@ -47,32 +49,29 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedApiSpecs) removeApiSpecUid(pathname); if (workspacePath) { - const yaml = require('js-yaml'); const workspaceFilePath = path.join(workspacePath, 'workspace.yml'); if (fs.existsSync(workspaceFilePath)) { const yamlContent = fs.readFileSync(workspaceFilePath, 'utf8'); const workspaceConfig = yaml.load(yamlContent); - if (workspaceConfig.apiSpecs) { - workspaceConfig.apiSpecs = workspaceConfig.apiSpecs.filter((a) => { - const apiSpecPathFromYml = a.path; - if (!apiSpecPathFromYml) return true; + const specs = workspaceConfig.specs || []; - const absoluteApiSpecPath = path.isAbsolute(apiSpecPathFromYml) - ? apiSpecPathFromYml - : path.resolve(workspacePath, apiSpecPathFromYml); + const filteredSpecs = specs.filter((a) => { + const specPathFromYml = a.path; + if (!specPathFromYml) return true; - return absoluteApiSpecPath !== pathname; - }); + const absoluteSpecPath = path.isAbsolute(specPathFromYml) + ? specPathFromYml + : path.resolve(workspacePath, specPathFromYml); - const updatedYamlContent = yaml.dump(workspaceConfig, { - indent: 2, - lineWidth: -1, - noRefs: true - }); - fs.writeFileSync(workspaceFilePath, updatedYamlContent); - } + return absoluteSpecPath !== pathname; + }); + + workspaceConfig.specs = filteredSpecs; + + const updatedYamlContent = generateYamlContent(workspaceConfig); + fs.writeFileSync(workspaceFilePath, updatedYamlContent); } } } diff --git a/packages/bruno-electron/src/ipc/workspace.js b/packages/bruno-electron/src/ipc/workspace.js index 9799552d7..99ef566a7 100644 --- a/packages/bruno-electron/src/ipc/workspace.js +++ b/packages/bruno-electron/src/ipc/workspace.js @@ -169,19 +169,19 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => { return []; } - const apiSpecs = workspaceConfig.apiSpecs || []; + const specs = workspaceConfig.specs || []; - const resolvedApiSpecs = apiSpecs.map((apiSpec) => { - if (apiSpec.path && !path.isAbsolute(apiSpec.path)) { + const resolvedSpecs = specs.map((spec) => { + if (spec.path && !path.isAbsolute(spec.path)) { return { - ...apiSpec, - path: path.join(workspacePath, apiSpec.path) + ...spec, + path: path.join(workspacePath, spec.path) }; } - return apiSpec; + return spec; }); - return resolvedApiSpecs; + return resolvedSpecs; } catch (error) { throw error; } diff --git a/packages/bruno-electron/src/store/default-workspace.js b/packages/bruno-electron/src/store/default-workspace.js index 8f259a9b9..5f07a7d35 100644 --- a/packages/bruno-electron/src/store/default-workspace.js +++ b/packages/bruno-electron/src/store/default-workspace.js @@ -1,11 +1,13 @@ const fs = require('fs'); const path = require('path'); const { app } = require('electron'); -const yaml = require('js-yaml'); const { generateUidBasedOnHash } = require('../utils/common'); const { writeFile, createDirectory } = require('../utils/filesystem'); const { getPreferences, savePreferences } = require('./preferences'); const { globalEnvironmentsStore } = require('./global-environments'); +const { generateYamlContent } = require('../utils/workspace-config'); + +const OPENCOLLECTION_VERSION = '1.0.0'; class DefaultWorkspaceManager { constructor() { @@ -112,22 +114,21 @@ class DefaultWorkspaceManager { await createDirectory(path.join(workspacePath, 'environments')); const workspaceConfig = { - name: 'My Workspace', - type: 'default', - version: '1.0.0', - docs: '', - collections: [] + opencollection: OPENCOLLECTION_VERSION, + info: { + name: 'My Workspace', + type: 'default' + }, + collections: [], + specs: [], + docs: '' }; if (migrateFromPreferences) { await this.migrateFromPreferences(workspacePath, workspaceConfig); } - const yamlContent = yaml.dump(workspaceConfig, { - indent: 2, - lineWidth: -1, - noRefs: true - }); + const yamlContent = generateYamlContent(workspaceConfig); await writeFile(path.join(workspacePath, 'workspace.yml'), yamlContent); await this.setDefaultWorkspacePath(workspacePath); diff --git a/packages/bruno-electron/src/store/workspace-environments.js b/packages/bruno-electron/src/store/workspace-environments.js index 0d5c7b74a..8f1c17da9 100644 --- a/packages/bruno-electron/src/store/workspace-environments.js +++ b/packages/bruno-electron/src/store/workspace-environments.js @@ -7,6 +7,7 @@ const { writeFile, createDirectory } = require('../utils/filesystem'); const { generateUidBasedOnHash, uuid } = require('../utils/common'); const { decryptStringSafe } = require('../utils/encryption'); const EnvironmentSecretsStore = require('./env-secrets'); +const { generateYamlContent } = require('../utils/workspace-config'); const environmentSecretsStore = new EnvironmentSecretsStore(); @@ -162,11 +163,7 @@ class GlobalEnvironmentsManager { workspaceConfig.activeEnvironmentUid = environmentUid; - const yamlOutput = yaml.dump(workspaceConfig, { - indent: 2, - lineWidth: -1, - noRefs: true - }); + const yamlOutput = generateYamlContent(workspaceConfig); await writeFile(workspaceFilePath, yamlOutput); return true; diff --git a/packages/bruno-electron/src/utils/workspace-config.js b/packages/bruno-electron/src/utils/workspace-config.js index ecb6ea5fc..f5c7db34d 100644 --- a/packages/bruno-electron/src/utils/workspace-config.js +++ b/packages/bruno-electron/src/utils/workspace-config.js @@ -4,6 +4,7 @@ const yaml = require('js-yaml'); const { writeFile, validateName } = require('./filesystem'); const WORKSPACE_TYPE = 'workspace'; +const OPENCOLLECTION_VERSION = '1.0.0'; const makeRelativePath = (workspacePath, absolutePath) => { if (!path.isAbsolute(absolutePath)) { @@ -61,14 +62,26 @@ const validateWorkspaceDirectory = (dirPath) => { }; const createWorkspaceConfig = (workspaceName) => ({ - name: workspaceName, - type: WORKSPACE_TYPE, - version: '1.0.0', - docs: '', + opencollection: OPENCOLLECTION_VERSION, + info: { + name: workspaceName, + type: WORKSPACE_TYPE + }, collections: [], - apiSpecs: [] + specs: [], + docs: '' }); +const normalizeWorkspaceConfig = (config) => { + return { + ...config, + name: config.info?.name, + type: config.info?.type, + collections: config.collections || [], + apiSpecs: config.specs || [] + }; +}; + const readWorkspaceConfig = (workspacePath) => { const workspaceFilePath = path.join(workspacePath, 'workspace.yml'); @@ -83,15 +96,69 @@ const readWorkspaceConfig = (workspacePath) => { throw new Error('Invalid workspace: workspace.yml is malformed'); } - return workspaceConfig; + return normalizeWorkspaceConfig(workspaceConfig); +}; + +const generateYamlContent = (config) => { + const yamlLines = []; + yamlLines.push(`opencollection: ${config.opencollection || OPENCOLLECTION_VERSION}`); + yamlLines.push('info:'); + yamlLines.push(` name: ${config.info?.name || config.name}`); + yamlLines.push(` type: ${config.info?.type || config.type || WORKSPACE_TYPE}`); + yamlLines.push(''); + + const collections = config.collections || []; + if (collections.length > 0) { + yamlLines.push('collections:'); + for (const collection of collections) { + yamlLines.push(` - name: ${collection.name}`); + yamlLines.push(` path: ${collection.path}`); + if (collection.remote) { + yamlLines.push(` remote: ${collection.remote}`); + } + if (collection.type) { + yamlLines.push(` type: ${collection.type}`); + } + } + } else { + yamlLines.push('collections:'); + } + yamlLines.push(''); + + const specs = config.specs || []; + if (specs.length > 0) { + yamlLines.push('specs:'); + for (const spec of specs) { + yamlLines.push(` - name: ${spec.name}`); + yamlLines.push(` path: ${spec.path}`); + } + } else { + yamlLines.push('specs:'); + } + yamlLines.push(''); + + const docs = config.docs || ''; + if (docs) { + const escapedDocs = docs.includes('\n') + ? `|-\n ${docs.split('\n').join('\n ')}` + : `'${docs.replace(/'/g, '\'\'')}'`; + yamlLines.push(`docs: ${escapedDocs}`); + } else { + yamlLines.push('docs: \'\''); + } + + if (config.activeEnvironmentUid) { + yamlLines.push(''); + yamlLines.push(`activeEnvironmentUid: ${config.activeEnvironmentUid}`); + } + + yamlLines.push(''); + + return yamlLines.join('\n'); }; const writeWorkspaceConfig = async (workspacePath, config) => { - const yamlContent = yaml.dump(config, { - indent: 2, - lineWidth: -1, - noRefs: true - }); + const yamlContent = generateYamlContent(config); await writeFile(path.join(workspacePath, 'workspace.yml'), yamlContent); }; @@ -100,11 +167,13 @@ const validateWorkspaceConfig = (config) => { throw new Error('Workspace configuration must be an object'); } - if (config.type !== WORKSPACE_TYPE) { + const type = config.info?.type || config.type; + if (type !== WORKSPACE_TYPE) { throw new Error('Invalid workspace: not a bruno workspace'); } - if (!config.name || typeof config.name !== 'string') { + const name = config.info?.name || config.name; + if (!name || typeof name !== 'string') { throw new Error('Workspace must have a valid name'); } @@ -114,6 +183,9 @@ const validateWorkspaceConfig = (config) => { const updateWorkspaceName = async (workspacePath, newName) => { const config = readWorkspaceConfig(workspacePath); config.name = newName; + if (config.info) { + config.info.name = newName; + } await writeWorkspaceConfig(workspacePath, config); return config; }; @@ -132,7 +204,6 @@ const addCollectionToWorkspace = async (workspacePath, collection) => { config.collections = []; } - // Normalize collection entry const normalizedCollection = { name: collection.name, path: collection.path @@ -142,7 +213,6 @@ const addCollectionToWorkspace = async (workspacePath, collection) => { normalizedCollection.remote = collection.remote; } - // Check if collection already exists const existingIndex = config.collections.findIndex((c) => c.name === normalizedCollection.name || c.path === normalizedCollection.path); if (existingIndex >= 0) { @@ -168,7 +238,6 @@ const removeCollectionFromWorkspace = async (workspacePath, collectionPath) => { return true; } - // Convert to absolute path for comparison const absoluteCollectionPath = path.isAbsolute(collectionPathFromYml) ? collectionPathFromYml : path.resolve(workspacePath, collectionPathFromYml); @@ -176,16 +245,15 @@ const removeCollectionFromWorkspace = async (workspacePath, collectionPath) => { if (path.normalize(absoluteCollectionPath) === path.normalize(collectionPath)) { removedCollection = c; - // Delete files only for workspace collections (not remote, not external absolute paths) const hasRemote = c.remote; const isExternalPath = path.isAbsolute(collectionPathFromYml); shouldDeleteFiles = !hasRemote && !isExternalPath; - return false; // Remove from array + return false; } - return true; // Keep in array + return true; }); await writeWorkspaceConfig(workspacePath, config); @@ -214,67 +282,63 @@ const getWorkspaceCollections = (workspacePath) => { const getWorkspaceApiSpecs = (workspacePath) => { const config = readWorkspaceConfig(workspacePath); - const apiSpecs = config.apiSpecs || []; + const specs = config.specs || []; - // Resolve relative paths to absolute - return apiSpecs.map((apiSpec) => { - if (apiSpec.path && !path.isAbsolute(apiSpec.path)) { + return specs.map((spec) => { + if (spec.path && !path.isAbsolute(spec.path)) { return { - ...apiSpec, - path: path.join(workspacePath, apiSpec.path) + ...spec, + path: path.join(workspacePath, spec.path) }; } - return apiSpec; + return spec; }); }; const addApiSpecToWorkspace = async (workspacePath, apiSpec) => { const config = readWorkspaceConfig(workspacePath); - if (!config.apiSpecs) { - config.apiSpecs = []; + if (!config.specs) { + config.specs = []; } - // Normalize API spec entry with relative path - const normalizedApiSpec = { + const normalizedSpec = { name: apiSpec.name, path: makeRelativePath(workspacePath, apiSpec.path) }; - // Check if API spec already exists - const existingIndex = config.apiSpecs.findIndex( - (a) => a.name === normalizedApiSpec.name || a.path === normalizedApiSpec.path + const existingIndex = config.specs.findIndex( + (a) => a.name === normalizedSpec.name || a.path === normalizedSpec.path ); if (existingIndex >= 0) { - config.apiSpecs[existingIndex] = normalizedApiSpec; + config.specs[existingIndex] = normalizedSpec; } else { - config.apiSpecs.push(normalizedApiSpec); + config.specs.push(normalizedSpec); } await writeWorkspaceConfig(workspacePath, config); - return config.apiSpecs; + return config.specs; }; const removeApiSpecFromWorkspace = async (workspacePath, apiSpecPath) => { const config = readWorkspaceConfig(workspacePath); - if (!config.apiSpecs) { + if (!config.specs) { return { removedApiSpec: null, updatedConfig: config }; } let removedApiSpec = null; - config.apiSpecs = config.apiSpecs.filter((a) => { - const apiSpecPathFromYml = a.path; - if (!apiSpecPathFromYml) return true; + config.specs = config.specs.filter((a) => { + const specPathFromYml = a.path; + if (!specPathFromYml) return true; - // Convert to absolute path for comparison - const absoluteApiSpecPath = path.isAbsolute(apiSpecPathFromYml) - ? apiSpecPathFromYml - : path.resolve(workspacePath, apiSpecPathFromYml); + const absoluteSpecPath = path.isAbsolute(specPathFromYml) + ? specPathFromYml + : path.resolve(workspacePath, specPathFromYml); - if (path.normalize(absoluteApiSpecPath) === path.normalize(apiSpecPath)) { + if (path.normalize(absoluteSpecPath) === path.normalize(apiSpecPath)) { removedApiSpec = a; return false; } @@ -306,5 +370,6 @@ module.exports = { getWorkspaceCollections, getWorkspaceApiSpecs, addApiSpecToWorkspace, - removeApiSpecFromWorkspace + removeApiSpecFromWorkspace, + generateYamlContent };