@adobe/acc-js-sdk 1.0.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,7 @@ governing permissions and limitations under the License.
22
22
  const { DomException, XPath } = require('./domUtil.js');
23
23
  const XtkCaster = require('./xtkCaster.js').XtkCaster;
24
24
  const EntityAccessor = require('./entityAccessor.js').EntityAccessor;
25
+ const { ArrayMap } = require('./util.js');
25
26
 
26
27
  const PACKAGE_STATUS = { "never": 0, "always": 1, "default": 2, "preCreate": 3 };
27
28
 
@@ -33,10 +34,6 @@ const PACKAGE_STATUS = { "never": 0, "always": 1, "default": 2, "preCreate": 3 }
33
34
  // Helper functions
34
35
  // ========================================================================================
35
36
 
36
- // Determine if a name is an attribute name, i.e. if it starts with the "@" character
37
- const isAttributeName = function(name) { return name.length > 0 && name[0] == '@'; };
38
-
39
-
40
37
  /**
41
38
  * Creates a schema object from an XML representation
42
39
  * This function is not intended to be used publicly.
@@ -102,22 +99,63 @@ class SchemaCache {
102
99
  class XtkSchemaKey {
103
100
 
104
101
  constructor(schema, xml, schemaNode) {
102
+
103
+ /**
104
+ * The schema this key belongs to
105
+ * @type {Campaign.XtkSchema}
106
+ */
105
107
  this.schema = schema;
108
+
109
+ /**
110
+ * The name of the key
111
+ * @type {string}
112
+ */
106
113
  this.name = EntityAccessor.getAttributeAsString(xml, "name");
114
+
115
+ /**
116
+ * A human friendly name for they key
117
+ * @type {string}
118
+ */
107
119
  this.label = EntityAccessor.getAttributeAsString(xml, "label");
120
+
121
+ /**
122
+ * A longer, human friendly description for they key
123
+ * @type {string}
124
+ */
108
125
  this.description = EntityAccessor.getAttributeAsString(xml, "desc");
126
+
127
+ /**
128
+ * Indicates if the key is internal or not
129
+ * @type {boolean}
130
+ */
109
131
  this.isInternal = EntityAccessor.getAttributeAsBoolean(xml, "internal");
132
+
133
+ /**
134
+ * Indicates if the fields (parts) of a composite key may be empty (null). At least one part must always be populated
135
+ * @type {boolean}
136
+ */
110
137
  this.allowEmptyPart = EntityAccessor.getAttributeAsString(xml, "allowEmptyPart");
111
- this.fields = {};
138
+
139
+ /**
140
+ * The fields making up the key
141
+ * @type {Utils.ArrayMap<Campaign.XtkSchemaNode>}
142
+ */
143
+ this.fields = new ArrayMap();
112
144
 
113
145
  for (var child of EntityAccessor.getChildElements(xml, "keyfield")) {
114
- const xpath = EntityAccessor.getAttributeAsString(child, "xpath");
115
- if (xpath == "") throw new DomException(`Cannot create XtkSchemaKey for key '${this.name}': keyfield does not have an xpath attribute`);
116
- const field = schemaNode.findNode(xpath);
117
- this.fields[field.name] = field;
146
+ const xpathString = EntityAccessor.getAttributeAsString(child, "xpath");
147
+ if (xpathString == "") throw new DomException(`Cannot create XtkSchemaKey for key '${this.name}': keyfield does not have an xpath attribute`);
148
+
149
+ // find key field
150
+ const xpath = new XPath(xpathString);
151
+ const elements = xpath.getElements();
152
+ let keyNode = schemaNode;
153
+ while (keyNode && elements.length > 0)
154
+ keyNode = keyNode.children[elements.shift()];
155
+ if (keyNode)
156
+ this.fields._push(xpathString, keyNode);
118
157
  }
119
158
  }
120
-
121
159
  }
122
160
 
123
161
  /**
@@ -132,10 +170,21 @@ class XtkSchemaKey {
132
170
  class XtkJoin {
133
171
 
134
172
  constructor(xml) {
173
+
174
+ /**
175
+ * The xpath of the join condition on the source table
176
+ * @type {string}
177
+ */
135
178
  this.src = EntityAccessor.getAttributeAsString(xml, "xpath-src");
136
- this.dst = EntityAccessor.getAttributeAsString(xml, "xpath-dst");
179
+
180
+ /**
181
+ * The xpath of the join condition on the destination table
182
+ * @type {string}
183
+ */
184
+ this.dst = EntityAccessor.getAttributeAsString(xml, "xpath-dst");
137
185
  }
138
186
  }
187
+
139
188
  // ========================================================================================
140
189
  // Schema nodes
141
190
  // ========================================================================================
@@ -224,7 +273,12 @@ class XtkSchemaNode {
224
273
  * @type {string}
225
274
  */
226
275
  this.img = EntityAccessor.getAttributeAsString(xml, "img");
227
- this.image = this.img;
276
+
277
+ /**
278
+ * An optional image for the node (alias to the img property)
279
+ * @type {string}
280
+ */
281
+ this.image = this.img;
228
282
 
229
283
  /**
230
284
  * Returns the name of the image of the current node in the form of a string of characters.
@@ -233,42 +287,47 @@ class XtkSchemaNode {
233
287
  this.enumerationImage = EntityAccessor.getAttributeAsString(xml, "enumImage");
234
288
 
235
289
  /**
236
- * The node type
290
+ * The node type. Attribute nodes without an explicitedly defined type will be reported as "string"
237
291
  * @type {string}
238
292
  */
239
293
  this.type = EntityAccessor.getAttributeAsString(xml, "type");
240
294
  if (!this.type && isAttribute) this.type = "string";
241
295
 
242
296
  /**
243
- * The node target
297
+ * For link type nodes, the target of the link
244
298
  * @type {string}
245
299
  */
246
300
  this.target = EntityAccessor.getAttributeAsString(xml, "target");
247
301
 
248
- /**
302
+ /**
249
303
  * The node integrity
250
304
  * @type {string}
251
305
  */
252
306
  this.integrity = EntityAccessor.getAttributeAsString(xml, "integrity");
253
307
 
254
- /**
308
+ /**
255
309
  * The node data length (applicable for string-types only)
256
310
  * @type {number}
257
311
  */
258
312
  this.length = EntityAccessor.getAttributeAsLong(xml, "length");
313
+
314
+ /**
315
+ * The node data length (applicable for string-types only)
316
+ * @type {number}
317
+ */
259
318
  this.size = this.length;
260
319
 
261
320
  /**
262
321
  * The enum of the node
263
322
  * @type {string}
264
323
  */
265
- this.enum = EntityAccessor.getAttributeAsString(xml, "enum");
324
+ this.enum = EntityAccessor.getAttributeAsString(xml, "enum");
266
325
 
267
326
  /**
268
327
  * Returns a string of characters which is the name of the user enumeration used by the current node.
269
328
  * @type {string}
270
329
  */
271
- this.userEnumeration = EntityAccessor.getAttributeAsString(xml, "userEnum");
330
+ this.userEnumeration = EntityAccessor.getAttributeAsString(xml, "userEnum");
272
331
 
273
332
  /**
274
333
  * Returns a boolean which indicates whether the value of the current node is linked to a user enumeration.
@@ -281,11 +340,17 @@ class XtkSchemaNode {
281
340
  * @type {string}
282
341
  */
283
342
  this.ref = EntityAccessor.getAttributeAsString(xml, "ref");
343
+
284
344
  /**
285
345
  * Has an unlimited number of children of the same type
286
346
  * @type {boolean}
287
347
  */
288
348
  this.unbound = EntityAccessor.getAttributeAsBoolean(xml, "unbound");
349
+
350
+ /**
351
+ * Has an unlimited number of children of the same type
352
+ * @type {boolean}
353
+ */
289
354
  this.isCollection = this.unbound;
290
355
 
291
356
  /**
@@ -293,17 +358,19 @@ class XtkSchemaNode {
293
358
  * @type {boolean}
294
359
  */
295
360
  this.isMappedAsXML = EntityAccessor.getAttributeAsBoolean(xml, "xml");
361
+
296
362
  /**
297
363
  * is an advanced node
298
364
  * @type {boolean}
299
365
  */
300
366
  this.isAdvanced = EntityAccessor.getAttributeAsBoolean(xml, "advanced");
367
+
301
368
  /**
302
369
  * Children of the node. This is a object whose key are the names of the children nodes (without the "@"
303
370
  * character for attributes)
304
- * @type {Object.<string, Campaign.XtkSchemaNode>}
371
+ * @type {Utils.ArrayMap.<Campaign.XtkSchemaNode>}
305
372
  */
306
- this.children = {};
373
+ this.children = new ArrayMap();
307
374
 
308
375
  /**
309
376
  * Count the children of a node
@@ -319,9 +386,9 @@ class XtkSchemaNode {
319
386
 
320
387
  /**
321
388
  * Schema root elements may have a list of keys. This is a dictionary whose names are the key names and values the keys
322
- * @type {Object<string, XtkSchemaKey>}
389
+ * @type {ArrayNode<Campaign.XtkSchemaKey>}
323
390
  */
324
- this.keys = {};
391
+ this.keys = new ArrayMap();
325
392
 
326
393
  /**
327
394
  * The full path of the node
@@ -342,25 +409,25 @@ class XtkSchemaNode {
342
409
  * Returns a boolean which indicates whether the current node is ordinary.
343
410
  * @type {boolean}
344
411
  */
345
- this.isAnyType = this.type === "ANY";
412
+ this.isAnyType = this.type === "ANY";
346
413
 
347
414
  /**
348
415
  * Returns a boolean which indicates whether the node is a link.
349
416
  * @type {boolean}
350
417
  */
351
- this.isLink = this.type === "link";
418
+ this.isLink = this.type === "link";
352
419
 
353
420
  /**
354
421
  * Returns a boolean which indicates whether the value of the current node is linked to an enumeration.
355
422
  * @type {boolean}
356
423
  */
357
- this.hasEnumeration = this.enum !== "";
424
+ this.hasEnumeration = this.enum !== "";
358
425
 
359
426
  /**
360
427
  * Returns a boolean which indicates whether the current node is linked to an SQL table.
361
428
  * @type {boolean}
362
429
  */
363
- this.hasSQLTable = this.sqlTable !== '';
430
+ this.hasSQLTable = this.sqlTable !== '';
364
431
 
365
432
  /**
366
433
  * The SQL name of the field. The property is an empty string if the object isn't an SQL type field.
@@ -378,7 +445,7 @@ class XtkSchemaNode {
378
445
  * Returns a boolean indicating whether the table is a temporary table. The table will not be created during database creation.
379
446
  * @type {boolean}
380
447
  */
381
- this.isTemporaryTable = EntityAccessor.getAttributeAsBoolean(xml, "temporaryTable");
448
+ this.isTemporaryTable = EntityAccessor.getAttributeAsBoolean(xml, "temporaryTable");
382
449
 
383
450
  /**
384
451
  * Returns a boolean which indicates whether the current node is a logical sub-division of the schema.
@@ -397,39 +464,39 @@ class XtkSchemaNode {
397
464
  * True if the node is a link and if the join is external.
398
465
  * @type {boolean}
399
466
  */
400
- this.isExternalJoin = EntityAccessor.getAttributeAsBoolean(xml, "externalJoin");
467
+ this.isExternalJoin = EntityAccessor.getAttributeAsBoolean(xml, "externalJoin");
401
468
 
402
469
  /**
403
470
  * Returns a boolean which indicates whether the current node is mapped by a Memo.
404
471
  * @type {boolean}
405
472
  */
406
- this.isMemo = this.type === "memo" || this.type === "CDATA";
473
+ this.isMemo = this.type === "memo" || this.type === "CDATA";
407
474
 
408
475
  /**
409
476
  * Returns a boolean which indicates whether the current node is mapped by a MemoData.
410
477
  * @type {boolean}
411
478
  */
412
- this.isMemoData = this.isMemo && this.name === 'data';
479
+ this.isMemoData = this.isMemo && this.name === 'data';
413
480
 
414
481
  /**
415
482
  * Returns a boolean which indicates whether the current node is a BLOB.
416
483
  * @type {boolean}
417
484
  */
418
- this.isBlob = this.type === "blob";
485
+ this.isBlob = this.type === "blob";
419
486
 
420
487
  /**
421
488
  * Returns a boolean which indicates whether the current node is mapped from CDATA type XML.
422
489
  * @type {boolean}
423
490
  */
424
- this.isCDATA = this.type === "CDATA";
491
+ this.isCDATA = this.type === "CDATA";
425
492
 
493
+ const notNull = EntityAccessor.getAttributeAsString(xml, "notNull");
494
+ const sqlDefault = EntityAccessor.getAttributeAsString(xml, "sqlDefault");
495
+ const notNullOverriden = notNull || sqlDefault === "NULL";
426
496
  /**
427
497
  * Returns a boolean which indicates whether or not the current node can take the null value into account.
428
498
  * @type {boolean}
429
499
  */
430
- const notNull = EntityAccessor.getAttributeAsString(xml, "notNull");
431
- const sqlDefault = EntityAccessor.getAttributeAsString(xml, "sqlDefault");
432
- const notNullOverriden = notNull || sqlDefault === "NULL"
433
500
  this.isNotNull = notNullOverriden ? XtkCaster.asBoolean(notNull) : this.type === "int64" || this.type === "short" ||
434
501
  this.type === "long" || this.type === "byte" || this.type === "float" || this.type === "double" ||
435
502
  this.type === "money" || this.type === "percent" || this.type === "time" || this.type === "boolean";
@@ -444,7 +511,7 @@ class XtkSchemaNode {
444
511
  * Returns a boolean which indicates whether the current node is mapped in SQL.
445
512
  * @type {boolean}
446
513
  */
447
- this.isSQL = !!this.SQLName || !!this.SQLTable || (this.isLink && this.schema.mappingType === 'sql' && !this.isMappedAsXML);
514
+ this.isSQL = !!this.SQLName || !!this.SQLTable || (this.isLink && this.schema.mappingType === 'sql' && !this.isMappedAsXML);
448
515
 
449
516
  /**
450
517
  * The SQL name of the field. The property is an empty string if the object isn't an SQL type field.
@@ -462,20 +529,20 @@ class XtkSchemaNode {
462
529
  * Returns a boolean which indicates whether the value of the current node is the result of a calculation.
463
530
  * @type {boolean}
464
531
  */
465
- this.isCalculated = false;
532
+ this.isCalculated = false;
466
533
 
467
534
  /**
468
535
  * Expression associated with the node
469
536
  * @type {string}
470
537
  */
471
- this.expr = EntityAccessor.getAttributeAsString(xml, "expr");
538
+ this.expr = EntityAccessor.getAttributeAsString(xml, "expr");
472
539
  if (this.expr) this.isCalculated = true;
473
540
 
474
541
  /**
475
542
  * Returns a boolean which indicates whether the value of the current node is incremented automatically.
476
543
  * @type {boolean}
477
544
  */
478
- this.isAutoIncrement = EntityAccessor.getAttributeAsBoolean(xml, "autoIncrement");
545
+ this.isAutoIncrement = EntityAccessor.getAttributeAsBoolean(xml, "autoIncrement");
479
546
 
480
547
  /**
481
548
  * Returns a boolean which indicates whether the current node is a primary key.
@@ -509,29 +576,31 @@ class XtkSchemaNode {
509
576
 
510
577
  // Children (elements and attributes)
511
578
  const childNodes = [];
512
- for (const child of EntityAccessor.getChildElements(xml, "attribute")) {
513
- const node = new XtkSchemaNode();
514
- node.init(schema, child, this, true);
515
- childNodes.push(node);
516
- }
517
- for (const child of EntityAccessor.getChildElements(xml, "element")) {
518
- const node = new XtkSchemaNode();
519
- node.init(schema, child, this, false);
520
- childNodes.push(node);
579
+ for (const child of EntityAccessor.getChildElements(xml)) {
580
+ if (child.tagName === "attribute") {
581
+ const node = new XtkSchemaNode();
582
+ node.init(schema, child, this, true);
583
+ childNodes.push(node);
584
+ }
585
+ if (child.tagName === "element") {
586
+ const node = new XtkSchemaNode();
587
+ node.init(schema, child, this, false);
588
+ childNodes.push(node);
589
+ }
590
+ if (child.tagName === "compute-string") {
591
+ this.expr = EntityAccessor.getAttributeAsString(child, "expr");
592
+ this.isCalculated = false;
593
+ }
521
594
  }
522
595
  for (const childNode of childNodes) {
523
- if (this.children[childNode.name]) {
524
- // already a child with the name => there's a problem with the schema
525
- throw new DomException(`Failed to create schema node '${childNode.name}': there's a already a node with this name`);
526
- }
527
- this.children[childNode.name] = childNode;
596
+ this.children._push(childNode.name, childNode);
528
597
  this.childrenCount = this.childrenCount + 1;
529
598
  }
530
599
 
531
600
  // Keys (after elements and attributes have been found)
532
601
  for (const child of EntityAccessor.getChildElements(xml, "key")) {
533
602
  const key = new XtkSchemaKey(schema, child, this);
534
- this.keys[key.name] = key;
603
+ this.keys._push(key.name, key);
535
604
  }
536
605
 
537
606
  // Propagate implicit values
@@ -539,21 +608,6 @@ class XtkSchemaNode {
539
608
  propagateImplicitValues(this);
540
609
  }
541
610
 
542
- /**
543
- * Does the node have a child with the given name?
544
- *
545
- * @param {string} name the child name, without the "@" character for attributes
546
- * @returns {boolean} a boolean indicating whether the node contains a child with the given name
547
- */
548
- hasChild(name) {
549
- var child = this.children[name];
550
- if (child) return true;
551
- // TODO: handle ref target
552
- // if (this.hasRefTarget())
553
- // return this.refTarget().hasChild(name);
554
- return false;
555
- }
556
-
557
611
  /**
558
612
  * Indicates whether the current node has an unlimited number of children of the same type.
559
613
  *
@@ -591,34 +645,78 @@ class XtkSchemaNode {
591
645
  return new XPath(path);
592
646
  }
593
647
 
648
+ /**
649
+ * Find the target of a ref node.
650
+ * @returns {Promise<Campaign.XtkNode>} the target node, or undefined if not found
651
+ */
652
+ async refTarget() {
653
+ if (!this.ref) return;
654
+ const index = this.ref.lastIndexOf(':');
655
+ if (index !== -1) {
656
+ // find the associated schame
657
+ const refSchemaId = this.ref.substring(0, index);
658
+ if (refSchemaId.indexOf(':') === -1)
659
+ throw Error(`Cannot find ref target '${this.ref}' from node '${this.nodePath}' of schema '${this.schema.id}': ref value is not correct (expeted <schemaId>:<path>)`);
660
+ const refPath = this.ref.substring(index + 1);
661
+ // inside current schema ?
662
+ if (refSchemaId === this.schema.id)
663
+ return this.schema.findNode(refPath);
664
+ const refSchema = await this.schema._application.getSchema(refSchemaId);
665
+ if (!refSchema) return;
666
+ return refSchema.findNode(refPath);
667
+ }
668
+ else {
669
+ // ref is in the current schema
670
+ return this.schema.findNode(this.ref);
671
+ }
672
+ }
673
+
674
+ /**
675
+ * Find the target of a link node.
676
+ * @returns {Promise<Campaign.XtkNode>} the target node, or undefined if not found
677
+ */
678
+ async linkTarget() {
679
+ if (this.type !== "link") return this.schema.root;
680
+ let schemaId = this.target;
681
+ let xpath = "";
682
+ if (this.target.indexOf(',') !== -1)
683
+ throw new Error(`Cannot find target of link '${this.target}': target has multiple schemas`);
684
+ const index = this.target.indexOf('/');
685
+ if (index !== -1) {
686
+ xpath = this.target.substring(index + 1);
687
+ schemaId = this.target.substring(0, index);
688
+ xpath = this.target.substring(index + 1);
689
+ }
690
+ if (schemaId.indexOf(':') === -1)
691
+ throw new Error(`Cannot find target of link '${this.target}': target is not a valid link target (missing schema id)`);
692
+ const schema = await this.schema._application.getSchema(schemaId);
693
+ if (!schema) return;
694
+ const root = schema.root;
695
+ if (!root) return;
696
+ if (!xpath) return root;
697
+ return await root.findNode(xpath);
698
+ }
594
699
 
595
700
  /**
596
- * Returns an instance of XtkSchemaNode or null if the node doesn't exist and the mustExist parameter is set to false.
701
+ * Returns an instance of XtkSchemaNode or null if the node doesn't exist. In version 1.1.0 and above, this function is
702
+ * asynchronous (returns a Promise)
597
703
  *
598
704
  * @param {XML.XPath|string} path XPath represents the name of the node to be searched
599
- * @param {boolean} strict indicates whether (strict to false) or not, when the name of the last item in the path does not exist as is, it should be searched for as an attribute or an element. By default to true.
600
- * @param {boolean} mustExist indicates whether an exception must be raised if the node does not exist. true by default
601
- * @returns Returns a XtkSchemaNode instance if the node can be found, or null if the mustExist parameter is set to false.
602
- * @throws {Error} if the request cannot be find (when mustExist is set)
705
+ * @returns {Promise<XtkSchemaNode>} Returns a XtkSchemaNode instance if the node can be found
603
706
  */
604
- findNode(path, strict, mustExist) {
605
- if (strict === undefined) strict = true;
606
- if (mustExist === undefined) mustExist = true;
607
- if (typeof path == "string")
608
- path = new XPath(path);
707
+ async findNode(path) {
708
+ if (typeof path == "string") path = new XPath(path);
609
709
 
610
710
  // Find the starting node
611
711
  var node = this;
612
712
  if (path.isEmpty() || path.isAbsolute()) {
613
713
  node = this.schema.root;
614
- if (!node)
615
- throw new DomException(`Cannot find node '${path}' in node ${this.name} : schema ${this.schema.name} does not have a root node`);
714
+ if (!node) return;
616
715
  path = path.getRelativePath();
617
716
  }
618
717
 
619
718
  // Special case for current path "."
620
- if (path.isSelf())
621
- return this;
719
+ if (path.isSelf()) return this;
622
720
 
623
721
  const elements = path.getElements();
624
722
  while (node && elements.length > 0) {
@@ -626,57 +724,27 @@ class XtkSchemaNode {
626
724
  var name = element.asString();
627
725
 
628
726
  // TODO: if the path is a collection path, ignore the collection index
629
- // TODO: handle ref elements (consider the ref target instead)
630
- // TODO: Handle link between schemas
631
- // TODO: Handle any type
632
-
633
- if (!strict && elements.length == 0 && (!node.children[name] || !isAttributeName(name))) {
634
- // name is the final part of the path and the associated definition
635
- // does not exists. Since strict is set to false we check if the
636
- // alternate name exists (element name for an attribute or attribute
637
- // name for an element).
638
- var found = node.children[name];
639
- if (!found && isAttributeName(name)) found = node.children[name.substring(1)];
640
- if (!found && !isAttributeName(name)) found = node.children[`@${name}`];
641
- if (found) name = found.name;
642
- }
727
+
728
+ // handle ref elements (consider the ref target instead)
729
+ if (node.ref) node = await node.refTarget();
730
+ if (!node) break;
731
+
732
+ if (node.type === "link") node = await node.linkTarget();
733
+ if (!node) break;
734
+
735
+ // Don't continue for any type
736
+ // kludge to accept immediate child of an ANY type node (cas in packages)
737
+ if (node.type === 'ANY') return this.children[name];
643
738
 
644
739
  var childNode = null;
645
- if (element.isSelf())
646
- childNode = node;
647
- else if (element.isParent())
648
- childNode = node.parent;
649
- else
650
- childNode = node._getChildDefAutoExpand(name, mustExist);
740
+ if (element.isSelf()) childNode = node;
741
+ else if (element.isParent()) childNode = node.parent;
742
+ else childNode = await node.children[name];
651
743
  node = childNode;
652
744
  }
653
745
  return node;
654
746
  }
655
747
 
656
- // See CXtkNodeDef::GetChildDefAutoExpand
657
- _getChildDefAutoExpand(name, mustExist) {
658
- var child = this.children[name];
659
- if (child)
660
- return child;
661
-
662
- // TODO: handle ref
663
-
664
- if (mustExist) {
665
- // TODO: handle auto-expand schemas
666
- const path = this._getNodePath();
667
- const isAttribute = isAttributeName(name);
668
- const schemaDesc = this.schema.userDescription;
669
- if( path.isRootPath() ) {
670
- if (isAttribute) throw new DomException(`Unknown attribute '${name.substring(1)}' (see definition of schema '${schemaDesc}').`);
671
- else throw new DomException(`Unknown element '${name}' (see definition of schema '${schemaDesc}').`);
672
- }
673
- if (isAttribute) throw new DomException(`Unknown attribute '${name.substring(1)}' (see definition of element '${path.asString()}' in schema '${schemaDesc}').`);
674
- else throw new DomException(`Unknown element '${name}' (see definition of element '${path.asString()}' in schema '${schemaDesc}').`);
675
- }
676
-
677
- return null;
678
- }
679
-
680
748
  /**
681
749
  * Internal recursive function used to create a multi-line debug string representing the schema
682
750
  *
@@ -687,12 +755,103 @@ class XtkSchemaNode {
687
755
  toString(indent) {
688
756
  indent = indent || "";
689
757
  var s = `${indent}${this.label} (${this.name})\n`;
690
- for (var name in this.children) {
691
- s = s + this.children[name].toString(` ${indent}`);
758
+ for (var child of this.children) {
759
+ s = s + child.toString(` ${indent}`);
692
760
  }
693
761
  return s;
694
762
  }
695
763
 
764
+ /**
765
+ * Return the XtkSchemaNodes making up the join of a link-type node
766
+ * @returns {Promise<Array>} returns an array of joins. Each join is an element having a source and destination attributes, whose value is the corresponding XtkSchemaNode
767
+ */
768
+ async joinNodes() {
769
+ if (!this.isLink) return;
770
+ const joinParts = [];
771
+ for (const join of this.joins) {
772
+ const source = await this.parent.findNode(join.src);
773
+ let destination = await this.linkTarget();
774
+ if (destination)
775
+ destination = await destination.findNode(join.dst);
776
+ if (source && destination)
777
+ joinParts.push({
778
+ source: source,
779
+ destination: destination
780
+ });
781
+ }
782
+ return joinParts;
783
+ }
784
+
785
+ /**
786
+ * Returns the reverse link node of a link-type node
787
+ * @returns {Promise<Campaign.XtkSchemaNode>}
788
+ */
789
+ async reverseLink() {
790
+ if (!this.isLink) return;
791
+ const target = await this.linkTarget();
792
+ if (!target) return;
793
+ const revLink = await target.findNode(this.revLink);
794
+ return revLink;
795
+ }
796
+
797
+ /**
798
+ * Returns the compute string of a node. As the node can be a link or a reference, this function is asynchronous
799
+ * @returns {Promise<string>}
800
+ */
801
+ async computeString() {
802
+ if (this.expr) return this.expr;
803
+ // if we are a ref: ask the target of the ref
804
+ if (this.ref) {
805
+ const refTarget = await this.refTarget();
806
+ if (!refTarget) return "";
807
+ return await refTarget.computeString();
808
+ }
809
+ // No compute-string found: generate a default one (first key field)
810
+ if (this.keys && this.keys.length > 0) {
811
+ const key = this.keys[0];
812
+ if (key && key.fields && key.fields.length > 0 && key.fields[0])
813
+ return this.schema._application.client.sdk.expandXPath(key.fields[0].nodePath);
814
+ }
815
+ return "";
816
+ }
817
+
818
+ /**
819
+ * Returns an Enumeration object which is the enumeration linked to the current node or null if there is no enumeration.
820
+ * @param {string} an optional enumeration name. If none is specified, the node `enum` property will be used
821
+ * @returns Promise<Campaign.XtkEnumeration>
822
+ */
823
+ async enumeration(optionalName) {
824
+ const name = optionalName || this.enum;
825
+ if (!name) return;
826
+ const enumaration = await this.schema._application.getSysEnum(name, this.schema);
827
+ return enumaration;
828
+ }
829
+
830
+ /**
831
+ * Get the first internal key (if there is one)
832
+ * @returns {Campaign.XtkSchemaKey}
833
+ */
834
+ firstInternalKeyDef() {
835
+ return this.keys.find((k) => k.isInternal);
836
+ }
837
+
838
+ /**
839
+ * Get the first external key (if there is one)
840
+ * @returns {Campaign.XtkSchemaKey}
841
+ */
842
+ firstExternalKeyDef() {
843
+ return this.keys.find((k) => !k.isInternal);
844
+ }
845
+
846
+ /**
847
+ * Get the first key (internal first)
848
+ * @returns {Campaign.XtkSchemaKey}
849
+ */
850
+ firstKeyDef() {
851
+ let key = this.firstInternalKeyDef();
852
+ if (!key) key = this.firstExternalKeyDef();
853
+ return key;
854
+ }
696
855
  }
697
856
 
698
857
  // ========================================================================================
@@ -771,7 +930,7 @@ function XtkEnumerationValue(xml, baseType) {
771
930
  class XtkEnumeration {
772
931
  constructor(schemaId, xml) {
773
932
  /**
774
- * The system enumeration name
933
+ * The system enumeration name, fully qualified, i.e. prefixed with the schema id
775
934
  * @type {string}
776
935
  */
777
936
  this.name = EntityAccessor.getAttributeAsString(xml, "name");
@@ -781,37 +940,42 @@ class XtkEnumeration {
781
940
  * @type {string}
782
941
  */
783
942
  this.label = EntityAccessor.getAttributeAsString(xml, "label");
943
+
784
944
  /**
785
945
  * A human friendly long description of the enumeration
786
946
  * @type {string}
787
947
  */
788
948
  this.description = EntityAccessor.getAttributeAsString(xml, "desc");
949
+
789
950
  /**
790
- * The type of the enumeration
951
+ * The type of the enumeration, usually "string" or "byte"
791
952
  * @type {Campaign.XtkEnumerationType}
792
953
  */
793
954
  this.baseType = EntityAccessor.getAttributeAsString(xml, "basetype");
955
+
794
956
  /**
795
957
  * The default value of the enumeration
796
958
  * @type {Campaign.XtkEnumerationValue}
797
959
  */
798
960
  this.default = null;
961
+
799
962
  /**
800
963
  * Indicates if the enumeration has an image, i.e. if any of its values has an image
801
964
  * @type {boolean}
802
965
  */
803
966
  this.hasImage = false;
967
+
804
968
  /**
805
969
  * The enumerations values
806
- * @type {Object<string, Campaign.XtkEnumerationValue>}
970
+ * @type {Utils.ArrayMap<Campaign.XtkEnumerationValue>}
807
971
  */
808
- this.values = {};
972
+ this.values = new ArrayMap();
809
973
 
810
974
  var defaultValue = EntityAccessor.getAttributeAsString(xml, "default");
811
975
 
812
976
  for (var child of EntityAccessor.getChildElements(xml, "value")) {
813
977
  const e = new XtkEnumerationValue(child, this.baseType);
814
- this.values[e.name] = e;
978
+ this.values._push(e.name, e);
815
979
  if (e.image != "") this.hasImage = true;
816
980
  const stringValue = EntityAccessor.getAttributeAsString(child, "value");
817
981
  if (defaultValue == stringValue)
@@ -819,6 +983,13 @@ class XtkEnumeration {
819
983
  }
820
984
 
821
985
  propagateImplicitValues(this, true);
986
+
987
+ /**
988
+ * The system enumeration name, without the schema id prefix
989
+ * @type {string}
990
+ */
991
+ this.shortName = this.name;
992
+ this.name = `${schemaId}:${this.shortName}`;
822
993
  }
823
994
  }
824
995
 
@@ -847,32 +1018,38 @@ class XtkSchema extends XtkSchemaNode {
847
1018
  * @type {string}
848
1019
  */
849
1020
  this.namespace = EntityAccessor.getAttributeAsString(xml, "namespace");
1021
+
850
1022
  /**
851
1023
  * The schema id, in the form "namespace:name"
852
1024
  * @type {string}
853
1025
  */
854
1026
  this.name = EntityAccessor.getAttributeAsString(xml, "name");
855
1027
  this.id = `${this.namespace}:${this.name}`;
1028
+
856
1029
  /**
857
1030
  * Indicates whether the schema is a library schema or not
858
1031
  * @type {boolean}
859
1032
  */
860
1033
  this.isLibrary = EntityAccessor.getAttributeAsBoolean(xml, "library");
1034
+
861
1035
  /**
862
1036
  * A human name for the schema, in singular
863
1037
  * @type {string}
864
1038
  */
865
1039
  this.labelSingular = EntityAccessor.getAttributeAsString(xml, "labelSingular");
1040
+
866
1041
  /**
867
1042
  * The schema mappgin type, following the xtk:srcSchema:mappingType enumeration
868
1043
  * @type {Campaign.XtkSchemaMappingType}
869
1044
  */
870
1045
  this.mappingType = EntityAccessor.getAttributeAsString(xml, "mappingType");
1046
+
871
1047
  /**
872
- * The MD5 code of the schema in the form of a hexadecimal string
1048
+ * The MD5 checksum of the schema in the form of a hexadecimal string
873
1049
  * @type {string}
874
1050
  */
875
1051
  this.md5 = EntityAccessor.getAttributeAsString(xml, "md5");
1052
+
876
1053
  /**
877
1054
  * The schema definition
878
1055
  * @private
@@ -897,13 +1074,12 @@ class XtkSchema extends XtkSchemaNode {
897
1074
  /**
898
1075
  * Enumerations in this schema, as a dictionary whose keys are enumeration names and values are the
899
1076
  * corresponding enumeration definitions
900
- * @type {Object<string, XtkEnumeration>}
1077
+ * @type {Utils.ArrayMap<Campaign.XtkEnumeration>}
901
1078
  */
902
- this.enumerations = {};
903
-
1079
+ this.enumerations = new ArrayMap();
904
1080
  for (var child of EntityAccessor.getChildElements(xml, "enumeration")) {
905
1081
  const e = new XtkEnumeration(this.id, child);
906
- this.enumerations[e.name] = e;
1082
+ this.enumerations._push(e.shortName, e);
907
1083
  }
908
1084
  }
909
1085
 
@@ -914,8 +1090,8 @@ class XtkSchema extends XtkSchemaNode {
914
1090
  */
915
1091
  toString() {
916
1092
  var s = `${this.userDescription}\n`;
917
- for (var name in this.children) {
918
- s = s + this.children[name].toString(" - ");
1093
+ for (var child of this.children) {
1094
+ s = s + child.toString(" - ");
919
1095
  }
920
1096
  return s;
921
1097
  }
@@ -945,21 +1121,25 @@ class CurrentLogin {
945
1121
  * @type {string}
946
1122
  */
947
1123
  this.login = EntityAccessor.getAttributeAsString(userInfo, "login");
1124
+
948
1125
  /**
949
1126
  * The operator login id
950
1127
  * @type {number}
951
1128
  */
952
1129
  this.id = EntityAccessor.getAttributeAsLong(userInfo, "loginId");
1130
+
953
1131
  /**
954
1132
  * A human friendly string naming the operator (compute string)
955
1133
  * @type {string}
956
1134
  */
957
1135
  this.computeString = EntityAccessor.getAttributeAsString(userInfo, "loginCS");
1136
+
958
1137
  /**
959
1138
  * The operator timezone
960
1139
  * @type {string}
961
1140
  */
962
1141
  this.timezone = EntityAccessor.getAttributeAsString(userInfo, "timezone");
1142
+
963
1143
  /**
964
1144
  * The llist of operator rights
965
1145
  * @type {string[]}
@@ -1081,6 +1261,32 @@ class Application {
1081
1261
  }
1082
1262
  return false;
1083
1263
  }
1264
+
1265
+ /**
1266
+ * Get a system enumeration
1267
+ *
1268
+ * @param {string} enumerationName The name of the enumeration, which can be fully qualified (ex: "nms:recipient:gender") or not (ex: "gender")
1269
+ * @param {string} schemaOrSchemaId An optional schema id. If the enumerationName is not qualified, the search for the enumeration will be done in this schema
1270
+ * @returns {XtkEnumeration} the enumeration
1271
+ */
1272
+ async getSysEnum(enumerationName, schemaOrSchemaId) {
1273
+ const index = enumerationName.lastIndexOf(':');
1274
+ if (index === -1) {
1275
+ let schema = schemaOrSchemaId;
1276
+ if (schema && typeof schema === "string")
1277
+ schema = await this.getSchema(schema);
1278
+ // unqualified enumeration name
1279
+ if (!schema) return;
1280
+ return schema.enumerations[enumerationName];
1281
+ }
1282
+ // qualified enumeration name
1283
+ const schemaId = enumerationName.substring(0, index);
1284
+ if (schemaId.indexOf(':') === -1)
1285
+ throw Error(`Invalid enumeration name '${enumerationName}': expecting {name} or {schemaId}:{name}`);
1286
+ let schema = await this.getSchema(schemaId);
1287
+ if (!schema) return;
1288
+ return schema.enumerations[enumerationName.substring(index + 1)];
1289
+ }
1084
1290
  }
1085
1291
 
1086
1292