UNPKG

json-schema-remote

Version:

Node.js module to validate JSON objects against a JSON Schema, including remote references ($ref)

266 lines (248 loc) 7.49 kB
const tv4 = require('tv4'); const tv4formats = require('tv4-formats'); const validatorJS = require('validator'); const superagent = require('superagent'); const schemaSchema = require('./schema/schema.json'); const isString = require('lodash.isstring'); const isObject = require('lodash.isobject'); tv4.addFormat(tv4formats); tv4.addSchema(schemaSchema); let log = console.log; const requestCache = new Map(); function setLoggingFunction(fn) { if (typeof fn !== 'function') { throw new Error('logging function is no function!'); } log = fn; } /** * Preload a JSON Schema so it will not be necessary to remotely load it when validating * @param {string} [url] URL/ID of the schema. Can be omitted if schema contains id property * @param {object} schema JSON Schema to add */ function preload(url, schema) { if (schema) { return tv4.addSchema(url, schema); } return tv4.addSchema(url); } /** * Drop all schemas from tv4 cache. * @returns {undefined} */ function dropSchemas() { tv4.dropSchemas(); tv4.addSchema(schemaSchema); } /** * Get a JSON Schema from tv4 cache. * @param {string} url URL/ID of the schema * @returns {object} Returns the schema from the cache. */ function getSchema(url) { return tv4.getSchema(url); } function makeRequest(url) { return Promise.resolve().then(() => { if (requestCache.has(url)) { return requestCache.get(url); } log('downloading from ', url, '\n'); const req = superagent.get(url); const isNode = module && !!module.exports && !(typeof navigator !== 'undefined' && navigator.product === 'ReactNative'); // const isBrowser = typeof process === 'undefined' || typeof process.browser !== 'undefined'; if (!isNode) { return req.then((res) => { if (res.body) { return res.body; } return JSON.parse(res.text); }); } const promise = req .then((res) => { try { return JSON.parse(res.text); } catch (err) { return res.body; } }) .then((res) => { if (requestCache.has(url)) { requestCache.delete(url); } return res; }); requestCache.set(url, promise); return promise; }); } /** * loads a JSON object by URL or directly * @param data JSON object or URL to a JSON object * @param [callback] called with (error, data) */ function loadData(data, callback) { return Promise.resolve() .then(() => { const tld = !isString(data) || data.indexOf('localhost') === -1; if (isString(data) && validatorJS.isURL(data, { require_tld: tld })) { return makeRequest(data) .catch((error) => { if ( error.hasOwnProperty('response') && error.hasOwnProperty('statusCode') && error.response.statusCode !== 200 ) { return Promise.reject(new Error('Could not load remote data.')); } return Promise.reject(error); }) .then((body) => { if (!isObject(body)) { return Promise.reject(new Error('No valid JSON Object')); } return body; }); } return data; }) .then((result) => { if (callback) { return callback(null, result); } return result; }) .catch((error) => { if (callback) { return callback(error); } return Promise.reject(error); }); } /** * loads a Schema by URL or directly and checks for JSON Schema compliance * @param schema Schema or URL to a schema * @param [callback] called with (error, schema) */ function loadSchema(schema, callback) { return Promise.resolve() .then(() => { const tld = !isString(schema) || schema.indexOf('localhost') === -1; if (isString(schema) && validatorJS.isURL(schema, { require_tld: tld })) { const cachedSchema = tv4.getSchema(schema); if (cachedSchema) { return cachedSchema; } return makeRequest(schema) .catch((error) => { if ( error.hasOwnProperty('response') && error.hasOwnProperty('statusCode') && error.response.statusCode !== 200 ) { return Promise.reject(new Error('Could not load schema.')); } return Promise.reject(error); }) .then((parsedBody) => tv4Validate(parsedBody, schemaSchema).then(() => parsedBody)); } if (!isObject(schema)) { return Promise.reject(new Error('No valid JSON Schema')); } // check if valid schema return tv4Validate(schema, schemaSchema).then(() => schema); }) .then((result) => { tv4.addSchema(result.id, result); if (callback) { return callback(null, result); } return result; }) .catch((error) => { if (callback) { return callback(error); } return Promise.reject(error); }); } /** * recursively validates against JSON schema. Loads missing $rel´s * @param {object} data JSON object to check * @param {object} schema JSON schema to check against * @param {function} [callback] called with (error, result) */ function tv4Validate(data, schema, callback) { return Promise.resolve() .then(() => { const result = tv4.validateMultiple(data, schema); if (result.missing.length > 0) { // Missing Schemas return Promise.all( result.missing.map((schemaID) => { if (tv4.getSchema(schemaID)) { throw new Error( 'json-schema-remote: tv4 is missing schema it already has in cache. possible faulty schema.' ); } return loadSchema(schemaID).then((loadedSchema) => tv4.addSchema(schemaID, loadedSchema)); }) ).then(() => tv4Validate(data, schema)); } else if (result.errors.length > 0) { // Invalid const error = new Error(schema === schemaSchema.id ? 'No valid JSON Schema' : 'JSON Schema Validation error'); error.errors = result.errors; return Promise.reject(error); } // Valid return true; }) .then((isValid) => { if (callback) { return callback(null, isValid); } return isValid; }) .catch((error) => { if (callback) { return callback(error, false); } return Promise.reject(error); }); } /** * validates a JSON object against a JSON Schema. Both values can either be the actual objects or * URLs. * @param {string, object} dataOrURL A JSON object to validate or a URL to a JSON object. * @param {string, object} schemaOrURL A JSON schema to validate against or a URL to a JSON * schema. * @param {validate~callback} [callback] Callback function */ function validate(dataOrURL, schemaOrURL, callback) { return Promise.all([loadData(dataOrURL), loadSchema(schemaOrURL)]) .then((loaded) => tv4Validate(loaded[0], loaded[1])) .then((isValid) => { if (callback) { return callback(null, isValid); } return isValid; }) .catch((error) => { if (callback) { return callback(error, false); } return Promise.reject(error); }); } module.exports = { validate, preload, dropSchemas, getSchema, setLoggingFunction, loadData, loadSchema, tv4Validate, };