@scalar/oas-utils
Version:
Open API spec and Yaml handling utilities
283 lines (280 loc) • 10.1 kB
JavaScript
import { camelToTitleWords, capitalize } from '../../helpers/string.js';
import { parseLocalStorage } from '../local-storage.js';
/** V-0.0.0 to V-2.1.0 migration */
const migrate_v_2_1_0 = (data) => {
console.info('Performing data migration v-0.0.0 to v-2.1.0');
// Augment the previous data
const oldData = {
...data,
// @ts-expect-error Tags used to be called folders
folders: parseLocalStorage('folder'),
};
/** To grab requests and tags we must traverse children, also for security */
const flattenChildren = (childUids) => childUids.reduce((prev, uid) => {
const request = oldData.requests[uid];
// Request
if (request) {
prev.requestUids.add(uid);
// Security
request.securitySchemeUids?.forEach((s) => prev.authUids.add(s));
}
// Folder -> tag
else if (oldData.folders[uid]) {
const { requestUids, tagUids, authUids } = flattenChildren(oldData.folders[uid].childUids ?? []);
prev.tagUids.add(uid);
requestUids.forEach((r) => prev.requestUids.add(r));
tagUids.forEach((t) => prev.tagUids.add(t));
authUids.forEach((a) => prev.authUids.add(a));
}
return prev;
}, {
requestUids: new Set(),
tagUids: new Set(),
authUids: new Set(),
});
/** Migrate values from old securitySchemes to the new auth */
const migrateAuth = (scheme) => {
if (scheme.type === 'apiKey') {
// ApiKey
return { type: 'apiKey', name: scheme.name, value: scheme.value ?? '' };
}
// HTTP
if (scheme.type === 'http') {
return {
type: 'http',
username: scheme.value ?? '',
password: scheme.secondValue ?? '',
token: scheme.value ?? '',
};
}
// Oauth2 Implicit
if (scheme.type === 'oauth2' && scheme.flow.type === 'implicit') {
return {
type: 'oauth-implicit',
token: scheme.flow.token ?? '',
};
}
// Oauth2 Password
if (scheme.type === 'oauth2' && scheme.flow.type === 'password') {
return {
type: 'oauth-password',
token: scheme.flow.token ?? '',
username: scheme.flow.value ?? '',
password: scheme.flow.secondValue ?? '',
clientSecret: scheme.flow.clientSecret ?? '',
};
}
// Oauth2 clientCredentials
if (scheme.type === 'oauth2' && scheme.flow.type === 'clientCredentials') {
return {
type: 'oauth-clientCredentials',
token: scheme.flow.token ?? '',
clientSecret: scheme.flow.clientSecret ?? '',
};
}
// Oauth2 Authorization Code
if (scheme.type === 'oauth2' && scheme.flow.type === 'authorizationCode') {
return {
type: 'oauth-authorizationCode',
token: scheme.flow.token ?? '',
clientSecret: scheme.flow.clientSecret ?? '',
};
}
// Default - should not get hit
return {
type: 'apiKey',
name: '',
value: '',
};
};
/** This is needed due to our previous data being poluted, we will only allow auth on a requst which is in the spec */
const requestSecurityDict = {};
// Collections
const collections = Object.values(oldData.collections ?? {}).reduce((prev, c) => {
const { requestUids, tagUids, authUids } = flattenChildren(c.childUids ?? []);
// Ensure we got unique uids
const securitySchemesSet = new Set([...authUids, ...Object.values(c.securitySchemeDict ?? {})]);
const securitySchemes = [...securitySchemesSet];
// Add this auth to each request
requestUids.forEach((r) => (requestSecurityDict[r] = securitySchemes));
// Migrate auth
const auth = securitySchemes.reduce((_prev, uid) => {
const scheme = oldData.securitySchemes[uid];
if (scheme?.uid && _prev) {
_prev[uid] = migrateAuth(scheme);
}
return _prev;
}, {});
prev[c.uid] = {
'type': 'collection',
'openapi': c.spec?.openapi || '3.1.0',
'info': c.spec?.info || { title: 'OpenAPI Spec', version: '0.0.1' },
'security': c.spec?.security || [],
'externalDocs': c.spec?.externalDocs,
'uid': c.uid,
securitySchemes,
'selectedSecuritySchemeUids': [],
'selectedServerUid': c.selectedServerUid || c.spec?.serverUids?.[0] || '',
'servers': c.spec?.serverUids || [],
'requests': [...requestUids],
'tags': [...tagUids],
auth,
'children': c.childUids || [],
'x-scalar-icon': 'interface-content-folder',
'watchMode': false,
'watchModeStatus': 'IDLE',
};
return prev;
}, {});
// Cookies
const cookies = oldData.cookies ?? {};
// Environments
const environments = Object.values(oldData.environments ?? {}).reduce((prev, e) => {
prev[e.uid] = {
...e,
value: e.raw ?? '',
};
return prev;
}, {});
// Requests
const requests = Object.values(oldData.requests ?? {}).reduce((prev, r) => {
// Convert parameters
const parameters = [
...Object.values(r.parameters?.path ?? {}),
...Object.values(r.parameters?.query ?? {}),
...Object.values(r.parameters?.headers ?? {}),
...Object.values(r.parameters?.cookies ?? {}),
].filter((p) => p);
// Ensure this request can access these schemes
const selectedSecuritySchemeUids = (r.selectedSecuritySchemeUids || []).filter((s) => requestSecurityDict[r.uid]?.includes(s));
prev[r.uid] = {
...r,
parameters,
type: 'request',
method: r.method?.toLowerCase() ?? 'get',
examples: r.childUids || [],
selectedSecuritySchemeUids,
selectedServerUid: '',
servers: [],
};
return prev;
}, {});
// Request Examples
const requestExamples = Object.values(oldData.requestExamples ?? {}).reduce((prev, e) => {
prev[e.uid] = {
...e,
type: 'requestExample',
};
return prev;
}, {});
/** Specifically handle each oauth2 flow */
const migrateFlow = (flow) => {
const base = {
refreshUrl: flow.refreshUrl || '',
selectedScopes: flow.selectedScopes || [],
scopes: flow.scopes || {},
};
if (flow.type === 'implicit') {
return {
...flow,
...base,
'type': 'implicit',
'x-scalar-redirect-uri': ('redirectUri' in flow ? flow.redirectUri : '') || '',
};
}
if (flow.type === 'password') {
return {
...flow,
...base,
tokenUrl: flow.tokenUrl || '',
};
}
if (flow.type === 'clientCredentials') {
return {
...flow,
...base,
tokenUrl: flow.tokenUrl || '',
};
}
return {
...flow,
...base,
'x-usePkce': 'no',
'x-scalar-redirect-uri': ('redirectUri' in flow ? flow.redirectUri : '') || '',
'authorizationUrl': flow.authorizationUrl || '',
'tokenUrl': flow.tokenUrl || '',
};
};
/** Generate a nameKey based on the type of oauth */
const getNameKey = (scheme) => {
switch (scheme?.type) {
case 'apiKey':
return `${capitalize(scheme.in)}`;
case 'http': {
return `${capitalize(scheme.scheme)} Authentication`;
}
case 'oauth2':
return camelToTitleWords(scheme.flow.type);
case 'openIdConnect':
return 'Open ID Connect';
default:
return 'None';
}
};
// Security Schemes
const securitySchemes = Object.values(oldData.securitySchemes ?? {}).reduce((prev, s) => {
prev[s.uid] =
s.type === 'oauth2'
? {
...s,
'nameKey': getNameKey(s),
'x-scalar-client-id': s.clientId || '',
'flow': migrateFlow(s.flow),
}
: { ...s, nameKey: getNameKey(s) };
return prev;
}, {});
// Servers
const servers = Object.values(oldData.servers ?? {}).reduce((prev, s) => {
prev[s.uid] = {
...s,
variables: s.variables ?? {},
};
return prev;
}, {});
// Tags
const tags = Object.values(oldData.folders ?? {}).reduce((prev, f) => {
prev[f.uid] = {
'type': 'tag',
'uid': f.uid,
'name': f.name || 'unknownTag',
'description': f.description,
'children': f.childUids || [],
'x-scalar-children': [],
};
return prev;
}, {});
// Workspaces
const workspaces = Object.values(oldData.workspaces ?? {}).reduce((prev, w) => {
prev[w.uid] = {
...w,
description: w.description ?? 'Basic Scalar Workspace',
cookies: w.cookieUids || [],
collections: w.collectionUids || [],
environments: w.environmentUids || [],
};
return prev;
}, {});
return {
collections,
cookies,
environments,
requestExamples,
requests,
securitySchemes,
servers,
tags,
workspaces,
};
};
export { migrate_v_2_1_0 };