restclients
Version:
A simple and secured rest api client
638 lines (589 loc) • 22.1 kB
JavaScript
/**
* © Copyright restclients. 2024 All Rights Reserved
* Project name: restclients
* This project is licensed under the Apache 2 License, see LICENSE
*/
const { metaType, varType, metaTypeName } = require("./parser");
const { datetimeAdd, datetimeFormat, isArray, logging } = require("./util");
const timestamp = (offset, option) => {
const date = new Date();
if (offset && option) {
datetimeAdd(date, Number(offset), option);
}
return { value: Math.floor(date.valueOf() / 1000).toString() };
};
const utcDatetime = (type, offset, option) => {
const date = new Date();
if (offset && option) {
datetimeAdd(date, Number(offset), option);
}
if (type === "rfc1123") {
return {
value: datetimeFormat(date, "UTC", "ddd, DD MMM YYYY HH:mm:ss [GMT]"),
};
} else if (type === "iso8601") {
return { value: datetimeFormat(date, "UTC", "YYYY-MM-DDTHH:mm:ss.SSS[Z]") };
} else {
return { value: datetimeFormat(date, "UTC", type) };
}
};
const localDatetime = (type, offset, option) => {
const date = new Date();
if (offset && option) {
datetimeAdd(date, Number(offset), option);
}
if (type === "rfc1123") {
return { value: datetimeFormat(date, "", "ddd, DD MMM YYYY HH:mm:ss ZZ") };
} else if (type === "iso8601") {
return { value: datetimeFormat(date, "", "") };
} else {
return { value: datetimeFormat(date, "", type) };
}
};
const uuidv4 = () => {
var id_str = [];
var hxDigits = "0123456789abcdef";
for (var i = 0; i < 36; i++) {
id_str[i] = hxDigits[Math.floor(Math.random() * 0x10)];
}
id_str[14] = "4"; // bits 12-15 is for time_hi_and_version field, set to to 0010
id_str[19] = hxDigits[(id_str[19] & 0x3) | 0x8]; // bits 6-7 for the clock_seq_hi_and_reserved to 01
id_str[8] = id_str[13] = id_str[18] = id_str[23] = "-";
var guid = id_str.join("");
return guid;
};
const randomInt = (min, max) => {
max = Number(max);
min = Number(min);
if (min <= max) {
return { value: (Math.floor(Math.random() * (max - min)) + min).toString() };
}
};
const environmentVariable = (() => {
let currentEnvironmentVariable = {};
let availableEnvironmentVariable = {};
let currentEnvironment = "";
// new RegExp(`\\{{2}\\$${environment} (.+?)\\}{2}`);
const populateVariableValue = (environment, des, src) => {
const variableRegex = new RegExp(`\\{{2}\\s*\\$${environment} (.+?)\\}{2}`, "gm");
for (const [key, value] of Object.entries(des)) {
if (typeof value === "string") {
let match;
let populated = false;
let populatedValue = value;
while ((match = variableRegex.exec(value)) !== null) {
const referenceKey = match[1].trim();
populatedValue = populatedValue.replace(match[0], src[referenceKey]);
populated = true;
}
if (populated) {
des[key] = populatedValue;
}
}
}
};
const resolver = (key) => {
if (Object.hasOwnProperty.call(availableEnvironmentVariable, key)) {
return availableEnvironmentVariable[key].toString();
}
};
const setEnvironmentVariable = (environmentVariable) => {
currentEnvironmentVariable = environmentVariable;
const data = Object.assign({}, environmentVariable);
populateVariableValue("shared", data["$shared"], data["$shared"]);
if (currentEnvironment !== "" && Object.hasOwnProperty.call(currentEnvironmentVariable, currentEnvironment)) {
populateVariableValue("shared", data[currentEnvironment], data["$shared"]);
populateVariableValue(currentEnvironment, data[currentEnvironment], data[currentEnvironment]);
availableEnvironmentVariable = Object.assign({}, data["$shared"], data[currentEnvironment]);
} else {
availableEnvironmentVariable = Object.assign({}, data["$shared"]);
}
};
const selectEnvironment = (environment) => {
if (
currentEnvironment !== environment &&
Object.hasOwnProperty.call(currentEnvironmentVariable, environment) &&
environment[0] !== "$"
) {
currentEnvironment = environment;
setEnvironmentVariable(currentEnvironmentVariable);
}
};
const addEnvironmentVariable = (key, value) => {
if (Object.hasOwnProperty.call(availableEnvironmentVariable, key)) {
logging.warn(
"overwrite environment variable, key: %s, value: %s, newValue: %s",
key,
availableEnvironmentVariable[key],
value
);
}
availableEnvironmentVariable[key] = value;
};
return {
resolver,
setEnvironmentVariable,
selectEnvironment,
addEnvironmentVariable,
};
})();
const expandVariableValue = (value) => {
const end = value.length - 1;
const isDoubleQuoted = value[0] === '"' && value[end] === '"';
const isSingleQuoted = value[0] === "'" && value[end] === "'";
// if single or double quoted, remove quotes
if (isSingleQuoted || isDoubleQuoted) {
value = value.substring(1, end);
// if double quoted, expand newlines
if (isDoubleQuoted) {
value = value.replace(/\\n/g, "\n");
value = value.replace(/\\r/g, "\r");
}
} else {
// remove surrounding whitespace
value = value.trim();
}
return value;
};
const dotenvVariable = (() => {
let dotenv = {};
let lineReg =
/(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm;
const resolver = (key) => {
if (Object.hasOwnProperty.call(dotenv, key)) {
return dotenv[key];
}
};
// load default .env file path
// const dotenvPath = path.resolve(process.cwd(), '.env')
const setDotenv = (src) => {
// Convert buffer to string
let lines = src.toString();
// Convert line breaks to same format
lines = lines.replace(/\r\n?/gm, "\n");
let match;
while ((match = lineReg.exec(lines)) != null) {
const key = match[1];
// Default undefined or null to empty string
let value = match[2] || "";
// Add to object
dotenv[key] = expandVariableValue(value);
}
};
const resetDotenv = () => {
dotenv = {};
};
return {
resolver,
setDotenv,
resetDotenv,
};
})();
const variable = (exprs) => {
const resolvePromptVariable = async (promptVariables) => {
let prompt = {};
if (promptVariables.length > 0) {
const readlinePromise = require("node:readline/promises");
const { stdin: input, stdout: output } = require("node:process");
const rl = readlinePromise.createInterface({ input, output });
for (const item of promptVariables) {
const input = await rl.question(`Input value for "${item[0]}"${`${item[1] ? ` (${item[1]})` : ""}`}\n`);
prompt[item[0]] = { value: input, args: null };
}
rl.close();
}
return prompt;
};
const resolveEnvironmentVariable = (key) => {
// load from setting
return { value: environmentVariable.resolver(key) };
};
const resolveDotenvVariable = (key) => {
// load from .env file
if (key && key.startsWith("%")) {
key = environmentVariable.resolver(key.slice(1));
}
return { value: dotenvVariable.resolver(key) };
};
const resolveProcessEnvVariable = (key) => {
if (key && key.startsWith("%")) {
key = environmentVariable.resolver(key.slice(1));
}
if (process && process.env) {
const envValue = process.env[key];
if (envValue !== undefined) {
return { value: envValue.toString() };
}
}
return { value: undefined };
};
const resolveDynamicVariable = (args) => {
switch (args[0]) {
case "$aadToken": {
// {{$aadToken [new] [public|cn|de|us|ppe] [<domain|tenantId>] [aud:<domain|tenantId>]}}
return;
}
case "$aadV2Token": {
// {{$aadV2Token [new] [AzureCloud|AzureChinaCloud|AzureUSGovernment|ppe] [appOnly ][scopes:<scope[,]>] [tenantid:<domain|tenantId>] [clientid:<clientId>]}}
return;
}
case "$guid": {
// {{$guid}}
return { value: uuidv4() };
}
case "$processEnv": {
// {{$processEnv [%]envVarName}}
return resolveProcessEnvVariable(args[1]);
}
case "$dotenv": {
// {{$dotenv [%]variableName}}
return resolveDotenvVariable(args[1]);
}
case "$randomInt": {
// {{$randomInt min max}}
return randomInt(...args.slice(1));
}
case "$timestamp": {
// {{$timestamp [offset option]}}
return timestamp(...args.slice(1));
}
case "$datetime": {
// {{$datetime rfc1123|iso8601|"custom format"|'custom format' [offset option]}}
return utcDatetime(...args.slice(1));
}
case "$localDatetime": {
// {{$localDatetime rfc1123|iso8601|"custom format"|'custom format' [offset option]}}
return localDatetime(...args.slice(1));
}
}
};
const getReferenceKey = (key) => {
if (typeof key === "string" && (key.startsWith("%") || key.startsWith("@"))) {
return key.substring(1);
}
return key;
};
const populateVariableValue = (variable, target, key, replacementKey, resolvedValue) => {
let item = target ? variable[target] : variable;
if (key[0] === "%") {
item.value = item.value.replace(replacementKey, encodeURIComponent(resolvedValue));
} else if (key[0] === "@") {
item.value = item.value.replace(replacementKey, Buffer.from(resolvedValue).toString("base64"));
} else {
item.value = item.value.replace(replacementKey, resolvedValue);
}
};
const documentVariable = ((exprs) => {
let fileVariable = {};
let requestVariable = {};
let resolvedVariables = [];
let reference = {};
for (const expr of exprs) {
// build file variable
if (expr && expr.type === varType && isArray(expr.value) && expr.value.length >= 2) {
if (expr.value.length > 2 && isArray(expr.value[2]) && expr.value[2].length >= 2) {
fileVariable[expr.value[0]] = { value: expr.value[1], args: expr.value[2] };
} else {
fileVariable[expr.value[0]] = { value: expr.value[1], args: null };
resolvedVariables.push(expr.value[0]);
}
}
// build request variable
if (
expr &&
expr.type === metaType &&
isArray(expr.value) &&
expr.value.length >= 2 &&
expr.value[0] === metaTypeName
) {
requestVariable[expr.value[1]] = null;
}
}
for (const [key, { args }] of Object.entries(fileVariable)) {
if (isArray(args) && args.length >= 2) {
for (let i = 0; i < args.length - 1; i += 2) {
if (isArray(args[i + 1]) && args[i + 1].length === 1 && !args[i + 1][0].startsWith("$")) {
let referenceKey = getReferenceKey(args[i + 1][0]);
if (Object.hasOwnProperty.call(fileVariable, referenceKey)) {
if (reference[referenceKey]) {
reference[referenceKey].push(key);
} else {
reference[referenceKey] = [key];
}
}
}
}
}
}
while (resolvedVariables.length > 0) {
let resolved = resolvedVariables.pop();
if (reference[resolved] && fileVariable[resolved]) {
let resolvedValue = fileVariable[resolved].value;
for (const target of reference[resolved]) {
if (fileVariable[target]) {
let args = fileVariable[target].args;
let resolvedIndex = [];
if (isArray(args) && args.length >= 2) {
for (let i = 0; i < args.length - 1; i += 2) {
if (isArray(args[i + 1]) && args[i + 1].length === 1 && getReferenceKey(args[i + 1][0]) === resolved) {
populateVariableValue(fileVariable, target, args[i + 1][0], args[i], resolvedValue);
resolvedIndex.push(i);
}
}
}
if (resolvedIndex.length > 0) {
let removedCount = 0;
for (const removed of resolvedIndex) {
args.splice(removed - removedCount, 2);
removedCount += 2;
}
}
if (args.length < 2) {
// all dependency resolved
resolvedVariables.push(target);
}
}
}
}
}
logging.debug("fileVariable: %j, reference %j", fileVariable, reference);
const resolveFileVariable = (variables, resolvedFileVariable) => {
const resolved = [];
resolvedFileVariable = resolvedFileVariable || {};
const resolveFileVariableDependencies = (variable) => {
let fileVariableDependencies = [];
if (isArray(variable.args) && variable.args.length >= 2) {
let resolvedIndex = [];
for (let i = 0; i < variable.args.length - 1; i += 2) {
if (
isArray(variable.args[i + 1]) &&
variable.args[i + 1].length === 1 &&
!variable.args[i + 1][0].startsWith("$")
) {
let referenceKey = getReferenceKey(variable.args[i + 1][0]);
if (Object.hasOwnProperty.call(fileVariable, referenceKey)) {
if (
fileVariable[referenceKey].args === null ||
(isArray(fileVariable[referenceKey].args) && fileVariable[referenceKey].args.length < 2)
) {
// no dependency variable
populateVariableValue(
variable,
undefined,
variable.args[i + 1][0],
variable.args[i],
fileVariable[referenceKey].value
);
resolvedIndex.push(i);
} else {
if (
!Object.hasOwnProperty.call(resolvedFileVariable, referenceKey) &&
fileVariableDependencies.indexOf(referenceKey) < 0
) {
fileVariableDependencies.push(referenceKey);
}
}
} else {
let { value } = resolveEnvironmentVariable(referenceKey);
if (value !== undefined) {
populateVariableValue(variable, undefined, variable.args[i + 1][0], variable.args[i], value);
resolvedIndex.push(i);
}
}
} else {
let { value } = resolveDynamicVariable(variable.args[i + 1]);
if (value !== undefined) {
populateVariableValue(variable, undefined, variable.args[i + 1][0], variable.args[i], value);
resolvedIndex.push(i);
}
}
}
if (resolvedIndex.length > 0) {
let removedCount = 0;
for (const removed of resolvedIndex) {
variable.args.splice(removed - removedCount, 2);
removedCount += 2;
}
}
}
return fileVariableDependencies;
};
for (const variable of variables) {
let dependencies = resolveFileVariableDependencies(variable);
while (dependencies.length > 0) {
let dependency = dependencies.pop();
if (!Object.hasOwnProperty.call(resolvedFileVariable, dependency)) {
resolvedFileVariable[dependency] = structuredClone(fileVariable[dependency]);
if (
resolvedFileVariable[dependency].args === null ||
(isArray(resolvedFileVariable[dependency].args) && resolvedFileVariable[dependency].args.length < 2)
) {
if (resolved.indexOf(dependency) < 0) {
resolved.push(dependency);
}
}
for (const [referenceKey, dependents] of Object.entries(reference)) {
if (dependents.indexOf(dependency) >= 0) {
if (!Object.hasOwnProperty.call(resolvedFileVariable, referenceKey)) {
dependencies.push(referenceKey);
}
}
}
}
}
}
logging.debug("resolvedFileVariable: %j, resolved: %j", resolvedFileVariable, resolved);
for (let key of Object.keys(resolvedFileVariable)) {
let args = resolvedFileVariable[key].args;
let resolvedIndex = [];
if (isArray(args) && args.length >= 2) {
for (let i = 0; i < args.length - 1; i += 2) {
if (isArray(args[i + 1]) && args[i + 1].length === 1 && !args[i + 1][0].startsWith("$")) {
let referenceKey = getReferenceKey(args[i + 1][0]);
if (!Object.hasOwnProperty.call(fileVariable, referenceKey)) {
let { value } = resolveEnvironmentVariable(referenceKey);
if (value !== undefined) {
populateVariableValue(resolvedFileVariable, key, args[i + 1][0], args[i], value);
resolvedIndex.push(i);
}
}
} else {
let { value } = resolveDynamicVariable(args[i + 1]);
if (value !== undefined) {
populateVariableValue(resolvedFileVariable, key, args[i + 1][0], args[i], value);
resolvedIndex.push(i);
}
}
}
if (resolvedIndex.length > 0) {
let removedCount = 0;
for (const removed of resolvedIndex) {
args.splice(removed - removedCount, 2);
removedCount += 2;
}
}
if (args.length < 2) {
// all dependency resolved
if (resolved.indexOf(key) < 0) {
resolved.push(key);
}
}
}
}
logging.debug("resolvedFileVariable: %j, resolved: %j", resolvedFileVariable, resolved);
while (resolved.length > 0) {
let resolvedKey = resolved.pop();
if (reference[resolvedKey] && resolvedFileVariable[resolvedKey]) {
let resolvedValue = resolvedFileVariable[resolvedKey].value;
for (const target of reference[resolvedKey]) {
if (resolvedFileVariable[target]) {
let args = resolvedFileVariable[target].args;
let resolvedIndex = [];
if (isArray(args) && args.length >= 2) {
for (let i = 0; i < args.length - 1; i += 2) {
if (isArray(args[i + 1]) && args[i + 1].length === 1 && getReferenceKey(args[i + 1][0])) {
populateVariableValue(resolvedFileVariable, target, args[i + 1][0], args[i], resolvedValue);
resolvedIndex.push(i);
}
}
}
if (resolvedIndex.length > 0) {
let removedCount = 0;
for (const removed of resolvedIndex) {
args.splice(removed - removedCount, 2);
removedCount += 2;
}
}
if (args.length < 2) {
// all dependency resolved
resolved.push(target);
}
}
}
}
}
for (const variable of variables) {
if (isArray(variable.args) && variable.args.length >= 2) {
let resolvedIndex = [];
for (let i = 0; i < variable.args.length - 1; i += 2) {
if (
isArray(variable.args[i + 1]) &&
variable.args[i + 1].length === 1 &&
!variable.args[i + 1][0].startsWith("$")
) {
let referenceKey = getReferenceKey(variable.args[i + 1][0]);
if (Object.hasOwnProperty.call(resolvedFileVariable, referenceKey)) {
populateVariableValue(
variable,
undefined,
variable.args[i + 1][0],
variable.args[i],
resolvedFileVariable[referenceKey].value
);
resolvedIndex.push(i);
}
}
}
if (resolvedIndex.length > 0) {
let removedCount = 0;
for (const removed of resolvedIndex) {
variable.args.splice(removed - removedCount, 2);
removedCount += 2;
}
}
}
}
};
const setRequestVariable = (key, req) => {
requestVariable[key] = req;
};
const addFileVariable = (key, value) => {
if (Object.hasOwnProperty.call(fileVariable, key)) {
logging.warn("overwrite file variable, key: %s, value: %s, newValue: %s", key, fileVariable[key], value);
}
fileVariable[key] = { value: value, args: null };
};
return {
resolveFileVariable,
setRequestVariable,
addFileVariable,
};
})(exprs);
return {
selectEnvironment: environmentVariable.selectEnvironment,
setEnvironmentVariable: environmentVariable.setEnvironmentVariable,
addEnvironmentVariable: environmentVariable.addEnvironmentVariable,
resetDotenvVariable: dotenvVariable.resetDotenv,
setDotenvVariable: dotenvVariable.setDotenv,
resolvePromptVariable,
resolveDynamicVariable,
resolveEnvironmentVariable,
resolveFileVariable: documentVariable.resolveFileVariable,
setRequestVariable: documentVariable.setRequestVariable,
addFileVariable: documentVariable.addFileVariable,
};
};
const variableToContext = (vars, resolvedVariables) => {
return {
addEnvironmentVariable: vars.addEnvironmentVariable,
addFileVariable: vars.addFileVariable,
resolveVariables: (...args) => {
const variables = (isArray(args[0]) ? args[0] : args).map((arg) => {
return {
value: `{{${arg}}}`,
args: [`{{${arg}}}`, [`${arg}`]],
};
});
vars.resolveFileVariable(variables, resolvedVariables);
return variables.map((variable) => variable.value);
},
};
};
module.exports = {
variable,
variableToContext,
};
// setting env ($shared, env...)
// local env file (same folder .env file)
// file variable
// request variable