UNPKG

@wral/resource-deref

Version:

A Library for dereferencing $ref references in Javascript objects

146 lines (129 loc) 4.22 kB
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 }; } }