@sap/cli-core
Version:
Command-Line Interface (CLI) Core Module
315 lines (314 loc) • 11.9 kB
JavaScript
import { kebabCase, get } from "lodash-es";
import { CLI_NAME, OPTION_OUTPUT, OPTION_NO_PRETTY, X_CSRF_TOKEN, } from "../../constants.js";
import { get as loggerGet } from "../../logger/index.js";
import { parseVersion } from "../../utils/utils.js";
import { get as getConfig } from "../../config/index.js";
import { createForceHandler, createInputHandler } from "../handler/index.js";
import { ResultHandlerFactory } from "../../result/ResultHandlerFactory.js";
const getLogger = () => loggerGet("commands.openAPI.utils");
export const updatePath = (doc, path) => {
const version = parseVersion(doc.info.version);
if (version.major < 2023) {
return `/dwaas-core${path}`;
}
return path;
};
export const isPathParameter = (segment) => /^{.*}$/.test(segment);
function getParameterName(parameter) {
return `--${kebabCase(parameter.slice(1, -1))}`;
}
export const writeParameter = (parameter, value) => `${getParameterName(parameter)} ${value}`;
export const getSegments = (value) => {
const segments = value.split("/");
return segments.filter((segment) => {
const s = segment.trim();
return s !== "" && s !== "marketplace";
});
};
const handleParameter = (params, pathSegment, locationSegment) => {
if (isPathParameter(pathSegment)) {
return params
? `${params} ${writeParameter(pathSegment, locationSegment)}`
: writeParameter(pathSegment, locationSegment);
}
return params;
};
const getPathItem = (doc, operation, path) => {
const pathItem = doc.paths[path];
if (pathItem.get) {
return pathItem;
}
throw new Error(`path ${operation["x-read-path"]} does not support GET operation`);
};
function handleOptions(options, option, value) {
if (isPathParameter(option)) {
return { ...options, [getParameterName(option)]: value };
}
return options;
}
const outputReadCommand = (doc, operation, response, path) => {
const locationSegments = getSegments(response.headers.location);
const pathSegments = getSegments(path);
let params = "";
let options = {};
for (let i = 0; i < pathSegments.length; i++) {
params = handleParameter(params, pathSegments[i], locationSegments[i]);
options = handleOptions(options, pathSegments[i], locationSegments[i]);
}
const pathItem = getPathItem(doc, operation, path);
ResultHandlerFactory.get().setResult({
command: pathItem.get.operationId,
options,
});
const { output } = getLogger();
output(`Use ${getConfig()[CLI_NAME]} ${pathItem.get.operationId} ${params} to retrieve the entity you just created`);
};
const readPathHandlerFunction = (doc, operation, path) => async (response) => {
const { debug } = getLogger();
if (!response.headers.location) {
debug("response contains no location header");
}
else {
outputReadCommand(doc, operation, response, path);
}
};
const dummyFunction = async () => {
// This is intentional
};
export const handleReadPathHandler = (doc, operation) => {
const path = operation["x-read-path"];
if (path) {
return readPathHandlerFunction(doc, operation, path);
}
return dummyFunction;
};
export const getDescriptionForCommand = (command, operation) => command.description || operation.description || operation.summary || "";
const getReferenceFromDocument = (reference, document) => {
const { trace } = getLogger();
const segments = reference.$ref.split("/");
if (segments.length < 4 || // #(1)/components(2)/(parameters|schemas|responses|...)(3)/<name>(4)
segments[0] !== "#" ||
segments[1] !== "components") {
throw new Error(`invalid reference ${reference.$ref}`);
}
const path = segments
.slice(1)
.reduce((prev, curr) => (prev !== "" ? `${prev}.${curr}` : curr), "");
trace("reading reference %s, path %s", reference.$ref, path);
return get(document, path);
};
export const getSchema = (obj, doc) => {
if (obj.$ref) {
return getReferenceFromDocument(obj, doc);
}
return obj;
};
const getKeysFromSchema = (schema, doc, key = "", required = false) => {
const { trace } = getLogger();
trace(`retrieving keys for key ${key} from schema ${JSON.stringify(schema)}`);
if (schema.$ref) {
// eslint-disable-next-line no-param-reassign
schema = getSchema(schema, doc);
}
if (schema.type === "object" && schema.properties) {
let keys = [];
Object.keys(schema.properties).forEach((property) => {
keys = keys.concat(getKeysFromSchema(schema.properties[property], doc, key ? `${key}.${property}` : property, schema.required?.includes(property)));
});
return keys;
}
return [{ key, schema, required }];
};
const flattenType = (type, doc) => {
if (type.oneOf) {
let result = [];
type.oneOf.forEach((t) => {
result = result.concat(flattenType(t, doc));
});
return result;
}
if (type.$ref) {
const schema = getSchema(type, doc);
return flattenType({
...schema,
allowEmptyValue: type.allowEmptyValue,
required: type.required,
}, doc);
}
if (type.type === "object") {
throw new Error("type: Object not supported");
}
return [type];
};
export const addOptionToCommand = (option, options) => {
const { trace } = getLogger();
options.push(option);
trace("added option %s", JSON.stringify(option));
};
const checkTypes = (types) => {
for (const t of types) {
if (types.length > 1 && t.enum?.length === 1) {
throw new Error("constants not allowed for multiple types");
}
}
};
const initParams = (types) => {
const params = { booleanAvailable: false, choices: [] };
for (const t of types) {
if (t.type === "boolean") {
params.booleanAvailable = true;
}
else {
t.enum?.forEach((e) => params.choices.push(e.toString()));
}
params.default = t.default;
}
return params;
};
const handleOptionName = (name, params, description) => {
let optionName = kebabCase(name);
let def;
let newDescription = description;
if (params.default !== undefined) {
if (params.booleanAvailable && params.default === true) {
// see npmjs.com/package/commander#other-option-types-negatable-boolean-and-booleanvalue
optionName = `no-${optionName}`;
if (description) {
newDescription = `do not ${description}`;
}
}
else {
def = params.default.toString();
}
}
return {
optionName,
def,
description: newDescription,
};
};
const buildOption = (commandName, longName, types, params, description, def) => {
const option = {
longName,
required: !types[0].allowEmptyValue && types[0].required,
description: description || "",
default: def === "false" ? undefined : def,
args: params.booleanAvailable && types.length === 1
? undefined
: [
{
name: longName,
optional: params.booleanAvailable && types.length > 1,
},
],
choices: params.choices.length > 0 ? params.choices : undefined,
};
if (option.required) {
const desc = option.description ? ` (${option.description})` : "";
option.prompts = {
message: `Provide a value for option ${option.longName}${desc}:`,
type: params.choices.length > 0 ? "select" : "text",
};
}
return option;
};
const handleOption = (commandName, name, params, types, options, description) => {
const { optionName, def, description: newDescription, } = handleOptionName(name, params, description);
const option = buildOption(commandName, optionName, types, params, newDescription, def);
addOptionToCommand(option, options);
};
const requiresUserInput = (name) => name !== X_CSRF_TOKEN;
export const buildOptionFromType = (commandName, doc, parameterIn, name, type, parameterMappings, options, description) => {
try {
const types = flattenType(type, doc);
checkTypes(types);
if (types[0].enum?.length === 1) {
parameterMappings.push({
in: parameterIn,
name,
source: { type: "value", value: types[0].enum[0] },
});
}
else {
const params = initParams(types);
if (requiresUserInput(name)) {
handleOption(commandName, name, params, types, options, description);
}
parameterMappings.push({
in: parameterIn,
name,
source: {
type: "option",
name: kebabCase(name),
dataType: params.booleanAvailable ? "boolean" : "string", // we don't care whether it's a string or number, it simply must not be boolean in this case
},
});
}
}
catch (err) {
if (!type.required) {
const { trace } = getLogger();
trace(`option ${name} silently ignored since not required`, err.stack);
}
else {
throw err;
}
}
};
export const handleRequestBody = (operation, handler, doc, parameterMappings, command) => {
if (operation["x-requestbody-fileonly"]) {
handler.push(createInputHandler());
}
else if (operation.requestBody) {
const schema = getSchema(Object.values(operation.requestBody.content)[0].schema, doc);
const keys = getKeysFromSchema(schema, doc);
keys.forEach((key) => {
if (key.schema.oneOf) {
throw new Error("invalid request body parameter resolution, oneOf not supported");
}
buildOptionFromType(command.command, doc, "body", key.key, {
...key.schema,
required: key.required,
}, parameterMappings, command.options, key.schema.description);
});
}
};
export const handleForceOption = (operation, handler) => {
if (operation["x-user-to-confirm"]) {
handler.push(createForceHandler(operation["x-user-to-confirm"]));
}
};
export const handleResponses = (operation, command) => {
if (operation.responses?.[200]) {
addOptionToCommand(OPTION_OUTPUT, command.options);
}
addOptionToCommand(OPTION_NO_PRETTY, command.options);
};
export const handleParameters = (operation, doc, parameterMappings, command, topLevelParameters = []) => {
const { error } = getLogger();
(operation.parameters || []).concat(topLevelParameters).forEach((p) => {
try {
const parameter = getSchema(p, doc);
buildOptionFromType(command.command, doc, parameter.in, parameter.name, {
...parameter.schema,
allowEmptyValue: parameter.allowEmptyValue,
required: parameter.required,
}, parameterMappings, command.options, parameter.description);
}
catch (err) {
error(`cannot add option ${p} for operation ${operation.operationId}`, err.stack);
}
});
};
export const handleDeprecationNotice = (operation, command) => {
if (operation.deprecated && command.type === "command") {
// eslint-disable-next-line no-param-reassign
command.deprecationInfo = {
deprecated: true,
deprecatedWithWave: operation["x-deprecated-with-wave"],
decommissionedAfterWave: operation["x-decommissioned-after-wave"],
newCommand: operation["x-deprecation-new-command"],
sapHelpUrl: operation["x-deprecation-sap-help-url"],
};
}
};