UNPKG

insomnia-importers

Version:

Various data importers for Insomnia

626 lines 23.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.convert = exports.description = exports.name = exports.id = void 0; const swagger_parser_1 = __importDefault(require("@apidevtools/swagger-parser")); const change_case_1 = require("change-case"); const crypto_1 = __importDefault(require("crypto")); const ramda_adjunct_1 = require("ramda-adjunct"); const url_1 = require("url"); const yaml_1 = __importDefault(require("yaml")); const utils_1 = require("../utils"); exports.id = 'openapi3'; exports.name = 'OpenAPI 3.0'; exports.description = 'Importer for OpenAPI 3.0 specification (json/yaml)'; /* eslint-disable camelcase -- some camecase is required by the parsing of the spec itself */ const SUPPORTED_OPENAPI_VERSION = /^3\.\d+\.\d+$/; // 3.x.x const MIMETYPE_JSON = 'application/json'; const MIMETYPE_LITERALLY_ANYTHING = '*/*'; const SUPPORTED_MIME_TYPES = [MIMETYPE_JSON, MIMETYPE_LITERALLY_ANYTHING]; const WORKSPACE_ID = '__WORKSPACE_ID__'; const SECURITY_TYPE = { HTTP: 'http', API_KEY: 'apiKey', OAUTH: 'oauth2', OPEN_ID: 'openIdConnect', }; const HTTP_AUTH_SCHEME = { BASIC: 'basic', BEARER: 'bearer', }; const OAUTH_FLOWS = { AUTHORIZATION_CODE: 'authorizationCode', CLIENT_CREDENTIALS: 'clientCredentials', IMPLICIT: 'implicit', PASSWORD: 'password', }; const SUPPORTED_SECURITY_TYPES = [ SECURITY_TYPE.HTTP, SECURITY_TYPE.API_KEY, SECURITY_TYPE.OAUTH, ]; const SUPPORTED_HTTP_AUTH_SCHEMES = [ HTTP_AUTH_SCHEME.BASIC, HTTP_AUTH_SCHEME.BEARER, ]; const VARIABLE_SEARCH_VALUE = /{([^}]+)}/g; let requestCounts = {}; /** * Gets a server to use as the default * Either the first server defined in the specification, or an example if none are specified * * @returns the resolved server URL */ const getDefaultServerUrl = (api) => { const exampleServer = 'http://example.com/'; const servers = api.servers || []; const firstServer = servers[0]; const foundServer = firstServer && firstServer.url; if (!foundServer) { return (0, url_1.parse)(exampleServer); } const url = resolveVariables(firstServer); return (0, url_1.parse)(url); }; /** * Resolve default variables for a server url * * @returns the resolved url */ const resolveVariables = (server) => { let resolvedUrl = server.url; const variables = server.variables || {}; let shouldContinue = true; do { // Regexp contain the global flag (g), meaning we must execute our regex on the original string. // https://stackoverflow.com/a/27753327 const [replace, name] = VARIABLE_SEARCH_VALUE.exec(server.url) || []; const variable = variables && variables[name]; const value = variable && variable.default; if (name && !value) { // We found a variable in the url (name) but we have no default to replace it with (value) throw new Error(`Server variable "${name}" missing default value`); } shouldContinue = !!name; resolvedUrl = replace ? resolvedUrl.replace(replace, value) : resolvedUrl; } while (shouldContinue); return resolvedUrl; }; /** * Parse string data into openapi 3 object (https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#oasObject) */ const parseDocument = (rawData) => { try { return ((0, utils_1.unthrowableParseJson)(rawData) || yaml_1.default.parse(rawData)); } catch (err) { return null; } }; /** * Checks if the given property name is an open-api extension * @param property The property name */ const isSpecExtension = (property) => { return property.indexOf('x-') === 0; }; /** * Create request definitions based on openapi document. */ const parseEndpoints = (document) => { var _a, _b; if (!document) { return []; } const rootSecurity = document.security; const securitySchemes = (_a = document.components) === null || _a === void 0 ? void 0 : _a.securitySchemes; const defaultParent = WORKSPACE_ID; const endpointsSchemas = Object.keys(document.paths) .map(path => { const schemasPerMethod = document.paths[path]; if (!schemasPerMethod) { return []; } const methods = Object.entries(schemasPerMethod) // Only keep entries that are plain objects and not spec extensions .filter(([key, value]) => (0, ramda_adjunct_1.isPlainObject)(value) && !isSpecExtension(key)); return methods.map(([method]) => ({ ...(schemasPerMethod[method]), path, method, })); }) .flat(); const folders = ((_b = document.tags) === null || _b === void 0 ? void 0 : _b.map(importFolderItem(defaultParent))) || []; const folderLookup = folders.reduce((accumulator, folder) => ({ ...accumulator, ...(folder.name ? { [folder.name]: folder._id } : {}), }), {}); const requests = []; endpointsSchemas.forEach(endpointSchema => { let { tags } = endpointSchema; if (!tags || tags.length === 0) { tags = ['']; } tags.forEach(tag => { const parentId = folderLookup[tag] || defaultParent; const resolvedSecurity = endpointSchema.security || rootSecurity; requests.push(importRequest(endpointSchema, parentId, resolvedSecurity, securitySchemes)); }); }); return [ ...folders, ...requests, ]; }; /** * Return Insomnia folder / request group */ const importFolderItem = (parentId) => (item) => { const hash = crypto_1.default .createHash('sha1') // @ts-expect-error -- this is not present on the official types, yet was here in the source code .update(item.name) .digest('hex') .slice(0, 8); return { parentId, _id: `fld___WORKSPACE_ID__${hash}`, _type: 'request_group', // @ts-expect-error -- this is not present on the official types, yet was here in the source code name: item.name || 'Folder {requestGroupCount}', description: item.description || '', }; }; /** * Return path with parameters replaced by insomnia variables * * I.e. "/foo/:bar" => "/foo/{{ bar }}" */ const pathWithParamsAsVariables = (path) => { var _a; return (_a = path === null || path === void 0 ? void 0 : path.replace(VARIABLE_SEARCH_VALUE, '{{ $1 }}')) !== null && _a !== void 0 ? _a : ''; }; /** * Return Insomnia request */ const importRequest = (endpointSchema, parentId, security, securitySchemes) => { var _a; const name = endpointSchema.summary || endpointSchema.path; const id = generateUniqueRequestId(endpointSchema); const paramHeaders = prepareHeaders(endpointSchema); const { authentication, headers: securityHeaders, parameters: securityParams, } = parseSecurity(security, securitySchemes); return { _type: 'request', _id: id, parentId: parentId, name, method: (_a = endpointSchema.method) === null || _a === void 0 ? void 0 : _a.toUpperCase(), url: `{{ base_url }}${pathWithParamsAsVariables(endpointSchema.path)}`, body: prepareBody(endpointSchema), headers: [...paramHeaders, ...securityHeaders], authentication: authentication, parameters: [...prepareQueryParams(endpointSchema), ...securityParams], }; }; /** * Imports insomnia definitions of query parameters. */ const prepareQueryParams = (endpointSchema) => { var _a; return convertParameters((_a = endpointSchema.parameters) === null || _a === void 0 ? void 0 : _a.filter(parameter => (parameter.in === 'query'))); }; /** * Imports insomnia definitions of header parameters. */ const prepareHeaders = (endpointSchema) => { var _a; return convertParameters((_a = endpointSchema.parameters) === null || _a === void 0 ? void 0 : _a.filter(parameter => (parameter.in === 'header'))); }; /** * Parse OpenAPI 3 securitySchemes into insomnia definitions of authentication, headers and parameters * @returns headers or basic|bearer http authentication details */ const parseSecurity = (security, securitySchemes) => { if (!security || !securitySchemes) { return { authentication: {}, headers: [], parameters: [], }; } const supportedSchemes = security .flatMap(securityPolicy => { return Object.keys(securityPolicy).map((securityRequirement) => { return { // @ts-expect-error the base types do not include an index but from what I can tell, they should schemeDetails: securitySchemes[securityRequirement], securityScopes: securityPolicy[securityRequirement], }; }); }) .filter(({ schemeDetails }) => (schemeDetails && SUPPORTED_SECURITY_TYPES.includes(schemeDetails.type))); const apiKeySchemes = supportedSchemes.filter(scheme => (scheme.schemeDetails.type === SECURITY_TYPE.API_KEY)); const apiKeyHeaders = apiKeySchemes .filter(scheme => scheme.schemeDetails.in === 'header') .map(scheme => { const variableName = (0, change_case_1.camelCase)(scheme.schemeDetails.name); return { name: scheme.schemeDetails.name, disabled: false, value: `{{ ${variableName} }}`, }; }); const apiKeyCookies = apiKeySchemes .filter(scheme => scheme.schemeDetails.in === 'cookie') .map(scheme => { const variableName = (0, change_case_1.camelCase)(scheme.schemeDetails.name); return `${scheme.schemeDetails.name}={{ ${variableName} }}`; }); const apiKeyCookieHeader = { name: 'Cookie', disabled: false, value: apiKeyCookies.join('; '), }; const apiKeyParams = apiKeySchemes .filter(scheme => scheme.schemeDetails.in === 'query') .map(scheme => { const variableName = (0, change_case_1.camelCase)(scheme.schemeDetails.name); return { name: scheme.schemeDetails.name, disabled: false, value: `{{ ${variableName} }}`, }; }); if (apiKeyCookies.length > 0) { apiKeyHeaders.push(apiKeyCookieHeader); } const authentication = (() => { const authScheme = supportedSchemes.find(scheme => [SECURITY_TYPE.HTTP, SECURITY_TYPE.OAUTH].includes(scheme.schemeDetails.type) && (scheme.schemeDetails.type === SECURITY_TYPE.OAUTH || SUPPORTED_HTTP_AUTH_SCHEMES.includes(scheme.schemeDetails.scheme))); if (!authScheme) { return {}; } switch (authScheme.schemeDetails.type) { case SECURITY_TYPE.HTTP: return parseHttpAuth(authScheme.schemeDetails.scheme); case SECURITY_TYPE.OAUTH: return parseOAuth2(authScheme.schemeDetails, authScheme.securityScopes); default: return {}; } })(); return { authentication, headers: apiKeyHeaders, parameters: apiKeyParams, }; }; /** * Get Insomnia environment variables for OpenAPI securitySchemes * * @returns Insomnia environment variables containing security information */ const getSecurityEnvVariables = (securitySchemeObject) => { if (!securitySchemeObject) { return {}; } const securitySchemes = Object.values(securitySchemeObject); const apiKeyVariableNames = securitySchemes .filter(scheme => scheme.type === SECURITY_TYPE.API_KEY) .map(scheme => (0, change_case_1.camelCase)(scheme.name)); const variables = {}; Array.from(new Set(apiKeyVariableNames)).forEach(name => { variables[name] = name; }); const hasHttpBasicScheme = securitySchemes.some(scheme => (scheme.type === SECURITY_TYPE.HTTP && scheme.scheme === 'basic')); if (hasHttpBasicScheme) { variables.httpUsername = 'username'; variables.httpPassword = 'password'; } const hasHttpBearerScheme = securitySchemes.some(scheme => (scheme.type === SECURITY_TYPE.HTTP && scheme.scheme === 'bearer')); if (hasHttpBearerScheme) { variables.bearerToken = 'bearerToken'; } const oauth2Variables = securitySchemes.reduce((accumulator, scheme) => { if (scheme.type === SECURITY_TYPE.OAUTH) { accumulator.oauth2ClientId = 'clientId'; const flows = scheme.flows || {}; if (flows.authorizationCode || flows.implicit) { accumulator.oauth2RedirectUrl = 'http://localhost/'; } if (flows.authorizationCode || flows.clientCredentials || flows.password) { accumulator.oauth2ClientSecret = 'clientSecret'; } if (flows.password) { accumulator.oauth2Username = 'username'; accumulator.oauth2Password = 'password'; } } return accumulator; }, {}); return { ...variables, ...oauth2Variables, }; }; /** * 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 = (endpointSchema) => { const { content } = (endpointSchema.requestBody || { content: {} }); const mimeTypes = Object.keys(content); const supportedMimeType = SUPPORTED_MIME_TYPES.find(mimeType => mimeTypes.includes(mimeType)); if (supportedMimeType === MIMETYPE_JSON) { const bodyParameter = content[supportedMimeType]; if (bodyParameter == null) { return { mimeType: MIMETYPE_JSON, }; } const example = generateParameterExample(bodyParameter.schema); const text = JSON.stringify(example, null, 2); return { mimeType: MIMETYPE_JSON, text, }; } if (mimeTypes && mimeTypes.length && mimeTypes[0] !== MIMETYPE_LITERALLY_ANYTHING) { return { mimeType: mimeTypes[0] || undefined, }; } else { return {}; } }; /** * Converts openapi schema of parameters into insomnia one. */ const convertParameters = (parameters = []) => { return parameters.map(parameter => { const { required, name, schema } = parameter; return { name, disabled: required !== true, value: `${generateParameterExample(schema)}`, }; }); }; /** * 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. */ // @ts-expect-error -- ran out of time during TypeScript conversion to handle this particular recursion const generateParameterExample = (schema) => { const typeExamples = { 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: (schema) => { const example = {}; const { properties } = schema; if (properties) { for (const propertyName of Object.keys(properties)) { example[propertyName] = generateParameterExample(properties[propertyName]); } } return example; }, // @ts-expect-error -- ran out of time during TypeScript conversion to handle this particular recursion array: (schema) => { // @ts-expect-error -- ran out of time during TypeScript conversion to handle this particular recursion const value = generateParameterExample(schema.items); if (schema.collectionFormat === 'csv') { return value; } else { return [value]; } }, }; if (typeof schema === 'string') { // @ts-expect-error -- ran out of time during TypeScript conversion to handle this particular recursion return typeExamples[schema]; } if (schema instanceof Object) { const { type, format, example, readOnly, default: defaultValue } = schema; if (readOnly) { return undefined; } if (example) { return example; } if (defaultValue) { return defaultValue; } // @ts-expect-error -- ran out of time during TypeScript conversion to handle this particular recursion const factory = typeExamples[`${type}_${format}`] || typeExamples[type]; if (!factory) { return null; } return factory(schema); } }; /** * Generates a unique and deterministic request ID based on the endpoint schema */ const generateUniqueRequestId = (endpointSchema) => { // `operationId` is already unique to the workspace, so we can just use that, combined with the workspace id to get something globally unique const uniqueKey = endpointSchema.operationId || `[${endpointSchema.method}]${endpointSchema.path}`; const hash = crypto_1.default .createHash('sha1') .update(uniqueKey) .digest('hex') .slice(0, 8); // Suffix the ID with a counter in case we try creating two with the same hash if (requestCounts.hasOwnProperty(hash)) { requestCounts[hash] += 1; } else { requestCounts[hash] = 0; } return `req_${WORKSPACE_ID}${hash}${requestCounts[hash] || ''}`; }; const parseHttpAuth = (scheme) => { switch (scheme) { case HTTP_AUTH_SCHEME.BASIC: return { type: 'basic', username: '{{ httpUsername }}', password: '{{ httpPassword }}', }; case HTTP_AUTH_SCHEME.BEARER: return { type: 'bearer', token: '{{bearerToken}}', prefix: '', }; default: return {}; } }; const parseOAuth2Scopes = (flow, selectedScopes) => { if (!(flow === null || flow === void 0 ? void 0 : flow.scopes)) { return ''; } const scopes = Object.keys(flow.scopes || {}); return scopes.filter(scope => selectedScopes.includes(scope)).join(' '); }; const mapOAuth2GrantType = (grantType) => { const types = { [OAUTH_FLOWS.AUTHORIZATION_CODE]: 'authorization_code', [OAUTH_FLOWS.CLIENT_CREDENTIALS]: 'client_credentials', [OAUTH_FLOWS.IMPLICIT]: 'implicit', [OAUTH_FLOWS.PASSWORD]: 'password', }; return types[grantType]; }; const parseOAuth2 = (scheme, selectedScopes) => { var _a, _b, _c, _d, _e; const flows = Object.keys(scheme.flows); if (!flows.length) { return {}; } const grantType = flows[0]; const flow = scheme.flows[grantType]; if (!flow) { return {}; } const base = { clientId: '{{ oauth2ClientId }}', grantType: mapOAuth2GrantType(grantType), scope: parseOAuth2Scopes(flow, selectedScopes), type: 'oauth2', }; switch (grantType) { case OAUTH_FLOWS.AUTHORIZATION_CODE: return { ...base, clientSecret: '{{ oauth2ClientSecret }}', redirectUrl: '{{ oauth2RedirectUrl }}', accessTokenUrl: (_a = flow) === null || _a === void 0 ? void 0 : _a.tokenUrl, authorizationUrl: (_b = flow) === null || _b === void 0 ? void 0 : _b.authorizationUrl, }; case OAUTH_FLOWS.CLIENT_CREDENTIALS: return { ...base, clientSecret: '{{ oauth2ClientSecret }}', accessTokenUrl: (_c = flow) === null || _c === void 0 ? void 0 : _c.tokenUrl, }; case OAUTH_FLOWS.IMPLICIT: return { ...base, redirectUrl: '{{ oauth2RedirectUrl }}', authorizationUrl: (_d = flow) === null || _d === void 0 ? void 0 : _d.authorizationUrl, }; case OAUTH_FLOWS.PASSWORD: return { ...base, clientSecret: '{{ oauth2ClientSecret }}', username: '{{ oauth2Username }}', password: '{{ oauth2Password }}', accessTokenUrl: (_e = flow) === null || _e === void 0 ? void 0 : _e.tokenUrl, }; default: return {}; } }; const convert = async (rawData) => { var _a; // Reset requestCounts = {}; // Validate let apiDocument = parseDocument(rawData); if (!apiDocument || !SUPPORTED_OPENAPI_VERSION.test(apiDocument.openapi)) { return null; } try { apiDocument = await swagger_parser_1.default.validate(apiDocument, { dereference: { circular: 'ignore', }, }); } catch (err) { console.log('[openapi-3] Import file validation failed', err); } // Import const workspace = { _type: 'workspace', _id: WORKSPACE_ID, parentId: null, name: `${apiDocument.info.title} ${apiDocument.info.version}`, description: apiDocument.info.description || '', // scope is not set because it could be imported for design OR to generate requests }; const baseEnv = { _type: 'environment', _id: '__BASE_ENVIRONMENT_ID__', parentId: WORKSPACE_ID, name: 'Base environment', data: { base_url: '{{ scheme }}://{{ host }}{{ base_path }}', }, }; const defaultServerUrl = getDefaultServerUrl(apiDocument); const securityVariables = getSecurityEnvVariables((_a = apiDocument.components) === null || _a === void 0 ? void 0 : _a.securitySchemes); const protocol = defaultServerUrl.protocol || ''; // Base path is pulled out of the URL, and the trailing slash is removed const basePath = (defaultServerUrl.pathname || '').replace(/\/$/, ''); const openapiEnv = { _type: 'environment', _id: 'env___BASE_ENVIRONMENT_ID___sub', parentId: baseEnv._id, name: 'OpenAPI env', data: { // note: `URL.protocol` returns with trailing `:` (i.e. "https:") scheme: protocol.replace(/:$/, '') || ['http'], base_path: basePath, host: defaultServerUrl.host || '', ...securityVariables, }, }; const endpoints = parseEndpoints(apiDocument); return [ workspace, baseEnv, openapiEnv, ...endpoints, ]; }; exports.convert = convert; //# sourceMappingURL=openapi-3.js.map