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

ml-modules.root.data-hub.4.impl.flow-lib.xqy Maven / Gradle / Ivy

There is a newer version: 6.1.1
Show newest version
(:
  Copyright (c) 2021 MarkLogic Corporation

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
:)
xquery version "1.0-ml";

module namespace flow = "http://marklogic.com/data-hub/flow-lib";

import module namespace consts = "http://marklogic.com/data-hub/consts"
  at "/data-hub/4/impl/consts.xqy";

import module namespace debug = "http://marklogic.com/data-hub/debug"
  at "/data-hub/4/impl/debug-lib.xqy";

import module namespace hul = "http://marklogic.com/data-hub/hub-utils-lib"
  at "/data-hub/4/impl/hub-utils-lib.xqy";

import module namespace json="http://marklogic.com/xdmp/json"
  at "/MarkLogic/json/json.xqy";

import module namespace functx = "http://www.functx.com"
  at "/MarkLogic/functx/functx-1.0-nodoc-2007-01.xqy";

import module namespace rfc = "http://marklogic.com/data-hub/run-flow-context"
  at "/data-hub/4/impl/run-flow-context.xqy";

import module namespace trace = "http://marklogic.com/data-hub/trace"
  at "/data-hub/4/impl/trace-lib.xqy";

import module namespace es = "http://marklogic.com/entity-services"
  at "/MarkLogic/entity-services/entity-services.xqy";

declare namespace hub = "http://marklogic.com/data-hub";

declare option xdmp:mapping "false";

(: the directory where entities live :)
declare variable $ENTITIES-DIR := "/entities/";

declare variable $PLUGIN-NS := "http://marklogic.com/data-hub/plugins";

declare variable $FLOW-CACHE-KEY-PREFIX := "flow-cache-";
declare variable $MAIN-CACHE-KEY-PREFIX := "main-cache-";

declare variable $context-queue := map:map();
declare variable $writer-queue :=
  let $writer-map := map:map()
  let $_ := map:set-javascript-by-ref($writer-map, fn:true())
  return $writer-map;

declare variable $current-database := xdmp:database();

declare function flow:get-module-ns(
  $type as xs:string) as xs:string?
{
  if ($type eq $consts:JAVASCRIPT) then ()
  else
    $PLUGIN-NS
};

(:~
 : Returns a flow by name. This xml is dynamically constructed
 : by looking in the modules database.
 :
 : @param $entity-name - name of the entity that owns the flow
 : @param $flow-name - name of the flow to retrieve
 : @return - xml describing the flow
 :)
declare function flow:get-flow(
  $entity-name as xs:string,
  $flow-name as xs:string,
  $flow-type as xs:string?) as element(hub:flow)?
{
  let $duration := xs:dayTimeDuration("PT10S")
  let $key := $FLOW-CACHE-KEY-PREFIX||$entity-name||$flow-name||$flow-type
  let $flow :=  hul:from-field-cache-or-empty($key, $duration)
  return
    if ($flow) then
      $flow
    else
      hul:set-field-cache(
        $key,
        flow:get-flow-nocache($entity-name, $flow-name, $flow-type),
        $duration
      )
};

declare function flow:get-flow-nocache(
  $entity-name as xs:string,
  $flow-name as xs:string,
  $flow-type as xs:string?) as element(hub:flow)?
{

  hul:run-in-modules(function() {
    let $flow := /hub:flow[
    fn:lower-case(hub:entity) = fn:lower-case($entity-name) and
      hub:name = $flow-name]
    [
    if (fn:exists($flow-type)) then
      hub:type = $flow-type
    else
      fn:true()
    ]
    return
      if ((fn:count($flow)) > 1) then
        /hub:flow[
        hub:entity = $entity-name and
          hub:name = $flow-name]
        [
        if (fn:exists($flow-type)) then
          hub:type = $flow-type
        else
          fn:true()
        ]
      else
        $flow
  }) ! hul:deep-copy(.)
};

(:~
 : Returns the flows that belong to the given entity in the database
 :
 : @param $entity-name - the name of the entity containing the flows
 : @return - xml describing the flows
 :)
declare function flow:get-flows(
  $entity-name as xs:string) as element(hub:flows)
{
  element hub:flows {
    hul:run-in-modules(function() {
      /hub:flow[hub:entity = $entity-name]
    })
  }
};

(:~
 : Returns an entity by name. This xml is dynamically constructed
 : by looking in the modules database.
 :
 : @param $entity-name - name of the entity to retrieve
 : @return - xml describing the entity
 :)
declare function flow:get-entity(
  $entity-name as xs:string)
{
  let $uris :=
    hul:run-in-modules(function() {
      cts:uri-match($ENTITIES-DIR || $entity-name || "/*")
    })
  return
    flow:get-entity($entity-name, $uris)
};

(:~
 : Returns a entity by name. This xml is dynamically constructed
 : by looking in the modules database.
 :
 : @param $entity-name - name of the entity to retrieve
 : @param $uris - uris used to build the entity xml
 : @return - xml describing the entity
 :)
declare %private function flow:get-entity(
  $entity-name as xs:string,
  $uris as xs:string*)
  as element(hub:entity)
{
  
    {$entity-name}
    {
      flow:get-flows($entity-name)
    }
  
};

(:~
 : Returns the entities in the database
 :
 : @return - xml describing the entities
 :)
declare function flow:get-entities() as element(hub:entities)
{
  let $uris := hul:run-in-modules(function() {
    cts:uri-match($ENTITIES-DIR || "*")
  })
  let $entity-names :=
    fn:distinct-values(
      let $regex := $ENTITIES-DIR || "([^/]+)/.*$"
      for $flow in $uris[fn:matches(., $regex)]
      let $name := fn:replace($flow, $regex, "$1")
      return
        $name)
  let $entities :=
    for $entity-name in $entity-names
    return
      flow:get-entity($entity-name, $uris[fn:matches(., $ENTITIES-DIR || $entity-name || "/.+")])
  return
    
    {
      $entities
    }
    
};

(:~
 : Runs a collector
 :
 : @param $module-uri - the uri of the collector module
 : @param $options - a map of options passed in by the client
 : @return - a sequence of strings
 :)
declare function flow:run-collector(
  $flow as element(hub:flow),
  $job-id as xs:string,
  $options as map:map) as item()*
{
    (: sanity check on required info :)
  if (fn:empty($flow/hub:collector/@module) or fn:empty($flow/hub:collector/@code-format)) then
    fn:error((), "DATAHUB-INVALID-PLUGIN", "The plugin definition is invalid.")
  else (),

  (: assert that we are in query mode :)
  let $_ts as xs:unsignedLong := xdmp:request-timestamp()

  let $item-context := rfc:new-item-context()
    => rfc:with-options($options)
    => rfc:with-trace(trace:new-trace())

  let $_ := trace:set-plugin-label("collector")

  (: configure the global run context :)
  let $_ := (
    rfc:with-job-id($job-id),
    rfc:with-flow($flow),
    rfc:with-module-uri($flow/hub:collector/@module),
    rfc:with-code-format($flow/hub:collector/@code-format)
  )

  let $func := flow:make-function(
    $flow/hub:collector/@code-format,
    "collect",
    $flow/hub:collector/@module
  )
  let $before := xdmp:elapsed-time()
  let $resp :=
    try {
      $func($options)
    }
    catch($ex) {
      debug:log(xdmp:describe($ex, (), ())),
      trace:error-trace($item-context, $ex, xdmp:elapsed-time() - $before),
      xdmp:rethrow()
    }
  (: log and write the trace :)
  let $_ := (
    trace:plugin-trace($item-context, xdmp:describe($resp, 1000000, 1000000), xdmp:elapsed-time() - $before),
    trace:write-trace($item-context)
  )
  return
    $resp
};

(:~
 : Runs a given flow
 :
 : @param $flow - xml describing the flow
 : @param $identifier - the identifier to send to the flow steps (URI in corb lingo)
 : @param $options - a map of options passed in by the client
 : @return - nothing
 :)
declare function flow:run-flow(
  $job-id as xs:string,
  $flow as element(hub:flow),
  $identifier as xs:string,
  $options as map:map,
  $mainFunc)
{
  flow:run-flow($job-id, $flow, $identifier, (), $options, $mainFunc)
};

(:~
 : Runs a given flow
 :
 : @param $flow - xml describing the flow
 : @param $identifier - the identifier to send to the flow steps (URI in corb lingo)
 : @param $content - the content being loaded
 : @param $options - a map of options passed in by the client
 : @return - nothing
 :)
declare function flow:run-flow(
  $job-id as xs:string,
  $flow as element(hub:flow),
  $identifier as xs:string,
  $content as item()?,
  $options as map:map,
  $mainFunc)
{
  (: assert that we are in query mode :)
  (: This statement has been removed because server prevents it in 9.0-7 :)
  (: That means that mlcp flows are probably running in update mode :)
  (: let $_must_run_in_query_mode as xs:unsignedLong := xdmp:request-timestamp() :)

  (: configure the global context :)
  let $_ := (
    rfc:with-job-id($job-id),
    rfc:with-flow($flow),
    map:get($options, "target-database") ! rfc:with-target-database(.)
  )

  (: configure the item context :)
  let $item-context := rfc:new-item-context()
    => rfc:with-id($identifier)
    => rfc:with-content(
        if ($content instance of document-node()) then
          if (fn:count($content/node()) > 1) then
            $content
          else
            $content/node()
        else $content
      )
    => rfc:with-options($options)
    => rfc:with-trace(trace:new-trace())

  (: run the users main.(sjs|xqy) :)
  return
    flow:run-main($item-context, $mainFunc)
};

declare function flow:clean-data($resp, $destination, $data-format)
{
  let $resp :=
    typeswitch($resp)
      case document-node() return
        if (fn:count($resp/node()) > 1) then
          if (fn:count($resp/element()) > 1) then
            fn:error((), "DATAHUB-TOO-MANY-NODES", "Too Many Nodes!. Return just 1 node")
          else
            $resp
        else
          $resp/node()
      default return
        $resp

  (: clean up output :)
  return
    typeswitch($resp)
      case binary() return xs:hexBinary($resp)
      case object-node() | json:object return
        (: object with $type key is ES response type :)
        if ($resp instance of map:map and map:keys($resp) = "$type") then
          $resp
        else if ($data-format = $consts:XML) then
          json:transform-from-json($resp, json:config("custom"))
        else
          $resp
      case json:array return
        if ($data-format = $consts:XML) then
          json:array-values($resp)
        else
          $resp
      case empty-sequence() return
        if ($destination = "headers" and $data-format = $consts:JSON) then
          json:object()
        else if ($destination = "triples" and $data-format = $consts:JSON) then
          json:array()
        else
          $resp
      default return
        if ($data-format = $consts:JSON and
            rfc:get-code-format() = $consts:XQUERY and
            $destination = "triples") then
          json:to-array($resp)
        else
          $resp
};

(:~
 : parse out invalid elements from json conversion, such as comments and PI
 :
 : @param $input - the xml you want cleaned
 : @return - a copy of the xml without the bad elements
 :)
declare function flow:clean-xml-for-json($input as item()*) as item()* {
  for $node in $input
  return
    typeswitch($node)
      case text()
        return fn:replace($node,"<\?[^>]+\?>","")
      case element()
        return
          element {name($node)} {

          (: output each attribute in this element :)
            for $att in $node/@*
            return
              attribute {name($att)} {$att}
            ,
            (: output all the sub-elements of this element recursively :)
            for $child in $node
            return flow:clean-xml-for-json($child/node())

          }
      case processing-instruction()
        return ()
      case comment()
        return ()
    (: otherwise pass it through.  Used for text(), comments, and PIs :)
      default return $node
};


(:~
 : Construct an envelope
 :
 : @param $map - a map with all the stuff in it
 : @return - the newly constructed envelope
 :)
declare function flow:make-envelope($content, $headers, $triples, $data-format)
  as document-node()
{
  let $content := flow:clean-data($content, "content", $data-format)
  let $headers := flow:clean-data($headers, "headers", $data-format)
  let $triples := flow:clean-data($triples, "triples", $data-format)
  return
    if ($data-format = $consts:JSON) then
      let $envelope :=
        let $o := json:object()
        let $_ := (
          map:put($o, "headers", $headers),
          map:put($o, "triples", $triples),
          map:put($o, "instance",
            if ($content instance of map:map and map:keys($content) = "$type") then
              let $json := flow:instance-to-canonical-json($content)
              let $info :=
                let $o :=json:object()
                let $_ := (
                  map:put($o, "title", map:get($content, "$type")),
                  map:put($o, "version",  map:get($content, "$version"))
                )
                return $o
              let $_ := map:put($json, "info", $info)
              return $json
            else
              $content
          ),
          map:put($o, "attachments",
            if ($content instance of map:map and map:keys($content) = "$attachments") then
              if(map:get($content, "$attachments")/node() instance of element()) then
                let $c := json:config("custom")
                let $_ := map:put($c,"whitespace" , "ignore" )
                let $_ := map:put($c, "element-namespace", "http://marklogic.com/entity-services")
                return json:transform-to-json(flow:clean-xml-for-json(map:get($content, "$attachments")/node()),$c)
              else
                map:get($content, "$attachments")
            else
              ()
          )
        )
        return
          $o
      let $wrapper := json:object()
      let $_ := map:put($wrapper, "envelope", $envelope)
      return
        xdmp:to-json($wrapper)
    else if ($data-format = $consts:XML) then
      document {
        
          {$headers}
          {$triples}
          
          {
            if ($content instance of map:map and map:keys($content) = "$type") then (
              
                {map:get($content, "$type")}
                {map:get($content, "$version")}
              ,
              flow:instance-to-canonical-xml($content)
            )
            else
              $content
          }
          
          
          {
            if ($content instance of map:map and map:keys($content) = "$attachments") then
              if(map:get($content, "$attachments") instance of element() or
                 map:get($content, "$attachments")/node() instance of element()) then
                map:get($content, "$attachments")
              else
                let $c := json:config("basic")
                let $_ := map:put($c,"whitespace" , "ignore" )
                return
                 json:transform-from-json(map:get($content, "$attachments"),$c)
            else
              ()
          }
          
        
      }
    else
      fn:error((), "RESTAPI-INVALIDCONTENT", "Invalid data format: " || $data-format)
};

declare function flow:make-legacy-envelope($content, $headers, $triples, $data-format)
  as document-node()
{
  let $content := flow:clean-data($content, "content", $data-format)
  let $headers := flow:clean-data($headers, "headers", $data-format)
  let $triples := flow:clean-data($triples, "triples", $data-format)
  return
    if ($data-format = $consts:JSON) then
      let $envelope :=
        let $o := json:object()
        let $_ := (
          map:put($o, "headers", $headers),
          map:put($o, "triples", $triples),
          map:put($o, "content", $content)
        )
        return
          $o
      let $wrapper := json:object()
      let $_ := map:put($wrapper, "envelope", $envelope)
      return
        xdmp:to-json($wrapper)
    else if ($data-format = $consts:XML) then
      document {
        
          {$headers}
          {$triples}
          {$content}
        
      }
    else
      fn:error((), "RESTAPI-INVALIDCONTENT", "Invalid data format: " || $data-format)
};

declare function flow:instance-to-canonical-json(
  $entity-instance as map:map) as json:object
{
  let $o :=
    if ( map:contains($entity-instance, "$ref") ) then
      map:get($entity-instance, "$ref")
    else
      let $o := json:object()
      let $_ := (
        for $key in map:keys($entity-instance)
        let $instance-property := map:get($entity-instance, $key)
        where ($key castable as xs:NCName and not($key = ("$type", "$attachments","$version")))
        return
          typeswitch ($instance-property)
          (: This branch handles embedded objects.  You can choose to prune
             an entity's representation of extend it with lookups here. :)
          case json:object+ return
            for $prop in $instance-property
            return
              map:put($o, $key, flow:instance-to-canonical-json($prop))
          (: An array can also treated as multiple elements :)
          case json:array return
            let $a := json:array()
            let $_ :=
              for $val in json:array-values($instance-property)
              return
                if ($val instance of json:object) then
                  json:array-push($a, flow:instance-to-canonical-json($val))
                else
                  json:array-push($a, $val)
            return
              map:put($o, $key, $a)
          (: A sequence of values should be simply treated as multiple elements :)
          case item()+ return
            for $val in $instance-property
            return
              map:put($o, $key, $val)
          default return
            map:put($o, $key, $instance-property)
      )
      return
        $o
  let $root-object :=
    if (map:contains($entity-instance, "$type")) then
      let $object := json:object()
      let $_ := map:put($object, map:get($entity-instance, "$type"), $o)
      return $object
    else
      $o
  return
    $root-object
};

declare function flow:instance-to-canonical-xml(
  $entity-instance as map:map) as element()
{
  (: Construct an element that is named the same as the Entity Type :)
  let $namespace := map:get($entity-instance, "$namespace")
  let $namespace-prefix := map:get($entity-instance, "$namespacePrefix")
  let $nsdecl :=
    if ($namespace) then
      namespace {$namespace-prefix} {$namespace}
    else ()
  let $type-name := map:get($entity-instance, '$type')
  let $type-qname :=
    if ($namespace)
    then fn:QName($namespace, $namespace-prefix || ":" || $type-name)
    else $type-name
  return
    element { $type-qname } {
      $nsdecl,
      if ( map:contains($entity-instance, "$ref")) then
        map:get($entity-instance, "$ref")
      else
        for $key in map:keys($entity-instance)
        let $instance-property := map:get($entity-instance, $key)
        let $ns-key :=
          if ($namespace and $key castable as xs:NCName)
          then fn:QName($namespace, $namespace-prefix || ":" || $key)
          else $key
        where ($key castable as xs:NCName and $key ne "$type")
        return
          typeswitch ($instance-property)
          (: This branch handles embedded objects.  You can choose to prune
           an entity's representation of extend it with lookups here. :)
            case json:object+ return
              for $prop in $instance-property
              return
                element {$ns-key} {
                  flow:instance-to-canonical-xml($prop)
                }
          (: An array can also treated as multiple elements :)
            case json:array return
              for $val in json:array-values($instance-property)
              return
                if ($val instance of json:object) then
                  element {$ns-key} {
                    attribute datatype {"array"},
                    flow:instance-to-canonical-xml($val)
                  }
                else
                  element {$ns-key} {
                    attribute datatype {"array"},
                    $val
                  }
          (: A sequence of values should be simply treated as multiple elements :)
            case item()+ return
              for $val in $instance-property
              return
                element {$ns-key} {$val}
            default return
              element {$ns-key} {$instance-property}
    }
};

(: Formats the input for a trace based on the plugin type :)
declare function flow:get-trace-input(
  $plugin as element(hub:plugin),
  $data-format as xs:string,
  $content as item()?,
  $headers as item()*,
  $flow-type as xs:string)
{
  let $destination as xs:string := $plugin/@dest
  let $content :=
    if ($content instance of document-node()) then
      $content/node()
    else
      $content
  let $o := json:object()
  let $_ :=
    switch($destination)
      case "content" return
        if ($flow-type = $consts:INPUT_FLOW) then
          if ($content instance of binary()) then
            map:put($o, "rawContent", xs:hexBinary($content))
          else
            map:put($o, "rawContent", $content)
        else
          ()
      case "headers" return
        map:put($o, "content", $content)
      case "triples" return
      (
        map:put($o, "content", $content),
        map:put($o, "headers", ($headers, null-node{})[1])
      )
      default return ()
  return
    if ($data-format = $consts:XML) then
      for $key in map:keys($o)
      return
        element { $key } {
          let $value := map:get($o, $key)
          return
            if ($value instance of null-node()) then ()
            else $value
        }
    else
      $o
};

declare function flow:set-default-options(
  $options as map:map,
  $flow as element(hub:flow))
{
  map:put($options, "entity", fn:string($flow/hub:entity)),
  map:put($options, "flow", fn:string($flow/hub:name)),
  map:put($options, "flowType", fn:string($flow/hub:type)),
  map:put($options, "dataFormat", fn:string($flow/hub:data-format))
};

declare function flow:get-main(
  $main as element(hub:main))
{
  let $module-uri as xs:string? := $main/@module
  let $_ :=
    (: sanity check on required info :)
    if (fn:empty($module-uri) or fn:empty($main/@code-format)) then
      fn:error((), "DATAHUB-INVALID-PLUGIN", "The plugin definition is invalid.")
    else ()

  let $_ := rfc:with-module-uri($module-uri)
  let $_ := rfc:with-code-format($main/@code-format)
  let $duration := xs:dayTimeDuration("PT10S")
  let $key := $MAIN-CACHE-KEY-PREFIX||$module-uri
  let $main-func :=  hul:from-field-cache-or-empty($key, $duration)
  return
    if (fn:exists($main-func)) then $main-func
    else
      let $main-func := flow:make-function($main/@code-format, "main", $module-uri)
      let $_ := hul:set-field-cache($key, $main-func, $duration)
      return
        $main-func
};

declare function flow:run-main(
  $item-context as map:map,
  $func)
{
  let $before := xdmp:elapsed-time()
  let $_ := map:put($context-queue, rfc:get-id($item-context), $item-context)
  let $resp := try {
    let $options := rfc:get-options($item-context)
    let $_ := map:set-javascript-by-ref($options, fn:true())
    let $resp :=
      if (rfc:get-flow-type() eq $consts:HARMONIZE_FLOW) then
        $func(rfc:get-id($item-context), $options)
      else
        $func(rfc:get-id($item-context), rfc:get-content($item-context), $options)
    return
      $resp
  }
  catch($ex) {
    if ($ex/error:code eq "DATAHUB-PLUGIN-ERROR") then
      (: plugin errors are already handled :)
      ()
    else (
      (: this is an error in main.(sjs|xqy) :)
      debug:log(xdmp:describe($ex, (), ())),

      (: log the trace event for main :)
      trace:set-plugin-label(rfc:get-trace($item-context), "main"),
      trace:error-trace($item-context, $ex, xdmp:elapsed-time() - $before)
    ),
    xdmp:rethrow()
  }
  return
    $resp
};

declare function flow:queue-writer(
  $writer-function,
  $identifier as xs:string,
  $envelope as item(),
  $options as map:map)
{
  $writer-queue =>
    map:with($identifier,
      map:map()
        => map:with("writer-function", $writer-function)
        => map:with("envelope", $envelope)
        => map:with("options", $options)
    )
};

declare function flow:populate-queue(
  $queue as map:map)
{
  let $populate-queue :=
    for $key in map:keys($queue)
      return map:put($writer-queue, $key, map:get($queue, $key))
  return $populate-queue
};

declare function flow:set-database(
  $database-name as xs:string)
{
  (xdmp:set($current-database, xdmp:database($database-name)), $current-database)
};

declare function flow:run-writers(
  $identifiers as xs:string*)
{
  let $updated-settings := xdmp:eval('
    import module namespace flow = "http://marklogic.com/data-hub/flow-lib"
      at "/data-hub/4/impl/flow-lib.xqy";

    import module namespace rfc = "http://marklogic.com/data-hub/run-flow-context"
      at "/data-hub/4/impl/run-flow-context.xqy";

    import module namespace trace = "http://marklogic.com/data-hub/trace"
      at "/data-hub/4/impl/trace-lib.xqy";

    declare variable $identifiers external;
    declare variable $writer-queue external;

    declare option xdmp:mapping "false";

    let $_ :=
      for $identifier in $identifiers
      let $writer-info := map:get($writer-queue, $identifier)
      return (
        if (fn:exists($writer-info)) then
          flow:run-writer(
            map:get($writer-info, "writer-function"),
            $identifier,
            map:get($writer-info, "envelope"),
            map:get($writer-info, "options")
          )
        else ()
      )
    return
      map:map()
  ',
  map:new((
    map:entry("identifiers", $identifiers),
    map:entry("writer-queue", $writer-queue)
  )),
  map:new((
    map:entry("ignoreAmps", fn:true()),
    map:entry("isolation", "different-transaction"),
    map:entry("database", $current-database),
    map:entry("commit", "auto"),
    map:entry("update", "true")
  )))
  return
    xdmp:set($trace:current-trace-settings, map:map())
};

(:~
 : Run a given writer
 :
 : @param $writer - xml describing the writer to run
 : @param $identifier - the identifier to send to the flow steps (URI in corb lingo)
 : @param $envelope - the envelope
 : @param $options - a map of options passed in by the client
 : @return - the output of the writer. It varies.
 :)
declare function flow:run-writer(
  $writer-function,
  $identifier as xs:string,
  $envelope as item(),
  $options as map:map)
{
  let $resp := $writer-function($identifier, $envelope, $options)
  return $resp
};

declare function flow:make-error-json(
  $errors as json:object,
  $entity as xs:string,
  $flow  as xs:string,
  $plugin as xs:string,
  $ex
) {
  let $eo :=
    if (map:contains($errors, $entity)) then
      map:get($errors, $entity)
    else
      let $e := map:map()
      let $_ := map:put($errors, $entity, $e)
      return
        $e
  let $fo :=
    if (map:contains($eo, $flow)) then
      map:get($eo, $flow)
    else
      let $f := map:map()
      let $_ := map:put($eo, $flow, $f)
      return
        $f

  let $f := $ex/error:stack/error:frame[1]
  return
    map:put($fo, $plugin, map:new((
      map:entry("uri", $f/error:uri/fn:data()),
      map:entry("line", $f/error:line/fn:data()),
      map:entry("column", $f/error:column/fn:data()),
      map:entry("msg", $ex/error:format-string/fn:data())
    )))
};

declare function flow:validate-entities()
{
  let $errors := json:object()
  let $options := map:map()
  let $_ :=
    for $entity in flow:get-entities()/hub:entity
    for $flow in $entity/hub:flows/hub:flow
    let $data-format := $flow/hub:data-format
    (: validate collector :)
    let $_ :=
      try {
        let $collector := $flow/hub:collector
        return
          if ($collector) then
            let $module-uri := $collector/@module
            let $ns := flow:get-module-ns($collector/@code-format)
            return
              if ($collector/@code-format eq $consts:XQUERY) then
                xdmp:eval(
                  'import module namespace x = "' || $ns || '" at "' || $module-uri || '"; ' ||
                  '()',
                  map:new((map:entry("staticCheck", fn:true())))
                )
              else
                xdmp:javascript-eval(
                  'var x = require("' || $module-uri || '");',
                  map:new((map:entry("staticCheck", fn:true())))
                )
          else ()
      }
      catch($ex) {
        flow:make-error-json(
          $errors,
          $entity/hub:name,
          $flow/hub:name,
          "collector",
          $ex)
      }
    (: validate plugins :)
    let $_ :=
      for $main in $flow/hub:main
      let $module-uri := $main/@module
      let $ns := flow:get-module-ns($main/@code-format)
      return
        (:
         : Note that we are static checking the files.
         : This is because there is no reasonable way to actually
         : call the plugins and pass in data that will work for all plugins.
         :
         : The disadvantage to static checking is that we will not catch typos
         : like ctsdoc <- (missing period) because Javascript is dynamically
         : typed. Static checking will only catch syntax errors in sjs.
         :)
        try {
          if ($main/@code-format eq $consts:XQUERY) then
            xdmp:eval(
              'import module namespace x = "' || $ns || '" at "' || $module-uri || '"; ' ||
              '()',
           map:new((map:entry("staticCheck", fn:true())))
            )
          else
            xdmp:javascript-eval(
              'var x = require("' || $module-uri || '");',
              map:new((map:entry("staticCheck", fn:true())))
            )
        }
        catch($ex) {
          let $plugin :=
            let $uri := ($ex/error:stack/error:frame)[1]/error:uri
            let $name := hul:get-file-name(hul:get-file-from-uri($uri))
            return
              if ($name eq "[anonymous]") then
                fn:replace($ex/error:expr, ".+/([^.]+)\.(sjs|xqy).*", "$1")
              else
                $name
          return
            flow:make-error-json(
              $errors,
              $entity/hub:name,
              $flow/hub:name,
              $plugin,
              $ex)
        }
    return
      ()
  return
    map:new(
      map:entry("errors", $errors)
    )
};

declare function flow:safe-run($func)
{
  let $before := xdmp:elapsed-time()
  return
    try {
      let $resp := $func()
      let $duration := xdmp:elapsed-time() - $before
      let $_ := trace:plugin-trace($rfc:item-context, $resp, $duration)
      return
        $resp
    }
    catch($ex) {
      debug:log(xdmp:describe($ex, (), ())),
      trace:error-trace($rfc:item-context, $ex, xdmp:elapsed-time() - $before),
      fn:error((), "DATAHUB-PLUGIN-ERROR", $ex)
    }
};

declare %private function flow:make-function(
  $code-format as xs:string?,
  $func-name as xs:string,
  $module-uri as xs:string)
{
  let $ns := flow:get-module-ns($code-format)
  return
    xdmp:function(fn:QName($ns, $func-name), $module-uri)
};




© 2015 - 2024 Weber Informatics LLC | Privacy Policy