@wral/resource-deref
Version:
A Library for dereferencing $ref references in Javascript objects
146 lines (129 loc) • 4.22 kB
JavaScript
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
/**
* Dereference a $ref by fetching it, with optional recursion
* and deep traversal for nested $ref resolution
* @param {Object} obj - The object to dereference
* @param {Object} options
* @param {Object} options.fetchOptions - Options for fetch requests
* @param {Object} options.trustedDomains - Domains that allow auth
* @param {Number} options.depth - Maximum depth for recursion (default: 5)
* @returns {Promise<Object>} - Resolved object with all $refs dereferenced
*/
export function deref(obj, {
fetchOptions = {},
trustedDomains = {},
depth = 5,
} = {}) {
if (depth <= 0) {
return Promise.resolve(obj); // Stop recursion at depth limit
}
// Helper function to deeply resolve refs within the object
const deepDeref = (data, depth) => {
if (depth <= 0) {
return Promise.resolve(data); // No further dereferencing at depth limit
}
if (Array.isArray(data)) {
// Resolve each element in arrays
return Promise.all(data.map(item => deepDeref(item, depth)));
} else if (data && typeof data === 'object') {
// If there's a $ref, dereference it
if (data.$ref) {
return retrieveResource(data, { fetchOptions, trustedDomains })
.then(({contentType, raw}) => transformResource(raw, {contentType}))
.then(fetchedData => {
// Recursively deref fetched data if it also has $refs
return deepDeref(fetchedData, depth - 1);
});
}
// Otherwise, resolve $ref within the object's properties
const keys = Object.keys(data);
const promises = keys.map(key =>
deepDeref(data[key], depth).then(resolved => {
data[key] = resolved; // replace property with resolved value
})
);
return Promise.all(promises).then(() => data);
}
// Return non-object values as is
return Promise.resolve(data);
};
// Start deep dereferencing with the root object
return deepDeref(obj, depth);
}
/**
* Compile a fetch wrapper that dereferences $refs
* @param {Object} options
* @returns {Function}
*/
export function compile(options={}) {
const fn = (obj, fetchOptions) =>
deref(obj, { ...options, fetchOptions });
return fn;
}
export function trustedFetch (trustedDomains, url, options) {
const domain = new URL(url).hostname;
const _options = Object.keys(trustedDomains).includes(domain)
? applyAuthOptions(options, trustedDomains[domain]):
options;
return fetch(url, _options);
}
/**
* Apply authorization to fetch options
* @param {Object} fetchOptions
* @param {Object} options
* @param {Object} options.headers - map of HTTP headers with auth data
* @returns {Object}
*/
export function applyAuthOptions(fetchOptions, { headers }) {
const options = Object.assign({}, fetchOptions);
if (headers) {
options.headers = Object.assign({}, options.headers, headers);
}
return options;
}
export function retrieveResource({ $ref }, options) {
const url = new URL($ref);
switch (url.protocol) {
case 'http:':
case 'https:':
return trustedFetch(options.trustedDomains, $ref, options.fetchOptions)
.then(async (res) => {
if (!res.ok) {
throw new Error(`Failed to fetch ${$ref}: ${res.status}`,
{ cause: res });
}
const [contentType] = res.headers.get('content-type')
.split(';').map(s => s.trim());
const raw = await res.arrayBuffer();
return { contentType, raw };
});
case 's3:':
return retrieveS3Resource({ $ref }, options);
default:
console.warn('Unsupported protocol: ' + url.protocol);
return {};
}
}
function retrieveS3Resource({ $ref }) {
const url = new URL($ref);
const s3 = new S3Client();
const command = new GetObjectCommand({
Bucket: url.hostname,
Key: url.pathname.replace(/^\//, ''),
});
return s3.send(command).then(async (res) => {
const [contentType] = res.ContentType
.split(';').map(s => s.trim());
const raw = await res.Body.transformToByteArray();
return { contentType, raw };
});
}
export function transformResource(raw, { contentType }) {
switch (contentType) {
case 'application/json':
return JSON.parse(Buffer.from(raw).toString('utf8'));
default:
console.warn('Unsupported content type: ' + contentType);
return { _raw: raw };
}
}