@cap-js-community/mtx-tool
Version:
Multitenancy and Extensibility Tool is a cli to reduce operational overhead for multitenant Cloud Foundry applications
392 lines (348 loc) • 11 kB
JavaScript
;
// NOTE: static here means we only allow imports from the node standard library
const readline = require("readline");
const {
accessSync,
readFileSync,
writeFileSync,
unlinkSync,
constants: { R_OK },
} = require("fs");
const { writeFile: writeFileAsync } = require("fs/promises");
const net = require("net");
const childProcess = require("child_process");
const util = require("util");
const isUUID = (input) =>
input && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(input);
const isJWT = (input) => input && /^[0-9a-z-_.]+$/i.test(input);
const isDashedWord = (input) => input && /^[0-9a-z-_]+$/i.test(input);
const sleep = async (milliseconds) =>
new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
const question = async (ask, prefill) =>
new Promise((resolve, reject) => {
const rli = readline.createInterface({
terminal: true,
input: process.stdin,
output: process.stdout,
});
let result = "";
rli.question(ask + " ", (answer) => {
result = answer;
rli.close();
});
rli.on("close", () => {
resolve(result);
});
rli.on("SIGINT", (err) => {
reject(err);
});
prefill && rli.write(prefill);
});
const tryReadJsonSync = (filepath) => {
try {
const data = readFileSync(filepath, "utf8");
return JSON.parse(data);
} catch (err) {
return null;
}
};
const tryAccessSync = (filepath, mode = R_OK) => {
try {
accessSync(filepath, mode);
return true;
} catch (err) {
return null;
}
};
const tryJsonParse = (input) => {
try {
return JSON.parse(input);
} catch (err) {
return null;
}
};
const writeTextSync = (filepath, data) => writeFileSync(filepath, data);
const writeTextAsync = async (filepath, data) => await writeFileAsync(filepath, data);
const writeJsonSync = (filepath, data) => writeFileSync(filepath, JSON.stringify(data, null, 2) + "\n");
const writeJsonAsync = async (filepath, data) => await writeFileAsync(filepath, JSON.stringify(data, null, 2) + "\n");
const deleteFileSync = (filepath) => unlinkSync(filepath);
/**
* @typedef TableListOptions
* @type options
*
* @property {number|null} [sortCol]
* @property {boolean} [noHeader]
* @property {boolean} [withRowNumber]
*/
/**
* @param rows
* @param {TableListOptions} options
*/
const tableList = (rows, { sortCol = 0, noHeader = false, withRowNumber = true } = {}) => {
if (!rows || !rows.length || !rows[0] || !rows[0].length) {
return null;
}
const hasSortCol = Number.isInteger(sortCol);
if (withRowNumber) {
hasSortCol && sortCol++;
if (noHeader) {
rows = rows.map((row, index) => [String(index + 1)].concat(row));
} else {
rows = rows.map((row, index) => [String(index)].concat(row));
rows[0][0] = "#";
}
}
const columnCount = rows[0].length;
const columnWidth = rows.reduce((result, row) => {
row.forEach((cell, index) => {
if (index < columnCount) {
result[index] = Math.max(result[index], String(cell).length);
}
});
return result;
}, new Array(columnCount).fill(0));
const header = noHeader ? [] : rows[0];
let body = noHeader ? rows : rows.slice(1);
if (hasSortCol && sortCol < columnCount) {
body.sort((rowA, rowB) => {
const cellA = rowA[sortCol] ? rowA[sortCol].toUpperCase() : "";
const cellB = rowB[sortCol] ? rowB[sortCol].toUpperCase() : "";
return cellA < cellB ? -1 : cellA > cellB ? 1 : 0;
});
if (withRowNumber) {
body = body.map((row, index) => [String(index + 1)].concat(row.slice(1)));
}
}
const sortedRows = noHeader ? rows : [header].concat(body);
return sortedRows
.map((row) =>
row
.slice(0, columnCount)
.map((cell, columnIndex) => cell + " ".repeat(columnWidth[columnIndex] - String(cell).length))
.join(" ")
)
.join("\n");
};
const orderedStringify = (value, replacer, space) => {
const allKeys = Object.create(null);
JSON.stringify(value, (k, v) => {
allKeys[k] = null;
return v;
});
return JSON.stringify(value, Object.keys(allKeys).sort(), space);
};
const partition = (array, isValid) =>
array.reduce(
(result, elem) => {
isValid(elem) ? result[0].push(elem) : result[1].push(elem);
return result;
},
[[], []]
);
const spawnAsync = (command, args, options) =>
new Promise((_resolve, _reject) => {
const child = childProcess.spawn(command, args, {
detached: false,
stdio: "pipe",
...options,
});
const childCleanup = () => child.kill();
process.on("SIGINT", childCleanup);
process.on("SIGTERM", childCleanup);
const bufferStdout = [];
const bufferStderr = [];
const reject = (err) => {
err.stdout = Buffer.concat(bufferStdout).toString();
err.stderr = Buffer.concat(bufferStderr).toString();
process.removeListener("SIGINT", childCleanup);
process.removeListener("SIGTERM", childCleanup);
return _reject(err);
};
const resolve = () => {
process.removeListener("SIGINT", childCleanup);
process.removeListener("SIGTERM", childCleanup);
return _resolve([Buffer.concat(bufferStdout).toString(), Buffer.concat(bufferStderr).toString()]);
};
child.stdout.on("data", (data) => bufferStdout.push(data));
child.stderr.on("data", (data) => bufferStderr.push(data));
child.on("error", (err) => reject(err));
child.on("exit", (code, signal) => {
if (signal) {
return reject(new Error(util.format("termination signal %s", signal)));
}
if (code) {
return reject(new Error(util.format("non-zero return code %i", code)));
}
return resolve();
});
});
const isPortFree = (port) =>
new Promise((resolve, reject) => {
const tester = net
.createServer()
.once("error", (err) => (err.code === "EADDRINUSE" ? resolve(false) : reject(err)))
.once("listening", () => tester.once("close", () => resolve(true)).close())
.listen(port, "127.0.0.1");
});
const nextFreePort = async (port) => {
for (; port <= 65535; port++) {
if (await isPortFree(port)) {
return port;
}
}
};
const dateDiffInDays = (from, to) => {
const fromDate = Date.UTC(from.getFullYear(), from.getMonth(), from.getDate());
const toDate = Date.UTC(to.getFullYear(), to.getMonth(), to.getDate());
return Math.floor((toDate - fromDate) / 1000 / 60 / 60 / 24);
};
const formatTimestampWithRelativeDays = (input, nowDate = new Date()) => {
if (!input) {
return "";
}
const inputDate = new Date(input);
const daysAgo = dateDiffInDays(inputDate, nowDate);
const outputAbsolute = inputDate.toISOString().replace(/\.[0-9]{3}/, "");
return `${outputAbsolute} (${daysAgo} ${daysAgo === 1 ? "day" : "days"} ago)`;
};
const formatTimestampsWithRelativeDays = (inputs, nowDate = new Date()) =>
inputs.map((input) => formatTimestampWithRelativeDays(input, nowDate));
const compareFor =
(cb, descending = false) =>
(a, b) => {
const aVal = cb(a);
const bVal = cb(b);
if (descending) {
return aVal < bVal ? 1 : aVal > bVal ? -1 : 0;
} else {
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
}
};
const resolveTenantArg = (tenant) => (isUUID(tenant) ? { tenantId: tenant } : { subdomain: tenant });
const balancedSplit = (input, k) => {
let result = [];
const n = input.length;
const l = Math.ceil(n / k);
let part = [];
for (let i = 0; i < n; i++) {
part.push(input[i]);
if (part.length >= l) {
result.push(part);
part = [];
}
}
if (part.length) {
result.push(part);
}
if (result.length < k) {
result = result.concat(Array.from({ length: k - result.length }, () => []));
}
return result;
};
const CHAR_POINTS = Object.freeze({
// 33 -- 47 are 15 symbols
// 58 -- 64 are 7 symbols again
// 91 -- 96 are 6 symbols again
// 123 -- 126 are 4 symbols again
SYMBOLS: [].concat(
Array.from({ length: 15 }, (_, i) => i + 33),
Array.from({ length: 7 }, (_, i) => i + 58),
Array.from({ length: 6 }, (_, i) => i + 91),
Array.from({ length: 4 }, (_, i) => i + 123)
),
// 48 -- 57 are 10 numbers
NUMBERS: Array.from({ length: 10 }, (_, i) => i + 48),
// 65 -- 90 are 26 upper case letters
UPPER_CASE_LETTERS: Array.from({ length: 26 }, (_, i) => i + 65),
// 97 -- 122 are 26 lower case letters
LOWER_CASE_LETTERS: Array.from({ length: 26 }, (_, i) => i + 97),
});
const randomString = (
len,
{ doNumbers = true, doUpperCaseLetters = true, doLowerCaseLetters = true, doSymbols = false } = {}
) => {
const alphabet = [].concat(
doNumbers ? CHAR_POINTS.NUMBERS : [],
doUpperCaseLetters ? CHAR_POINTS.UPPER_CASE_LETTERS : [],
doLowerCaseLetters ? CHAR_POINTS.LOWER_CASE_LETTERS : [],
doSymbols ? CHAR_POINTS.SYMBOLS : []
);
return alphabet.length === 0
? []
: String.fromCharCode.apply(
null,
Array.from({ length: len }, () => alphabet[Math.floor(Math.random() * alphabet.length)])
);
};
const isObject = (input) => input !== null && typeof input === "object";
const safeUnshift = (baseArray, ...args) => {
baseArray.unshift(...args.filter((arg) => arg !== undefined));
return baseArray;
};
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
const reHasRegExpChar = RegExp(reRegExpChar.source);
/**
* Escapes the `RegExp` special characters "^", "$", "\", ".", "*", "+",
* "?", "(", ")", "[", "]", "{", "}", and "|" in `input`.
*
* @see https://github.com/lodash/lodash/blob/master/escapeRegExp.js
*
* @param {string} input The string to escape.
* @returns {string} Returns the escaped string.
*/
const escapeRegExp = (input) => {
return input && reHasRegExpChar.test(input) ? input.replace(reRegExpChar, "\\$&") : input;
};
const makeOneTime = (cb) => {
const oneTimeCb = async (...args) => {
if (!Object.prototype.hasOwnProperty.call(oneTimeCb, "__result")) {
oneTimeCb.__result = cb(...args);
}
return await oneTimeCb.__result;
};
return oneTimeCb;
};
const resetOneTime = (cb) => Reflect.deleteProperty(cb, "__result");
const parseIntWithFallback = (input, fallback) => {
if (typeof input !== "string") {
return fallback;
}
const result = parseInt(input);
return isNaN(result) ? fallback : result;
};
module.exports = {
isPortFree,
nextFreePort,
isUUID,
isJWT,
isDashedWord,
sleep,
question,
tryReadJsonSync,
writeTextSync,
writeTextAsync,
writeJsonSync,
writeJsonAsync,
deleteFileSync,
tryAccessSync,
tryJsonParse,
tableList,
orderedStringify,
compareFor,
partition,
spawnAsync,
dateDiffInDays,
formatTimestampWithRelativeDays,
formatTimestampsWithRelativeDays,
resolveTenantArg,
balancedSplit,
randomString,
isObject,
safeUnshift,
escapeRegExp,
makeOneTime,
resetOneTime,
parseIntWithFallback,
};