Compare commits

...

2 Commits

Author SHA1 Message Date
Anoop M D
17365b0d63 wip: abstracting parse and stringify operations 2025-02-07 14:44:27 +05:30
Anoop M D
0b19b26ce7 feat: yaml lang - parseRequest and stringifyRequest 2025-02-06 19:41:38 +05:30
17 changed files with 1419 additions and 162 deletions

View File

@@ -5,6 +5,16 @@ const chokidar = require('chokidar');
const { hasBruExtension, isWSLPath, normalizeAndResolvePath, normalizeWslPath, sizeInMB } = require('../utils/filesystem');
const { bruToEnvJson, bruToJson, bruToJsonViaWorker ,collectionBruToJson } = require('../bru');
const { dotenvToJson } = require('@usebruno/lang');
const {
parseRequest,
stringifyRequest,
parseCollection,
stringifyCollection,
parseFolder,
stringifyFolder,
parseEnvironment,
stringifyEnvironment
} = require('../filestore');
const { uuid } = require('../utils/common');
const { getRequestUid } = require('../cache/requestUids');
@@ -80,7 +90,7 @@ const addEnvironmentFile = async (win, pathname, collectionUid, collectionPath)
let bruContent = fs.readFileSync(pathname, 'utf8');
file.data = await bruToEnvJson(bruContent);
file.data = await parseEnvironment(bruContent, { format: 'bru' });
file.data.name = basename.substring(0, basename.length - 4);
file.data.uid = getRequestUid(pathname);
@@ -209,8 +219,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
try {
let bruContent = fs.readFileSync(pathname, 'utf8');
file.data = await collectionBruToJson(bruContent);
file.data = await parseCollection(bruContent, { format: 'bru' });
hydrateBruCollectionFileWithUuid(file.data);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
return;
@@ -234,8 +243,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
try {
let bruContent = fs.readFileSync(pathname, 'utf8');
file.data = await collectionBruToJson(bruContent);
file.data = await parseFolder(bruContent, { format: 'bru' });
hydrateBruCollectionFileWithUuid(file.data);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
return;
@@ -259,7 +267,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
// If worker thread is not used, we can directly parse the file
if (!useWorkerThread) {
try {
file.data = await bruToJson(bruContent);
file.data = await parseRequest(bruContent, { format: 'bru' });
file.partial = false;
file.loading = false;
file.size = sizeInMB(fileStats?.size);
@@ -279,7 +287,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
type: 'http-request'
};
const metaJson = await bruToJson(parseBruFileMeta(bruContent), true);
const metaJson = await parseRequest(parseBruFileMeta(bruContent), { format: 'bru' });
file.data = metaJson;
file.partial = true;
file.loading = false;
@@ -296,7 +304,7 @@ const add = async (win, pathname, collectionUid, collectionPath, useWorkerThread
win.webContents.send('main:collection-tree-updated', 'addFile', file);
// This is to update the file info in the UI
file.data = await bruToJsonViaWorker(bruContent);
file.data = await parseRequest(bruContent, { format: 'bru', useWorker: true });
file.partial = false;
file.loading = false;
hydrateRequestWithUuid(file.data, pathname);

View File

@@ -4,116 +4,31 @@ const {
jsonToBruV2,
bruToEnvJsonV2,
envJsonToBruV2,
collectionBruToJson: _collectionBruToJson,
jsonToCollectionBru: _jsonToCollectionBru
collectionBruToJson,
jsonToCollectionBru
} = require('@usebruno/lang');
const BruParserWorker = require('./workers');
const bruParserWorker = new BruParserWorker();
const collectionBruToJson = async (data, parsed = false) => {
try {
const json = parsed ? data : _collectionBruToJson(data);
const transformedJson = {
request: {
headers: _.get(json, 'headers', []),
auth: _.get(json, 'auth', {}),
script: _.get(json, 'script', {}),
vars: _.get(json, 'vars', {}),
tests: _.get(json, 'tests', '')
},
docs: _.get(json, 'docs', '')
};
// add meta if it exists
// this is only for folder bru file
// in the future, all of this will be replaced by standard bru lang
if (json.meta) {
transformedJson.meta = {
name: json.meta.name
};
}
return transformedJson;
} catch (error) {
return Promise.reject(error);
}
};
const jsonToCollectionBru = async (json, isFolder) => {
try {
const collectionBruJson = {
headers: _.get(json, 'request.headers', []),
script: {
req: _.get(json, 'request.script.req', ''),
res: _.get(json, 'request.script.res', '')
},
vars: {
req: _.get(json, 'request.vars.req', []),
res: _.get(json, 'request.vars.res', [])
},
tests: _.get(json, 'request.tests', ''),
docs: _.get(json, 'docs', '')
};
// add meta if it exists
// this is only for folder bru file
// in the future, all of this will be replaced by standard bru lang
if (json?.meta) {
collectionBruJson.meta = {
name: json.meta.name
};
}
if (!isFolder) {
collectionBruJson.auth = _.get(json, 'request.auth', {});
}
return _jsonToCollectionBru(collectionBruJson);
} catch (error) {
return Promise.reject(error);
}
};
const bruToEnvJson = async (bru) => {
try {
const json = bruToEnvJsonV2(bru);
// the app env format requires each variable to have a type
// this need to be evaluated and safely removed
// i don't see it being used in schema validation
if (json && json.variables && json.variables.length) {
_.each(json.variables, (v) => (v.type = 'text'));
}
return json;
} catch (error) {
return Promise.reject(error);
}
};
const envJsonToBru = async (json) => {
try {
const bru = envJsonToBruV2(json);
return bru;
} catch (error) {
return Promise.reject(error);
}
};
/**
* The transformer function for converting a BRU file to JSON.
*
* We map the json response from the bru lang and transform it into the DSL
* format that the app uses
*
* @param {string} data The BRU file content.
* @param {string} bru The BRU file content.
* @returns {object} The JSON representation of the BRU file.
*/
const bruToJson = (data, parsed = false) => {
const parseRequest = async (bru, options = {}) => {
try {
const json = parsed ? data : bruToJsonV2(data);
let json;
if(options.useWorker) {
json = await bruParserWorker?.bruToJson(data);
} else {
json = bruToJsonV2(bru);
}
let requestType = _.get(json, 'meta.type');
if (requestType === 'http') {
@@ -153,15 +68,6 @@ const bruToJson = (data, parsed = false) => {
}
};
const bruToJsonViaWorker = async (data) => {
try {
const json = await bruParserWorker?.bruToJson(data);
return bruToJson(json, true);
} catch (e) {
return Promise.reject(e);
}
};
/**
* The transformer function for converting a JSON to BRU file.
*
@@ -171,7 +77,7 @@ const bruToJsonViaWorker = async (data) => {
* @param {object} json The JSON representation of the BRU file.
* @returns {string} The BRU file content.
*/
const jsonToBru = async (json) => {
const stringifyRequest = async (json, options = {}) => {
let type = _.get(json, 'type');
if (type === 'http-request') {
type = 'http';
@@ -208,59 +114,140 @@ const jsonToBru = async (json) => {
docs: _.get(json, 'request.docs', '')
};
const bru = jsonToBruV2(bruJson);
return bru;
if(options.useWorker) {
return await bruParserWorker?.jsonToBru(bruJson);
} else {
return jsonToBruV2(bruJson);
}
};
const jsonToBruViaWorker = async (json) => {
let type = _.get(json, 'type');
if (type === 'http-request') {
type = 'http';
} else if (type === 'graphql-request') {
type = 'graphql';
} else {
type = 'http';
const parseCollection = async (bru) => {
try {
const json = collectionBruToJson(bru);
const transformedJson = {
request: {
headers: _.get(json, 'headers', []),
auth: _.get(json, 'auth', {}),
script: _.get(json, 'script', {}),
vars: _.get(json, 'vars', {}),
tests: _.get(json, 'tests', '')
},
docs: _.get(json, 'docs', '')
};
return transformedJson;
} catch (error) {
return Promise.reject(error);
}
};
const sequence = _.get(json, 'seq');
const bruJson = {
meta: {
name: _.get(json, 'name'),
type: type,
seq: !isNaN(sequence) ? Number(sequence) : 1
},
http: {
method: _.lowerCase(_.get(json, 'request.method')),
url: _.get(json, 'request.url'),
auth: _.get(json, 'request.auth.mode', 'none'),
body: _.get(json, 'request.body.mode', 'none')
},
params: _.get(json, 'request.params', []),
headers: _.get(json, 'request.headers', []),
auth: _.get(json, 'request.auth', {}),
body: _.get(json, 'request.body', {}),
script: _.get(json, 'request.script', {}),
vars: {
req: _.get(json, 'request.vars.req', []),
res: _.get(json, 'request.vars.res', [])
},
assertions: _.get(json, 'request.assertions', []),
tests: _.get(json, 'request.tests', ''),
docs: _.get(json, 'request.docs', '')
};
const stringifyCollection = async (json) => {
try {
const collectionBruJson = {
headers: _.get(json, 'request.headers', []),
auth: _.get(json, 'request.auth', {}),
script: {
req: _.get(json, 'request.script.req', ''),
res: _.get(json, 'request.script.res', '')
},
vars: {
req: _.get(json, 'request.vars.req', []),
res: _.get(json, 'request.vars.res', [])
},
tests: _.get(json, 'request.tests', ''),
docs: _.get(json, 'docs', '')
};
const bru = await bruParserWorker?.jsonToBru(bruJson)
return bru;
return jsonToCollectionBru(collectionBruJson);
} catch (error) {
return Promise.reject(error);
}
};
const parseFolder = async (bru) => {
try {
const json = collectionBruToJson(bru);
const transformedJson = {
meta: {
name: _.get(json, 'meta.name')
},
request: {
headers: _.get(json, 'headers', []),
auth: _.get(json, 'auth', {}),
script: _.get(json, 'script', {}),
vars: _.get(json, 'vars', {}),
tests: _.get(json, 'tests', '')
},
docs: _.get(json, 'docs', '')
};
return transformedJson;
} catch (error) {
return Promise.reject(error);
}
};
const stringifyFolder = async (json) => {
try {
const folderBruJson = {
meta: {
name: _.get(json, 'meta.name')
},
headers: _.get(json, 'request.headers', []),
script: {
req: _.get(json, 'request.script.req', ''),
res: _.get(json, 'request.script.res', '')
},
vars: {
req: _.get(json, 'request.vars.req', []),
res: _.get(json, 'request.vars.res', [])
},
tests: _.get(json, 'request.tests', ''),
docs: _.get(json, 'docs', '')
};
return jsonToCollectionBru(folderBruJson);
} catch (error) {
return Promise.reject(error);
}
};
const parseEnvironment = async (bru) => {
try {
const json = bruToEnvJsonV2(bru);
// the app env format requires each variable to have a type
// this need to be evaluated and safely removed
// i don't see it being used in schema validation
if (json && json.variables && json.variables.length) {
_.each(json.variables, (v) => (v.type = 'text'));
}
return json;
} catch (error) {
return Promise.reject(error);
}
};
const stringifyEnvironment = async (json) => {
try {
const bru = envJsonToBruV2(json);
return bru;
} catch (error) {
return Promise.reject(error);
}
};
module.exports = {
bruToJson,
bruToJsonViaWorker,
jsonToBru,
bruToEnvJson,
envJsonToBru,
collectionBruToJson,
jsonToCollectionBru,
jsonToBruViaWorker
parseRequest,
stringifyRequest,
parseCollection,
stringifyCollection,
parseFolder,
stringifyFolder,
parseEnvironment,
stringifyEnvironment
};

View File

@@ -0,0 +1,69 @@
import {
parseRequest as bruParseRequest,
stringifyRequest as bruStringifyRequest,
parseFolder as bruParseFolder,
stringifyFolder as bruStringifyFolder,
parseCollection as bruParseCollection,
stringifyCollection as bruStringifyCollection,
parseEnvironment as bruParseEnvironment,
stringifyEnvironment as bruStringifyEnvironment
} from './bru';
const parseRequest = (bru, options = {}) => {
if (options.format === 'bru') {
return bruParseRequest(bru);
}
};
const stringifyRequest = (json, options = {}) => {
if (options.format === 'bru') {
return bruStringifyRequest(json);
}
};
const parseFolder = (bru, options = {}) => {
if (options.format === 'bru') {
return bruParseFolder(bru);
}
};
const stringifyFolder = (json, options = {}) => {
if (options.format === 'bru') {
return bruStringifyFolder(json);
}
};
const parseCollection = (bru, options = {}) => {
if (options.format === 'bru') {
return bruParseCollection(bru);
}
};
const stringifyCollection = (json, options = {}) => {
if (options.format === 'bru') {
return bruStringifyCollection(json);
}
};
const parseEnvironment = (bru, options = {}) => {
if (options.format === 'bru') {
return bruParseEnvironment(bru);
}
};
const stringifyEnvironment = (json, options = {}) => {
if (options.format === 'bru') {
return bruStringifyEnvironment(json);
}
};
module.exports = {
parseRequest,
stringifyRequest,
parseFolder,
stringifyFolder,
parseCollection,
stringifyCollection,
parseEnvironment,
stringifyEnvironment
};

View File

@@ -0,0 +1,346 @@
const _ = require('lodash');
const getMeta = (json) => {
const sequence = _.get(json, 'seq');
const meta = {
name: _.get(json, 'name')
};
const description = _.get(json, 'description');
if (description) {
meta.description = description;
}
meta.seq = !isNaN(sequence) ? Number(sequence) : 1;
return meta;
};
const getParams = (req) => {
return {
query: _.map(_.filter(req?.params || [], param => param.type === 'query'), (param) => {
const paramObj = {
name: param.name,
value: param.value,
type: param.type
};
if (param.description) {
paramObj.description = param.description;
}
if (param.enabled === false) {
paramObj.disabled = true;
}
return paramObj;
}),
path: _.map(_.filter(req?.params || [], param => param.type === 'path'), (param) => {
const paramObj = {
name: param.name,
value: param.value,
type: param.type
};
if (param.description) {
paramObj.description = param.description;
}
if (param.enabled === false) {
paramObj.disabled = true;
}
return paramObj;
})
}
};
const getHeaders = (req) => {
return _.map(_.get(req, 'headers', []), (header) => {
const headerObj = {
name: header.name,
value: header.value,
};
if (header.description) {
headerObj.description = header.description;
}
if (header.enabled === false) {
headerObj.disabled = true;
}
return headerObj;
});
};
const getBody = (req) => {
const body = _.get(req, 'body', {});
const mode = _.get(body, 'mode', 'none');
if (mode === 'none') {
return null;
}
if (mode === 'graphql') {
return {
type: 'graphql',
query: _.get(body, 'graphql.query', ''),
variables: _.get(body, 'graphql.variables', '')
};
}
if (mode === 'sparql') {
return {
type: 'sparql',
query: _.get(body, 'sparql', '')
};
}
if (mode === 'formUrlEncoded') {
return {
type: 'form-urlencoded',
data: _.map(_.get(body, 'formUrlEncoded', []), (param) => {
const paramObj = {
name: param.name,
value: param.value
};
if (param.description) {
paramObj.description = param.description;
}
if (param.enabled === false) {
paramObj.disabled = true;
}
return paramObj;
})
};
}
if (mode === 'multipartForm') {
return {
type: 'multipart-form',
data: _.map(_.get(body, 'multipartForm', []), (param) => {
const paramObj = {
name: param.name,
value: param.value,
type: param.type
};
if (param.description) {
paramObj.description = param.description;
}
if (param.enabled === false) {
paramObj.disabled = true;
}
if (param.contentType) {
paramObj.content_type = param.contentType;
}
return paramObj;
})
};
}
let data = '';
switch(mode) {
case 'json':
data = _.get(body, 'json', '');
break;
case 'text':
data = _.get(body, 'text', '');
break;
case 'xml':
data = _.get(body, 'xml', '');
break;
}
return {
type: mode,
data
};
};
const getAuth = (req) => {
const auth = {};
const mode = _.get(req, 'auth.mode', 'none');
if (req?.auth?.awsv4) {
auth.awsv4 = {
access_key_id: req?.auth?.awsv4?.accessKeyId,
secret_access_key: req?.auth?.awsv4?.secretAccessKey,
session_token: req?.auth?.awsv4?.sessionToken,
service: req?.auth?.awsv4?.service,
region: req?.auth?.awsv4?.region,
profile_name: req?.auth?.awsv4?.profileName
};
}
if (req?.auth?.basic) {
auth.basic = {
username: req?.auth?.basic?.username,
password: req?.auth?.basic?.password
};
}
if (req?.auth?.bearer) {
auth.bearer = {
token: req?.auth?.bearer?.token
};
}
if (req?.auth?.digest) {
auth.digest = {
username: req?.auth?.digest?.username,
password: req?.auth?.digest?.password
};
}
if (req?.auth?.ntlm) {
auth.ntlm = {
username: req?.auth?.ntlm?.username,
password: req?.auth?.ntlm?.password,
domain: req?.auth?.ntlm?.domain
};
}
if (req?.auth?.oauth2) {
auth.oauth2 = {};
if (req?.auth?.oauth2?.grantType === 'password') {
auth.oauth2 = {
grant_type: 'password',
access_token_url: req?.auth?.oauth2?.accessTokenUrl || '',
username: req?.auth?.oauth2?.username || '',
password: req?.auth?.oauth2?.password || '',
client_id: req?.auth?.oauth2?.clientId || '',
client_secret: req?.auth?.oauth2?.clientSecret || '',
scope: req?.auth?.oauth2?.scope || ''
};
}
if (req?.auth?.oauth2?.grantType === 'authorization_code') {
auth.oauth2 = {
grant_type: 'authorization_code',
callback_url: req?.auth?.oauth2?.callbackUrl || '',
authorization_url: req?.auth?.oauth2?.authorizationUrl || '',
access_token_url: req?.auth?.oauth2?.accessTokenUrl || '',
client_id: req?.auth?.oauth2?.clientId || '',
client_secret: req?.auth?.oauth2?.clientSecret || '',
scope: req?.auth?.oauth2?.scope || '',
state: req?.auth?.oauth2?.state || '',
pkce: req?.auth?.oauth2?.pkce || false
};
}
if (req?.auth?.oauth2?.grantType === 'client_credentials') {
auth.oauth2 = {
grant_type: 'client_credentials',
access_token_url: req?.auth?.oauth2?.accessTokenUrl || '',
client_id: req?.auth?.oauth2?.clientId || '',
client_secret: req?.auth?.oauth2?.clientSecret || '',
scope: req?.auth?.oauth2?.scope || ''
};
}
}
if (req?.auth?.apikey) {
auth.apikey = {
key: req?.auth?.apikey?.key,
value: req?.auth?.apikey?.value,
placement: req?.auth?.apikey?.placement
};
}
return {
type: mode,
...auth
};
};
const getVars = (req) => {
const preRequest = _.map(_.get(req, 'vars.req', []), (variable) => {
const varObj = {
name: variable.name,
value: variable.value
};
if (variable.description) {
varObj.description = variable.description;
}
if (variable.enabled === false) {
varObj.disabled = true;
}
return varObj;
});
const postResponse = _.map(_.get(req, 'vars.res', []), (variable) => {
const varObj = {
name: variable.name,
value: variable.value
};
if (variable.description) {
varObj.description = variable.description;
}
if (variable.enabled === false) {
varObj.disabled = true;
}
return varObj;
});
const vars = {};
if(preRequest.length) {
vars['pre-request'] = preRequest;
}
if(postResponse.length) {
vars['post-response'] = postResponse;
}
return !_.isEmpty(vars) ? vars : null;
};
const getScripts = (req) => {
const preRequestScript = _.get(req, 'script.req', '');
const postResponseScript = _.get(req, 'script.res', '');
const scripts = {};
if (preRequestScript) {
scripts['pre-request'] = preRequestScript;
}
if (postResponseScript) {
scripts['post-response'] = postResponseScript;
}
return !_.isEmpty(scripts) ? scripts : null;
};
const getTests = (req) => {
return _.get(req, 'tests', '');
};
const getDocs = (req) => {
return _.get(req, 'docs', '');
};
module.exports = {
getMeta,
getParams,
getHeaders,
getBody,
getAuth,
getVars,
getScripts,
getTests,
getDocs
};

View File

@@ -0,0 +1,8 @@
const _ = require('lodash');
const stringifyRequest = require('./stringifyRequest');
const parseRequest = require('./parseRequest');
module.exports = {
parseRequest,
stringifyRequest,
};

View File

@@ -0,0 +1,235 @@
const yaml = require('js-yaml');
const _ = require('lodash');
const parseRequest = (yamlContent) => {
const yamlData = yaml.load(yamlContent);
const isHttp = !!yamlData.http;
const request = isHttp ? yamlData.http : yamlData.graphql;
const item = {
name: yamlData.meta.name,
description: yamlData.meta.description || '',
seq: yamlData.meta.seq,
type: isHttp ? 'http-request' : 'graphql-request'
};
item.request = {
method: request.method.toUpperCase(),
url: request.url,
headers: _.map(request.headers || [], header => ({
name: header.name,
value: header.value,
description: header.description || '',
enabled: !header.disabled
})),
params: _.flatMap(request.params || {}, (params, type) => {
return _.map(params, param => ({
name: param.name,
value: param.value,
type,
description: param.description || '',
enabled: !param.disabled
}));
}),
body: {
mode: 'none',
json: null,
text: null,
xml: null,
sparql: null,
formUrlEncoded: [],
multipartForm: [],
graphql: null
},
auth: {
mode: 'none',
awsv4: null,
basic: null,
bearer: null,
ntlm: null,
digest: null,
oauth2: null,
wsse: null,
apikey: null
},
vars: {
req: [],
res: []
},
script: {
req: '',
res: ''
},
tests: '',
docs: ''
};
// Handle body
if (request.body) {
item.request.body.mode = request.body.type;
switch(request.body.type) {
case 'json':
item.request.body.json = request.body.data;
break;
case 'text':
item.request.body.text = request.body.data;
break;
case 'xml':
item.request.body.xml = request.body.data;
break;
case 'sparql':
item.request.body.sparql = request.body.data;
break;
case 'formUrlEncoded':
item.request.body.formUrlEncoded = _.map(request.body.data, formItem => ({
name: formItem.name,
value: formItem.value,
description: formItem.description || '',
enabled: !formItem.disabled
}));
break;
case 'multipartForm':
item.request.body.multipartForm = _.map(request.body.data, formItem => ({
name: formItem.name,
value: formItem.value,
description: formItem.description || '',
enabled: !formItem.disabled,
contentType: formItem.content_type || ''
}));
break;
case 'graphql':
item.request.body.graphql = {
query: request.body.data.query || '',
variables: request.body.data.variables || ''
};
break;
}
}
// Handle auth
if (request.auth) {
item.request.auth.mode = request.auth.type;
switch(request.auth.type) {
case 'awsv4':
item.request.auth.awsv4 = {
accessKeyId: request.auth.access_key_id,
secretAccessKey: request.auth.secret_access_key,
sessionToken: request.auth.session_token,
service: request.auth.service,
region: request.auth.region,
profileName: request.auth.profile_name
}
break;
case 'basic':
item.request.auth.basic = {
username: request.auth.username,
password: request.auth.password
}
break;
case 'bearer':
item.request.auth.bearer = {
token: request.auth.token
}
break;
case 'digest':
item.request.auth.digest = {
username: request.auth.username,
password: request.auth.password
}
break;
case 'ntlm':
item.request.auth.ntlm = {
username: request.auth.username,
password: request.auth.password,
domain: request.auth.domain
}
break;
case 'wsse':
item.request.auth.wsse = {
username: request.auth.username,
password: request.auth.password
}
break;
case 'apikey':
item.request.auth.apikey = {
key: request.auth.key,
value: request.auth.value,
placement: request.auth.placement
}
break;
case 'oauth2':
if (request.auth.grant_type === 'password') {
item.request.auth.oauth2 = {
grantType: 'password',
accessTokenUrl: request.auth.access_token_url || '',
clientId: request.auth.client_id || '',
clientSecret: request.auth.client_secret || '',
scope: request.auth.scope || '',
username: request.auth.username || '',
password: request.auth.password || ''
};
} else if (request.auth.grant_type === 'authorization_code') {
item.request.auth.oauth2 = {
grantType: 'authorization_code',
accessTokenUrl: request.auth.access_token_url || '',
clientId: request.auth.client_id || '',
clientSecret: request.auth.client_secret || '',
scope: request.auth.scope || '',
callbackUrl: request.auth.callback_url || '',
authorizationUrl: request.auth.authorization_url || '',
state: request.auth.state || '',
pkce: request.auth.pkce || false
};
} else if (request.auth.grant_type === 'client_credentials') {
item.request.auth.oauth2 = {
grantType: 'client_credentials',
accessTokenUrl: request.auth.access_token_url || '',
clientId: request.auth.client_id || '',
clientSecret: request.auth.client_secret || '',
scope: request.auth.scope || ''
};
}
break;
}
}
// Handle vars
if (yamlData.vars) {
item.request.vars = {
req: _.map(yamlData.vars['pre-request'] || [], v => ({
name: v.name,
value: v.value,
description: v.description || '',
enabled: !v.disabled
})),
res: _.map(yamlData.vars['post-response'] || [], v => ({
name: v.name,
value: v.value,
description: v.description || '',
enabled: !v.disabled
}))
};
}
// Handle scripts
if (yamlData.scripts) {
item.request.script = {
req: yamlData.scripts['pre-request'] || '',
res: yamlData.scripts['post-response'] || ''
};
}
// Handle tests and docs
if (yamlData.tests) {
item.request.tests = yamlData.tests;
}
if (yamlData.docs) {
item.request.docs = yamlData.docs;
}
return item;
};
module.exports = parseRequest;

View File

@@ -0,0 +1,54 @@
const yaml = require('js-yaml');
const _ = require('lodash');
const { getMeta, getParams, getHeaders, getVars, getScripts, getBody, getAuth, getTests, getDocs } = require('./common');
const stringifyRequest = (json) => {
const request = json?.request;
const requestBody = getBody(request);
const requestAuth = getAuth(request);
const isGraphql = requestBody?.type === 'graphql';
const requestData = {
method: _.lowerCase(_.get(json, 'request.method')),
url: _.get(json, 'request.url'),
params: getParams(request),
headers: getHeaders(request),
};
if (requestBody && requestBody.type !== 'none') {
requestData.body = requestBody;
}
if (requestAuth && requestAuth.mode !== 'none') {
requestData.auth = requestAuth;
}
const finalJson = {
meta: getMeta(json),
[isGraphql ? 'graphql' : 'http']: requestData
};
const vars = getVars(request);
if (vars) {
finalJson.vars = vars;
}
const scripts = getScripts(request);
if (scripts) {
finalJson.scripts = scripts;
}
const tests = getTests(request);
if (tests) {
finalJson.tests = tests;
}
const docs = getDocs(request);
if (docs) {
finalJson.docs = docs;
}
return yaml.dump(finalJson);
};
module.exports = stringifyRequest;

View File

@@ -0,0 +1,38 @@
{
"name": "Get User GraphQL",
"seq": 1,
"request": {
"method": "POST",
"url": "https://api.example.com/graphql",
"headers": [
{
"name": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "graphql",
"graphql": {
"query": "query GetUser($id: ID!) {\n user(id: $id) {\n id\n name\n email\n }\n}",
"variables": "{\n \"id\": \"123\"\n}"
}
},
"auth": {
"mode": "bearer",
"bearer": {
"token": "jwt-token"
}
},
"vars": {
"req": [
{
"name": "userId",
"value": "123"
}
]
},
"script": {
"req": "// Pre-request script for GraphQL"
}
}
}

View File

@@ -0,0 +1,36 @@
meta:
name: Get User GraphQL
seq: 1
graphql:
method: post
url: https://api.example.com/graphql
params:
query: []
path: []
headers:
- name: Content-Type
value: application/json
body:
type: graphql
query: |-
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
variables: |-
{
"id": "123"
}
auth:
type: bearer
bearer:
token: jwt-token
vars:
pre-request:
- name: userId
value: '123'
scripts:
pre-request: // Pre-request script for GraphQL

View File

@@ -0,0 +1,70 @@
{
"name": "HTTP Methods",
"seq": 1,
"request": {
"method": "POST",
"url": "https://api.example.com/users",
"headers": [
{
"name": "Content-Type",
"value": "application/json",
"description": "Content type header",
"enabled": true
},
{
"name": "Accept",
"value": "application/json",
"enabled": false
}
],
"params": [
{
"name": "userId",
"value": "123",
"type": "path",
"description": "User ID parameter",
"enabled": true
},
{
"name": "filter",
"value": "active",
"type": "query",
"enabled": false
}
],
"body": {
"mode": "json",
"json": "{\n \"name\": \"John Doe\",\n \"email\": \"john@example.com\"\n}"
},
"auth": {
"mode": "basic",
"basic": {
"username": "admin",
"password": "secret"
}
},
"vars": {
"req": [
{
"name": "userId",
"value": "123",
"description": "User ID variable",
"enabled": true
}
],
"res": [
{
"name": "token",
"value": "response.token",
"enabled": false
}
]
},
"script": {
"req": "// Pre-request script\nconsole.log('pre-request');",
"res": "// Post-response script\nconsole.log('post-response');"
},
"tests": "// Test script\nassert.response.status === 200;",
"docs": "# User Creation API\nThis endpoint creates a new user."
}
}

View File

@@ -0,0 +1,58 @@
meta:
name: HTTP Methods
seq: 1
http:
method: post
url: https://api.example.com/users
params:
query:
- name: filter
value: active
type: query
disabled: true
path:
- name: userId
value: '123'
type: path
description: User ID parameter
headers:
- name: Content-Type
value: application/json
description: Content type header
- name: Accept
value: application/json
disabled: true
body:
type: json
data: |-
{
"name": "John Doe",
"email": "john@example.com"
}
auth:
type: basic
basic:
username: admin
password: secret
vars:
pre-request:
- name: userId
value: '123'
description: User ID variable
post-response:
- name: token
value: response.token
disabled: true
scripts:
pre-request: |-
// Pre-request script
console.log('pre-request');
post-response: |-
// Post-response script
console.log('post-response');
tests: |-
// Test script
assert.response.status === 200;
docs: |-
# User Creation API
This endpoint creates a new user.

View File

@@ -0,0 +1,103 @@
const { stringifyRequest } = require('../../src');
const yaml = require('js-yaml');
const path = require('path');
const fs = require('fs');
describe('GraphQL Request Handling', () => {
const loadFixture = (filename) => {
const filePath = path.join(__dirname, '__fixtures__', filename);
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
};
it('should generate exact YAML matching the fixture', () => {
const json = loadFixture('graphql-request.json');
const expectedYaml = fs.readFileSync(
path.join(__dirname, '__fixtures__', 'graphql-request.yml'),
'utf8'
);
const generatedYaml = stringifyRequest(json);
// Normalize line endings and whitespace for comparison
const normalizeString = (str) => str.replace(/\r\n/g, '\n').trim();
expect(normalizeString(generatedYaml)).toBe(normalizeString(expectedYaml));
});
it('should correctly format GraphQL request', () => {
const json = loadFixture('graphql-request.json');
const result = yaml.load(stringifyRequest(json));
// Verify GraphQL section exists instead of HTTP
expect(result.graphql).toBeDefined();
expect(result.http).toBeUndefined();
// Verify basic properties
expect(result.graphql.method).toBe('post');
expect(result.graphql.url).toBe('https://api.example.com/graphql');
// Verify headers
expect(result.graphql.headers).toHaveLength(1);
expect(result.graphql.headers[0]).toEqual({
name: 'Content-Type',
value: 'application/json'
});
// Verify body
expect(result.graphql.body).toEqual({
type: 'graphql',
query: 'query GetUser($id: ID!) {\n user(id: $id) {\n id\n name\n email\n }\n}',
variables: '{\n \"id\": \"123\"\n}'
});
// Verify auth
expect(result.graphql.auth).toEqual({
type: 'bearer',
bearer: {
token: 'jwt-token'
}
});
// Verify vars
expect(result.vars['pre-request']).toHaveLength(1);
expect(result.vars['pre-request'][0]).toEqual({
name: 'userId',
value: '123'
});
// Verify scripts
expect(result.scripts['pre-request']).toBe('// Pre-request script for GraphQL');
});
it('should handle GraphQL request without variables', () => {
const json = loadFixture('graphql-request.json');
json.request.body.graphql.variables = '';
const result = yaml.load(stringifyRequest(json));
expect(result.graphql.body.variables).toBe('');
});
it('should handle GraphQL request without optional components', () => {
const json = loadFixture('graphql-request.json');
// Remove optional components
delete json.request.auth;
delete json.request.vars;
delete json.request.script;
const result = yaml.load(stringifyRequest(json));
expect(result.graphql.auth).toEqual({ type: 'none' });
expect(result.vars).toBeUndefined();
expect(result.scripts).toBeUndefined();
});
it('should detect GraphQL request based on body mode', () => {
const json = loadFixture('graphql-request.json');
// Change URL but keep GraphQL body
json.request.url = 'https://api.example.com/not-graphql';
const result = yaml.load(stringifyRequest(json));
expect(result.graphql).toBeDefined();
expect(result.http).toBeUndefined();
});
});

View File

@@ -0,0 +1,244 @@
const { stringifyRequest } = require('../../src');
const yaml = require('js-yaml');
const path = require('path');
const fs = require('fs');
describe('HTTP Request Handling', () => {
const loadFixture = (filename) => {
const filePath = path.join(__dirname, '__fixtures__', filename);
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
};
it('should generate exact YAML matching the fixture', () => {
const json = loadFixture('http-request.json');
const expectedYaml = fs.readFileSync(
path.join(__dirname, '__fixtures__', 'http-request.yml'),
'utf8'
);
const generatedYaml = stringifyRequest(json);
// Normalize line endings and whitespace for comparison
const normalizeString = (str) => str.replace(/\r\n/g, '\n').trim();
expect(normalizeString(generatedYaml)).toBe(normalizeString(expectedYaml));
});
it('should correctly format basic HTTP request', () => {
const json = loadFixture('http-request.json');
const result = yaml.load(stringifyRequest(json));
// Verify HTTP section exists instead of GraphQL
expect(result.http).toBeDefined();
expect(result.graphql).toBeUndefined();
// Verify basic properties
expect(result.meta.name).toBe('HTTP Methods');
expect(result.meta.seq).toBe(1);
expect(result.http.method).toBe('post');
expect(result.http.url).toBe('https://api.example.com/users');
});
it('should handle request with query parameters', () => {
const json = loadFixture('http-request.json');
const result = yaml.load(stringifyRequest(json));
expect(result.http.params.query).toEqual([
{
name: 'filter',
value: 'active',
type: 'query',
disabled: true
}
]);
});
it('should handle request with path parameters', () => {
const json = loadFixture('http-request.json');
const result = yaml.load(stringifyRequest(json));
expect(result.http.params.path).toEqual([
{
name: 'userId',
value: '123',
type: 'path',
description: 'User ID parameter'
}
]);
});
it('should handle request with headers', () => {
const json = loadFixture('http-request.json');
const result = yaml.load(stringifyRequest(json));
expect(result.http.headers).toEqual([
{
name: 'Content-Type',
value: 'application/json',
description: 'Content type header'
},
{
name: 'Accept',
value: 'application/json',
disabled: true
}
]);
});
describe('Body Handling', () => {
it('should handle JSON body', () => {
const json = loadFixture('http-request.json');
const result = yaml.load(stringifyRequest(json));
expect(result.http.body).toEqual({
type: 'json',
data: '{\n "name": "John Doe",\n "email": "john@example.com"\n}'
});
});
it('should handle form-urlencoded body', () => {
const json = loadFixture('http-request.json');
json.request.body = {
mode: 'formUrlEncoded',
formUrlEncoded: [
{ name: 'username', value: 'johndoe', description: 'Username field', enabled: true },
{ name: 'password', value: 'secret', enabled: false }
]
};
const result = yaml.load(stringifyRequest(json));
expect(result.http.body).toEqual({
type: 'form-urlencoded',
data: [
{ name: 'username', value: 'johndoe', description: 'Username field' },
{ name: 'password', value: 'secret', disabled: true }
]
});
});
it('should handle multipart-form body', () => {
const json = loadFixture('http-request.json');
json.request.body = {
mode: 'multipartForm',
multipartForm: [
{ name: 'file', value: ['path/to/file'], type: 'file', enabled: true },
{ name: 'description', value: 'profile photo', type: 'text', enabled: true }
]
};
const result = yaml.load(stringifyRequest(json));
expect(result.http.body).toEqual({
type: 'multipart-form',
data: [
{ name: 'file', value: ['path/to/file'], type: 'file' },
{ name: 'description', value: 'profile photo', type: 'text' }
]
});
});
});
describe('Auth Handling', () => {
it('should handle basic auth', () => {
const json = loadFixture('http-request.json');
const result = yaml.load(stringifyRequest(json));
expect(result.http.auth).toEqual({
type: 'basic',
basic: {
username: 'admin',
password: 'secret'
}
});
});
it('should handle bearer auth', () => {
const json = loadFixture('http-request.json');
json.request.auth = {
mode: 'bearer',
bearer: { token: 'xyz123' }
};
const result = yaml.load(stringifyRequest(json));
expect(result.http.auth).toEqual({
type: 'bearer',
bearer: {
token: 'xyz123'
}
});
});
it('should handle oauth2 password grant', () => {
const json = loadFixture('http-request.json');
json.request.auth = {
mode: 'oauth2',
oauth2: {
grantType: 'password',
accessTokenUrl: 'https://api.example.com/oauth/token',
username: 'user',
password: 'pass',
clientId: 'client123',
clientSecret: 'secret123',
scope: 'read write'
}
};
const result = yaml.load(stringifyRequest(json));
expect(result.http.auth).toEqual({
type: 'oauth2',
oauth2: {
grant_type: 'password',
access_token_url: 'https://api.example.com/oauth/token',
username: 'user',
password: 'pass',
client_id: 'client123',
client_secret: 'secret123',
scope: 'read write'
}
});
});
});
describe('Variables and Scripts', () => {
it('should handle pre-request variables', () => {
const json = loadFixture('http-request.json');
const result = yaml.load(stringifyRequest(json));
expect(result.vars['pre-request']).toEqual([
{ name: 'userId', value: '123', description: 'User ID variable' }
]);
});
it('should handle post-response variables', () => {
const json = loadFixture('http-request.json');
const result = yaml.load(stringifyRequest(json));
expect(result.vars['post-response']).toEqual([
{ name: 'token', value: 'response.token', disabled: true }
]);
});
it('should handle scripts', () => {
const json = loadFixture('http-request.json');
const result = yaml.load(stringifyRequest(json));
expect(result.scripts['pre-request']).toBe('// Pre-request script\nconsole.log(\'pre-request\');');
expect(result.scripts['post-response']).toBe('// Post-response script\nconsole.log(\'post-response\');');
});
});
it('should handle tests and docs', () => {
const json = loadFixture('http-request.json');
const result = yaml.load(stringifyRequest(json));
expect(result.tests).toBe('// Test script\nassert.response.status === 200;');
expect(result.docs).toBe('# User Creation API\nThis endpoint creates a new user.');
});
it('should handle disabled components', () => {
const json = loadFixture('http-request.json');
json.request.headers[0].enabled = false;
json.request.params[0].enabled = false;
const result = yaml.load(stringifyRequest(json));
expect(result.http.headers[0].disabled).toBe(true);
expect(result.http.params.query[0].disabled).toBe(true);
});
});

View File

@@ -41,7 +41,7 @@ const varsSchema = Yup.object({
// todo
// anoop(4 feb 2023) - nobody uses this, and it needs to be removed
local: Yup.boolean()
local: Yup.boolean().optional()
})
.noUnknown(true)
.strict();
@@ -298,9 +298,10 @@ const folderRootSchema = Yup.object({
const itemSchema = Yup.object({
uid: uidSchema,
name: Yup.string().min(1, 'name must be at least 1 character').required('name is required'),
description: Yup.string().nullable(),
type: Yup.string().oneOf(['http-request', 'graphql-request', 'folder', 'js']).required('type is required'),
seq: Yup.number().min(1),
name: Yup.string().min(1, 'name must be at least 1 character').required('name is required'),
request: requestSchema.when('type', {
is: (type) => ['http-request', 'graphql-request'].includes(type),
then: (schema) => schema.required('request is required when item-type is request')