insomnia-importers
Version:
Various data importers for Insomnia
593 lines (520 loc) • 15.9 kB
text/typescript
import SwaggerParser from '@apidevtools/swagger-parser';
import crypto from 'crypto';
import { OpenAPIV2 } from 'openapi-types';
import YAML from 'yaml';
import { Converter, Header, ImportRequest } from '../entities';
import { unthrowableParseJson } from '../utils';
const SUPPORTED_SWAGGER_VERSION = '2.0';
const MIMETYPE_JSON = 'application/json';
const MIMETYPE_URLENCODED = 'application/x-www-form-urlencoded';
const MIMETYPE_MULTIPART = 'multipart/form-data';
const SUPPORTED_MIME_TYPES = [
MIMETYPE_JSON,
MIMETYPE_URLENCODED,
MIMETYPE_MULTIPART,
];
const WORKSPACE_ID = '__WORKSPACE_ID__';
let requestCount = 1;
export const id = 'swagger2';
export const name = 'Swagger 2.0';
export const description = 'Importer for Swagger 2.0 specification (json/yaml)';
/* eslint-disable camelcase -- this file uses camel case too often */
/**
* Return Insomnia folder / request group
*/
const importFolderItem = (parentId: string) => (
item: OpenAPIV2.TagObject,
): ImportRequest => {
const hash = crypto
.createHash('sha1')
.update(item.name)
.digest('hex')
.slice(0, 8);
return {
parentId,
_id: `fld___WORKSPACE_ID__${hash}`,
_type: 'request_group',
name: item.name || 'Folder {requestGroupCount}',
description: item.description || '',
};
};
/**
* Parse string data into swagger 2.0 object (https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#swagger-object)
*/
const parseDocument = (rawData: string) => {
try {
return unthrowableParseJson(rawData) || YAML.parse(rawData);
} catch (err) {
return null;
}
};
/**
* Create request definitions based on swagger document.
*/
const parseEndpoints = (document: OpenAPIV2.Document) => {
const defaultParent = WORKSPACE_ID;
const globalMimeTypes = document.consumes ?? [];
const endpointsSchemas: OpenAPIV2.OperationObject[] = Object.keys(
document.paths,
)
.map((path: keyof OpenAPIV2.PathsObject) => {
const schemasPerMethod: OpenAPIV2.PathItemObject = document.paths[path];
const methods = Object.keys(
schemasPerMethod,
) as (keyof OpenAPIV2.PathItemObject)[];
return methods
.filter(method => method !== 'parameters' && method !== '$ref')
.map(method => ({
...(schemasPerMethod[method] as OpenAPIV2.OperationObject),
path,
method,
}));
})
.flat();
const tags = document.tags || [];
const implicitTags = endpointsSchemas
.map(endpointSchema => endpointSchema.tags)
.flat()
.reduce((distinct, value) => {
// remove duplicates
if (value !== undefined && !distinct.includes(value)) {
distinct.push(value);
}
return distinct;
}, [] as string[])
.filter(tag => !tags.map(tag => tag.name).includes(tag))
.map(name => ({ name, description: '' }));
const folders = [...tags, ...implicitTags].map(
importFolderItem(defaultParent),
);
const folderLookup = folders.reduce(
(accumulator, { _id, name }) => ({
...accumulator,
...(name === undefined ? {} : { [name]: _id }),
}),
{} as { [name: string]: string | undefined },
);
const requests: ImportRequest[] = [];
endpointsSchemas.map(endpointSchema => {
let { tags } = endpointSchema;
if (!tags || tags.length === 0) {
tags = [''];
}
tags.forEach((tag, index) => {
const requestId = endpointSchema.operationId
? `${endpointSchema.operationId}${index > 0 ? index : ''}`
: `__REQUEST_${requestCount++}__`;
const parentId = folderLookup[tag] || defaultParent;
requests.push(
importRequest(
document,
endpointSchema,
globalMimeTypes,
requestId,
parentId,
),
);
});
});
return [...folders, ...requests];
};
const importRequest = (
document: OpenAPIV2.Document,
endpointSchema: OpenAPIV2.OperationObject<{ method?: string; path?: string }>,
globalMimeTypes: OpenAPIV2.MimeTypes,
requestId: string,
parentId: string,
): ImportRequest => {
const name =
endpointSchema.summary || `${endpointSchema.method} ${endpointSchema.path}`;
const body = prepareBody(document, endpointSchema, globalMimeTypes);
let headers = prepareHeaders(endpointSchema);
const noContentTypeHeader = !headers?.find(
header => header.name === 'Content-Type',
);
if (body.mimeType && noContentTypeHeader) {
headers = [
{
name: 'Content-Type',
disabled: false,
value: body.mimeType,
},
...headers,
];
}
const request: ImportRequest = {
_type: 'request',
_id: requestId,
parentId: parentId,
name,
method: endpointSchema.method?.toUpperCase(),
url: `{{ base_url }}${pathWithParamsAsVariables(endpointSchema.path)}`,
body,
description: endpointSchema.description || '',
headers,
parameters: prepareQueryParams(endpointSchema),
};
return setupAuthentication(
document.securityDefinitions,
endpointSchema,
request,
);
};
/**
* Populate Insomnia request with authentication
*/
const setupAuthentication = (
securityDefinitions: OpenAPIV2.SecurityDefinitionsObject | undefined,
endpointSchema: OpenAPIV2.OperationObject | undefined,
request: ImportRequest,
) => {
if (!securityDefinitions) {
return request;
}
if (!endpointSchema?.security || endpointSchema.security.length === 0) {
return request;
}
const usedDefinitions = endpointSchema.security.reduce(
(collect, requirement) => [
...collect,
...(Object.keys(requirement) as string[]),
],
[] as string[],
);
const scopes = endpointSchema.security.reduce((accumulator, security) => {
for (const defname of Object.keys(security)) {
if (security[defname].length === 0) {
continue;
}
return accumulator.concat(security[defname]);
}
return accumulator;
}, [] as string[]);
for (const usedDefinition of usedDefinitions) {
const securityScheme = securityDefinitions[usedDefinition];
if (securityScheme.type === 'basic') {
request.authentication = {
type: 'basic',
disabled: false,
password: '{{ password }}',
username: '{{ username }}',
};
}
if (securityScheme.type === 'apiKey') {
if (securityScheme.in === 'header') {
request.headers?.push({
name: securityScheme.name,
disabled: false,
value: '{{ api_key }}',
});
}
if (securityScheme.in === 'query') {
request.parameters?.push({
name: securityScheme.name,
disabled: false,
value: '{{ api_key }}',
});
}
}
if (securityScheme.type === 'oauth2') {
if (securityScheme.flow === 'implicit') {
request.authentication = {
type: 'oauth2',
grantType: 'authorization_code',
disabled: false,
authorizationUrl: securityScheme.authorizationUrl,
clientId: '{{ client_id }}',
scope: scopes.join(' '),
};
}
if (securityScheme.flow === 'password') {
request.authentication = {
type: 'oauth2',
grantType: 'password',
disabled: false,
accessTokenUrl: securityScheme.tokenUrl,
username: '{{ username }}',
password: '{{ password }}',
clientId: '{{ client_id }}',
clientSecret: '{{ client_secret }}',
scope: scopes.join(' '),
};
}
if (securityScheme.flow === 'application') {
request.authentication = {
type: 'oauth2',
grantType: 'client_credentials',
disabled: false,
accessTokenUrl: securityScheme.tokenUrl,
clientId: '{{ client_id }}',
clientSecret: '{{ client_secret }}',
scope: scopes.join(' '),
};
}
if (securityScheme.flow === 'accessCode') {
request.authentication = {
type: 'oauth2',
grantType: 'authorization_code',
disabled: false,
accessTokenUrl: securityScheme.tokenUrl,
authorizationUrl: securityScheme.authorizationUrl,
clientId: '{{ client_id }}',
clientSecret: '{{ client_secret }}',
scope: scopes.join(' '),
};
}
}
}
return request;
};
/**
* Return path with parameters replaced by insomnia variables
*
* I.e. "/foo/:bar" => "/foo/{{ bar }}"
*/
const pathWithParamsAsVariables = (path?: string) => {
return path?.replace(/{([^}]+)}/g, '{{ $1 }}');
};
/**
* Imports insomnia definitions of query parameters.
*/
const prepareQueryParams = (endpointSchema: OpenAPIV2.OperationObject) => {
return (
convertParameters(
((endpointSchema.parameters as unknown) as OpenAPIV2.Parameter[])?.filter(
parameter => parameter.in === 'query',
),
) || []
);
};
/**
* Imports insomnia definitions of header parameters.
*/
const prepareHeaders = (
endpointSchema: OpenAPIV2.OperationObject,
): Header[] => {
return (
(convertParameters(
((endpointSchema.parameters as unknown) as OpenAPIV2.Parameter[])?.filter(
parameter => parameter.in === 'header',
),
) as Header[]) || []
);
};
const resolve$ref = (document: OpenAPIV2.Document, $ref: string) => {
const [, ...parts] = $ref.split('/') as (keyof OpenAPIV2.Document)[];
return parts.reduce(
(accumulator, path) => accumulator[path] as OpenAPIV2.Document,
document,
);
};
/**
* Imports insomnia request body definitions, including data mock (if available)
*
* If multiple types are available, the one for which an example can be generated will be selected first (i.e. application/json)
*/
const prepareBody = (
document: OpenAPIV2.Document,
endpointSchema: OpenAPIV2.OperationObject,
globalMimeTypes: OpenAPIV2.MimeTypes,
) => {
const mimeTypes = endpointSchema.consumes || globalMimeTypes || [];
const supportedMimeType = SUPPORTED_MIME_TYPES.find(mimeType =>
mimeTypes.includes(mimeType),
);
if (supportedMimeType === MIMETYPE_JSON) {
const parameters = endpointSchema.parameters || [];
const bodyParameter = parameters.find(
parameter => (parameter as OpenAPIV2.Parameter).in === 'body',
);
if (!bodyParameter) {
return {};
}
const type = (bodyParameter as OpenAPIV2.ParameterObject).type || 'object';
const example = generateParameterExample(type);
let text;
if (type === 'object') {
const { schema } = bodyParameter as OpenAPIV2.ParameterObject;
if (schema.$ref) {
const definition = resolve$ref(document, schema.$ref);
text = JSON.stringify(example(definition), null, 2);
} else {
text = JSON.stringify(example(schema), null, 2);
}
} else {
text = JSON.stringify(example, null, 2);
}
return {
mimeType: supportedMimeType,
text,
};
}
if (
supportedMimeType === MIMETYPE_URLENCODED ||
supportedMimeType === MIMETYPE_MULTIPART
) {
const parameters = endpointSchema.parameters || [];
const formDataParameters = ((parameters as unknown) as OpenAPIV2.Parameter[]).filter(
parameter => parameter.in === 'formData',
);
if (formDataParameters.length === 0) {
return {};
}
return {
mimeType: supportedMimeType,
params: convertParameters(formDataParameters),
};
}
if (mimeTypes && mimeTypes.length) {
return {
mimeType: mimeTypes[0] || undefined,
};
} else {
return {};
}
};
type TypeExample =
| 'string'
| 'string_email'
| 'string_date-time'
| 'string_byte'
| 'number'
| 'number_float'
| 'number_double'
| 'integer'
| 'boolean'
| 'object'
| 'array';
/**
* Generate example value of parameter based on it's schema.
* Returns example / default value of the parameter, if any of those are defined. If not, returns value based on parameter type.
*/
const generateParameterExample = (
parameter: OpenAPIV2.Parameter | TypeExample,
ancestors: OpenAPIV2.Parameter[] = [],
) => {
const typeExamples: {
[kind in TypeExample]: (
parameter: OpenAPIV2.Parameter
) => null | string | boolean | number | Record<string, unknown>;
} = {
string: () => 'string',
string_email: () => 'user@example.com',
'string_date-time': () => new Date().toISOString(),
string_byte: () => 'ZXhhbXBsZQ==',
number: () => 0,
number_float: () => 0.0,
number_double: () => 0.0,
integer: () => 0,
boolean: () => true,
object: (parameter: OpenAPIV2.Parameter) => {
if (ancestors.indexOf(parameter) !== -1) {
return {};
}
const example = {};
const { properties } = parameter;
if (properties) {
ancestors.push(parameter);
Object.keys(properties).forEach(propertyName => {
// @ts-expect-error there's no way, so far as I'm aware, for TypeScript to know what's actually going on here.
example[propertyName] = generateParameterExample(
properties[propertyName],
ancestors,
);
});
ancestors.pop();
}
return example;
},
array: ({ items, collectionFormat }: OpenAPIV2.Parameter) => {
const value = generateParameterExample(items, ancestors);
if (collectionFormat === 'csv') {
return value;
} else {
return [value];
}
},
};
if (typeof parameter === 'string') {
return typeExamples[parameter];
}
if (typeof parameter === 'object') {
const { type, format, example, default: defaultValue } = parameter;
if (example) {
return example;
}
if (defaultValue) {
return defaultValue;
}
const factory =
typeExamples[`${type}_${format}` as TypeExample] ||
typeExamples[type as TypeExample];
if (!factory) {
return null;
}
return factory(parameter);
}
};
/**
* Converts swagger schema of parameters into insomnia one.
*/
const convertParameters = (parameters?: OpenAPIV2.Parameter[]) => {
return parameters?.map(parameter => {
const { required, name, type } = parameter;
if (type === 'file') {
return {
name,
disabled: required !== true,
type: 'file',
};
}
return {
name,
disabled: required !== true,
value: `${generateParameterExample(parameter) as string}`,
};
});
};
export const convert: Converter = async rawData => {
requestCount = 1; // Validate
let api = await parseDocument(rawData);
if (!api || api.swagger !== SUPPORTED_SWAGGER_VERSION) {
return null;
} // Await here so we catch any exceptions
try {
api = await SwaggerParser.validate(api);
} catch (err) {
// We already know it's a Swagger doc so we will try to import it anyway instead
// of bailing out here.
console.log('[swagger] Import file validation failed', err);
} // Import
const workspace: ImportRequest = {
_type: 'workspace',
_id: WORKSPACE_ID,
parentId: null,
name: `${api.info.title} ${api.info.version}`,
description: api.info.description || '', // scope is not set because it could be imported for design OR to generate requests
};
const baseEnv: ImportRequest = {
_type: 'environment',
_id: '__BASE_ENVIRONMENT_ID__',
parentId: WORKSPACE_ID,
name: 'Base environment',
data: {
base_url: '{{ scheme }}://{{ host }}{{ base_path }}',
},
};
const swaggerEnv: ImportRequest = {
_type: 'environment',
_id: 'env___BASE_ENVIRONMENT_ID___sub',
parentId: baseEnv._id,
name: 'Swagger env',
data: {
base_path: api.basePath || '',
scheme: (api.schemes || ['http'])[0],
host: api.host || '',
},
};
const endpoints = parseEndpoints(api);
return [workspace, baseEnv, swaggerEnv, ...endpoints];
};