All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ml-modules.root.data-hub.5.builtins.steps.mapping.entity-services.main.mjs Maven / Gradle / Ivy

There is a newer version: 6.1.1
Show newest version
import consts from "/data-hub/5/impl/consts.mjs";
import defaultLib from "/data-hub/5/builtins/steps/mapping/default/lib.mjs";
import flowUtils from "/data-hub/5/impl/flow-utils.mjs";
import lib from "/data-hub/5/builtins/steps/mapping/entity-services/lib.mjs";
import mappingLibrary from "/data-hub/5/mapping/mapping-lib.mjs";
import hubUtils from "/data-hub/5/impl/hub-utils.mjs";
import httpUtils from "/data-hub/5/impl/http-utils.mjs";

import entityValidationLib from '/data-hub/5/builtins/steps/mapping/entity-services/entity-validation-lib.mjs';
const xqueryLib = require('/data-hub/5/builtins/steps/mapping/entity-services/xquery-lib.xqy');

// caching mappings in key to object since tests can have multiple mappings run in same transaction
let mappings = {};
let entityModelMap = {};
const traceEvent = consts.TRACE_MAPPING_DEBUG;

function main(contentSequence, options, stepExecutionContext) {
  // The flow framework will pass a sequence, but a number of tests still pass a single object, as this step used to be
  // acceptsBatch=false prior to 5.5.
  contentSequence = hubUtils.normalizeToSequence(contentSequence);

  let outputFormat = options.outputFormat ? options.outputFormat.toLowerCase() : consts.DEFAULT_FORMAT;
  if (outputFormat !== consts.JSON && outputFormat !== consts.XML) {
    throw Error('The output format of type ' + outputFormat + ' is invalid. Valid options are ' + consts.XML + ' or ' + consts.JSON + '.');
  }

  let mappingKey = options.mapping ? `${options.mapping.name}:${options.mapping.version}` : null;
  let mapping = mappings[mappingKey];
  if (!mapping && options.mapping && options.mapping.name && options.mapping.version) {
    let version = parseInt(options.mapping.version);
    if (isNaN(version)) {
      throw Error('Mapping version ('+options.mapping.version+') is invalid.');
    }
    mapping = defaultLib.getMappingWithVersion(options.mapping.name, version);
    mappings[mappingKey] = mapping;
  } else if (!mapping && options.mapping && options.mapping.name) {
    mapping = defaultLib.getMapping(options.mapping.name);
    mappings[mappingKey] = mapping;
  } else if (!mapping) {
    throw Error('You must specify a mapping name.');
  }

  if (fn.empty(mapping)) {
    let mapError = 'Could not find mapping: ' + options.mapping.name;
    if (options.mapping.version) {
      mapError += ' with version #' + options.mapping.version;
    }
    throw Error(mapError);
  }
  let mappingURIforXML = fn.replace(xdmp.nodeUri(mapping), 'json$', 'xml');
  let targetEntityType = fn.string(mapping.root.targetEntityType);
  if (targetEntityType === '') {
    let errMsg = `Could not find targetEntityType in mapping: ${xdmp.nodeUri(mapping)}.`;
    throw Error(errMsg);
  }
  let targetEntityName = lib.getEntityName(targetEntityType);

  const mappingStep = fn.head(mapping).toObject();
  buildEntityModelMap(mappingStep);

  const userMappingParameterMap = getUserMappingParameterMap(stepExecutionContext, contentSequence);

  let outputContentArray = [];
  let currentContentUri;

  for (const content of contentSequence) {
    currentContentUri = content.uri;
    try {
      if (xdmp.traceEnabled(consts.TRACE_FLOW_DEBUG)) {
        hubUtils.hubTrace(consts.TRACE_FLOW_DEBUG, `Mapping: ${currentContentUri}`);
      }

      let doc = content.value;
      const instance = lib.getSourceRecordForMapping(mappingStep, doc);

      const mappingParams = Object.assign({}, {"URI": currentContentUri}, userMappingParameterMap);

      // For not-yet-known reasons, catching this error and then simply rethrowing it causes the MappingTest JUnit class
      // to pass due to the "message" part of the JSON in the stepOutput containing the expected output when this is done.
      let arrayOfInstanceArrays;
      try {
        arrayOfInstanceArrays = xqueryLib.dataHubMapToCanonical(instance, mappingURIforXML, mappingParams, {"format": outputFormat});
      } catch (e) {
        const errorMessage = mappingLibrary.extractFriendlyErrorMessage(e);
        if (errorMessage) {
          throw Error(errorMessage);
        } else {
          throw Error(e);
        }
      }
      hubUtils.hubTrace(traceEvent, `Entity instances with mapping ${mappingStep.name} and source document ${currentContentUri}: ${arrayOfInstanceArrays}`);

      let counter = 0;
      let contentResponse = [];

      for (const instanceArray of xdmp.arrayValues(arrayOfInstanceArrays)) {
        /* The first instance in the array is the target entity instance. 'permissions' and 'collections' for target instance
        are applied outside of main.sjs */
        let entityName ;
        let entityContext = {};

        if (counter == 0) {
          entityName = targetEntityName;
          entityContext = content.context;
        } else {
          let currentRelatedMapping = mappingStep.relatedEntityMappings[counter-1];
          entityName = lib.getEntityName(fn.string(currentRelatedMapping.targetEntityType));
          entityContext = createContextForRelatedEntityInstance(currentRelatedMapping, content);
        }
        const entityModel = entityModelMap[entityName];

        for (const entityInstance of xdmp.arrayValues(instanceArray)) {
          let entityContent = {};
          if (entityName == targetEntityName) {
            entityContent = Object.assign(entityContent, content);
          }
          entityContent["value"] = entityInstance.value;
          entityContent["uri"] = buildUri(entityInstance, entityName, outputFormat);
          const entityInstanceContext = Object.assign({}, entityContext);
          entityContent = validateEntityInstanceAndBuildEnvelope(doc, entityContent, entityInstanceContext, entityModel, outputFormat, options);
          hubUtils.hubTrace(traceEvent, `Entity instance envelope created with mapping ${mappingStep.name} and source document ${currentContentUri}: ${entityContent.value}`);
          contentResponse.push(entityContent);
        }
        counter++;
      }

      outputContentArray = outputContentArray.concat(contentResponse);
    } catch (error) {
      // This should always be defined, but some DHF unit tests do not pass it in
      if (stepExecutionContext != null) {
        if (stepExecutionContext.isStopOnError()) {
          stepExecutionContext.stopWithError(error, currentContentUri);
          return [];
        }
        stepExecutionContext.addStepErrorForItem(error, currentContentUri);
      } else {
        throw error;
      }
    }
  }

  // A number of DHF tests expect a single object returned, while the flow framework is fine with one or an array.
  return outputContentArray.length == 1 ? outputContentArray[0] : outputContentArray;
}

function buildUri(entityInstance, entityName, outputFormat) {
  if (String(entityInstance.uri)) {
    return flowUtils.properExtensionURI(String(entityInstance.uri), outputFormat);
  } else {
    httpUtils.throwBadRequest(`Unable to write mapped instance for entity model '${entityName}'; cause: The Context or URI expression is inapplicable to the respective source document and will lead to null outputs for the remaining fields below.`);
  }
}

function getUserMappingParameterMap(stepExecutionContext, contentSequence) {
  if (stepExecutionContext != null) {
    const path = stepExecutionContext.flowStep.options.mappingParametersModulePath;
    return path ? hubUtils.requireFunction(path, "getParameterValues")(contentSequence) : {};
  }
  return {};
}

function validateEntityInstanceAndBuildEnvelope(doc, entityContent, entityContext, entityModel, outputFormat, options) {
  // Must validate before building an envelope so that validaton errors can be added to the headers
  entityValidationLib.validateEntity(entityContent.value, options, entityModel.info);

  entityContent["value"] = buildEnvelope(entityModel.info, doc, entityContent.value, outputFormat, options);

  // Must remove these so that they're not carried over to another item in a batch
  entityValidationLib.removeValidationErrorsFromHeaders(options);
  entityContent["context"] = entityContext;
  return entityContent;
}

function createContextForRelatedEntityInstance(relatedEntityMapping, content) {
  let entityContext = {};
  let relatedEntityPermissions = fn.string(relatedEntityMapping.permissions);
  let relatedEntityCollections = relatedEntityMapping.collections;
  if (relatedEntityMapping.additionalCollections) {
    relatedEntityCollections = relatedEntityCollections.concat(relatedEntityMapping.additionalCollections);
  }
  entityContext["permissions"] = hubUtils.parsePermissions(relatedEntityPermissions);
  entityContext["collections"] = relatedEntityCollections;
  if (content.context && content.context.originalCollections) {
    entityContext.originalCollections = content.context.originalCollections;
  }
  entityContext["useContextCollectionsOnly"] = true;
  return entityContext;

}

function buildEntityModelMap(mappingStep) {
  let targetEntityName = lib.getEntityName(mappingStep.targetEntityType);
  let targetEntityModel= lib.getTargetEntity(mappingStep.targetEntityType);
  entityModelMap[targetEntityName] = targetEntityModel;
  if (mappingStep.relatedEntityMappings) {
    mappingStep.relatedEntityMappings.forEach(relatedEntityMapping => {
      let relatedEntityName = lib.getEntityName(relatedEntityMapping.targetEntityType);
      if (!entityModelMap[relatedEntityName]) {
        let relatedEntityModel= lib.getTargetEntity(relatedEntityMapping.targetEntityType);
        entityModelMap[relatedEntityName] = relatedEntityModel;
      }
    });
  }
}

// Extracted for unit testing purposes
function buildEnvelope(entityInfo, doc, instance, outputFormat, options) {
  let triples = [];
  let headers = flowUtils.createHeaders(options);

  if (options.triples && Array.isArray(options.triples)) {
    for (let triple of options.triples) {
      triples.push(sem.triple(triple));
    }
  }

  let docHeaders = flowUtils.normalizeValuesInNode(flowUtils.getHeaders(doc)) || {};
  let docTriples = flowUtils.normalizeValuesInNode(flowUtils.getTriples(doc)) || [];


  headers = flowUtils.mergeHeaders(headers, docHeaders, outputFormat);
  headers = flowUtils.updateHeaders(headers, outputFormat);
  triples = triples.concat(hubUtils.normalizeToArray(docTriples));
  let attachments = flowUtils.cleanData(doc, "content", outputFormat);
  let nb = new NodeBuilder().startDocument();
  if (outputFormat === consts.JSON) {
    if (hubUtils.isXmlNode(attachments)) {
      attachments = xdmp.quote(attachments);
    }
    nb.addNode({
      envelope: {
        headers: headers,
        triples: triples.map((triple) => flowUtils.normalizeTriple(triple)).reduce((arr, triple) => arr.concat(triple), []),
        instance: Object.assign({
          info: entityInfo
        }, instance.toObject()),
        attachments: options.attachSourceDocument ? attachments : undefined
      }
    });
  } else {
    nb.startElement("envelope", "http://marklogic.com/entity-services");
    nb.startElement("headers", "http://marklogic.com/entity-services");
    if (flowUtils.isNonStringIterable(headers)) {
      for (let header of headers) {
        nb.addNode(header);
      }
    } else if (headers) {
      nb.addNode(headers);
    }
    nb.endElement();

    nb.startElement("triples", "http://marklogic.com/entity-services");
    if (flowUtils.isNonStringIterable(triples)) {
      for (let triple of triples) {
        nb.addNode(flowUtils.tripleToXml(flowUtils.normalizeTriple(triple)));
      }
    } else if (triples) {
      nb.addNode(flowUtils.tripleToXml(flowUtils.normalizeTriple(triples)));
    }
    nb.endElement();
    if (instance.nodeName === 'instance') {
      nb.addNode(instance);
    } else {
      nb.startElement("instance", "http://marklogic.com/entity-services");
      nb.startElement("info", "http://marklogic.com/entity-services");
      nb.startElement("baseUri", "http://marklogic.com/entity-services");
      nb.addText(entityInfo.baseUri);
      nb.endElement();
      nb.startElement("title", "http://marklogic.com/entity-services");
      nb.addText(entityInfo.title);
      nb.endElement();
      nb.startElement("version", "http://marklogic.com/entity-services");
      nb.addText(entityInfo.version);
      nb.endElement();
      nb.endElement();
      if (instance[Symbol.iterator]) {
        for (let n of instance) {
          nb.addNode(n);
        }
      } else {
        nb.addNode(instance);
      }
      nb.endElement();
    }
    if (attachments && options.attachSourceDocument) {
      nb.startElement("attachments", "http://marklogic.com/entity-services");
      if (hubUtils.isJsonNode(attachments)) {
        nb.addText(xdmp.quote(attachments));
      } else {
        // can get sequence of nodes in JSON to XML scenario
        if (flowUtils.isNonStringIterable(attachments)) {
          for (let attachment of attachments) {
            nb.addNode(attachment);
          }
        } else if (attachments) {
          nb.addNode(attachments);
        }
      }
      nb.endElement();
    }
    nb.endElement();
  }
  nb.endDocument();
  return nb.toNode();
}

export default {
  buildEnvelope,
  main
};




© 2015 - 2024 Weber Informatics LLC | Privacy Policy