UNPKG

usezap-cli

Version:

Zap CLI - Command-line interface for Zap API client

488 lines (432 loc) 15.7 kB
const { get, each, find, compact } = require('lodash'); const os = require('os'); const fs = require('fs'); const path = require('path'); const { jsonToZapV2, envJsonToZapV2, jsonToCollectionZap } = require('@usezap/lang'); const { sanitizeName } = require('./filesystem'); const { zapToJson, collectionZapToJson } = require('./zap'); const constants = require('../constants'); const chalk = require('chalk'); const createCollectionJsonFromPathname = (collectionPath) => { const environmentsPath = path.join(collectionPath, `environments`); // get the collection zap json config [<collection-path>/zap.json] const zapConfig = getCollectionZapJsonConfig(collectionPath); // get the collection root [<collection-path>/collection.zap] const collectionRoot = getCollectionRoot(collectionPath); // get the collection items recursively const traverse = (currentPath) => { const filesInCurrentDir = fs.readdirSync(currentPath); if (currentPath.includes('node_modules')) { return; } const currentDirItems = []; for (const file of filesInCurrentDir) { const filePath = path.join(currentPath, file); const stats = fs.lstatSync(filePath); if (stats.isDirectory()) { if (filePath === environmentsPath) continue; if (filePath.startsWith('.git') || filePath.startsWith('node_modules')) continue; // get the folder root let folderItem = { name: file, pathname: filePath, type: 'folder', items: traverse(filePath) } const folderZapJson = getFolderRoot(filePath); if (folderZapJson) { folderItem.root = folderZapJson; folderItem.seq = folderZapJson.meta.seq; } currentDirItems.push(folderItem); } else { if (['collection.zap', 'folder.zap'].includes(file)) continue; if (path.extname(filePath) !== '.zap') continue; // get the request item const zapContent = fs.readFileSync(filePath, 'utf8'); const requestItem = zapToJson(zapContent); currentDirItems.push({ name: file, pathname: filePath, ...requestItem }); } } let currentDirFolderItems = currentDirItems?.filter((iter) => iter.type === 'folder'); let sortedFolderItems = currentDirFolderItems?.sort((a, b) => a.seq - b.seq); let currentDirRequestItems = currentDirItems?.filter((iter) => iter.type !== 'folder'); let sortedRequestItems = currentDirRequestItems?.sort((a, b) => a.seq - b.seq); return sortedFolderItems?.concat(sortedRequestItems); }; let collectionItems = traverse(collectionPath); let collection = { zapConfig, root: collectionRoot, pathname: collectionPath, items: collectionItems } return collection; }; const getCollectionZapJsonConfig = (dir) => { // right now, zap must be run from the root of the collection // will add support in the future to run it from anywhere inside the collection const zapJsonPath = path.join(dir, 'zap.json'); const zapJsonExists = fs.existsSync(zapJsonPath); if (!zapJsonExists) { console.error(chalk.red(`You can run only at the root of a collection`)); process.exit(constants.EXIT_STATUS.ERROR_NOT_IN_COLLECTION); } const zapConfigFile = fs.readFileSync(zapJsonPath, 'utf8'); let zapConfig; try { zapConfig = JSON.parse(zapConfigFile); } catch (error) { console.error(chalk.red(`Error parsing zap.json: ${error.message}`)); console.error(chalk.red(`File content: ${zapConfigFile}`)); process.exit(constants.EXIT_STATUS.ERROR_NOT_IN_COLLECTION); } return zapConfig; } const getCollectionRoot = (dir) => { const collectionRootPath = path.join(dir, 'collection.zap'); const exists = fs.existsSync(collectionRootPath); if (!exists) { return {}; } const content = fs.readFileSync(collectionRootPath, 'utf8'); return collectionZapToJson(content); }; const getFolderRoot = (dir) => { const folderRootPath = path.join(dir, 'folder.zap'); const exists = fs.existsSync(folderRootPath); if (!exists) { return null; } const content = fs.readFileSync(folderRootPath, 'utf8'); return collectionZapToJson(content); }; const mergeHeaders = (collection, request, requestTreePath) => { let headers = new Map(); let collectionHeaders = get(collection, 'root.request.headers', []); collectionHeaders.forEach((header) => { if (header.enabled) { headers.set(header.name, header.value); } }); for (let i of requestTreePath) { if (i.type === 'folder') { let _headers = get(i, 'root.request.headers', []); _headers.forEach((header) => { if (header.enabled) { headers.set(header.name, header.value); } }); } else { const _headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []); _headers.forEach((header) => { if (header.enabled) { headers.set(header.name, header.value); } }); } } request.headers = Array.from(headers, ([name, value]) => ({ name, value, enabled: true })); }; const mergeVars = (collection, request, requestTreePath) => { let reqVars = new Map(); let collectionRequestVars = get(collection, 'root.request.vars.req', []); let collectionVariables = {}; collectionRequestVars.forEach((_var) => { if (_var.enabled) { reqVars.set(_var.name, _var.value); collectionVariables[_var.name] = _var.value; } }); let folderVariables = {}; let requestVariables = {}; for (let i of requestTreePath) { if (i.type === 'folder') { let vars = get(i, 'root.request.vars.req', []); vars.forEach((_var) => { if (_var.enabled) { reqVars.set(_var.name, _var.value); folderVariables[_var.name] = _var.value; } }); } else { const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []); vars.forEach((_var) => { if (_var.enabled) { reqVars.set(_var.name, _var.value); requestVariables[_var.name] = _var.value; } }); } } request.collectionVariables = collectionVariables; request.folderVariables = folderVariables; request.requestVariables = requestVariables; if(request?.vars) { request.vars.req = Array.from(reqVars, ([name, value]) => ({ name, value, enabled: true, type: 'request' })); } let resVars = new Map(); let collectionResponseVars = get(collection, 'root.request.vars.res', []); collectionResponseVars.forEach((_var) => { if (_var.enabled) { resVars.set(_var.name, _var.value); } }); for (let i of requestTreePath) { if (i.type === 'folder') { let vars = get(i, 'root.request.vars.res', []); vars.forEach((_var) => { if (_var.enabled) { resVars.set(_var.name, _var.value); } }); } else { const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []); vars.forEach((_var) => { if (_var.enabled) { resVars.set(_var.name, _var.value); } }); } } if(request?.vars) { request.vars.res = Array.from(resVars, ([name, value]) => ({ name, value, enabled: true, type: 'response' })); } }; const mergeScripts = (collection, request, requestTreePath, scriptFlow) => { let collectionPreReqScript = get(collection, 'root.request.script.req', ''); let collectionPostResScript = get(collection, 'root.request.script.res', ''); let collectionTests = get(collection, 'root.request.tests', ''); let combinedPreReqScript = []; let combinedPostResScript = []; let combinedTests = []; for (let i of requestTreePath) { if (i.type === 'folder') { let preReqScript = get(i, 'root.request.script.req', ''); if (preReqScript && preReqScript.trim() !== '') { combinedPreReqScript.push(preReqScript); } let postResScript = get(i, 'root.request.script.res', ''); if (postResScript && postResScript.trim() !== '') { combinedPostResScript.push(postResScript); } let tests = get(i, 'root.request.tests', ''); if (tests && tests?.trim?.() !== '') { combinedTests.push(tests); } } } request.script.req = compact([collectionPreReqScript, ...combinedPreReqScript, request?.script?.req || '']).join(os.EOL); if (scriptFlow === 'sequential') { request.script.res = compact([collectionPostResScript, ...combinedPostResScript, request?.script?.res || '']).join(os.EOL); } else { request.script.res = compact([request?.script?.res || '', ...combinedPostResScript.reverse(), collectionPostResScript]).join(os.EOL); } if (scriptFlow === 'sequential') { request.tests = compact([collectionTests, ...combinedTests, request?.tests || '']).join(os.EOL); } else { request.tests = compact([request?.tests || '', ...combinedTests.reverse(), collectionTests]).join(os.EOL); } }; const findItem = (items = [], pathname) => { return find(items, (i) => i.pathname === pathname); }; const findItemInCollection = (collection, pathname) => { let flattenedItems = flattenItems(collection.items); return findItem(flattenedItems, pathname); }; const findParentItemInCollection = (collection, pathname) => { let flattenedItems = flattenItems(collection.items); return find(flattenedItems, (item) => { return item.items && find(item.items, (i) => i.pathname === pathname); }); }; const flattenItems = (items = []) => { const flattenedItems = []; const flatten = (itms, flattened) => { each(itms, (i) => { flattened.push(i); if (i.items && i.items.length) { flatten(i.items, flattened); } }); }; flatten(items, flattenedItems); return flattenedItems; }; const getTreePathFromCollectionToItem = (collection, _item) => { let path = []; let item = findItemInCollection(collection, _item.pathname); while (item) { path.unshift(item); item = findParentItemInCollection(collection, item.pathname); } return path; }; const mergeAuth = (collection, request, requestTreePath) => { let collectionAuth = collection?.root?.request?.auth || { mode: 'none' }; let effectiveAuth = collectionAuth; for (let i of requestTreePath) { if (i.type === 'folder') { const folderAuth = i?.root?.request?.auth; if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') { effectiveAuth = folderAuth; } } } if (request.auth && request.auth.mode === 'inherit') { request.auth = effectiveAuth; } } const getAllRequestsInFolder = (folderItems = [], recursive = true) => { let requests = []; if (folderItems && folderItems.length) { folderItems.forEach((item) => { if (item.type !== 'folder') { requests.push(item); } else { if (recursive) { requests = requests.concat(getAllRequestsInFolder(item.items, recursive)); } } }); } return requests; }; const getAllRequestsAtFolderRoot = (folderItems = []) => { return getAllRequestsInFolder(folderItems, false); } /** * Safe write file implementation to handle errors * @param {string} filePath - Path to write file * @param {string} content - Content to write */ const safeWriteFileSync = (filePath, content) => { try { fs.writeFileSync(filePath, content, { encoding: 'utf8' }); } catch (error) { console.error(`Error writing file ${filePath}:`, error); } }; /** * Creates a Zap collection directory structure from a Zap collection object * * @param {Object} collection - The Zap collection object * @param {string} dirPath - The output directory path */ const createCollectionFromZapObject = async (collection, dirPath) => { // Create zap.json const zapConfig = { version: '1', name: collection.name, type: 'collection', ignore: ['node_modules', '.git'] }; fs.writeFileSync( path.join(dirPath, 'zap.json'), JSON.stringify(zapConfig, null, 2) ); // Create collection.zap if root exists if (collection.root) { const collectionContent = await jsonToCollectionZap(collection.root); fs.writeFileSync(path.join(dirPath, 'collection.zap'), collectionContent); } // Process environments if (collection.environments && collection.environments.length) { const envDirPath = path.join(dirPath, 'environments'); fs.mkdirSync(envDirPath, { recursive: true }); for (const env of collection.environments) { const content = await envJsonToZapV2(env); const filename = sanitizeName(`${env.name}.zap`); fs.writeFileSync(path.join(envDirPath, filename), content); } } // Process collection items await processCollectionItems(collection.items, dirPath); return dirPath; }; /** * Recursively processes collection items to create files and folders * * @param {Array} items - Collection items * @param {string} currentPath - Current directory path */ const processCollectionItems = async (items = [], currentPath) => { for (const item of items) { if (item.type === 'folder') { // Create folder let sanitizedFolderName = sanitizeName(item?.filename || item?.name); const folderPath = path.join(currentPath, sanitizedFolderName); fs.mkdirSync(folderPath, { recursive: true }); // Create folder.zap file if root exists if (item?.root?.meta?.name) { const folderZapFilePath = path.join(folderPath, 'folder.zap'); if (item.seq) { item.root.meta.seq = item.seq; } const folderContent = await jsonToCollectionZap( item.root, true ); safeWriteFileSync(folderZapFilePath, folderContent); } // Process folder items recursively if (item.items && item.items.length) { await processCollectionItems(item.items, folderPath); } } else if (['http-request', 'graphql-request'].includes(item.type)) { // Create request file let sanitizedFilename = sanitizeName(item?.filename || `${item.name}.zap`); if (!sanitizedFilename.endsWith('.zap')) { sanitizedFilename += '.zap'; } // Convert JSON to Zap format based on the item type let type = item.type === 'http-request' ? 'http' : 'graphql'; const zapJson = { meta: { name: item.name, type: type, seq: typeof item.seq === 'number' ? item.seq : 1 }, http: { method: (item.request?.method || 'GET').toLowerCase(), url: item.request?.url || '', auth: item.request?.auth?.mode || 'none', body: item.request?.body?.mode || 'none' }, params: item.request?.params || [], headers: item.request?.headers || [], auth: item.request?.auth || {}, body: item.request?.body || {}, script: item.request?.script || {}, vars: { req: item.request?.vars?.req || [], res: item.request?.vars?.res || [] }, assertions: item.request?.assertions || [], tests: item.request?.tests || '', docs: item.request?.docs || '' }; // Convert to ZAP format and write to file const content = await jsonToZapV2(zapJson); safeWriteFileSync(path.join(currentPath, sanitizedFilename), content); } } }; module.exports = { createCollectionJsonFromPathname, mergeHeaders, mergeVars, mergeScripts, findItemInCollection, getTreePathFromCollectionToItem, createCollectionFromZapObject, mergeAuth, getAllRequestsInFolder, getAllRequestsAtFolderRoot }