@rxap/nest-open-api
Version:
This package provides tools and utilities for integrating OpenAPI specifications into NestJS applications. It includes features for handling upstream API requests, managing server configurations, and generating OpenAPI documentation. It also offers interc
348 lines (347 loc) • 16.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenApiOperationCommand = void 0;
exports.IsReferenceObject = IsReferenceObject;
const tslib_1 = require("tslib");
const axios_1 = require("@nestjs/axios");
const common_1 = require("@nestjs/common");
const utilities_1 = require("@rxap/utilities");
const rxjs_1 = require("rxjs");
const http_params_1 = require("./http.params");
const open_api_config_service_1 = require("./open-api-config.service");
const open_api_operation_command_exception_1 = require("./open-api-operation-command-exception");
const tokens_1 = require("./tokens");
function IsReferenceObject(obj) {
return !!obj && '$ref' in obj;
}
let OpenApiOperationCommand = class OpenApiOperationCommand {
constructor(http, openApiConfigService, logger) {
this.http = http;
this.openApiConfigService = openApiConfigService;
this.logger = logger;
/**
* true (default) - after the requests completes the result is printed to the console
*/
this.log = true;
/**
* Request timeout in ms (default: 60000ms)
*/
this.timeout = 60000;
const metadata = this.getOperationFromMetaData();
this.operation = typeof metadata.operation === 'string' ? JSON.parse(metadata.operation) : metadata.operation;
this.serverId = metadata.serverId;
}
stringifyData(data) {
if (data instanceof FormData) {
return '<form-data>';
}
else {
return JSON.stringify(data, (key, value) => {
if (Array.isArray(value) && value.length > 3) {
return value.slice(0, 3).concat([(value.length - 3) + ' more items ...']);
}
return value;
});
}
}
async execute(args = {}) {
if (!this.operation) {
throw new Error('FATAL: The constructor of the OpenApiOperationCommand should be called');
}
let config;
try {
config = await this.buildRequestConfig(args);
}
catch (e) {
throw new common_1.InternalServerErrorException(`Could not build command request config: ${e.message}`, e.stack);
}
const requestId = (function randomNum() {
return Math.floor(Math.random() * 9999999).toFixed(0).padStart(7, '0');
})();
try {
if (this.log !== false) {
this.logger.debug(`[${requestId}] ${config.method?.toUpperCase()} ${config.url}${http_params_1.HttpParams.ToHttpQueryString(config.params)}`, this.constructor.name);
if (config.data) {
this.logger.verbose(`[${requestId}] REQUEST ${this.stringifyData(config.data)}`, this.constructor.name);
}
}
const now = Date.now();
const result = await (0, rxjs_1.firstValueFrom)(this.http.request(config).pipe((0, rxjs_1.tap)({
next: (response) => {
if (this.log !== false) {
if (response.data) {
this.logger.verbose(`[${requestId}] RESPONSE ${response.status} ${this.stringifyData(response.data)} +${Date.now() -
now}ms`, this.constructor.name);
}
else {
this.logger.verbose('[${id}] RESPONSE <empty>', this.constructor.name);
}
}
},
error: (error) => {
if (error.isAxiosError) {
if (error.config) {
this.logger.log(`[${requestId}] ${error.config.method?.toUpperCase()} ${error.status ??
error.response?.status} ${error.config.url}${http_params_1.HttpParams.ToHttpQueryString(error.config.params)} +${Date.now() -
now}ms`, this.constructor.name);
if (this.log !== false) {
if (error.config.data) {
this.logger.verbose(`[${requestId}] REQUEST ${this.stringifyData(error.config.data)}`, this.constructor.name);
}
if (error.response) {
if (error.response.data) {
this.logger.verbose(`[${requestId}] RESPONSE ${this.stringifyData(error.response.data)}`, this.constructor.name);
}
}
else {
console.log(error.cause);
this.logger.error(`[${requestId}] Internal Axios Error without response object: ${error.message}`, this.constructor.name);
}
}
}
else {
this.logger.error(`[${requestId}] AxiosError without config object: ${error.message}`, this.constructor.name);
}
}
else {
this.logger.error(`[${requestId}] NonAxiosError in command execution: ${error.message}`, this.constructor.name);
}
},
})));
return result.data;
}
catch (e) {
if (e.isAxiosError) {
if (e.response) {
const message = e.response.data?.message ?? e.message;
throw new open_api_operation_command_exception_1.OpenApiOperationCommandException(this.serverId, e.response, config, this.operation, requestId, message);
}
this.logger.verbose(`[${requestId}] Http request throws Axios Error without response object`, e.message, this.constructor.name);
}
this.logger.debug(`[${requestId}] Http request throws non Axios Error`, e.message, e.constructor.name, this.constructor.name);
throw new common_1.InternalServerErrorException(e.message);
}
}
buildUrl(args) {
const path = this.buildPathParams(this.operation.path, args.parameters);
return this.openApiConfigService.buildUrl(path, this.serverId);
}
async buildRequestConfig(args) {
let config = {};
config.url = this.buildUrl(args);
config.method = this.operation.method;
config.headers = this.buildHeaders(args.parameters);
const [data, contentType] = this.buildRequestBody(args.body);
if (data !== undefined) {
config.data = data;
if (contentType !== undefined) {
config.headers ??= {};
config.headers['Content-Type'] = contentType;
}
}
config.params = this.buildRequestParams(args.parameters);
config.paramsSerializer = {
indexes: null,
};
config.responseType = this.getResponseType();
config.timeout = this.timeout;
if (!args.skipInterceptors) {
const interceptors = this.openApiConfigService.getInterceptors(this.serverId);
for (const interceptor of interceptors) {
config = await interceptor.intercept(config);
}
}
return config;
}
buildPathParams(path, parameters) {
const operationParameters = (0, utilities_1.coerceArray)(this.operation.parameters);
if (!operationParameters.some(p => p.in === 'path')) {
return path;
}
if (!parameters) {
throw new common_1.InternalServerErrorException(`Path parameters for operation '${this.operation.operationId}' are not defined`);
}
const pathParams = {};
for (const parameter of operationParameters.filter(p => p.in === 'path')) {
if (parameters[parameter.name] !== undefined && parameters[parameter.name] !== null) {
pathParams[parameter.name] = encodeURIComponent(parameters[parameter.name]);
}
}
const matches = path.match(/\{[^}]+}/g);
if (matches) {
for (const match of matches) {
const param = match.substr(1, match.length - 2);
if (!pathParams[param]) {
throw new common_1.InternalServerErrorException(`Path params for remote method '${this.operation.operationId}' has not a defined value for '${param}'`);
}
path = path.replace(match, pathParams[param]);
}
}
else {
throw new common_1.InternalServerErrorException(`The path of the operation '${this.operation.operationId}' should have parameters`);
}
return path;
}
buildHeaders(parameters) {
const operationParameters = (0, utilities_1.coerceArray)(this.operation.parameters);
const header = {};
for (const parameter of operationParameters.filter(p => p.in === 'header')) {
if (parameters && parameters[parameter.name] !== undefined && parameters[parameter.name] !== null) {
header[parameter.name] = parameters[parameter.name];
}
else if (parameter.required) {
throw new common_1.InternalServerErrorException(`The header '${parameter.name}' is required for the operation '${this.operation.operationId}'`);
}
}
function IsRequestBodyObject(obj) {
return obj && obj['content'];
}
if (IsRequestBodyObject(this.operation.requestBody) && this.operation.requestBody.content['application/json']) {
header['Content-Type'] ??= 'application/json';
}
return header;
}
buildRequestParams(parameters) {
const operationParameters = (0, utilities_1.coerceArray)(this.operation.parameters);
if (!operationParameters.some(p => p.in === 'query')) {
return {};
}
const params = {};
for (const parameter of operationParameters.filter(p => p.in === 'query')) {
if (parameters && parameters[parameter.name] !== undefined && parameters[parameter.name] !== null) {
if (Array.isArray(parameters[parameter.name])) {
if (parameters[parameter.name].length) {
params[parameter.name] =
parameters[parameter.name].map((item) => typeof item === 'object' ? JSON.stringify(item) : item);
}
}
else {
params[parameter.name] = typeof parameters[parameter.name] === 'object' ?
JSON.stringify(parameters[parameter.name]) :
parameters[parameter.name];
}
}
if (parameter.required && params[parameter.name] === undefined) {
throw new common_1.InternalServerErrorException(`The query '${parameter.name}' is required for the operation '${this.operation.operationId}'`);
}
}
return params;
}
buildRequestBody(requestBody) {
const accept = [];
if (this.operation.requestBody && !IsReferenceObject(this.operation.requestBody)) {
if (this.operation.requestBody.content) {
for (const contentType of Object.keys(this.operation.requestBody.content)) {
accept.push(contentType);
}
}
}
if (!accept.length) {
if (requestBody) {
this.logger.verbose('No content type found for the request body, BUT a requestBody parameter is passed to the command! Omitting the body!', this.constructor.name);
}
return [undefined, undefined];
}
if (accept.length > 1) {
this.logger.warn('Multiple content types found for the request body! Using the first one!', this.constructor.name);
}
const contentType = accept[0];
switch (contentType) {
case 'application/json':
(0, utilities_1.assertsObject)(requestBody);
return [requestBody, contentType];
case 'application/x-www-form-urlencoded':
(0, utilities_1.assertsObject)(requestBody);
// eslint-disable-next-line no-case-declarations
const params = new http_params_1.HttpParams();
for (const [key, value] of Object.entries(requestBody)) {
params.set(key, value);
}
return [params.toString(), contentType];
case 'multipart/form-data':
(0, utilities_1.assertsObject)(requestBody);
// eslint-disable-next-line no-case-declarations
const formData = new FormData();
// eslint-disable-next-line no-case-declarations
const append = (key, value) => {
const filename = typeof value === 'object' && 'filename' in value ? value.filename : undefined;
if (typeof value === 'string') {
formData.append(key, value);
}
else if (typeof value === 'boolean') {
formData.append(key, value.toString());
}
else if (typeof value === 'number') {
formData.append(key, value.toString());
}
else if (value instanceof Blob) {
formData.append(key, value, filename);
}
else if (value instanceof File) {
formData.append(key, value, filename);
}
else if (value instanceof Buffer) {
formData.append(key, new Blob([value]), filename);
}
else if (value instanceof ArrayBuffer) {
formData.append(key, new Blob([value]), filename);
}
else if (value instanceof Uint32Array) {
formData.append(key, new Blob([value]), filename);
}
else if (value instanceof Uint8Array) {
formData.append(key, new Blob([value]), filename);
}
else if (value instanceof Uint16Array) {
formData.append(key, new Blob([value]), filename);
}
else {
throw new common_1.InternalServerErrorException(`Unsupported value type for multipart/form-data: (${key}) ${typeof value}`);
}
};
// Iterate through the JSON object and append each field to FormData
for (const [key, value] of Object.entries(requestBody)) {
if (Array.isArray(value)) {
value.forEach((v) => {
append(key, v);
});
}
else {
append(key, value);
}
}
return [formData, contentType];
default:
return [requestBody, contentType];
}
}
getResponseType() {
const response = (this.operation.responses['200'] ??
this.operation.responses['201']);
if (response) {
if (response['content']) {
if (response['content']['application/json']) {
return 'json';
}
}
return 'text';
}
return 'json';
}
getOperationFromMetaData() {
if (!Reflect.hasMetadata(tokens_1.OPERATION_COMMAND_META_DATA_KEY, this.constructor)) {
throw new common_1.InternalServerErrorException(`Ensure the is used on the class '${this.constructor.name}'`);
}
return Reflect.getMetadata(tokens_1.OPERATION_COMMAND_META_DATA_KEY, this.constructor);
}
};
exports.OpenApiOperationCommand = OpenApiOperationCommand;
exports.OpenApiOperationCommand = OpenApiOperationCommand = tslib_1.__decorate([
(0, common_1.Injectable)(),
tslib_1.__param(0, (0, common_1.Inject)(axios_1.HttpService)),
tslib_1.__param(1, (0, common_1.Inject)(open_api_config_service_1.OpenApiConfigService)),
tslib_1.__param(2, (0, common_1.Inject)(common_1.Logger)),
tslib_1.__metadata("design:paramtypes", [axios_1.HttpService,
open_api_config_service_1.OpenApiConfigService,
common_1.Logger])
], OpenApiOperationCommand);