@adobe/acc-js-sdk 1.0.7 → 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,9 @@ 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');
26
+
27
+ const PACKAGE_STATUS = { "never": 0, "always": 1, "default": 2, "preCreate": 3 };
25
28
 
26
29
  /**
27
30
  * @namespace Campaign
@@ -30,10 +33,6 @@ const EntityAccessor = require('./entityAccessor.js').EntityAccessor;
30
33
  // ========================================================================================
31
34
  // Helper functions
32
35
  // ========================================================================================
33
-
34
- // Determine if a name is an attribute name, i.e. if it starts with the "@" character
35
- const isAttributeName = function(name) { return name.length > 0 && name[0] == '@'; };
36
-
37
36
 
38
37
  /**
39
38
  * Creates a schema object from an XML representation
@@ -45,12 +44,42 @@ const isAttributeName = function(name) { return name.length > 0 && name[0] == '@
45
44
  * @see {@link XtkSchema}
46
45
  * @memberof Campaign
47
46
  */
48
- function newSchema(xml) {
47
+ function newSchema(xml, application) {
49
48
  if (xml.nodeType == 9) xml = xml.documentElement; // Document -> element
50
- var schema = new XtkSchema(xml);
49
+ var schema = new XtkSchema(application, xml);
51
50
  return schema;
52
51
  }
53
52
 
53
+ // Propagate implicit values
54
+ // Name -> Label -> Desc -> HelpText
55
+ function propagateImplicitValues(xtkDesc, labelOnly) {
56
+ if (!xtkDesc.label) {
57
+ if (xtkDesc.isAttribute) xtkDesc.label = xtkDesc.name.substring(1); // without @
58
+ else xtkDesc.label = xtkDesc.name;
59
+ // Force first letter as uppercase
60
+ xtkDesc.label = xtkDesc.label.substring(0, 1).toUpperCase() + xtkDesc.label.substring(1);
61
+ }
62
+ if (!labelOnly && !xtkDesc.description) xtkDesc.description = xtkDesc.label;
63
+ }
64
+
65
+ // ========================================================================================
66
+ // Schema Cache
67
+ // ========================================================================================
68
+ class SchemaCache {
69
+ constructor(client) {
70
+ this._client = client;
71
+ this._schemas = {};
72
+ }
73
+ async getSchema(schemaId) {
74
+ let schema = this._schemas[schemaId];
75
+ if (schema === undefined) {
76
+ schema = await this._client.application._getSchema(schemaId);
77
+ if (!schema) schema = null; // null = not found
78
+ this._schemas[schemaId] = schema;
79
+ }
80
+ return schema;
81
+ }
82
+ }
54
83
 
55
84
  // ========================================================================================
56
85
  // Keys
@@ -70,22 +99,63 @@ function newSchema(xml) {
70
99
  class XtkSchemaKey {
71
100
 
72
101
  constructor(schema, xml, schemaNode) {
102
+
103
+ /**
104
+ * The schema this key belongs to
105
+ * @type {Campaign.XtkSchema}
106
+ */
73
107
  this.schema = schema;
108
+
109
+ /**
110
+ * The name of the key
111
+ * @type {string}
112
+ */
74
113
  this.name = EntityAccessor.getAttributeAsString(xml, "name");
114
+
115
+ /**
116
+ * A human friendly name for they key
117
+ * @type {string}
118
+ */
75
119
  this.label = EntityAccessor.getAttributeAsString(xml, "label");
120
+
121
+ /**
122
+ * A longer, human friendly description for they key
123
+ * @type {string}
124
+ */
76
125
  this.description = EntityAccessor.getAttributeAsString(xml, "desc");
77
- this.isInternal = EntityAccessor.getAttributeAsString(xml, "internal");
126
+
127
+ /**
128
+ * Indicates if the key is internal or not
129
+ * @type {boolean}
130
+ */
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
+ */
78
137
  this.allowEmptyPart = EntityAccessor.getAttributeAsString(xml, "allowEmptyPart");
79
- this.fields = {};
138
+
139
+ /**
140
+ * The fields making up the key
141
+ * @type {Utils.ArrayMap<Campaign.XtkSchemaNode>}
142
+ */
143
+ this.fields = new ArrayMap();
80
144
 
81
145
  for (var child of EntityAccessor.getChildElements(xml, "keyfield")) {
82
- const xpath = EntityAccessor.getAttributeAsString(child, "xpath");
83
- if (xpath == "") throw new DomException(`Cannot create XtkSchemaKey for key '${this.name}': keyfield does not have an xpath attribute`);
84
- const field = schemaNode.findNode(xpath);
85
- 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);
86
157
  }
87
158
  }
88
-
89
159
  }
90
160
 
91
161
  /**
@@ -100,10 +170,21 @@ class XtkSchemaKey {
100
170
  class XtkJoin {
101
171
 
102
172
  constructor(xml) {
173
+
174
+ /**
175
+ * The xpath of the join condition on the source table
176
+ * @type {string}
177
+ */
103
178
  this.src = EntityAccessor.getAttributeAsString(xml, "xpath-src");
104
- 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");
105
185
  }
106
186
  }
187
+
107
188
  // ========================================================================================
108
189
  // Schema nodes
109
190
  // ========================================================================================
@@ -137,62 +218,123 @@ class XtkSchemaNode {
137
218
  * @type {XtkSchema}
138
219
  */
139
220
  this.schema = schema;
221
+
222
+ /**
223
+ * Returns a string of characters which provides the data policy of the current node.
224
+ * @type {string}
225
+ */
226
+ this.dataPolicy = EntityAccessor.getAttributeAsString(xml, "dataPolicy");
227
+
228
+ /**
229
+ * Returns a string of characters which specifies the editing type of the current node.
230
+ * @type {string}
231
+ */
232
+ this.editType = EntityAccessor.getAttributeAsString(xml, "editType");
233
+
234
+ /**
235
+ * Only on the root node, returns a string which contains the folder template(s). On the other nodes, it returns undefined.
236
+ * @type {string}
237
+ */
238
+ this.folderModel = EntityAccessor.getAttributeAsString(xml, "folderModel");
239
+
140
240
  /**
141
241
  * The parent node
142
242
  * @type {XtkSchemaNode}
143
243
  */
144
244
  this.parent = parentNode;
245
+
145
246
  /**
146
247
  * Indicates if the node is an attribute or not (element or schema itself)
147
248
  * @type {boolean}
148
249
  */
149
250
  this.isAttribute = isAttribute;
251
+
150
252
  /**
151
- * The attribute or the node name (without the "@" sign for attributes)
253
+ * The attribute or the node name (with the "@" sign for attributes)
152
254
  * @type {string}
153
255
  */
154
256
  this.name = (this.isAttribute ? "@" : "") + EntityAccessor.getAttributeAsString(xml, "name");
257
+
155
258
  /**
156
259
  * A human friendly name for the node. If the node is the schema node, the label will be in the plural form and "labelSingular"
157
260
  * should be used for the singular form
158
261
  * @type {string}
159
262
  */
160
263
  this.label = EntityAccessor.getAttributeAsString(xml, "label");
264
+
161
265
  /**
162
266
  * A long description of the node
163
267
  * @type {string}
164
268
  */
165
269
  this.description = EntityAccessor.getAttributeAsString(xml, "desc");
270
+
166
271
  /**
167
272
  * An optional image for the node
168
273
  * @type {string}
169
274
  */
170
275
  this.img = EntityAccessor.getAttributeAsString(xml, "img");
276
+
171
277
  /**
172
- * The node type
278
+ * An optional image for the node (alias to the img property)
279
+ * @type {string}
280
+ */
281
+ this.image = this.img;
282
+
283
+ /**
284
+ * Returns the name of the image of the current node in the form of a string of characters.
285
+ * @type {string}
286
+ */
287
+ this.enumerationImage = EntityAccessor.getAttributeAsString(xml, "enumImage");
288
+
289
+ /**
290
+ * The node type. Attribute nodes without an explicitedly defined type will be reported as "string"
173
291
  * @type {string}
174
292
  */
175
293
  this.type = EntityAccessor.getAttributeAsString(xml, "type");
294
+ if (!this.type && isAttribute) this.type = "string";
295
+
176
296
  /**
177
- * The node target
297
+ * For link type nodes, the target of the link
178
298
  * @type {string}
179
299
  */
180
- this.target = EntityAccessor.getAttributeAsString(xml, "target");
300
+ this.target = EntityAccessor.getAttributeAsString(xml, "target");
301
+
181
302
  /**
182
303
  * The node integrity
183
304
  * @type {string}
184
305
  */
185
- this.integrity = EntityAccessor.getAttributeAsString(xml, "integrity");
306
+ this.integrity = EntityAccessor.getAttributeAsString(xml, "integrity");
307
+
186
308
  /**
187
309
  * The node data length (applicable for string-types only)
188
310
  * @type {number}
189
311
  */
190
312
  this.length = EntityAccessor.getAttributeAsLong(xml, "length");
313
+
314
+ /**
315
+ * The node data length (applicable for string-types only)
316
+ * @type {number}
317
+ */
318
+ this.size = this.length;
319
+
191
320
  /**
192
321
  * The enum of the node
193
322
  * @type {string}
194
323
  */
195
324
  this.enum = EntityAccessor.getAttributeAsString(xml, "enum");
325
+
326
+ /**
327
+ * Returns a string of characters which is the name of the user enumeration used by the current node.
328
+ * @type {string}
329
+ */
330
+ this.userEnumeration = EntityAccessor.getAttributeAsString(xml, "userEnum");
331
+
332
+ /**
333
+ * Returns a boolean which indicates whether the value of the current node is linked to a user enumeration.
334
+ * @type {boolean}
335
+ */
336
+ this.hasUserEnumeration = !!this.userEnumeration;
337
+
196
338
  /**
197
339
  * "ref" attribute of the node, which references another node
198
340
  * @type {string}
@@ -204,88 +346,266 @@ class XtkSchemaNode {
204
346
  * @type {boolean}
205
347
  */
206
348
  this.unbound = EntityAccessor.getAttributeAsBoolean(xml, "unbound");
349
+
350
+ /**
351
+ * Has an unlimited number of children of the same type
352
+ * @type {boolean}
353
+ */
354
+ this.isCollection = this.unbound;
355
+
356
+ /**
357
+ * is mapped as a xml
358
+ * @type {boolean}
359
+ */
360
+ this.isMappedAsXML = EntityAccessor.getAttributeAsBoolean(xml, "xml");
361
+
362
+ /**
363
+ * is an advanced node
364
+ * @type {boolean}
365
+ */
366
+ this.isAdvanced = EntityAccessor.getAttributeAsBoolean(xml, "advanced");
367
+
207
368
  /**
208
369
  * Children of the node. This is a object whose key are the names of the children nodes (without the "@"
209
370
  * character for attributes)
210
- * @type {Object.<string, Campaign.XtkSchemaNode>}
371
+ * @type {Utils.ArrayMap.<Campaign.XtkSchemaNode>}
211
372
  */
212
- this.children = {};
373
+ this.children = new ArrayMap();
374
+
213
375
  /**
214
376
  * Count the children of a node
215
377
  * @type {number}
216
378
  */
217
379
  this.childrenCount = 0;
380
+
218
381
  /**
219
382
  * Indicates if the node is the root node, i.e. the first child node of the schema, whose name is the same as the schema name
220
383
  * @type {boolean}
221
384
  */
222
385
  this.isRoot = this.parent && !this.parent.parent && this.parent.name == this.name;
223
- /**
224
- * A user desciption of the node, in the form "label (name)"
225
- * @type {string}
226
- */
227
- this.userDescription = (this.label == "" || this.label == this.name) ? this.name : `${this.label} (${this.name})`;
386
+
228
387
  /**
229
388
  * Schema root elements may have a list of keys. This is a dictionary whose names are the key names and values the keys
230
- * @type {Object<string, XtkSchemaKey>}
389
+ * @type {ArrayNode<Campaign.XtkSchemaKey>}
231
390
  */
232
- this.keys = {};
391
+ this.keys = new ArrayMap();
392
+
233
393
  /**
234
394
  * The full path of the node
235
395
  * @type {string}
236
396
  */
237
397
  this.nodePath = this._getNodePath(true)._path;
398
+
238
399
  /**
239
400
  * Element of type "link" has an array of XtkJoin
240
- * @type {@type {XtkJoin[]}}
401
+ * @type {XtkJoin[]}
241
402
  */
242
403
  this.joins = [];
243
-
244
404
  for (var child of EntityAccessor.getChildElements(xml, "join")) {
245
405
  this.joins.push(new XtkJoin(child));
246
406
  }
247
407
 
248
- // Children (elements and attributes)
408
+ /**
409
+ * Returns a boolean which indicates whether the current node is ordinary.
410
+ * @type {boolean}
411
+ */
412
+ this.isAnyType = this.type === "ANY";
413
+
414
+ /**
415
+ * Returns a boolean which indicates whether the node is a link.
416
+ * @type {boolean}
417
+ */
418
+ this.isLink = this.type === "link";
419
+
420
+ /**
421
+ * Returns a boolean which indicates whether the value of the current node is linked to an enumeration.
422
+ * @type {boolean}
423
+ */
424
+ this.hasEnumeration = this.enum !== "";
425
+
426
+ /**
427
+ * Returns a boolean which indicates whether the current node is linked to an SQL table.
428
+ * @type {boolean}
429
+ */
430
+ this.hasSQLTable = this.sqlTable !== '';
431
+
432
+ /**
433
+ * The SQL name of the field. The property is an empty string if the object isn't an SQL type field.
434
+ * @type {string}
435
+ */
436
+ this.SQLName = EntityAccessor.getAttributeAsString(xml, "sqlname");
437
+
438
+ /**
439
+ * The SQL name of the table. The property is an empty string if the object isn't the main element or if schema mapping isn't of SQL type.
440
+ * @type {string}
441
+ */
442
+ this.SQLTable = EntityAccessor.getAttributeAsString(xml, "sqltable");
443
+
444
+ /**
445
+ * Returns a boolean indicating whether the table is a temporary table. The table will not be created during database creation.
446
+ * @type {boolean}
447
+ */
448
+ this.isTemporaryTable = EntityAccessor.getAttributeAsBoolean(xml, "temporaryTable");
449
+
450
+ /**
451
+ * Returns a boolean which indicates whether the current node is a logical sub-division of the schema.
452
+ * @type {boolean}
453
+ */
454
+ // An element has no real value if its type is empty
455
+ this.isElementOnly = this.type === "";
456
+
457
+ /**
458
+ * Returns a boolean. If the value added is vrai, during record deduplication, the default value (defined in defaultValue) is automatically reapplied during recording.
459
+ * @type {boolean}
460
+ */
461
+ this.isDefaultOnDuplicate = EntityAccessor.getAttributeAsBoolean(xml, "defOnDuplicate");
462
+
463
+ /**
464
+ * True if the node is a link and if the join is external.
465
+ * @type {boolean}
466
+ */
467
+ this.isExternalJoin = EntityAccessor.getAttributeAsBoolean(xml, "externalJoin");
468
+
469
+ /**
470
+ * Returns a boolean which indicates whether the current node is mapped by a Memo.
471
+ * @type {boolean}
472
+ */
473
+ this.isMemo = this.type === "memo" || this.type === "CDATA";
474
+
475
+ /**
476
+ * Returns a boolean which indicates whether the current node is mapped by a MemoData.
477
+ * @type {boolean}
478
+ */
479
+ this.isMemoData = this.isMemo && this.name === 'data';
480
+
481
+ /**
482
+ * Returns a boolean which indicates whether the current node is a BLOB.
483
+ * @type {boolean}
484
+ */
485
+ this.isBlob = this.type === "blob";
486
+
487
+ /**
488
+ * Returns a boolean which indicates whether the current node is mapped from CDATA type XML.
489
+ * @type {boolean}
490
+ */
491
+ this.isCDATA = this.type === "CDATA";
492
+
493
+ const notNull = EntityAccessor.getAttributeAsString(xml, "notNull");
494
+ const sqlDefault = EntityAccessor.getAttributeAsString(xml, "sqlDefault");
495
+ const notNullOverriden = notNull || sqlDefault === "NULL";
496
+ /**
497
+ * Returns a boolean which indicates whether or not the current node can take the null value into account.
498
+ * @type {boolean}
499
+ */
500
+ this.isNotNull = notNullOverriden ? XtkCaster.asBoolean(notNull) : this.type === "int64" || this.type === "short" ||
501
+ this.type === "long" || this.type === "byte" || this.type === "float" || this.type === "double" ||
502
+ this.type === "money" || this.type === "percent" || this.type === "time" || this.type === "boolean";
503
+
504
+ /**
505
+ * Returns a boolean which indicates whether or not the value of the current node is mandatory.
506
+ * @type {boolean}
507
+ */
508
+ this.isRequired = EntityAccessor.getAttributeAsBoolean(xml, "required");
509
+
510
+ /**
511
+ * Returns a boolean which indicates whether the current node is mapped in SQL.
512
+ * @type {boolean}
513
+ */
514
+ this.isSQL = !!this.SQLName || !!this.SQLTable || (this.isLink && this.schema.mappingType === 'sql' && !this.isMappedAsXML);
515
+
516
+ /**
517
+ * The SQL name of the field. The property is an empty string if the object isn't an SQL type field.
518
+ * @type {string}
519
+ */
520
+ this.PKSequence = EntityAccessor.getAttributeAsString(xml, "pkSequence");
521
+
522
+ /**
523
+ * Name of the reverse link in the target schema
524
+ * @type {string}
525
+ */
526
+ this.revLink = EntityAccessor.getAttributeAsString(xml, "revLink");
527
+
528
+ /**
529
+ * Returns a boolean which indicates whether the value of the current node is the result of a calculation.
530
+ * @type {boolean}
531
+ */
532
+ this.isCalculated = false;
533
+
534
+ /**
535
+ * Expression associated with the node
536
+ * @type {string}
537
+ */
538
+ this.expr = EntityAccessor.getAttributeAsString(xml, "expr");
539
+ if (this.expr) this.isCalculated = true;
540
+
541
+ /**
542
+ * Returns a boolean which indicates whether the value of the current node is incremented automatically.
543
+ * @type {boolean}
544
+ */
545
+ this.isAutoIncrement = EntityAccessor.getAttributeAsBoolean(xml, "autoIncrement");
546
+
547
+ /**
548
+ * Returns a boolean which indicates whether the current node is a primary key.
549
+ * @type {boolean}
550
+ */
551
+ this.isAutoPK = EntityAccessor.getAttributeAsBoolean(xml, "autopk");
552
+
553
+ /**
554
+ * Returns a boolean which indicates whether the current node is an automatic UUID
555
+ * @type {boolean}
556
+ */
557
+ this.isAutoUUID = EntityAccessor.getAttributeAsBoolean(xml, "autouuid");
558
+
559
+ /**
560
+ * Returns a boolean which indicates whether the schema is a staging schema
561
+ * @type {boolean}
562
+ */
563
+ this.isAutoStg = EntityAccessor.getAttributeAsBoolean(xml, "autoStg");
564
+
565
+ /**
566
+ * Returns a string that gives the package status.
567
+ * @type {"never" | "always" | "default" | "preCreate"}
568
+ */
569
+ this.packageStatusString = EntityAccessor.getAttributeAsString(xml, "pkgStatus");
570
+
571
+ /**
572
+ * Returns a number that gives the package status.
573
+ * @type {0 | 1 | 2 | 3}
574
+ */
575
+ this.packageStatus = PACKAGE_STATUS[this.packageStatusString];
576
+
577
+ // Children (elements and attributes)
249
578
  const childNodes = [];
250
- for (const child of EntityAccessor.getChildElements(xml, "attribute")) {
251
- const node = new XtkSchemaNode();
252
- node.init(schema, child, this, true);
253
- childNodes.push(node);
254
- }
255
- for (const child of EntityAccessor.getChildElements(xml, "element")) {
256
- const node = new XtkSchemaNode();
257
- node.init(schema, child, this, false);
258
- 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
+ }
259
594
  }
260
595
  for (const childNode of childNodes) {
261
- if (this.children[childNode.name]) {
262
- // already a child with the name => there's a problem with the schema
263
- throw new DomException(`Failed to create schema node '${childNode.name}': there's a already a node with this name`);
264
- }
265
- this.children[childNode.name] = childNode;
596
+ this.children._push(childNode.name, childNode);
266
597
  this.childrenCount = this.childrenCount + 1;
267
598
  }
268
599
 
269
600
  // Keys (after elements and attributes have been found)
270
601
  for (const child of EntityAccessor.getChildElements(xml, "key")) {
271
602
  const key = new XtkSchemaKey(schema, child, this);
272
- this.keys[key.name] = key;
603
+ this.keys._push(key.name, key);
273
604
  }
274
- }
275
605
 
276
- /**
277
- * Does the node have a child with the given name?
278
- *
279
- * @param {string} name the child name, without the "@" character for attributes
280
- * @returns {boolean} a boolean indicating whether the node contains a child with the given name
281
- */
282
- hasChild(name) {
283
- var child = this.children[name];
284
- if (child) return true;
285
- // TODO: handle ref target
286
- // if (this.hasRefTarget())
287
- // return this.refTarget().hasChild(name);
288
- return false;
606
+ // Propagate implicit values
607
+ // Name -> Label -> Desc -> HelpText
608
+ propagateImplicitValues(this);
289
609
  }
290
610
 
291
611
  /**
@@ -325,34 +645,78 @@ class XtkSchemaNode {
325
645
  return new XPath(path);
326
646
  }
327
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
+ }
328
699
 
329
700
  /**
330
- * 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)
331
703
  *
332
704
  * @param {XML.XPath|string} path XPath represents the name of the node to be searched
333
- * @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.
334
- * @param {boolean} mustExist indicates whether an exception must be raised if the node does not exist. true by default
335
- * @returns Returns a XtkSchemaNode instance if the node can be found, or null if the mustExist parameter is set to false.
336
- * @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
337
706
  */
338
- findNode(path, strict, mustExist) {
339
- if (strict === undefined) strict = true;
340
- if (mustExist === undefined) mustExist = true;
341
- if (typeof path == "string")
342
- path = new XPath(path);
707
+ async findNode(path) {
708
+ if (typeof path == "string") path = new XPath(path);
343
709
 
344
710
  // Find the starting node
345
711
  var node = this;
346
712
  if (path.isEmpty() || path.isAbsolute()) {
347
713
  node = this.schema.root;
348
- if (!node)
349
- 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;
350
715
  path = path.getRelativePath();
351
716
  }
352
717
 
353
718
  // Special case for current path "."
354
- if (path.isSelf())
355
- return this;
719
+ if (path.isSelf()) return this;
356
720
 
357
721
  const elements = path.getElements();
358
722
  while (node && elements.length > 0) {
@@ -360,57 +724,27 @@ class XtkSchemaNode {
360
724
  var name = element.asString();
361
725
 
362
726
  // TODO: if the path is a collection path, ignore the collection index
363
- // TODO: handle ref elements (consider the ref target instead)
364
- // TODO: Handle link between schemas
365
- // TODO: Handle any type
366
-
367
- if (!strict && elements.length == 0 && (!node.children[name] || !isAttributeName(name))) {
368
- // name is the final part of the path and the associated definition
369
- // does not exists. Since strict is set to false we check if the
370
- // alternate name exists (element name for an attribute or attribute
371
- // name for an element).
372
- var found = node.children[name];
373
- if (!found && isAttributeName(name)) found = node.children[name.substring(1)];
374
- if (!found && !isAttributeName(name)) found = node.children[`@${name}`];
375
- if (found) name = found.name;
376
- }
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];
377
738
 
378
739
  var childNode = null;
379
- if (element.isSelf())
380
- childNode = node;
381
- else if (element.isParent())
382
- childNode = node.parent;
383
- else
384
- 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];
385
743
  node = childNode;
386
744
  }
387
745
  return node;
388
746
  }
389
747
 
390
- // See CXtkNodeDef::GetChildDefAutoExpand
391
- _getChildDefAutoExpand(name, mustExist) {
392
- var child = this.children[name];
393
- if (child)
394
- return child;
395
-
396
- // TODO: handle ref
397
-
398
- if (mustExist) {
399
- // TODO: handle auto-expand schemas
400
- const path = this._getNodePath();
401
- const isAttribute = isAttributeName(name);
402
- const schemaDesc = this.schema.userDescription;
403
- if( path.isRootPath() ) {
404
- if (isAttribute) throw new DomException(`Unknown attribute '${name.substring(1)}' (see definition of schema '${schemaDesc}').`);
405
- else throw new DomException(`Unknown element '${name}' (see definition of schema '${schemaDesc}').`);
406
- }
407
- if (isAttribute) throw new DomException(`Unknown attribute '${name.substring(1)}' (see definition of element '${path.asString()}' in schema '${schemaDesc}').`);
408
- else throw new DomException(`Unknown element '${name}' (see definition of element '${path.asString()}' in schema '${schemaDesc}').`);
409
- }
410
-
411
- return null;
412
- }
413
-
414
748
  /**
415
749
  * Internal recursive function used to create a multi-line debug string representing the schema
416
750
  *
@@ -420,13 +754,104 @@ class XtkSchemaNode {
420
754
  */
421
755
  toString(indent) {
422
756
  indent = indent || "";
423
- var s = `${indent}${this.userDescription}\n`;
424
- for (var name in this.children) {
425
- s = s + this.children[name].toString(` ${indent}`);
757
+ var s = `${indent}${this.label} (${this.name})\n`;
758
+ for (var child of this.children) {
759
+ s = s + child.toString(` ${indent}`);
426
760
  }
427
761
  return s;
428
762
  }
429
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
+ }
430
855
  }
431
856
 
432
857
  // ========================================================================================
@@ -489,6 +914,8 @@ function XtkEnumerationValue(xml, baseType) {
489
914
  * @type {*}
490
915
  */
491
916
  this.value = XtkCaster.as(stringValue, baseType);
917
+
918
+ propagateImplicitValues(this, true);
492
919
  }
493
920
 
494
921
  /**
@@ -500,52 +927,69 @@ function XtkEnumerationValue(xml, baseType) {
500
927
  * @param {XML.XtkObject} xml the enumeration definition
501
928
  * @memberof Campaign
502
929
  */
503
- function XtkEnumeration(xml) {
504
- /**
505
- * The system enumeration name
506
- * @type {string}
507
- */
508
- this.name = EntityAccessor.getAttributeAsString(xml, "name");
509
- /**
510
- * A human friendly name for the system enumeration
511
- * @type {string}
512
- */
513
- this.label = EntityAccessor.getAttributeAsString(xml, "label");
514
- /**
515
- * A human friendly long description of the enumeration
516
- * @type {string}
517
- */
518
- this.description = EntityAccessor.getAttributeAsString(xml, "desc");
519
- /**
520
- * The type of the enumeration
521
- * @type {Campaign.XtkEnumerationType}
522
- */
523
- this.baseType = EntityAccessor.getAttributeAsString(xml, "basetype");
524
- /**
525
- * The default value of the enumeration
526
- * @type {Campaign.XtkEnumerationValue}
527
- */
528
- this.default = null;
529
- /**
530
- * Indicates if the enumeration has an image, i.e. if any of its values has an image
531
- * @type {boolean}
532
- */
533
- this.hasImage = false;
534
- /**
535
- * The enumerations values
536
- * @type {Object<string, Campaign.XtkEnumerationValue>}
537
- */
538
- this.values = {};
930
+ class XtkEnumeration {
931
+ constructor(schemaId, xml) {
932
+ /**
933
+ * The system enumeration name, fully qualified, i.e. prefixed with the schema id
934
+ * @type {string}
935
+ */
936
+ this.name = EntityAccessor.getAttributeAsString(xml, "name");
937
+
938
+ /**
939
+ * A human friendly name for the system enumeration
940
+ * @type {string}
941
+ */
942
+ this.label = EntityAccessor.getAttributeAsString(xml, "label");
943
+
944
+ /**
945
+ * A human friendly long description of the enumeration
946
+ * @type {string}
947
+ */
948
+ this.description = EntityAccessor.getAttributeAsString(xml, "desc");
949
+
950
+ /**
951
+ * The type of the enumeration, usually "string" or "byte"
952
+ * @type {Campaign.XtkEnumerationType}
953
+ */
954
+ this.baseType = EntityAccessor.getAttributeAsString(xml, "basetype");
955
+
956
+ /**
957
+ * The default value of the enumeration
958
+ * @type {Campaign.XtkEnumerationValue}
959
+ */
960
+ this.default = null;
961
+
962
+ /**
963
+ * Indicates if the enumeration has an image, i.e. if any of its values has an image
964
+ * @type {boolean}
965
+ */
966
+ this.hasImage = false;
539
967
 
540
- var defaultValue = EntityAccessor.getAttributeAsString(xml, "default");
968
+ /**
969
+ * The enumerations values
970
+ * @type {Utils.ArrayMap<Campaign.XtkEnumerationValue>}
971
+ */
972
+ this.values = new ArrayMap();
973
+
974
+ var defaultValue = EntityAccessor.getAttributeAsString(xml, "default");
975
+
976
+ for (var child of EntityAccessor.getChildElements(xml, "value")) {
977
+ const e = new XtkEnumerationValue(child, this.baseType);
978
+ this.values._push(e.name, e);
979
+ if (e.image != "") this.hasImage = true;
980
+ const stringValue = EntityAccessor.getAttributeAsString(child, "value");
981
+ if (defaultValue == stringValue)
982
+ this.default = e;
983
+ }
984
+
985
+ propagateImplicitValues(this, true);
541
986
 
542
- for (var child of EntityAccessor.getChildElements(xml, "value")) {
543
- const e = new XtkEnumerationValue(child, this.baseType);
544
- this.values[e.name] = e;
545
- if (e.image != "") this.hasImage = true;
546
- const stringValue = EntityAccessor.getAttributeAsString(child, "value");
547
- if (defaultValue == stringValue)
548
- this.default = e;
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}`;
549
993
  }
550
994
  }
551
995
 
@@ -565,58 +1009,79 @@ function XtkEnumeration(xml) {
565
1009
  */
566
1010
  class XtkSchema extends XtkSchemaNode {
567
1011
 
568
- constructor(xml) {
1012
+ constructor(application, xml) {
569
1013
  super();
570
- this.init(this, xml);
1014
+ this._application = application;
571
1015
 
572
1016
  /**
573
1017
  * The namespace of the schema
574
1018
  * @type {string}
575
1019
  */
576
1020
  this.namespace = EntityAccessor.getAttributeAsString(xml, "namespace");
1021
+
577
1022
  /**
578
1023
  * The schema id, in the form "namespace:name"
579
1024
  * @type {string}
580
1025
  */
1026
+ this.name = EntityAccessor.getAttributeAsString(xml, "name");
581
1027
  this.id = `${this.namespace}:${this.name}`;
1028
+
582
1029
  /**
583
1030
  * Indicates whether the schema is a library schema or not
584
1031
  * @type {boolean}
585
1032
  */
586
1033
  this.isLibrary = EntityAccessor.getAttributeAsBoolean(xml, "library");
1034
+
587
1035
  /**
588
1036
  * A human name for the schema, in singular
589
1037
  * @type {string}
590
1038
  */
591
1039
  this.labelSingular = EntityAccessor.getAttributeAsString(xml, "labelSingular");
1040
+
592
1041
  /**
593
1042
  * The schema mappgin type, following the xtk:srcSchema:mappingType enumeration
594
1043
  * @type {Campaign.XtkSchemaMappingType}
595
1044
  */
596
1045
  this.mappingType = EntityAccessor.getAttributeAsString(xml, "mappingType");
1046
+
1047
+ /**
1048
+ * The MD5 checksum of the schema in the form of a hexadecimal string
1049
+ * @type {string}
1050
+ */
1051
+ this.md5 = EntityAccessor.getAttributeAsString(xml, "md5");
1052
+
597
1053
  /**
598
1054
  * The schema definition
599
1055
  * @private
600
1056
  * @type {XML.XtkObject}
601
1057
  */
602
1058
  this.xml = xml;
1059
+
1060
+ this.init(this, xml);
1061
+
603
1062
  /**
604
1063
  * The schema root node, if it has one, i.e. the first child whose name matches the schema name
605
1064
  * @type {Campaign.XtkSchemaNode}
606
1065
  */
607
1066
  this.root = this.children[this.name];
1067
+
1068
+ /**
1069
+ * A user desciption of the node, in the form "label (name)"
1070
+ * @type {string}
1071
+ */
1072
+ this.userDescription = (this.label == this.name) ? this.name : `${this.label} (${this.name})`;
1073
+
608
1074
  /**
609
1075
  * Enumerations in this schema, as a dictionary whose keys are enumeration names and values are the
610
1076
  * corresponding enumeration definitions
611
- * @type {Object<string, XtkEnumeration>}
1077
+ * @type {Utils.ArrayMap<Campaign.XtkEnumeration>}
612
1078
  */
613
- this.enumerations = {};
614
-
615
- for (var child of EntityAccessor.getChildElements(xml, "enumeration")) {
616
- const e = new XtkEnumeration(child);
617
- this.enumerations[e.name] = e;
618
- }
619
- }
1079
+ this.enumerations = new ArrayMap();
1080
+ for (var child of EntityAccessor.getChildElements(xml, "enumeration")) {
1081
+ const e = new XtkEnumeration(this.id, child);
1082
+ this.enumerations._push(e.shortName, e);
1083
+ }
1084
+ }
620
1085
 
621
1086
  /**
622
1087
  * Creates a multi-line debug string representing the schema
@@ -625,9 +1090,8 @@ class XtkSchema extends XtkSchemaNode {
625
1090
  */
626
1091
  toString() {
627
1092
  var s = `${this.userDescription}\n`;
628
- //s = s + ` enumerations: [${enumerations}]`
629
- for (var name in this.children) {
630
- s = s + this.children[name].toString(" - ");
1093
+ for (var child of this.children) {
1094
+ s = s + child.toString(" - ");
631
1095
  }
632
1096
  return s;
633
1097
  }
@@ -657,21 +1121,25 @@ class CurrentLogin {
657
1121
  * @type {string}
658
1122
  */
659
1123
  this.login = EntityAccessor.getAttributeAsString(userInfo, "login");
1124
+
660
1125
  /**
661
1126
  * The operator login id
662
1127
  * @type {number}
663
1128
  */
664
1129
  this.id = EntityAccessor.getAttributeAsLong(userInfo, "loginId");
1130
+
665
1131
  /**
666
1132
  * A human friendly string naming the operator (compute string)
667
1133
  * @type {string}
668
1134
  */
669
1135
  this.computeString = EntityAccessor.getAttributeAsString(userInfo, "loginCS");
1136
+
670
1137
  /**
671
1138
  * The operator timezone
672
1139
  * @type {string}
673
1140
  */
674
1141
  this.timezone = EntityAccessor.getAttributeAsString(userInfo, "timezone");
1142
+
675
1143
  /**
676
1144
  * The llist of operator rights
677
1145
  * @type {string[]}
@@ -680,8 +1148,8 @@ class CurrentLogin {
680
1148
  this._rightsSet = {};
681
1149
  for (var child of EntityAccessor.getChildElements(userInfo, "login-right")) {
682
1150
  const right = EntityAccessor.getAttributeAsString(child, "right");
683
- this.rights.push(right);
684
- this._rightsSet[right] = true;
1151
+ this.rights.push(right);
1152
+ this._rightsSet[right] = true;
685
1153
  }
686
1154
  }
687
1155
 
@@ -729,6 +1197,7 @@ class Application {
729
1197
  */
730
1198
  constructor(client) {
731
1199
  this.client = client;
1200
+ this._schemaCache = new SchemaCache(client);
732
1201
  const info = this.client.getSessionInfo();
733
1202
  // When using "SessionToken" authentication, there is no actual logon, and therefore
734
1203
  // no "sessionInfo" object
@@ -738,25 +1207,25 @@ class Application {
738
1207
  * The server build number
739
1208
  * @type {string}
740
1209
  */
741
- this.buildNumber = EntityAccessor.getAttributeAsString(serverInfo, "buildNumber");
1210
+ this.buildNumber = EntityAccessor.getAttributeAsString(serverInfo, "buildNumber");
742
1211
  /**
743
1212
  * The Campaign instance name
744
1213
  * @type {string}
745
1214
  */
746
- this.instanceName = EntityAccessor.getAttributeAsString(serverInfo, "instanceName");
1215
+ this.instanceName = EntityAccessor.getAttributeAsString(serverInfo, "instanceName");
747
1216
  const userInfo = EntityAccessor.getElement(info, "userInfo");
748
1217
  /**
749
1218
  * The logged operator
750
1219
  * @type {Campaign.CurrentLogin}
751
1220
  */
752
- this.operator = new CurrentLogin(userInfo);
1221
+ this.operator = new CurrentLogin(userInfo);
753
1222
  /**
754
1223
  * The list of installed packages
755
1224
  * @type {string[]}
756
1225
  */
757
- this.packages = [];
1226
+ this.packages = [];
758
1227
  for (var p of EntityAccessor.getChildElements(userInfo, "installed-package")) {
759
- this.packages.push(`${EntityAccessor.getAttributeAsString(p, "namespace")}:${EntityAccessor.getAttributeAsString(p, "name")}`);
1228
+ this.packages.push(`${EntityAccessor.getAttributeAsString(p, "namespace")}:${EntityAccessor.getAttributeAsString(p, "name")}`);
760
1229
  }
761
1230
  }
762
1231
  }
@@ -769,10 +1238,15 @@ class Application {
769
1238
  * @returns {Campaign.XtkSchema} the schema, or null if the schema was not found
770
1239
  */
771
1240
  async getSchema(schemaId) {
1241
+ return this._schemaCache.getSchema(schemaId);
1242
+ }
1243
+
1244
+ // Private function: get a schema without using the cache
1245
+ async _getSchema(schemaId) {
772
1246
  const xml = await this.client.getSchema(schemaId, "xml");
773
1247
  if (!xml)
774
1248
  return null;
775
- return newSchema(xml);
1249
+ return newSchema(xml, this);
776
1250
  }
777
1251
 
778
1252
  /**
@@ -787,6 +1261,32 @@ class Application {
787
1261
  }
788
1262
  return false;
789
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
+ }
790
1290
  }
791
1291
 
792
1292
 
@@ -797,4 +1297,5 @@ exports.Application = Application;
797
1297
  // For tests
798
1298
  exports.newSchema = newSchema;
799
1299
  exports.newCurrentLogin = newCurrentLogin;
1300
+ exports.SchemaCache = SchemaCache;
800
1301
  })();