UNPKG

insomnia-importers

Version:

Various data importers for Insomnia

593 lines (520 loc) 15.9 kB
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]; };