@@ -27,69 +27,94 @@ const xsltPermissions = [
2727
2828const reservedNamespaces = [ 'm' , 'map' ] ;
2929
30- function buildMappingXML ( mappingJSON ) {
31- // Obtain all linked JSON mappings
32- const relatedMappings = getRelatedMappings ( mappingJSON ) . map ( ( mappingDoc ) => mappingDoc . toObject ( ) ) ;
30+ /**
31+ * Build an XML mapping template in the http://marklogic.com/entity-services/mapping namespace, which can then be the
32+ * input to the Entity Services mappingPut function that generates an XSLT template.
33+ *
34+ * @param mappingDoc expected to be a document-node and not an object asdfasdf
35+ * @return {* }
36+ */
37+ function buildMappingXML ( mappingDoc ) {
3338 if ( dhMappingTraceIsEnabled ) {
3439 xdmp . trace ( dhMappingTrace , 'Building mapping XML' ) ;
3540 }
36- // for each mapping build out the mapping XML
37- const entityTemplates = [ ] ;
38- const mappingJsonObj = mappingJSON . toObject ( ) ;
39- const parentEntity = getTargetEntity ( fn . string ( mappingJsonObj . targetEntityType ) ) ;
40- for ( let mapping of relatedMappings ) {
41+
42+ const mappingObject = mappingDoc . toObject ( ) ;
43+ const rootEntityTypeTitle = getEntityName ( mappingObject . targetEntityType ) ;
44+
45+ // For the root mapping and for each nested object property (regardless of depth), build an object with a single
46+ // property of the path of the mapping and a value of the mapping. Each of these will then become an XML m:entity template.
47+ const rootMapping = { } ;
48+ rootMapping [ rootEntityTypeTitle ] = mappingObject ;
49+ let mappings = [ rootMapping ] ;
50+ mappings = mappings . concat ( getObjectPropertyMappings ( mappingObject , rootEntityTypeTitle ) ) ;
51+
52+ const parentEntity = getTargetEntity ( fn . string ( mappingObject . targetEntityType ) ) ;
53+
54+ // For each mapping, build an m:entity template
55+ const entityTemplates = mappings . map ( objectPropertyMapping => {
56+ const propertyPath = Object . keys ( objectPropertyMapping ) [ 0 ] ;
57+ const mapping = objectPropertyMapping [ propertyPath ] ;
4158 if ( dhMappingTraceIsEnabled ) {
42- xdmp . trace ( dhMappingTrace , `Generating template for ${ mapping . targetEntityType } ` ) ;
59+ xdmp . trace ( dhMappingTrace , `Generating template for propertyPath ' ${ propertyPath } ' and entityTypeId ' ${ mapping . targetEntityType } ' ` ) ;
4360 }
44- let entity = ( mapping . targetEntityType . startsWith ( "#/definitions/" ) ) ? parentEntity : getTargetEntity ( mapping . targetEntityType ) ;
45- let entityTemplate = buildEntityMappingXML ( mapping , entity ) ;
61+ const model = ( mapping . targetEntityType . startsWith ( "#/definitions/" ) ) ? parentEntity : getTargetEntity ( mapping . targetEntityType ) ;
62+ const template = buildEntityTemplate ( mapping , model , propertyPath ) ;
4663 if ( dhMappingTraceIsEnabled ) {
47- xdmp . trace ( dhMappingTrace , `Generated template: ${ entityTemplate } ` ) ;
64+ xdmp . trace ( dhMappingTrace , `Generated template: ${ template } ` ) ;
4865 }
49- entityTemplates . push ( entityTemplate ) ;
50- }
51- let entityName = getEntityName ( mappingJSON . root . targetEntityType ) ;
66+ return template ;
67+ } ) ;
68+
5269 const namespaces = [ ] ;
53- if ( mappingJsonObj . namespaces ) {
54- for ( const prefix of Object . keys ( mappingJsonObj . namespaces ) . sort ( ) ) {
55- if ( mappingJsonObj . namespaces . hasOwnProperty ( prefix ) ) {
70+ if ( mappingObject . namespaces ) {
71+ for ( const prefix of Object . keys ( mappingObject . namespaces ) . sort ( ) ) {
72+ if ( mappingObject . namespaces . hasOwnProperty ( prefix ) ) {
5673 if ( reservedNamespaces . includes ( prefix ) ) {
5774 throw new Error ( `'${ prefix } ' is a reserved namespace.` ) ;
5875 }
59- namespaces . push ( `xmlns:${ prefix } ="${ mappingJsonObj . namespaces [ prefix ] } "` ) ;
76+ namespaces . push ( `xmlns:${ prefix } ="${ mappingObject . namespaces [ prefix ] } "` ) ;
6077 }
6178 }
6279 }
63- // compose the final template
80+
6481 // Importing the "map" namespace fixes an issue when testing a mapping from QuickStart that hasn't been reproduced
6582 // yet in a unit test; it ensures that the map:* calls in the XSLT resolve to map functions.
66- let finalTemplate = `
67- <m:mapping xmlns:m="http://marklogic.com/entity-services/mapping" xmlns:map="http://marklogic.com/xdmp/map" ${ namespaces . join ( ' ' ) } >
83+ return xdmp . unquote ( `
84+ <m:mapping xmlns:m="http://marklogic.com/entity-services/mapping" xmlns:map="http://marklogic.com/xdmp/map" ${ namespaces . join ( ' ' ) } >
6885 ${ retrieveFunctionImports ( ) }
6986 ${ entityTemplates . join ( '\n' ) }
70- <!-- Default entity is ${ entityName } -->
87+ <!-- Default entity is ${ rootEntityTypeTitle } -->
7188 <m:output>
7289 <m:for-each><m:select>/</m:select>
73- <m:call-template name="${ entityName } " />
90+ <m:call-template name="${ rootEntityTypeTitle } " />
7491 </m:for-each>
7592 </m:output>
76- </m:mapping>
77- ` ;
78- return xdmp . unquote ( finalTemplate ) ;
93+ </m:mapping>` ) ;
7994}
8095
81- function buildMapProperties ( mapping , entityModel ) {
96+ /**
97+ * Returns a string of XML. The XML contains elements in the http://marklogic.com/entity-services/mapping namespace,
98+ * each of which represents a mapping expression in the given mapping.
99+ *
100+ * @param mapping a JSON mapping with a properties array containing mapping expressions
101+ * @param model the ES model, containing a definitions array of entity types
102+ * @param propertyPath the path in the entity type for the property being mapped. This is used for nested object
103+ * properties, where a call-template element must be built that references a template constructed by buildEntityTemplate
104+ * @return {string }
105+ */
106+ function buildMapProperties ( mapping , model , propertyPath ) {
82107 let mapProperties = mapping . properties ;
83108 let propertyLines = [ ] ;
84109 if ( dhMappingTraceIsEnabled ) {
85110 xdmp . trace ( dhMappingTrace , `Building mapping properties for '${ mapping . targetEntityType } ' with
86- '${ xdmp . describe ( entityModel ) } '` ) ;
111+ '${ xdmp . describe ( model ) } '` ) ;
87112 }
88113 let entityName = getEntityName ( mapping . targetEntityType ) ;
89114 if ( dhMappingTraceIsEnabled ) {
90115 xdmp . trace ( dhMappingTrace , `Using entity name: ${ entityName } ` ) ;
91116 }
92- let entityDefinition = entityModel . definitions [ entityName ] ;
117+ let entityDefinition = model . definitions [ entityName ] ;
93118 if ( dhMappingTraceIsEnabled ) {
94119 xdmp . trace ( dhMappingTrace , `Using entity definition: ${ entityDefinition } ` ) ;
95120 }
@@ -121,8 +146,9 @@ function buildMapProperties(mapping, entityModel) {
121146 if ( isInternalMapping || isArray ) {
122147 let propLine ;
123148 if ( isInternalMapping ) {
124- let subEntityName = getEntityName ( mapProperty . targetEntityType ) ;
125- propLine = `<${ propTag } ${ isArray ? 'datatype="array"' :'' } ><m:call-template name="${ subEntityName } "/></${ propTag } >` ;
149+ // The template name will match one of the templates constructed by getObjectPropertyTemplates
150+ const templateName = propertyPath == "" ? prop : propertyPath + "." + prop ;
151+ propLine = `<${ propTag } ${ isArray ? 'datatype="array"' :'' } ><m:call-template name="${ templateName } "/></${ propTag } >` ;
126152 } else {
127153 propLine = `<${ propTag } datatype="array" xsi:type="xs:${ dataType } "><m:val>.</m:val></${ propTag } >` ;
128154 }
@@ -141,16 +167,36 @@ function buildMapProperties(mapping, entityModel) {
141167 return propertyLines . join ( '\n' ) ;
142168}
143169
144- function getRelatedMappings ( mapping , related = [ mapping ] ) {
145- // get references to sub mappings
170+ /**
171+ * Recursive function that returns a mapping for each property with a targetEntityType, which signifies that it is
172+ * mapping to an object property. Each of these will need to be converted into an m:entity XML template. The name of
173+ * each template is guaranteed to be unique by being based on the propertyPath and the title of each object property
174+ * being mapped. This ensures that we have uniquely-named templates in the XSLT transform that's generated from the
175+ * XML mapping template.
176+ *
177+ * @param mapping
178+ * @param propertyPath
179+ * @param objectPropertyMappings
180+ * @return {*[] }
181+ */
182+ function getObjectPropertyMappings ( mapping , propertyPath , objectPropertyMappings = [ ] ) {
146183 if ( dhMappingTraceIsEnabled ) {
147184 xdmp . trace ( dhMappingTrace , `Getting related mappings for '${ xdmp . describe ( mapping ) } '` ) ;
148185 }
149- let internalMappings = mapping . xpath ( '/properties//object-node()[exists(targetEntityType) and exists(properties)]' ) ;
150- for ( let internalMapping of internalMappings ) {
151- related . push ( internalMapping ) ;
186+ if ( mapping . properties ) {
187+ Object . keys ( mapping . properties ) . forEach ( propertyTitle => {
188+ const property = mapping . properties [ propertyTitle ] ;
189+ if ( property . targetEntityType && property . properties ) {
190+ const propertyMapping = { } ;
191+ const nestedPropertyPath = propertyPath == "" ? propertyTitle : propertyPath + "." + propertyTitle ;
192+ propertyMapping [ nestedPropertyPath ] = property ;
193+ objectPropertyMappings . push ( propertyMapping ) ;
194+
195+ getObjectPropertyMappings ( property , nestedPropertyPath , objectPropertyMappings ) ;
196+ }
197+ } ) ;
152198 }
153- return related ;
199+ return objectPropertyMappings ;
154200}
155201
156202function getTargetEntity ( targetEntityType ) {
@@ -182,17 +228,27 @@ function retrieveFunctionImports() {
182228 return customImports . join ( '\n' ) ;
183229}
184230
185- function buildEntityMappingXML ( mapping , entity ) {
186- let entityTitle = entity . info . title ;
231+ /**
232+ * Build an "entity template", defined by an entity element in the http://marklogic.com/entity-services/mapping
233+ * namespace, for the given property mapping.
234+ *
235+ * @param mapping
236+ * @param model
237+ * @param propertyPath the path in the entity type for the property being mapped. This is used as the name of the
238+ * entity template, and thus it will also be used in call-template references to this template.
239+ *
240+ * @return {string }
241+ */
242+ function buildEntityTemplate ( mapping , model , propertyPath ) {
187243 let entityName = getEntityName ( mapping . targetEntityType ) ;
188- let entityDefinition = entity . definitions [ entityName ] ;
244+ let entityDefinition = model . definitions [ entityName ] ;
189245 let namespacePrefix = entityDefinition . namespacePrefix ;
190246 let entityTag = namespacePrefix ? `${ namespacePrefix } :${ entityName } ` : entityName ;
191247 let namespaceNode = `xmlns${ namespacePrefix ? `:${ namespacePrefix } ` : '' } ="${ entityDefinition . namespace || '' } "` ;
192248 return `
193- <m:entity name="${ entityName } " xmlns:m="http://marklogic.com/entity-services/mapping">
249+ <m:entity name="${ propertyPath } " xmlns:m="http://marklogic.com/entity-services/mapping">
194250 <${ entityTag } ${ namespaceNode } xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
195- ${ buildMapProperties ( mapping , entity ) }
251+ ${ buildMapProperties ( mapping , model , propertyPath ) }
196252 </${ entityTag } >
197253 </m:entity>` ;
198254}
@@ -446,7 +502,7 @@ module.exports = {
446502 xsltPermissions,
447503 xmlMappingCollections,
448504 buildMappingXML,
449- buildEntityMappingXML ,
505+ buildEntityTemplate ,
450506 extractInstance,
451507 getEntityName,
452508 getTargetEntity,
0 commit comments