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

ml-modules.root.data-hub.5.impl.template-generated.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 hent = "http://marklogic.com/data-hub/hub-entities";

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

import module namespace ext = "http://marklogic.com/data-hub/extensions/entity"
  at "/data-hub/extensions/entity/post-process-search-options.xqy";

 import module namespace sem = "http://marklogic.com/semantics" at "/MarkLogic/semantics.xqy";

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

declare namespace search = "http://marklogic.com/appservices/search";
declare namespace tde = "http://marklogic.com/xdmp/tde";


declare option xdmp:mapping "true";

declare %private variable $DEFAULT_BASE_URI := "http://example.org/";

declare function extraction-template-generate(
  $model as map:map
) as element(tde:template)
{
  let $top-id := map:get($model,"$id")
  let $schema-name := $model=>map:get("info")=>map:get("title")
  let $entity-type-names := $model=>map:get("definitions")=>map:keys()
  let $scalar-rows := map:map()
  let $secure-tde-name := fn:replace(?, "-", "_")
  let $path-namespaces := map:map()
  let $local-references :=
    if (count($entity-type-names)=1) then ()
    else local-references($model)
  let $top-entity := top-entity($model, true())
     let $top-entity-type := $model=>map:get("definitions")=>map:get($top-entity)
     let $top-primary-key-name := map:get($top-entity-type, "primaryKey")
      let $namespace-prefix := $top-entity-type=>map:get("namespacePrefix")
         let $namespace-uri := $top-entity-type=>map:get("namespace")
         let $prefix-value :=
           if ($namespace-uri)
           then
             $namespace-prefix || ":"
           else ""

  let $maybe-local-refs :=
    if (exists($top-entity)) then () else local-references($model)
  let $_ :=
    for $entity-type-name in $entity-type-names
    let $entity-type := $model=>map:get("definitions")=>map:get($entity-type-name)
    let $primary-key-name := map:get($entity-type, "primaryKey")
    let $required-properties := (map:get($entity-type, "required"))
    let $required-properties := (if (fn:exists($required-properties)) then json:array-values($required-properties) else (), $primary-key-name)
    let $properties := map:get($entity-type, "properties")
    let $primary-key-type := map:get( map:get($properties, $primary-key-name), "datatype" )
    let $namespace-prefix := $entity-type=>map:get("namespacePrefix")
    let $namespace-uri := $entity-type=>map:get("namespace")
    let $prefix-value :=
      (
        map:put($path-namespaces,
          $namespace-prefix,
          
            {$namespace-prefix}
            {$namespace-uri}
          ),
        if ($namespace-prefix)
        then $namespace-prefix || ":"
        else ""
      )
    return
      map:put($scalar-rows, $entity-type-name,
        
          
            { $schema-name=>$secure-tde-name() }
            { $entity-type-name=>$secure-tde-name() }
            sparse
            
              {
                for $property-name in map:keys($properties)
                let $property-definition := map:get($properties, $property-name)
                let $items-map := map:get($property-definition, "items")
                let $thisPropertyIsRequired := fn:exists(index-of(($required-properties), $property-name))
                let $datatype :=
                  if (map:get($property-definition, "datatype") eq "iri")
                  then "IRI"
                  else map:get($property-definition, "datatype")
                  let $is-nullable :=
                  if($thisPropertyIsRequired)
                  then false
                  else true
                return
                (: if the column is an array, skip it in scalar row :)
                  if (exists($items-map)) then ()
                  else
                    if ( map:contains($property-definition, "$ref") )
                    then
                      
                        { $property-name=>$secure-tde-name() }
                        { let $dt := ref-datatype($model, $entity-type-name, $property-name) return if ($dt="iri") then "IRI" else $dt} 
                        { if ($prefix-value) then "(" || $prefix-value || $property-name || "|" || $property-name || ")" else $property-name }/{ ref-prefixed-name($model, $entity-type-name, $property-name) }{ let $pk := ref-primary-key-name($model, $entity-type-name, $property-name) return if (empty($pk)) then () else "/"||$pk}
                        {$is-nullable}
                      
                    else
                      
                        { $property-name=>$secure-tde-name() }
                        { if ($datatype="iri") then "IRI" else $datatype }
                        { if ($prefix-value) then "(" || $prefix-value || $property-name || "|" || $property-name || ")" else $property-name }
                        {$is-nullable}
                      
              }
            
          
        )
  let $array-rows := map:map()
  let $triples-templates := map:map()
  let $_ :=
    (: this is a long loop.  It creates row-based templates for each
         :  entity-type name, as well as two triples that tie an entity
         :  instance document to its type and document IRI :)
    for $entity-type-name in $entity-type-names
    let $entity-type := $model=>map:get("definitions")=>map:get($entity-type-name)
    let $primary-key-name := map:get($entity-type, "primaryKey")
    let $namespace-prefix := $entity-type=>map:get("namespacePrefix")
    let $prefix-value :=
      if ($namespace-prefix)
      then $namespace-prefix || ":"
      else ""
    let $prefix-path :=
      if ((exists($top-entity) and ($top-entity=$entity-type-name)) or
        (empty($top-entity) and empty($maybe-local-refs)))
      then ""
      else ".//"
    let $required-properties := ($primary-key-name, json:array-values(map:get($entity-type, "required")))
    let $properties := map:get($entity-type, "properties")
    let $primary-key-type := map:get( map:get($properties, $primary-key-name), "datatype" )
    let $column-map := map:map()
    let $_ :=
      for $property-name in map:keys($properties)
      let $property-definition := map:get($properties, $property-name)
      let $items-map := map:get($property-definition, "items")
      let $is-ref := map:contains($items-map, "$ref")
      let $id-for-ref := map:get($items-map, "$id")
      let $id-for-ref :=
        if (empty($id-for-ref) or (exists($top-id) and $id-for-ref=$top-id))
        then ()
        else $id-for-ref
      let $is-local-ref := map:contains($items-map, "$ref") and starts-with( map:get($items-map, "$ref"), "#/definitions/") and empty($id-for-ref)
      let $is-external-ref := $is-ref and not($is-local-ref)
      let $ref-model :=
        if ($is-external-ref)
        then (
          let $ref := map:get($items-map, "$ref")
          let $ref := if (empty($ref) or contains($ref,"#")) then $ref else $ref||"#"
          let $ref-node :=
            if (empty($id-for-ref))
            then fn:doc(substring-before($ref,"#"))
            else fn:doc($id-for-ref)
          return
            if (empty($ref-node)) then $model else model-create($ref-node)
        ) else $model
      let $reference-value :=
        $property-definition=>map:get("items")=>map:get("$ref")
      let $ref-name := functx:substring-after-last($reference-value, "/")
       let $thisPropertyIsRequired := fn:exists(index-of(($required-properties), $property-name))
       let $is-nullable :=
       if($thisPropertyIsRequired)
       then false
       else true
      let $items-datatype :=
        if (map:get($items-map, "datatype") eq "iri")
        then "string"
        else map:get($items-map, "datatype")
      let $ref-primary-key := ref-primary-key-name($ref-model, $entity-type-name, $property-name)
      let $ref-type-name := ref-type-name($model, $entity-type-name, $property-name)
      where exists($items-map)
      return
        map:put($column-map, $property-name,
          
            { if ($prefix-value) then "(" || $prefix-value || $property-name || "|" || $property-name ||")" else $property-name}{if ($is-ref) then concat("/",$ref-name) else ()}[fn:string(.) ne ""]
            
              
                { $schema-name=>$secure-tde-name() }
                { $entity-type-name=>$secure-tde-name() }_{ $property-name=>$secure-tde-name() }
                sparse
                
                  { if (empty($primary-key-name))
                  then comment { "Warning, no primary key in enclosing type",
                    $entity-type-name }
                  else
                    
                      { comment { "This column joins to property",
                      $primary-key-name, "of",
                      $entity-type-name } }
                      { $primary-key-name=>$secure-tde-name() }
                      { if ($primary-key-type="iri") then "IRI" else $primary-key-type }
                      fn:head({if ($prefix-value) then "(ancestor::" || $prefix-value || $entity-type-name || "|" || "ancestor::" || $entity-type-name || ")" else "(ancestor::" || $entity-type-name || ")"}/{ if ($prefix-value) then  "("|| $prefix-value || $primary-key-name || "|" || $primary-key-name || ")" else $primary-key-name }[xs:string(.) ne ""])
                    ,
                  if ($is-local-ref and empty($ref-primary-key))
                  then
                    (
                      map:get($scalar-rows, $ref-type-name)/tde:row[tde:view-name eq $ref-type-name ]/tde:columns/tde:column,
                      map:put($scalar-rows, $ref-type-name,
                        comment { "No extraction template emitted for" ||
                        $ref-type-name ||
                        "as it was incorporated into another view. "
                        }
                      )
                    )
                  else if ($is-local-ref)
                  then
                    
                      { comment { "This column joins to primary key of",
                      $ref-type-name } }
                      { $property-name=>$secure-tde-name() || "_" || $ref-primary-key=>$secure-tde-name() }
                      { let $dt := ref-datatype($model, $entity-type-name, $property-name) return if ($dt="iri") then "IRI" else $dt }
                      { if ($prefix-value) then "(" || $prefix-value || $ref-primary-key || "|" || $ref-primary-key || ")" else $ref-primary-key }
                    
                  else
                    if ($is-external-ref)
                    then
                      
                        { comment { "This column joins to primary key of an external reference" } }
                        { $property-name=>$secure-tde-name() }
                        string
                        .
                        {$is-nullable}
                      
                    else
                      
                        { comment { "This column holds array values from property",
                        $primary-key-name, "of",
                        $entity-type-name } }
                        { $property-name=>$secure-tde-name() }
                        { if ($items-datatype="iri") then "IRI" else $items-datatype }
                        .
                        {$is-nullable}
                      
                  }
                
              
            
          
        )
   let $related-entity-type-property-names := for $property-name in map:keys($properties)
                                 let $property-info := map:get($properties, $property-name)
                                 let $related-entity-type := map:get($property-info, "relatedEntityType" )
                                 where exists($related-entity-type)
                                 return $property-name
   return
        (
          if (exists($primary-key-name))
          then
            map:put($triples-templates, $entity-type-name,
              
                { $prefix-path || (if ($prefix-value) then "(" || $prefix-value || $entity-type-name || "|" || $entity-type-name || ")" else  $entity-type-name)}
                
                  primary-key-valfn:encode-for-uri(fn:head(./{ if ($prefix-value) then "(" || 	$prefix-value || $primary-key-name || "|" || $primary-key-name || ")" else $primary-key-name } ! xs:string(.)[. ne ""]))
                  subject-iri{
                    (: Keep the top entity ID consistent :)
                    if ($entity-type-name eq $top-entity) then
                      '$top-subject-iri'
                    else
                      'sem:iri(concat("'|| model-graph-prefix($model) ||'/'|| $entity-type-name || '/", if (fn:empty($primary-key-val) or $primary-key-val eq "") then sem:uuid-string() else $primary-key-val))'

                  }
                  {
                    for $related-property-name in $related-entity-type-property-names
                    return
                      
                        related-{$related-property-name}
                        fn:encode-for-uri(xs:string(fn:head(./{ if ($prefix-value) then "(" || 	$prefix-value || $related-property-name || "|" || $related-property-name || ")" else $related-property-name })))
                      
                  }
                
                
                  
                    $subject-iri
                    $RDF_TYPE
                    sem:iri("{ model-graph-prefix($model) }/{ $entity-type-name }")
                  
                  
                    $subject-iri
                    sem:iri("http://www.w3.org/2000/01/rdf-schema#isDefinedBy")
                    fn:base-uri(.)
                  

                   
                   {
                      let $related-concepts :=
                          let $has-related-concepts := map:contains($entity-type, "relatedConcepts")
                          let $map-related-concepts := map:get($entity-type, "relatedConcepts")
                          for $concept in json:array-values($map-related-concepts)
                          let $predicate_concept:=map:get($concept, "predicate")
                          let $concept-context:=map:get($concept, "context")
                          let $context := get-property-xpath($model, $entity-type-name, fn:tokenize($concept-context, "/"))
                           let $defaultValueExpression:="sem:iri(fn:replace(fn:string(.),'\\s+', ''))"
                          let $expression:=map:get($concept, "conceptExpression")
                           let $conceptExpression :=
                                 if (fn:string($expression) eq "" or fn:empty($expression))
                                 then $defaultValueExpression
                                 else $expression
                           let $contextWithValidation :=
                                 if (fn:string($context) eq ".")
                                 then $context
                                 else $context || "[  xs:string(.) ne """"]"
                          let $concept_class:=map:get($concept, "conceptClass")
                          where exists($has-related-concepts)
                          return
                            
                             { $contextWithValidation}
                            
                            
                               $subject-iri
                               sem:iri("{ $predicate_concept}")
                                { $conceptExpression}
                            
                             
                               sem:iri("{ model-graph-prefix($model) }/{ $entity-type-name }")
                               sem:iri("http://www.marklogic.com/data-hub#relatedConcept")
                               sem:iri("{ $concept_class}")
                             
                             
                               sem:iri("{ $concept_class}")
                               sem:iri("http://www.marklogic.com/data-hub#conceptPredicate")
                               sem:iri("{ $predicate_concept}")
                             
                           
                            
                      let $referenced-entities :=
                         let $model-type-iri := model-graph-prefix($model) || "/" || $entity-type-name
                         for $property-name in map:keys($properties)
                         let $property-info :=map:get($properties, $property-name)
                         let $reference-to-entity-exists := fn:not($property-name = $primary-key-name) and xdmp:exists(cts:search(
                              fn:collection("http://marklogic.com/entity-services/models"),
                            cts:json-property-scope-query("properties", cts:and-query((
                                    cts:json-property-value-query("relatedEntityType", $model-type-iri),
                                    cts:json-property-value-query("joinPropertyName", $property-name)
                                )))
                            ))
                         where $reference-to-entity-exists
                         return
                            let $context := if ($prefix-value) then "("|| $prefix-value || $property-name || "|" || $property-name || ")" else $property-name
                            let $subject :=  "sem:iri(concat(""" ||$model-type-iri || "/"", fn:encode-for-uri(xs:string(.))))"
                            return  
                              { $context}[xs:string(.) ne ""]
                              
                                
                                  RDF"http://www.w3.org/1999/02/22-rdf-syntax-ns#"
                                
                                
                                  RDF_TYPEsem:iri(concat($RDF, "type"))
                                
                                related-subject-iri{$subject}
                              
                              
                              
                                $related-subject-iri
                                $RDF_TYPE
                                sem:iri("{$model-type-iri}")
                              
                              
                                $related-subject-iri
                                sem:iri("http://www.w3.org/2000/01/rdf-schema#isDefinedBy")
                                fn:base-uri(.)
                              
                              
                                fn:encode-for-uri(fn:head(./*:{$property-name} ! xs:string(.)[. ne ""]))
                                $RDF_TYPE
                                $model-type-iri
                              
                              
                              
                      let $related-properties :=
                                       for $property-name in $related-entity-type-property-names
                                       let $property-info := map:get($properties, $property-name)
                                       let $is-related-entity-type := map:contains($property-info, "relatedEntityType" )
                                       let $related-entity-type := map:get($property-info, "relatedEntityType" )
                                       where exists($related-entity-type)
                                       return
                                          
                                            {$property-name}[xs:string(.) ne ""]
                                            
                                           
                                             $top-subject-iri
                                             sem:iri("{ model-graph-prefix($model) }/{ $entity-type-name }/{$property-name}")
                                             sem:iri(concat("{ $related-entity-type}/", fn:encode-for-uri(fn:string(.))))
                                           
                                           
                                           
                      where fn:exists(($related-concepts,$referenced-entities,$related-properties))
                      return {($related-concepts,$referenced-entities,$related-properties)}
                   }
              )

          else (),
          if (exists(map:keys($column-map)))
          then map:put($array-rows, $entity-type-name, $column-map)
          else ()
        )
  let $entity-type-templates :=
    let $scalar-row-keys := map:keys($scalar-rows)
    for $entity-type-name in $scalar-row-keys
    let $entity-type := $model=>map:get("definitions")=>map:get($entity-type-name)
    let $namespace-prefix := $entity-type=>map:get("namespacePrefix")
    let $namespace-uri := $entity-type=>map:get("namespace")
    let $prefix-value :=
      if ($namespace-uri)
      then
        $namespace-prefix || ":"
      else ""
    let $prefix-path :=
      if ((exists($top-entity) and ($top-entity=$entity-type-name)) or
        (empty($top-entity) and empty($maybe-local-refs)))
      then ""
      else ".//"
    return
      if (empty ( ( json:array-values(
        $entity-type=>map:get("required")),
      $entity-type=>map:get("primaryKey"))
      ))
      then comment { "The standalone template for " || $entity-type-name ||
      " cannot be generated.  Each template row requires " ||
      "a primary key or at least one required property." }
      else
        (
          map:get($triples-templates, $entity-type-name),
          
            { $prefix-path }{ if ($prefix-value) then "(" || $prefix-value || $entity-type-name || "|" || $entity-type-name || ")" else $entity-type-name }
            {
              map:get($scalar-rows, $entity-type-name),
              if (map:contains($array-rows, $entity-type-name))
              then
                let $m := map:get($array-rows, $entity-type-name)
                return
                  { for $k in map:keys($m) return map:get($m, $k) }
              else ()
            }
          
        )
  return
    
      
        Extraction Template Generated from Entity Type Document
        graph uri: {model-graph-iri($model)}
      
      
      //*:instance[*:info/*:version = "{$model=>map:get("info")=>map:get("version")}"]
      {comment{
        " Replace the above with the following line to match XML instances only.  This may speed up indexing
        //es:instance[es:info/es:version =",concat('"',$model=>map:get("info")=>map:get("version"),'"]
')
      },
      comment{
        " Replace the above with the following line to match JSON instances only.  This may speed up indexing
        //instance[info/version =",concat('"',$model=>map:get("info")=>map:get("version"),'"]
')
      }}
      
        RDF"http://www.w3.org/1999/02/22-rdf-syntax-ns#"
        RDF_TYPEsem:iri(concat($RDF, "type"))
         top-primary-key-valfn:encode-for-uri(fn:head(./{ if ($prefix-value) then "(" || $prefix-value || $top-entity || "|" || $top-entity || ")" else $top-entity }/{ if ($prefix-value) then "(" || 	$prefix-value || $top-primary-key-name || "|" || $top-primary-key-name || ")" else $top-primary-key-name } ! xs:string(.)[. ne ""]))
         top-subject-irisem:iri(concat("{ model-graph-prefix($model) }/{ $top-entity }/", if (fn:empty($top-primary-key-val) or $top-primary-key-val eq "") then sem:uuid-string() else $top-primary-key-val))
      
      
        
          es
          http://marklogic.com/entity-services
        
        { ($path-namespaces=>map:keys()) ! ($path-namespaces=>map:get(.)) }
      
      {
        if ( $entity-type-templates/element() )
        then
          
            { $entity-type-templates }
          
        else
          comment { "An entity type must have at least one required column or a primary key to generate an extraction template." }
      }
    
};


declare function model-graph-iri(
    $model as map:map
) as sem:iri
{
    let $info := map:get($model, "info")
    let $base-uri-prefix := resolve-base-uri($info)
    return
    sem:iri(
        concat( $base-uri-prefix,
               map:get($info, "title"),
               "-" ,
               map:get($info, "version")))
};

(: resolves the default URI from a model's info section :)
declare function resolve-base-uri(
    $info as map:map
) as xs:string
{
    let $base-uri := fn:head((map:get($info, "baseUri"), $DEFAULT_BASE_URI))
    return
        if (fn:matches($base-uri, "[#/]$"))
        then $base-uri
        else concat($base-uri, "#")
};

declare function model-graph-prefix(
    $model as map:map
) as sem:iri
{
    let $info := map:get($model, "info")
    let $base-uri-prefix := resolve-base-prefix($info)
    return
    sem:iri(
        concat( $base-uri-prefix,
               map:get($info, "title"),
               "-" ,
               map:get($info, "version")))
};

declare %private function get-property-xpath($model as map:map, $entity-type-name as xs:string, $property-names)
{
  let $xpath := if(fn:empty($property-names)) then ""
  else
    let $curr-prop-name := $property-names[1]
    let $new-prop-names := fn:remove($property-names, 1)
    let $type := ref-type-name($model, $entity-type-name, $curr-prop-name)

    let $entity-type := $model=>map:get("definitions")=>map:get($entity-type-name)
    let $namespace-prefix := $entity-type=>map:get("namespacePrefix")
    let $prefix-value := if ($namespace-prefix) then $namespace-prefix || ":" else ""

    let $prop-name-with-ns := if ($prefix-value) then "("|| $prefix-value || $curr-prop-name || "|" || $curr-prop-name || ")" else $curr-prop-name
    let $type-with-ns := if ($prefix-value) then "("|| $prefix-value || $type || "|" || $type || ")" else $type

    let $curr-path := get-property-xpath($model, $type, $new-prop-names)
    return if(fn:empty($curr-path) or fn:string($curr-path) eq "") then
      if(fn:empty($type) or fn:string($type) eq "") then $prop-name-with-ns
      else fn:concat($prop-name-with-ns, "/", $type-with-ns)
    else fn:concat($prop-name-with-ns, "/", $type-with-ns, "/", $curr-path)
  return $xpath
};

declare %private function ref-prefixed-name(
    $model as map:map,
    $entity-type-name as xs:string,
    $property-name as xs:string
) as xs:string
{
    let $ref-type := ref-type( $model, $entity-type-name, $property-name )
    let $ref-name := ref-type-name($model, $entity-type-name, $property-name)
    let $namespace-prefix := $ref-type=>map:get("namespacePrefix")
    let $is-local-ref := is-local-reference($model, $entity-type-name, $property-name)
    return
        if ($namespace-prefix and $is-local-ref)
        then "(" || $namespace-prefix || ":" || $ref-name || "|" || $ref-name || ")"
        else $ref-name
};

declare %private function resolve-base-prefix(
    $info as map:map
) as xs:string
{
    replace(resolve-base-uri($info), "#", "/")
};


(:
 : Resolves a reference and returns its datatype
 : If the reference is external, return 'string'
 :)
declare %private function ref-datatype(
    $model as map:map,
    $entity-type-name as xs:string,
    $property-name as xs:string
) as xs:string
{
    let $ref-type := ref-type($model, $entity-type-name, $property-name)
    return
        if (is-local-reference($model, $entity-type-name, $property-name))
        then
            (: if the referent type has a primary key, use that type :)
            let $primary-key-property := map:get($ref-type, "primaryKey")
            return
                if (empty($primary-key-property))
                then "string"
                else map:get(
                        map:get(
                            map:get($ref-type, "properties"),
                            $primary-key-property),
                        "datatype")
        else "string"
};

declare %private function ref-type(
    $model as map:map,
    $entity-type-name as xs:string,
    $property-name as xs:string
) as map:map?
{
    $model
        =>map:get("definitions")
        =>map:get( ref-type-name($model, $entity-type-name, $property-name) )
};


(:
 : Given a model, an entity type name and a reference property,
 : return a reference's type name
 :)
declare function ref-type-name(
    $model as map:map,
    $entity-type-name as xs:string,
    $property-name as xs:string
) as xs:string
{
    let $property := $model
        =>map:get("definitions")
        =>map:get($entity-type-name)
        =>map:get("properties")
        =>map:get($property-name)
    let $ref-target := head( ($property=>map:get("$ref"),
                  $property=>map:get("items")=>map:get("$ref") ) )
    return functx:substring-after-last($ref-target, "/")
};


declare %private function is-local-reference(
    $model as map:map,
    $entity-type-name as xs:string,
    $property-name as xs:string
)
{
    let $top-id := map:get($model,"$id")
    let $property := $model
        =>map:get("definitions")
        =>map:get($entity-type-name)
        =>map:get("properties")
        =>map:get($property-name)
    let $id-target :=
      head( ($property=>map:get("$id"),
             $property=>map:get("items")=>map:get("$id") ) )
    let $id-target :=
      if (empty($id-target) or (exists($top-id) and $top-id=$id-target)) then ()
      else $id-target
    let $ref-target := head( ($property=>map:get("$ref"),
                              $property=>map:get("items")=>map:get("$ref") ) )
    return starts-with($ref-target, "#/definitions") and empty($id-target)
};


(: returns empty-sequence if no primary key :)
declare %private function ref-primary-key-name(
    $model as map:map,
    $entity-type-name as xs:string,
    $property-name as xs:string
) as xs:string?
{
    let $ref-type-name := ref-type-name($model, $entity-type-name, $property-name)
    let $ref-target := $model=>map:get("definitions")=>map:get($ref-type-name)
    return
        if (is-local-reference($model, $entity-type-name, $property-name)) then (
          if (map:contains($ref-target, "primaryKey"))
          then map:get($ref-target, "primaryKey")
          else ()
        ) else ()
};


declare %private function model-create(
    $model-descriptor
) as map:map
{
  let $tentative-result :=
    typeswitch ($model-descriptor)
    case document-node() return
        if ($model-descriptor/object-node())
        then xdmp:from-json($model-descriptor)
        else model-from-xml($model-descriptor/node())
    case element() return
        model-from-xml($model-descriptor)
    case object-node() return
        xdmp:from-json($model-descriptor)
    case map:map return $model-descriptor
    default return fn:error((), "ES-MODEL-INVALID")
  return
    if (is-modern-model($tentative-result))
    then $tentative-result
    else modernize($tentative-result)
};


declare function model-from-xml(
    $model as element(es:model)
) as map:map
{
    let $info := json:object()
        =>map:with("title", data($model/es:info/es:title))
        =>map:with("version", data($model/es:info/es:version))
        =>with-if-exists("baseUri", data($model/es:info/es:base-uri))
        =>with-if-exists("description", data($model/es:info/es:description))
    let $definitions :=
        let $d := json:object()
        let $_ :=
            for $entity-type-node in $model/es:definitions/*
            let $entity-type := json:object()
            let $properties := json:object()
            let $_ :=
                for $property-node in $entity-type-node/es:properties/*
                let $property-attributes := json:object()
                    =>with-if-exists("datatype", data($property-node/es:datatype))
                    =>with-if-exists("$ref", data($property-node/es:ref))
                    =>with-if-exists("description", data($property-node/es:description))
                    =>with-if-exists("collation", data($property-node/es:collation))
                    =>with-if-exists("$id", data($property-node/es:id))
                    =>with-if-exists("type", data($property-node/es:type))
                    =>with-if-exists("format", data($property-node/es:format))

                let $items-map := json:object()
                    =>with-if-exists("datatype", data($property-node/es:items/es:datatype))
                    =>with-if-exists("$ref", data($property-node/es:items/es:ref))
                    =>with-if-exists("description", data($property-node/es:items/es:description))
                    =>with-if-exists("collation", data($property-node/es:items/es:collation))
                    =>with-if-exists("$id", data($property-node/es:items/es:id))
                    =>with-if-exists("type", data($property-node/es:items/es:type))
                    =>with-if-exists("format", data($property-node/es:items/es:format))
                let $_ := if (count(map:keys($items-map)) gt 0)
                        then map:put($property-attributes, "items", $items-map)
                        else ()
                return map:put($properties, fn:local-name($property-node), $property-attributes)
            let $_ :=
                $entity-type
                  =>map:with("properties", $properties)
                  =>with-if-exists("primaryKey", data($entity-type-node/es:primary-key))
                  =>with-if-exists("required", json:to-array($entity-type-node/es:required/xs:string(.)))
                  =>with-if-exists("pii", json:to-array($entity-type-node/es:pii/xs:string(.)))
                  =>with-if-exists("namespace", $entity-type-node/es:namespace/xs:string(.))
                  =>with-if-exists("namespacePrefix", $entity-type-node/es:namespace-prefix/xs:string(.))
                  =>with-if-exists("rangeIndex", json:to-array($entity-type-node/es:range-index/xs:string(.)))
                  =>with-if-exists("pathRangeIndex", json:to-array($entity-type-node/es:path-range-index/xs:string(.)))
                  =>with-if-exists("elementRangeIndex", json:to-array($entity-type-node/es:element-range-index/xs:string(.)))
                  =>with-if-exists("wordLexicon", json:to-array($entity-type-node/es:word-lexicon/xs:string(.)))
                  =>with-if-exists("description", data($entity-type-node/es:description))
                  =>with-if-exists("$id", data($entity-type-node/es:id))
            return map:put($d, fn:local-name($entity-type-node), $entity-type)
        return $d

    let $properties :=
      let $top-entity := json:object()
      let $property-node := ($model/es:properties/*)[1] (: There can be only 1 :)
      return (
        (: {entity: { "$ref": "whatever" } } :)
        if (empty($property-node)) then () else (
          let $ref :=
            json:object()=>with-if-exists("$ref", data($property-node/es:ref))
          return json:object()=>map:with( fn:local-name($property-node), $ref )
        )
      )
    return json:object()
        =>map:with("lang","zxx")
        =>map:with("info", $info)
        =>map:with("definitions", $definitions)
        =>with-if-exists("$schema", data($model/es:schema))
        =>with-if-exists("$id", data($model/es:id))
        =>with-if-exists("required", json:to-array($model/es:required))
        =>with-if-exists("properties", $properties)
};



declare function
modernize($model as map:map) as map:map
{
  let $new-model := fix-references($model)
  let $top-entity := top-entity($new-model, true())
  return model-to-json-schema($new-model, $top-entity)
};

declare function
fix-references($model as map:map) as map:map
{
  let $_ :=
    let $top-id := map:get($model,"$id")
    for $entity-type in $model=>map:get("definitions")=>map:keys()
    let $entity := $model=>map:get("definitions")=>map:get($entity-type)
    return walk-to-fix-references($top-id, $entity)
  return $model
};

declare %private function with-if-exists(
    $map as map:map,
    $key-name as xs:string,
    $value as item()?
) as map:map
{
    typeswitch($value)
    case json:array return
        if (json:array-size($value) gt 0)
        then map:put($map, $key-name, $value)
        else ()
    default return
        if (exists($value))
        then map:put($map, $key-name, $value)
        else (),
    $map
};

(:
   Pick a top entity. If this is a modern model, we know already.
   If this is a legacy model, if there is only one entity, that's it.
   If there is only one entity that is NOT the target of a local reference,
   that must be it. Otherwise, we don't know, and we have to fall-back to
   lousy JSON Schemas, and lousy TDE template paths.
 :)
declare function
top-entity($model as map:map, $force as xs:boolean) as xs:string?
{
  let $entity-type-names := $model=>map:get("definitions")=>map:keys()
  return (
    if (is-modern-model($model)) then json:array-values($model=>map:get("required"))
    else if (count($entity-type-names)=1) then $entity-type-names
    else (
      let $local-refs := local-references($model)
      (: All the entities that are not the target of a local reference :)
      let $non-child-entities := $entity-type-names[not(. = $local-refs)]
      return (
        if (count($non-child-entities)=1) then (
          $non-child-entities
        ) else if ($force) then (
          (: No good reason for picking, but pick anyway :)
          $non-child-entities[1]
        ) else () (: No basis for picking :)
      )
    )
  )
};

declare function model-to-json-schema(
    $model as map:map,
    $entity-name as xs:string?
) as map:map
{
  if (is-modern-model($model)) then (
    $model
  ) else if (empty($entity-name)) then (
    model-to-json-schema($model)
  ) else (
    (:
       Take the model and add
       "$schema": "http://json-schema.org/draft-07/schema#" (if no $schema)
       "properties": { <$entity-name> : {"$ref": "#/definitions/<$entity-name>"} },
       "required": [<$entity-name>]
       If there is no definition with that name, error.
       If there is a info/baseUri property, add equivalent $id (if no $id)
       Fix external references.
       [53510] put primaryKey into required array if it is not there already
    :)
    let $new-model := fix-primary-keys(fix-references($model))
    return (
      if (empty($new-model=>map:get("definitions")=>map:get($entity-name))) then (
        fn:error((), "ES-ENTITY-NOTFOUND", $entity-name)
      ) else (
        if (empty($new-model=>map:get("$schema")))
        then map:put($new-model, "$schema", "http://json-schema.org/draft-07/schema#")
        else (),
        map:put($new-model,"lang","zxx"),
        map:put($new-model, "properties",
          map:map()=>map:with($entity-name,
            map:map()=>map:with("$ref", "#/definitions/"||$entity-name))),
        map:put($new-model, "required", json:to-array($entity-name)),
        let $baseUri := $new-model=>map:get("info")=>map:get("baseUri")
        where not(empty($baseUri)) and empty($new-model=>map:get("$id"))
        return map:put($new-model, "$id", $baseUri),
        $new-model
      )
    )
  )
};

declare function
fix-primary-keys($model as map:map) as map:map
{
  let $_ :=
    for $entity-type in $model=>map:get("definitions")=>map:keys()
    let $entity := $model=>map:get("definitions")=>map:get($entity-type)
    return walk-to-fix-primary-keys($entity)
  return $model
};


(:
    primary keys should be required
 :)
declare %private function
walk-to-fix-primary-keys($model as map:map) as empty-sequence()
{
  let $required := $model=>map:get("required")
  let $primaryKey := $model=>map:get("primaryKey")
  where (not(empty($primaryKey)) and
         (empty($required) or not($primaryKey = json:array-values($required)))
        )
  return (
    map:put($model, "required", json:to-array((json:array-values($required), $primaryKey)))
  )
  ,
  for $property in map:get($model, "properties")=>map:keys()
  let $propspec := map:get($model, "properties")=>map:get($property)
  return (
    walk-to-fix-primary-keys($propspec),
    if (exists($propspec=>map:get("items")))
    then walk-to-fix-primary-keys($propspec=>map:get("items"))
    else ()
  )
};


declare function model-to-json-schema(
    $model as map:map
) as map:map
{
  if (is-modern-model($model)) then (
    $model
  ) else (
    (:
       Take the model and add
       "$schema": "http://json-schema.org/draft-07/schema#" (if no $schema)
       "oneOf": [
         {"properties": { <$entity-name1> : {"$ref": "#/definitions/<$entity-name1>"} },
          "required": [<$entity-name1>]
         },
         {"properties": { <$entity-name2> : {"$ref": "#/definitions/<$entity-name2">} },
         "required": [<$entity-name2>]
         },
         ...
       ]
       If there are no definitions, error.
       If there is only one definition, just add it directly instead of the
       "oneOf"
       If there is a info/baseUri property, add equivalent $id (if no $id)
       [53510] put primaryKey into required array if it is not there already
    :)
    let $new-model := fix-primary-keys(fix-references($model))
    return (
      if (empty($new-model=>map:get("definitions")=>map:keys())) then (
        fn:error((), "ES-DEFINITIONS")
      ) else (
        if (empty($new-model=>map:get("$schema")))
        then map:put($new-model, "$schema", "http://json-schema.org/draft-07/schema#")
        else (),
        map:put($new-model,"lang","zxx"),
        let $alts :=
          for $entity-name in $new-model=>map:get("definitions")=>map:keys()
          return
            map:map()=>
              map:with("properties",
                map:map()=>map:with($entity-name,
                  map:map()=>map:with("$ref", "#/definitions/"||$entity-name)))=>
              map:with("required",
                json:to-array($entity-name))
        return (
          if (count($alts)=1) then (
            map:put($new-model, "properties", $alts=>map:get("properties")),
            map:put($new-model, "required", $alts=>map:get("required"))
          ) else (
            map:put($new-model, "oneOf", json:to-array($alts))
          )
        ),
        let $baseUri := $new-model=>map:get("info")=>map:get("baseUri")
        where not(empty($baseUri)) and empty($new-model=>map:get("$id"))
        return map:put($new-model, "$id", $baseUri),
        $new-model
      )
    )
  )
};

(:
   Old style external references are not consistent with $ref
   e.g. { "$ref": "http://example.com/base/OrderDetails" } should be
   { "$id": "http://example.com/base", "$ref": "#/definitions/OrderDetails" }
 :)
declare %private function
walk-to-fix-references($top-id as xs:string?, $model as map:map) as empty-sequence()
{
  (: Only consider $ref if it is non-absolute and doesn't already have the # :)
  let $ref := $model=>map:get("$ref")
  where (not(empty($ref)) and
         not(starts-with($ref,"#")) and
         not(contains($ref,"#")))
  return (
    let $ref-name := functx:substring-after-last($ref, "/")
    let $new-ref := "#/definitions/"||$ref-name
    let $new-id := functx:substring-before-last($ref, "/")
    return (
      map:put($model, "$ref", $new-ref),
      map:put($model, "$id", $new-id)
    )
  )
  ,
  for $property in map:get($model, "properties")=>map:keys()
  let $propspec := map:get($model, "properties")=>map:get($property)
  return (
    walk-to-fix-references($top-id, $propspec),
    if (exists($propspec=>map:get("items")))
    then walk-to-fix-references($top-id, $propspec=>map:get("items"))
    else ()
  )
};

(:
   A well-constructed modern model looks like:
  {
    $schema: ...,
    info: {
      ...
    },
    definitions: {
      E1: {...},
      E2: {...},
    },
    properties: {"E1": {"$ref":"#definitions/properties/E1"}},
    required: ["E1"]
  }
  With one required top level entity
 :)
declare function
is-modern-model($model as map:map) as xs:boolean
{
  exists($model=>map:get("$schema")) and
  exists($model=>map:get("properties")) and
  count($model=>map:get("properties")=>map:keys())=1 and
  exists($model=>map:get("properties")=>map:get(($model=>map:get("properties")=>map:keys())[1])=>map:get("$ref")) and
  exists($model=>map:get("required")) and
  count(json:array-values($model=>map:get("required")))=1
};

declare %private function
local-references($model as map:map) as xs:string*
{
  let $top-id := map:get($model,"$id")
  for $entity-type in $model=>map:get("definitions")=>map:keys()
  let $entity := $model=>map:get("definitions")=>map:get($entity-type)
  return walk-for-ref($top-id, $entity)
};


declare %private function
walk-for-ref($top-id as xs:string?, $model as map:map) as xs:string*
{
  (: Only consider $ref if there is not a $id that makes it non-local :)
  let $id := map:get($model,"$id")
  let $id :=
    if (exists($top-id)) then (
      if (exists($id) and $id!=$top-id) then $id else ()
    ) else (
      $id
    )
  let $ref := map:get($model,"$ref")
  where (exists($ref) and starts-with($ref,"#"))
  return tokenize($ref,"/")[last()]
  ,
  for $property in map:get($model, "properties") ! map:keys(.)
  let $propspec := map:get($model, "properties")=>map:get($property)
  return (
    walk-for-ref($top-id, $propspec),
    if (exists($propspec=>map:get("items")))
    then walk-for-ref($top-id, $propspec=>map:get("items"))
    else ()
  )
};




© 2015 - 2024 Weber Informatics LLC | Privacy Policy