feat(#122): supporting process.env vars in UI and electron layer

This commit is contained in:
Anoop M D 2023-09-23 02:55:54 +05:30
parent c91fef2264
commit e3ce420216
22 changed files with 367 additions and 56 deletions

View File

@ -5,10 +5,20 @@ const Wrapper = styled.div`
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
font-weight: 600; font-weight: 600;
table-layout: fixed;
thead, thead,
td { td {
border: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}; border: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder};
padding: 4px 10px;
&:nth-child(1) {
width: 30%;
}
&:nth-child(3) {
width: 70px;
}
} }
thead { thead {
@ -16,7 +26,7 @@ const Wrapper = styled.div`
font-size: 0.8125rem; font-size: 0.8125rem;
user-select: none; user-select: none;
} }
td { thead td {
padding: 6px 10px; padding: 6px 10px;
} }
} }

View File

@ -2,13 +2,16 @@ import React, { useReducer } from 'react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import { IconTrash } from '@tabler/icons'; import { IconTrash } from '@tabler/icons';
import { useTheme } from 'providers/Theme';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions'; import { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';
import reducer from './reducer'; import reducer from './reducer';
import SingleLineEditor from 'components/SingleLineEditor';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
const EnvironmentVariables = ({ environment, collection }) => { const EnvironmentVariables = ({ environment, collection }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { storedTheme } = useTheme();
const [state, reducerDispatch] = useReducer(reducer, { hasChanges: false, variables: environment.variables || [] }); const [state, reducerDispatch] = useReducer(reducer, { hasChanges: false, variables: environment.variables || [] });
const { variables, hasChanges } = state; const { variables, hasChanges } = state;
@ -86,15 +89,11 @@ const EnvironmentVariables = ({ environment, collection }) => {
/> />
</td> </td>
<td> <td>
<input <SingleLineEditor
type="text"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={variable.value} value={variable.value}
className="mousetrap" theme={storedTheme}
onChange={(e) => handleVarChange(e, variable, 'value')} onChange={(newValue) => handleVarChange({ target: { value: newValue } }, variable, 'value')}
collection={collection}
/> />
</td> </td>
<td> <td>

View File

@ -19,7 +19,7 @@ const Wrapper = styled.div`
align-items: flex-start; align-items: flex-start;
justify-content: center; justify-content: center;
overflow-y: auto; overflow-y: auto;
z-index: 1003; z-index: 10;
} }
.bruno-modal-card { .bruno-modal-card {
@ -28,7 +28,7 @@ const Wrapper = styled.div`
background: var(--color-background-top); background: var(--color-background-top);
border-radius: var(--border-radius); border-radius: var(--border-radius);
position: relative; position: relative;
z-index: 1003; z-index: 10;
max-width: calc(100% - var(--spacing-base-unit)); max-width: calc(100% - var(--spacing-base-unit));
box-shadow: var(--box-shadow-base); box-shadow: var(--box-shadow-base);
display: flex; display: flex;

View File

@ -19,6 +19,7 @@ const StyledWrapper = styled.div`
.CodeMirror-scroll { .CodeMirror-scroll {
overflow: hidden !important; overflow: hidden !important;
padding-bottom: 50px !important;
} }
.CodeMirror-hscrollbar { .CodeMirror-hscrollbar {

View File

@ -31,6 +31,7 @@ class SingleLineEditor extends Component {
brunoVarInfo: { brunoVarInfo: {
variables: getAllVariables(this.props.collection) variables: getAllVariables(this.props.collection)
}, },
scrollbarStyle: null,
extraKeys: { extraKeys: {
Enter: () => { Enter: () => {
if (this.props.onRun) { if (this.props.onRun) {

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import forOwn from 'lodash/forOwn'; import forOwn from 'lodash/forOwn';
import isObject from 'lodash/isObject';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import { uuid } from 'utils/common'; import { uuid } from 'utils/common';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
@ -15,6 +16,14 @@ const VariablesTable = ({ variables, collectionVariables }) => {
}); });
}); });
const getValueToDisplay = (value) => {
if (value === undefined) {
return '';
}
return isObject(value) ? JSON.stringify(value) : value;
};
return ( return (
<StyledWrapper> <StyledWrapper>
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
@ -24,7 +33,9 @@ const VariablesTable = ({ variables, collectionVariables }) => {
return ( return (
<div key={variable.uid} className="flex"> <div key={variable.uid} className="flex">
<div className="variable-name text-yellow-600 text-right pr-2">{variable.name}</div> <div className="variable-name text-yellow-600 text-right pr-2">{variable.name}</div>
<div className="variable-value pl-2 whitespace-normal text-left flex-grow">{variable.value}</div> <div className="variable-value pl-2 whitespace-normal text-left flex-grow">
{getValueToDisplay(variable.value)}
</div>
</div> </div>
); );
}) })
@ -38,7 +49,9 @@ const VariablesTable = ({ variables, collectionVariables }) => {
return ( return (
<div key={variable.uid} className="flex"> <div key={variable.uid} className="flex">
<div className="variable-name text-yellow-600 text-right pr-2">{variable.name}</div> <div className="variable-name text-yellow-600 text-right pr-2">{variable.name}</div>
<div className="variable-value pl-2 whitespace-normal text-left flex-grow">{variable.value}</div> <div className="variable-value pl-2 whitespace-normal text-left flex-grow">
{getValueToDisplay(variable.value)}
</div>
</div> </div>
); );
}) })

View File

@ -8,6 +8,7 @@ import {
collectionUnlinkDirectoryEvent, collectionUnlinkDirectoryEvent,
collectionUnlinkEnvFileEvent, collectionUnlinkEnvFileEvent,
scriptEnvironmentUpdateEvent, scriptEnvironmentUpdateEvent,
processEnvUpdateEvent,
collectionRenamedEvent, collectionRenamedEvent,
runRequestEvent, runRequestEvent,
runFolderEvent runFolderEvent
@ -97,6 +98,10 @@ const useCollectionTreeSync = () => {
dispatch(scriptEnvironmentUpdateEvent(val)); dispatch(scriptEnvironmentUpdateEvent(val));
}; };
const _processEnvUpdate = (val) => {
dispatch(processEnvUpdateEvent(val));
};
const _collectionRenamed = (val) => { const _collectionRenamed = (val) => {
dispatch(collectionRenamedEvent(val)); dispatch(collectionRenamedEvent(val));
}; };
@ -119,7 +124,8 @@ const useCollectionTreeSync = () => {
const removeListener6 = ipcRenderer.on('main:collection-renamed', _collectionRenamed); const removeListener6 = ipcRenderer.on('main:collection-renamed', _collectionRenamed);
const removeListener7 = ipcRenderer.on('main:run-folder-event', _runFolderEvent); const removeListener7 = ipcRenderer.on('main:run-folder-event', _runFolderEvent);
const removeListener8 = ipcRenderer.on('main:run-request-event', _runRequestEvent); const removeListener8 = ipcRenderer.on('main:run-request-event', _runRequestEvent);
const removeListener9 = ipcRenderer.on('main:console-log', (val) => { const removeListener9 = ipcRenderer.on('main:process-env-update', _processEnvUpdate);
const removeListener10 = ipcRenderer.on('main:console-log', (val) => {
console[val.type](...val.args); console[val.type](...val.args);
}); });
@ -133,6 +139,7 @@ const useCollectionTreeSync = () => {
removeListener7(); removeListener7();
removeListener8(); removeListener8();
removeListener9(); removeListener9();
removeListener10();
}; };
}, [isElectron]); }, [isElectron]);
}; };

View File

@ -177,6 +177,14 @@ export const collectionsSlice = createSlice({
collection.collectionVariables = collectionVariables; collection.collectionVariables = collectionVariables;
} }
}, },
processEnvUpdateEvent: (state, action) => {
const { collectionUid, processEnvVariables } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (collection) {
collection.processEnvVariables = processEnvVariables;
}
},
requestCancelled: (state, action) => { requestCancelled: (state, action) => {
const { itemUid, collectionUid } = action.payload; const { itemUid, collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid); const collection = findCollectionByUid(state.collections, collectionUid);
@ -1158,6 +1166,7 @@ export const {
renameItem, renameItem,
cloneItem, cloneItem,
scriptEnvironmentUpdateEvent, scriptEnvironmentUpdateEvent,
processEnvUpdateEvent,
requestCancelled, requestCancelled,
responseReceived, responseReceived,
saveRequest, saveRequest,

View File

@ -1 +1 @@
@import "buttons"; @import 'buttons';

View File

@ -1,4 +1,3 @@
:root { :root {
--color-brand: #546de5; --color-brand: #546de5;
--color-text: rgb(52 52 52); --color-text: rgb(52 52 52);
@ -21,7 +20,8 @@
--color-method-head: rgb(52 52 52); --color-method-head: rgb(52 52 52);
} }
html, body { html,
body {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-size: 1rem; font-size: 1rem;
@ -38,15 +38,18 @@ body {
font-size: 0.875rem; font-size: 0.875rem;
} }
body::-webkit-scrollbar, .CodeMirror-vscrollbar::-webkit-scrollbar { body::-webkit-scrollbar,
.CodeMirror-vscrollbar::-webkit-scrollbar {
width: 0.6rem; width: 0.6rem;
} }
body::-webkit-scrollbar-track, .CodeMirror-vscrollbar::-webkit-scrollbar-track { body::-webkit-scrollbar-track,
.CodeMirror-vscrollbar::-webkit-scrollbar-track {
background-color: #f1f1f1; background-color: #f1f1f1;
} }
body::-webkit-scrollbar-thumb, .CodeMirror-vscrollbar::-webkit-scrollbar-thumb { body::-webkit-scrollbar-thumb,
.CodeMirror-vscrollbar::-webkit-scrollbar-thumb {
background-color: #cdcdcd; background-color: #cdcdcd;
border-radius: 5rem; border-radius: 5rem;
} }

View File

@ -8,6 +8,7 @@
let CodeMirror; let CodeMirror;
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true; const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
const { get } = require('lodash');
if (!SERVER_RENDERED) { if (!SERVER_RENDERED) {
CodeMirror = require('codemirror'); CodeMirror = require('codemirror');
@ -20,7 +21,7 @@ if (!SERVER_RENDERED) {
// str is of format {{variableName}}, extract variableName // str is of format {{variableName}}, extract variableName
// we are seeing that from the gql query editor, the token string is of format variableName // we are seeing that from the gql query editor, the token string is of format variableName
const variableName = str.replace('{{', '').replace('}}', '').trim(); const variableName = str.replace('{{', '').replace('}}', '').trim();
const variableValue = options.variables[variableName]; const variableValue = get(options.variables, variableName);
const into = document.createElement('div'); const into = document.createElement('div');
const descriptionDiv = document.createElement('div'); const descriptionDiv = document.createElement('div');

View File

@ -542,6 +542,11 @@ export const getAllVariables = (collection) => {
return { return {
...environmentVariables, ...environmentVariables,
...collection.collectionVariables ...collection.collectionVariables,
process: {
env: {
...collection.processEnvVariables
}
}
}; };
}; };

View File

@ -1,3 +1,6 @@
import get from 'lodash/get';
import isString from 'lodash/isString';
let CodeMirror; let CodeMirror;
const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true; const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;
@ -5,6 +8,11 @@ if (!SERVER_RENDERED) {
CodeMirror = require('codemirror'); CodeMirror = require('codemirror');
} }
const pathFoundInVariables = (path, obj) => {
const value = get(obj, path);
return isString(value);
};
export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => { export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
CodeMirror.defineMode('brunovariables', function (config, parserConfig) { CodeMirror.defineMode('brunovariables', function (config, parserConfig) {
let variablesOverlay = { let variablesOverlay = {
@ -15,7 +23,8 @@ export const defineCodeMirrorBrunoVariablesMode = (variables, mode) => {
while ((ch = stream.next()) != null) { while ((ch = stream.next()) != null) {
if (ch == '}' && stream.next() == '}') { if (ch == '}' && stream.next() == '}') {
stream.eat('}'); stream.eat('}');
if (word in variables) { let found = pathFoundInVariables(word, variables);
if (found) {
return 'variable-valid'; return 'variable-valid';
} else { } else {
return 'variable-invalid'; return 'variable-invalid';

View File

@ -27,6 +27,7 @@
"form-data": "^4.0.0", "form-data": "^4.0.0",
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",
"graphql": "^16.6.0", "graphql": "^16.6.0",
"handlebars": "^4.7.8",
"is-valid-path": "^0.1.1", "is-valid-path": "^0.1.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mustache": "^4.2.0", "mustache": "^4.2.0",

View File

@ -4,11 +4,13 @@ const path = require('path');
const chokidar = require('chokidar'); const chokidar = require('chokidar');
const { hasJsonExtension, hasBruExtension, writeFile } = require('../utils/filesystem'); const { hasJsonExtension, hasBruExtension, writeFile } = require('../utils/filesystem');
const { bruToEnvJson, envJsonToBru, bruToJson, jsonToBru } = require('../bru'); const { bruToEnvJson, envJsonToBru, bruToJson, jsonToBru } = require('../bru');
const { dotenvToJson } = require('@usebruno/lang');
const { isLegacyEnvFile, migrateLegacyEnvFile, isLegacyBruFile, migrateLegacyBruFile } = require('../bru/migrate'); const { isLegacyEnvFile, migrateLegacyEnvFile, isLegacyBruFile, migrateLegacyBruFile } = require('../bru/migrate');
const { itemSchema } = require('@usebruno/schema'); const { itemSchema } = require('@usebruno/schema');
const { uuid } = require('../utils/common'); const { uuid } = require('../utils/common');
const { getRequestUid } = require('../cache/requestUids'); const { getRequestUid } = require('../cache/requestUids');
const { setDotEnvVars } = require('../store/process-env');
const isJsonEnvironmentConfig = (pathname, collectionPath) => { const isJsonEnvironmentConfig = (pathname, collectionPath) => {
const dirname = path.dirname(pathname); const dirname = path.dirname(pathname);
@ -17,6 +19,13 @@ const isJsonEnvironmentConfig = (pathname, collectionPath) => {
return dirname === collectionPath && basename === 'environments.json'; return dirname === collectionPath && basename === 'environments.json';
}; };
const isDotEnvFile = (pathname, collectionPath) => {
const dirname = path.dirname(pathname);
const basename = path.basename(pathname);
return dirname === collectionPath && basename === '.env';
};
const isBruEnvironmentConfig = (pathname, collectionPath) => { const isBruEnvironmentConfig = (pathname, collectionPath) => {
const dirname = path.dirname(pathname); const dirname = path.dirname(pathname);
const envDirectory = path.join(collectionPath, 'environments'); const envDirectory = path.join(collectionPath, 'environments');
@ -125,6 +134,25 @@ const unlinkEnvironmentFile = async (win, pathname, collectionUid) => {
const add = async (win, pathname, collectionUid, collectionPath) => { const add = async (win, pathname, collectionUid, collectionPath) => {
console.log(`watcher add: ${pathname}`); console.log(`watcher add: ${pathname}`);
if (isDotEnvFile(pathname, collectionPath)) {
try {
const content = fs.readFileSync(pathname, 'utf8');
const jsonData = dotenvToJson(content);
setDotEnvVars(collectionUid, jsonData);
const payload = {
collectionUid,
processEnvVariables: {
...process.env,
...jsonData
}
};
win.webContents.send('main:process-env-update', payload);
} catch (err) {
console.error(err);
}
}
if (isJsonEnvironmentConfig(pathname, collectionPath)) { if (isJsonEnvironmentConfig(pathname, collectionPath)) {
try { try {
const dirname = path.dirname(pathname); const dirname = path.dirname(pathname);
@ -220,6 +248,25 @@ const addDirectory = (win, pathname, collectionUid, collectionPath) => {
}; };
const change = async (win, pathname, collectionUid, collectionPath) => { const change = async (win, pathname, collectionUid, collectionPath) => {
if (isDotEnvFile(pathname, collectionPath)) {
try {
const content = fs.readFileSync(pathname, 'utf8');
const jsonData = dotenvToJson(content);
setDotEnvVars(collectionUid, jsonData);
const payload = {
collectionUid,
processEnvVariables: {
...process.env,
...jsonData
}
};
win.webContents.send('main:process-env-update', payload);
} catch (err) {
console.error(err);
}
}
if (isBruEnvironmentConfig(pathname, collectionPath)) { if (isBruEnvironmentConfig(pathname, collectionPath)) {
return changeEnvironmentFile(win, pathname, collectionUid); return changeEnvironmentFile(win, pathname, collectionUid);
} }

View File

@ -13,6 +13,7 @@ const { uuid } = require('../../utils/common');
const interpolateVars = require('./interpolate-vars'); const interpolateVars = require('./interpolate-vars');
const { sortFolder, getAllRequestsInFolderRecursively } = require('./helper'); const { sortFolder, getAllRequestsInFolderRecursively } = require('./helper');
const { getPreferences } = require('../../store/preferences'); const { getPreferences } = require('../../store/preferences');
const { getProcessEnvVars } = require('../../store/process-env');
// override the default escape function to prevent escaping // override the default escape function to prevent escaping
Mustache.escape = function (value) { Mustache.escape = function (value) {
@ -129,12 +130,14 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath collectionPath
); );
mainWindow.webContents.send('main:script-environment-update', { if (result) {
envVariables: result.envVariables, mainWindow.webContents.send('main:script-environment-update', {
collectionVariables: result.collectionVariables, envVariables: result.envVariables,
requestUid, collectionVariables: result.collectionVariables,
collectionUid requestUid,
}); collectionUid
});
}
} }
// run pre-request script // run pre-request script
@ -158,7 +161,9 @@ const registerNetworkIpc = (mainWindow) => {
}); });
} }
interpolateVars(request, envVars, collectionVariables); const processEnvVars = getProcessEnvVars(collectionUid);
interpolateVars(request, envVars, collectionVariables, processEnvVars);
// stringify the request url encoded params // stringify the request url encoded params
if (request.headers['content-type'] === 'application/x-www-form-urlencoded') { if (request.headers['content-type'] === 'application/x-www-form-urlencoded') {
@ -222,12 +227,14 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath collectionPath
); );
mainWindow.webContents.send('main:script-environment-update', { if (result) {
envVariables: result.envVariables, mainWindow.webContents.send('main:script-environment-update', {
collectionVariables: result.collectionVariables, envVariables: result.envVariables,
requestUid, collectionVariables: result.collectionVariables,
collectionUid requestUid,
}); collectionUid
});
}
} }
// run post-response script // run post-response script
@ -520,7 +527,21 @@ const registerNetworkIpc = (mainWindow) => {
const preRequestVars = get(request, 'vars.req', []); const preRequestVars = get(request, 'vars.req', []);
if (preRequestVars && preRequestVars.length) { if (preRequestVars && preRequestVars.length) {
const varsRuntime = new VarsRuntime(); const varsRuntime = new VarsRuntime();
varsRuntime.runPreRequestVars(preRequestVars, request, envVars, collectionVariables, collectionPath); const result = varsRuntime.runPreRequestVars(
preRequestVars,
request,
envVars,
collectionVariables,
collectionPath
);
if (result) {
mainWindow.webContents.send('main:script-environment-update', {
envVariables: result.envVariables,
collectionVariables: result.collectionVariables,
collectionUid
});
}
} }
// run pre-request script // run pre-request script
@ -543,8 +564,10 @@ const registerNetworkIpc = (mainWindow) => {
}); });
} }
const processEnvVars = getProcessEnvVars(collectionUid);
// interpolate variables inside request // interpolate variables inside request
interpolateVars(request, envVars, collectionVariables); interpolateVars(request, envVars, collectionVariables, processEnvVars);
// todo: // todo:
// i have no clue why electron can't send the request object // i have no clue why electron can't send the request object
@ -587,11 +610,13 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath collectionPath
); );
mainWindow.webContents.send('main:script-environment-update', { if (result) {
envVariables: result.envVariables, mainWindow.webContents.send('main:script-environment-update', {
collectionVariables: result.collectionVariables, envVariables: result.envVariables,
collectionUid collectionVariables: result.collectionVariables,
}); collectionUid
});
}
} }
// run response script // run response script

View File

@ -1,24 +1,51 @@
const Mustache = require('mustache'); const Handlebars = require('handlebars');
const { each, get, forOwn } = require('lodash'); const { each, forOwn, cloneDeep } = require('lodash');
// override the default escape function to prevent escaping const interpolateEnvVars = (str, processEnvVars) => {
Mustache.escape = function (value) { if (!str || !str.length || typeof str !== 'string') {
return value; return str;
}
const template = Handlebars.compile(str, { noEscape: true });
return template({
process: {
env: {
...processEnvVars
}
}
});
}; };
const interpolateVars = (request, envVars = {}, collectionVariables = {}) => { const interpolateVars = (request, envVars = {}, collectionVariables = {}, processEnvVars = {}) => {
// we clone envVars because we don't want to modify the original object
envVars = cloneDeep(envVars);
// envVars can inturn have values as {{process.env.VAR_NAME}}
// so we need to interpolate envVars first with processEnvVars
forOwn(envVars, (value, key) => {
envVars[key] = interpolateEnvVars(value, processEnvVars);
});
const interpolate = (str) => { const interpolate = (str) => {
if (!str || !str.length || typeof str !== 'string') { if (!str || !str.length || typeof str !== 'string') {
return str; return str;
} }
const template = Handlebars.compile(str, { noEscape: true });
// collectionVariables take precedence over envVars // collectionVariables take precedence over envVars
const combinedVars = { const combinedVars = {
...envVars, ...envVars,
...collectionVariables ...collectionVariables,
process: {
env: {
...processEnvVars
}
}
}; };
return Mustache.render(str, combinedVars); return template(combinedVars);
}; };
request.url = interpolate(request.url); request.url = interpolate(request.url);

View File

@ -0,0 +1,37 @@
/**
* This file stores all the process.env variables under collection scope
*
* process.env variables are sourced from 2 places:
* 1. .env file in the root of the project
* 2. process.env variables set in the OS
*
* Multiple collections can be opened in the same electron app.
* Each collection's .env file can have different values for the same process.env variable.
*/
const dotEnvVars = {};
// collectionUid is a hash based on the collection path)
const getProcessEnvVars = (collectionUid) => {
// if there are no .env vars for this collection, return the process.env
if (!dotEnvVars[collectionUid]) {
return {
...process.env
};
}
// if there are .env vars for this collection, return the process.env merged with the .env vars
return {
...process.env,
...dotEnvVars[collectionUid]
};
};
const setDotEnvVars = (collectionUid, envVars) => {
dotEnvVars[collectionUid] = envVars;
};
module.exports = {
getProcessEnvVars,
setDotEnvVars
};

View File

@ -41,10 +41,39 @@ const generateUidBasedOnHash = (str) => {
return `${hash}`.padEnd(21, '0'); return `${hash}`.padEnd(21, '0');
}; };
const flattenDataForDotNotation = (data) => {
var result = {};
function recurse(current, prop) {
if (Object(current) !== current) {
result[prop] = current;
} else if (Array.isArray(current)) {
for (var i = 0, l = current.length; i < l; i++) {
recurse(current[i], prop + '[' + i + ']');
}
if (l == 0) {
result[prop] = [];
}
} else {
var isEmpty = true;
for (var p in current) {
isEmpty = false;
recurse(current[p], prop ? prop + '.' + p : p);
}
if (isEmpty && prop) {
result[prop] = {};
}
}
}
recurse(data, '');
return result;
};
module.exports = { module.exports = {
uuid, uuid,
stringifyJson, stringifyJson,
parseJson, parseJson,
simpleHash, simpleHash,
generateUidBasedOnHash generateUidBasedOnHash,
flattenDataForDotNotation
}; };

View File

@ -0,0 +1,85 @@
const { flattenDataForDotNotation } = require('../../src/utils/common');
describe('utils: flattenDataForDotNotation', () => {
test('Flatten a simple object with dot notation', () => {
const input = {
person: {
name: 'John',
age: 30,
},
};
const expectedOutput = {
'person.name': 'John',
'person.age': 30,
};
expect(flattenDataForDotNotation(input)).toEqual(expectedOutput);
});
test('Flatten an object with nested arrays', () => {
const input = {
users: [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 28 },
],
};
const expectedOutput = {
'users[0].name': 'Alice',
'users[0].age': 25,
'users[1].name': 'Bob',
'users[1].age': 28,
};
expect(flattenDataForDotNotation(input)).toEqual(expectedOutput);
});
test('Flatten an empty object', () => {
const input = {};
const expectedOutput = {};
expect(flattenDataForDotNotation(input)).toEqual(expectedOutput);
});
test('Flatten an object with nested objects', () => {
const input = {
person: {
name: 'Alice',
address: {
city: 'New York',
zipcode: '10001',
},
},
};
const expectedOutput = {
'person.name': 'Alice',
'person.address.city': 'New York',
'person.address.zipcode': '10001',
};
expect(flattenDataForDotNotation(input)).toEqual(expectedOutput);
});
test('Flatten an object with arrays of objects', () => {
const input = {
teams: [
{ name: 'Team A', members: ['Alice', 'Bob'] },
{ name: 'Team B', members: ['Charlie', 'David'] },
],
};
const expectedOutput = {
'teams[0].name': 'Team A',
'teams[0].members[0]': 'Alice',
'teams[0].members[1]': 'Bob',
'teams[1].name': 'Team B',
'teams[1].members[0]': 'Charlie',
'teams[1].members[1]': 'David',
};
expect(flattenDataForDotNotation(input)).toEqual(expectedOutput);
});
});

View File

@ -4,6 +4,7 @@ const bruToJsonV2 = require('../v2/src/bruToJson');
const jsonToBruV2 = require('../v2/src/jsonToBru'); const jsonToBruV2 = require('../v2/src/jsonToBru');
const bruToEnvJsonV2 = require('../v2/src/envToJson'); const bruToEnvJsonV2 = require('../v2/src/envToJson');
const envJsonToBruV2 = require('../v2/src/jsonToEnv'); const envJsonToBruV2 = require('../v2/src/jsonToEnv');
const dotenvToJson = require('../v2/src/dotenvToJson');
module.exports = { module.exports = {
bruToJson, bruToJson,
@ -14,5 +15,7 @@ module.exports = {
bruToJsonV2, bruToJsonV2,
jsonToBruV2, jsonToBruV2,
bruToEnvJsonV2, bruToEnvJsonV2,
envJsonToBruV2 envJsonToBruV2,
dotenvToJson
}; };