Skip to content

Commit 13448f4

Browse files
rjrudinMarkLogic Builder
authored andcommitted
DHFPROD-4285: Can now map multiple properties of the same entity type
"m:entity" now refers to a unique property path instead of an entity type title.
1 parent 9b7f875 commit 13448f4

File tree

6 files changed

+207
-59
lines changed

6 files changed

+207
-59
lines changed

marklogic-data-hub/src/main/resources/ml-modules/root/data-hub/5/builtins/steps/mapping/entity-services/lib.sjs

Lines changed: 100 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -27,69 +27,94 @@ const xsltPermissions = [
2727

2828
const 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

156202
function 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,

marklogic-data-hub/src/test/ml-modules/root/test/data-hub-test-helper.xqy

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,12 @@ declare function get-first-batch-document()
130130
</options>
131131
)
132132
};
133+
134+
declare function get-modules-document($uri as xs:string)
135+
{
136+
xdmp:invoke-function(function() {fn:doc($uri)},
137+
<options xmlns="xdmp:eval">
138+
<database>{xdmp:database("data-hub-MODULES")}</database>
139+
</options>
140+
)
141+
};

marklogic-data-hub/src/test/ml-modules/root/test/suites/data-hub/5/builtins/steps/mapping/entity-services/createEntityServicesMapping.sjs

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ function applyDefOverridesAndSetCache(customerDefOverrides, orderDefOverrides) {
133133

134134
function constructSingleTemplateWithOverrides(customerDefOverrides, orderDefOverrides) {
135135
const updatedEntities = applyDefOverridesAndSetCache(customerDefOverrides, orderDefOverrides);
136-
return tidyXML(mappingLib.buildEntityMappingXML(baseCustomerMapping.toObject(), updatedEntities.customerEntity.toObject()));
136+
return tidyXML(mappingLib.buildEntityTemplate(baseCustomerMapping.toObject(), updatedEntities.customerEntity.toObject(), "Customer"));
137137
}
138138

139139
function constructEntireNestedTemplateWithOverrides(customerDefOverrides, orderDefOverrides) {
@@ -153,12 +153,12 @@ let expectedTemplate = tidyXML(`
153153
<m:optional><Date xsi:type="xs:dateTime"><m:val>parseDateTime(date, 'DD/MM/YYYY-hh:mm:ss')</m:val></Date></m:optional>
154154
<m:for-each><m:select>orders/order</m:select>
155155
<Orders datatype='array'>
156-
<m:call-template name="Order"/>
156+
<m:call-template name="Customer.Orders"/>
157157
</Orders>
158-
</m:for-each>
158+
</m:for-each>
159159
<m:for-each><m:select>customerName</m:select>
160160
<Name>
161-
<m:call-template name="Name"/>
161+
<m:call-template name="Customer.Name"/>
162162
</Name>
163163
</m:for-each>
164164
</Customer>
@@ -178,12 +178,12 @@ expectedTemplate = tidyXML(`
178178
<m:optional><Date xsi:type="xs:dateTime"><m:val>parseDateTime(date, 'DD/MM/YYYY-hh:mm:ss')</m:val></Date></m:optional>
179179
<m:for-each><m:select>orders/order</m:select>
180180
<Orders datatype='array'>
181-
<m:call-template name="Order"/>
181+
<m:call-template name="Customer.Orders"/>
182182
</Orders>
183183
</m:for-each>
184184
<m:for-each><m:select>customerName</m:select>
185185
<Name>
186-
<m:call-template name="Name"/>
186+
<m:call-template name="Customer.Name"/>
187187
</Name>
188188
</m:for-each>
189189
</Customer>
@@ -203,14 +203,14 @@ expectedTemplate = tidyXML(`
203203
<m:optional><myPrefix:Date xsi:type="xs:dateTime"><m:val>parseDateTime(date, 'DD/MM/YYYY-hh:mm:ss')</m:val></myPrefix:Date></m:optional>
204204
<m:for-each><m:select>orders/order</m:select>
205205
<myPrefix:Orders datatype='array'>
206-
<m:call-template name="Order"/>
206+
<m:call-template name="Customer.Orders"/>
207207
</myPrefix:Orders>
208-
</m:for-each>
208+
</m:for-each>
209209
<m:for-each><m:select>customerName</m:select>
210210
<myPrefix:Name>
211-
<m:call-template name="Name"/>
211+
<m:call-template name="Customer.Name"/>
212212
</myPrefix:Name>
213-
</m:for-each>
213+
</m:for-each>
214214
</myPrefix:Customer>
215215
</m:entity>
216216
`);
@@ -230,22 +230,22 @@ expectedTemplate = tidyXML(`
230230
<m:optional><Date xsi:type="xs:dateTime"><m:val>parseDateTime(date, 'DD/MM/YYYY-hh:mm:ss')</m:val></Date></m:optional>
231231
<m:for-each><m:select>orders/order</m:select>
232232
<Orders datatype='array'>
233-
<m:call-template name="Order"/>
233+
<m:call-template name="Customer.Orders"/>
234234
</Orders>
235235
</m:for-each>
236236
<m:for-each><m:select>customerName</m:select>
237237
<Name>
238-
<m:call-template name="Name"/>
238+
<m:call-template name="Customer.Name"/>
239239
</Name>
240-
</m:for-each>
240+
</m:for-each>
241241
</Customer>
242242
</m:entity>
243-
<m:entity name="Order" xmlns:m="http://marklogic.com/entity-services/mapping">
243+
<m:entity name="Customer.Orders" xmlns:m="http://marklogic.com/entity-services/mapping">
244244
<Order xmlns="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
245245
<m:optional><OrderID xsi:type="xs:string"><m:val>@id</m:val></OrderID></m:optional>
246246
</Order>
247247
</m:entity>
248-
<m:entity name="Name" xmlns:m="http://marklogic.com/entity-services/mapping">
248+
<m:entity name="Customer.Name" xmlns:m="http://marklogic.com/entity-services/mapping">
249249
<Name xmlns="" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
250250
<m:optional><FirstName xsi:type="xs:string"><m:val>ns1:givenName</m:val></FirstName></m:optional>
251251
<m:optional><LastName xsi:type="xs:string"><m:val>ns2:surName</m:val></LastName></m:optional>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const hubTest = require("/test/data-hub-test-helper.xqy");
2+
const lib = require("lib/lib.sjs");
3+
const mappingLib = require("/data-hub/5/builtins/steps/mapping/entity-services/lib.sjs");
4+
const test = require("/test/test-helper.xqy");
5+
6+
if (mappingLib.versionIsCompatibleWithES()) {
7+
// First verify that the nested object properties were processed correctly in the XML mapping template
8+
// Each template should have a name that is unique based on the path of the property that it references
9+
const mappingTemplate = hubTest.getModulesDocument("/mappings/PersonMapping/PersonMapping-6.mapping.xml");
10+
const namespaces = {"m": "http://marklogic.com/entity-services/mapping"};
11+
const assertions = [
12+
test.assertEqual("Person.name", mappingTemplate.xpath("/m:mapping/m:entity[@name/string() = 'Person']/Person/m:for-each/name/m:call-template/@name/string()", namespaces)),
13+
test.assertEqual("Person.alias", mappingTemplate.xpath("/m:mapping/m:entity[@name/string() = 'Person']/Person/m:for-each/alias/m:call-template/@name/string()", namespaces)),
14+
test.assertEqual(1, mappingTemplate.xpath("/m:mapping/m:entity[@name/string() = 'Person.name']", namespaces).toArray().length),
15+
test.assertEqual("Person.name.first", mappingTemplate.xpath("/m:mapping/m:entity[@name/string() = 'Person.name']/Name/m:for-each/first/m:call-template/@name/string()", namespaces)),
16+
test.assertEqual(1, mappingTemplate.xpath("/m:mapping/m:entity[@name/string() = 'Person.name.first']", namespaces).toArray().length),
17+
test.assertEqual(1, mappingTemplate.xpath("/m:mapping/m:entity[@name/string() = 'Person.alias']", namespaces).toArray().length)
18+
19+
];
20+
21+
const person = lib.invokeTestMapping("/content/person2.json", "PersonMapping", "6").Person;
22+
23+
assertions.push(
24+
test.assertEqual("222", fn.string(person.id)),
25+
test.assertEqual("Nicky", fn.string(person.nickname)),
26+
test.assertEqual("First", fn.string(person.name.Name.first.FirstName.value),
27+
"This verifies that when two properties are mapped to entities of the same type, the " +
28+
"templates that are generated have unique names"),
29+
test.assertEqual("SomePrefix", fn.string(person.name.Name.first.FirstName.prefix)),
30+
test.assertEqual("Last", fn.string(person.name.Name.last)),
31+
test.assertEqual("Middle", fn.string(person.alias.Name.middle))
32+
);
33+
34+
assertions;
35+
}

marklogic-data-hub/src/test/ml-modules/root/test/suites/data-hub/5/builtins/steps/mapping/entity-services/test-data/entities/Person.entity.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
"name": {
3737
"$ref": "#/definitions/Name"
3838
},
39+
"alias": {
40+
"$ref": "#/definitions/Name"
41+
},
3942
"nickname": {
4043
"datatype": "string"
4144
}

0 commit comments

Comments
 (0)