workspace schema update (#6374)

This commit is contained in:
naman-bruno
2025-12-10 19:38:47 +05:30
committed by GitHub
parent 632f8705e5
commit c00cbf6cb2
7 changed files with 185 additions and 133 deletions

View File

@@ -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);
}
}
}

View File

@@ -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,

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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
};