Source: utils/vc/schema.js

import jsonld from 'jsonld';
import { validate } from 'jsonschema';
import defaultDocumentLoader from './document-loader';

import {
  expandedSubjectProperty,
  expandedSchemaProperty,
  credentialIDField,
  credentialContextField,
} from './constants';

/**
 * The function uses `jsonschema` package to verify that the expanded `credential`'s subject `credentialSubject` has the JSON
 * schema `schema`
 * @param {object} credential - The credential to use, must be expanded JSON-LD
 * @param {object} schema - The schema to use
 * @param context
 * @param documentLoader
 * @returns {Promise<Boolean>} - Returns promise to a boolean or throws error
 */
export async function validateCredentialSchema(
  credential,
  schema,
  context,
  documentLoader,
) {
  const requiresID = schema.required && schema.required.indexOf('id') > -1;
  const credentialSubject = credential[expandedSubjectProperty] || [];
  const subjects = credentialSubject.length
    ? credentialSubject
    : [credentialSubject];
  for (let i = 0; i < subjects.length; i++) {
    const subject = { ...subjects[i] };
    if (!requiresID) {
      // The id will not be part of schema. The spec mentioned that id will be popped off from subject
      delete subject[credentialIDField];
    }

    // eslint-disable-next-line
    const compacted = await jsonld.compact(subject, context, {
      documentLoader: documentLoader || defaultDocumentLoader(),
    });
    delete compacted[credentialContextField];

    if (Object.keys(compacted).length === 0) {
      throw new Error('Compacted subject is empty, likely invalid');
    }

    const schemaObj = schema.schema || schema;
    const subjectSchema = (schemaObj.properties && schemaObj.properties.credentialSubject)
      || schemaObj;

    validate(compacted, subjectSchema, {
      throwError: true,
    });
  }
  return true;
}

/**
 * Get schema and run validation on credential if it contains both a credentialSubject and credentialSchema
 * @param {object} credential - a verifiable credential JSON object
 * @param {object} context - the context
 * @param {object} documentLoader - the document loader
 * @returns {Promise<void>}
 */
// eslint-disable-next-line sonarjs/cognitive-complexity
export async function getAndValidateSchemaIfPresent(
  credential,
  context,
  documentLoader,
) {
  const schemaList = credential[expandedSchemaProperty];
  if (schemaList) {
    const schema = schemaList[0];
    if (credential[expandedSubjectProperty] && schema) {
      const schemaUri = schema[credentialIDField];
      let schemaObj;

      const { document } = await documentLoader(schemaUri);
      if (Array.isArray(document) && document.length === 2) {
        const [author, data] = document;

        if (typeof data !== 'object' || data instanceof Uint8Array) {
          throw new Error('Incorrect schema format');
        }

        schemaObj = {
          ...data,
          id: schemaUri,
          author: author.toQualifiedEncodedString(),
        };
      } else {
        schemaObj = document;
      }

      try {
        await validateCredentialSchema(
          credential,
          schemaObj,
          context,
          documentLoader,
        );
      } catch (e) {
        throw new Error(`Schema validation failed: ${e}`);
      }
    }
  }
}