@adobe/acc-js-sdk 1.1.9 → 1.1.10

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/acc-js-sdk",
3
- "version": "1.1.9",
3
+ "version": "1.1.10",
4
4
  "description": "ACC Javascript SDK",
5
5
  "main": "src/index.js",
6
6
  "homepage": "https://github.com/adobe/acc-js-sdk#readme",
@@ -0,0 +1,160 @@
1
+ /*
2
+ Copyright 2022 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+ const { XtkCaster } = require("../src/xtkCaster.js");
13
+ const utils = require("./utils.js");
14
+
15
+ /**
16
+ * Basic samples illustrating the xtk:persist interface
17
+ */
18
+
19
+ ( async () => {
20
+
21
+ await utils.sample({
22
+ title: "NewInstance",
23
+ labels: [ "xtk:persist", "Basics", "NewInstance", "CRUD" ],
24
+ description: `The "NewInstance" method creates a new instance of an entity in memory`,
25
+ code: async() => {
26
+ return await utils.logon(async (client, NLWS) => {
27
+ // Create a new delivery object
28
+ const delivery = client.NLWS.nmsDelivery.create({ label: "Test #1", messageType: "0" });
29
+ await delivery.newInstance();
30
+
31
+ // At this point, the delivery has been given an id (delivery.entity.id) and is ready to
32
+ // be persisted using client.NLWS.xtkSession.write or save
33
+ // Optionally, attributes can be set/changed at this point
34
+ delivery.$desc = "My description";
35
+
36
+ // Now we're creating the delivery in the database
37
+ await client.NLWS.xtkSession.write(delivery);
38
+
39
+ // In order to update after creation, do not forget the "_operation=update" or the
40
+ // call will fail or create a duplicate
41
+ delivery._operation = "update";
42
+ delivery.label = "Test #4";
43
+ await delivery.save();
44
+
45
+ // Finally delete the object
46
+ delivery._operation = "delete";
47
+ await delivery.save();
48
+ });
49
+ }
50
+ });
51
+
52
+ await utils.sample({
53
+ title: "Duplicate",
54
+ labels: [ "xtk:persist", "Basics", "Duplicate", "CRUD" ],
55
+ description: `The "Duplicate" method creates a new instance of an entity in memory by duplicating an existing instance`,
56
+ code: async() => {
57
+ return await utils.logon(async (client, NLWS) => {
58
+
59
+ // Get the webApp operator id
60
+ const query = NLWS.xtkQueryDef.create({
61
+ schema: "xtk:operator", operation: "get",
62
+ select: { node: [ { expr: "@id" } ] },
63
+ where: { condition: [ { expr:`@name='webapp'` } ] }
64
+ });
65
+ const webAppOperator = await query.executeQuery();
66
+ const webAppOperatorId = XtkCaster.asLong(webAppOperator.id);
67
+
68
+ // Duplicate the webApp operator and name it 'alex'
69
+ const operator = client.NLWS.xtkOperator.create();
70
+ await operator.duplicate(`xtk:operator|${webAppOperatorId}`);
71
+ operator.name = "alex";
72
+ await operator.save();
73
+
74
+ // Change the operator name to 'Alexandre'
75
+ operator.entity.name = operator.entity.name + "andre";
76
+ operator._operation = "update";
77
+ await operator.save();
78
+ });
79
+ }
80
+ });
81
+
82
+ await utils.sample({
83
+ title: "Create a recipient",
84
+ labels: [ "xtk:persist", "Basics", "Write", "CRUD" ],
85
+ description: `The "Write" method creates or updates an entity`,
86
+ code: async() => {
87
+ return await utils.logon(async (client, NLWS) => {
88
+ await client.NLWS.xtkSession.write({ xtkschema:"nms:recipient", email: 'amorin@adobe.com' });
89
+ });
90
+ }
91
+ });
92
+
93
+ // Set recipient folder
94
+ await utils.sample({
95
+ title: "Set the folder of a recipient",
96
+ labels: [ "xtk:persist", "Basics", "Write", "CRUD" ],
97
+ description: `The "Write" method creates or updates an entity`,
98
+ code: async() => {
99
+ return await utils.logon(async (client, NLWS) => {
100
+
101
+ // Reserve an id and create a recipient with this id
102
+ const idList = XtkCaster.asArray(await client.NLWS.xtkSession.GetNewIdsEx(1, "XtkNewId"));
103
+ console.log(`Reserved ids: ${idList}`);
104
+ await client.NLWS.xtkSession.write({ xtkschema:"nms:recipient", id: idList[0], email: 'amorin@adobe.com' });
105
+
106
+ // Move to default folder
107
+ await client.NLWS.xtkSession.write({
108
+ xtkschema:"nms:recipient",
109
+ id: idList[0], _operation:"update",
110
+ folder: {
111
+ _operation: "none",
112
+ name: "nmsRootRecipient"
113
+ }
114
+ });
115
+ });
116
+ }
117
+ });
118
+
119
+ // Update folder label by key
120
+ await utils.sample({
121
+ title: "Update folder label",
122
+ labels: [ "xtk:persist", "Basics", "Write", "CRUD" ],
123
+ description: `The "Write" method creates or updates an entity`,
124
+ code: async() => {
125
+ return await utils.logon(async (client, NLWS) => {
126
+
127
+ // Create a folder
128
+ const name = 'acc_js_sdk_005';
129
+ console.log(`Creating a test folder with internal name '${name}'`);
130
+ const folder = client.NLWS.xtkFolder.create()
131
+ folder.name = name;
132
+ folder.label = 'Test folder for the ACC JS SDK (sample #5)';
133
+ await folder.save();
134
+
135
+ // Modify the folder label
136
+ console.log(`Modifying the folder label`);
137
+ await client.NLWS.xtkSession.write({
138
+ xtkschema: "xtk:folder",
139
+ _operation: "update", name: name,
140
+ label: "Hello World",
141
+ });
142
+
143
+ // Loading the folder again
144
+ // Notice that changing the label has also triggered a change of the full name
145
+ const query = NLWS.xtkQueryDef.create({
146
+ schema: "xtk:folder", operation: "get",
147
+ select: { node: [ { expr: "@id" }, { expr: "@name" }, { expr: "@label" }, { expr: "@fullName" } ] },
148
+ where: { condition: [ { expr:`@name='${name}'` } ] }
149
+ });
150
+ const databaseFolder = await query.executeQuery();
151
+ console.log(`>> databaseFolder: ${JSON.stringify(databaseFolder)}`);
152
+
153
+ // Delete the folder
154
+ await client.NLWS.xtkSession.write({ xtkschema: "xtk:folder", _operation: "delete", name: name });
155
+ });
156
+ }
157
+ });
158
+
159
+ })();
160
+
@@ -608,6 +608,11 @@ class XtkSchemaNode {
608
608
  */
609
609
  this.packageStatus = PACKAGE_STATUS[this.packageStatusString];
610
610
 
611
+ /**
612
+ * Returns a string (a schema id) which indicates the custom/extended entity, attribute belongs to.
613
+ */
614
+ this.belongsTo = EntityAccessor.getAttributeAsString(xml, "belongsTo");
615
+
611
616
  // Children (elements and attributes)
612
617
  const childNodes = [];
613
618
  for (const child of EntityAccessor.getChildElements(xml)) {
package/src/client.js CHANGED
@@ -71,8 +71,24 @@ const { Util } = require('./util.js');
71
71
  * @memberof Campaign
72
72
  */
73
73
  const xtkObjectHandler = {
74
+ set: function(callContext, prop, value) {
75
+ const object = callContext.object;
76
+ object[prop] = value;
77
+ },
78
+
74
79
  get: function(callContext, methodName) {
75
- if (methodName == ".") return callContext;
80
+ if (methodName == ".")
81
+ return callContext;
82
+ if (methodName === "__xtkProxy")
83
+ return true;
84
+ if (methodName === "save") {
85
+ return async () => {
86
+ return callContext.client.NLWS.xtkSession.write(callContext.object);
87
+ };
88
+ }
89
+ if (methodName === "entity") {
90
+ return callContext.object;
91
+ }
76
92
 
77
93
  const caller = function(thisArg, argumentsList) {
78
94
  const callContext = thisArg["."];
@@ -149,7 +165,8 @@ const clientHandler = (representation, headers, pushDownOptions) => {
149
165
  for (let h in pushDownOptions) callContext.pushDownOptions[h] = pushDownOptions[h];
150
166
  }
151
167
 
152
- if (methodName == ".") return callContext;
168
+ if (methodName == ".")
169
+ return callContext;
153
170
 
154
171
  // get Schema id from namespace (find first upper case letter)
155
172
  var schemaId = "";
@@ -187,7 +204,8 @@ const clientHandler = (representation, headers, pushDownOptions) => {
187
204
 
188
205
  if (methodName == "create") {
189
206
  return function(body) {
190
- callContext.object = body;
207
+ callContext.object = body || {}; // supports empty bodies
208
+ if (!callContext.object.xtkschema) callContext.object.xtkschema = callContext.schemaId;
191
209
  return new Proxy(callContext, xtkObjectHandler);
192
210
  };
193
211
  }
@@ -1380,16 +1398,20 @@ class Client {
1380
1398
  * @returns {XML.XtkObject} the schema definition, as either a DOM document or a JSON object
1381
1399
  */
1382
1400
  async getSchema(schemaId, representation, internal) {
1383
- var that = this;
1384
- var entity = that._entityCache.get("xtk:schema", schemaId);
1401
+ var entity = this._entityCache.get("xtk:schema", schemaId);
1385
1402
  if (!entity) {
1386
- entity = await that.getEntityIfMoreRecent("xtk:schema", schemaId, "xml", internal);
1403
+ entity = await this.getEntityIfMoreRecent("xtk:schema", schemaId, "xml", internal);
1387
1404
  if (entity) {
1388
- that._entityCache.put("xtk:schema", schemaId, entity);
1389
- that._methodCache.put(entity);
1405
+ const impls = DomUtil.getAttributeAsString(entity, "implements");
1406
+ if (impls === "xtk:persist" && schemaId !== "xtk:session" && schemaId !== "xtk:persist") {
1407
+ // Ensure xtk:persist is present by loading the xtk:session schema
1408
+ await this.getSchema("xtk:session", "xml", true);
1409
+ }
1410
+ this._entityCache.put("xtk:schema", schemaId, entity);
1411
+ this._methodCache.put(entity);
1390
1412
  }
1391
1413
  }
1392
- entity = that._toRepresentation(entity, representation);
1414
+ entity = this._toRepresentation(entity, representation);
1393
1415
  return entity;
1394
1416
  }
1395
1417
 
@@ -1481,9 +1503,13 @@ class Client {
1481
1503
 
1482
1504
  const rootName = schemaId.substr(schemaId.indexOf(':') + 1);
1483
1505
  object = that._fromRepresentation(rootName, object, callContext.representation);
1506
+ // The xtk:persist#NewInstance requires a xtkschema attribute which we can compute here
1507
+ // Actually, we're always adding it, for all non-static methods
1508
+ const xmlRoot = object.nodeType === 9 ? object.documentElement : object;
1509
+ if (!xmlRoot.hasAttribute("xtkschema"))
1510
+ xmlRoot.setAttribute("xtkschema", schemaId);
1484
1511
  soapCall.writeDocument("document", object);
1485
1512
  }
1486
-
1487
1513
  const parametersIsArray = (typeof parameters == "object") && parameters.length;
1488
1514
  const params = DomUtil.getFirstChildElement(method, "parameters");
1489
1515
  if (params) {
@@ -1494,10 +1520,12 @@ class Client {
1494
1520
  if (!inout || inout=="in") {
1495
1521
  const type = DomUtil.getAttributeAsString(param, "type");
1496
1522
  const paramName = DomUtil.getAttributeAsString(param, "name");
1497
- const paramValue = parametersIsArray ? parameters[paramIndex] : parameters;
1523
+ let paramValue = parametersIsArray ? parameters[paramIndex] : parameters;
1498
1524
  paramIndex = paramIndex + 1;
1499
1525
  if (type == "string")
1500
1526
  soapCall.writeString(paramName, XtkCaster.asString(paramValue));
1527
+ else if (type == "primarykey")
1528
+ soapCall.writeString(paramName, XtkCaster.asString(paramValue));
1501
1529
  else if (type == "boolean")
1502
1530
  soapCall.writeBoolean(paramName, XtkCaster.asBoolean(paramValue));
1503
1531
  else if (type == "byte")
@@ -1516,22 +1544,35 @@ class Client {
1516
1544
  soapCall.writeDate(paramName, XtkCaster.asDate(paramValue));
1517
1545
  else if (type == "DOMDocument" || type == "DOMElement") {
1518
1546
  var docName = undefined;
1519
- // Hack for workflow API. The C++ code checks that the name of the XML element is <variables>. When
1520
- // using xml representation at the SDK level, it's ok since the SDK caller will set that. But this does
1521
- // not work when using "BadgerFish" representation where we do not know the root element name.
1522
- if (schemaId == "xtk:workflow" && methodName == "StartWithParameters" && paramName == "parameters")
1523
- docName = "variables";
1524
- if (schemaId == "nms:rtEvent" && methodName == "PushEvent")
1525
- docName = "rtEvent";
1526
- // Try to guess the document name. This is usually available in the xtkschema attribute
1527
- var xtkschema = EntityAccessor.getAttributeAsString(paramValue, "xtkschema");
1528
- if (!xtkschema) xtkschema = paramValue["@xtkschema"];
1529
- if (xtkschema) {
1547
+ let representation = callContext.representation;
1548
+ if (paramValue.__xtkProxy) {
1549
+ // A xtk proxy object is passed as a parameter. The call context contains the schema so we
1550
+ // can use it to determine the XML document root (docName)
1551
+ const paramValueContext = paramValue["."];
1552
+ paramValue = paramValueContext.object;
1553
+ const xtkschema = paramValueContext.schemaId;
1530
1554
  const index = xtkschema.indexOf(":");
1531
- docName = xtkschema.substr(index+1);
1555
+ docName = xtkschema.substring(index+1);
1556
+ representation = paramValueContext.representation; // xtk proxy may have it's own representation
1557
+ }
1558
+ else {
1559
+ // Hack for workflow API. The C++ code checks that the name of the XML element is <variables>. When
1560
+ // using xml representation at the SDK level, it's ok since the SDK caller will set that. But this does
1561
+ // not work when using "BadgerFish" representation where we do not know the root element name.
1562
+ if (schemaId == "xtk:workflow" && methodName == "StartWithParameters" && paramName == "parameters")
1563
+ docName = "variables";
1564
+ if (schemaId == "nms:rtEvent" && methodName == "PushEvent")
1565
+ docName = "rtEvent";
1566
+ // Try to guess the document name. This is usually available in the xtkschema attribute
1567
+ var xtkschema = EntityAccessor.getAttributeAsString(paramValue, "xtkschema");
1568
+ if (!xtkschema) xtkschema = paramValue["@xtkschema"];
1569
+ if (xtkschema) {
1570
+ const index = xtkschema.indexOf(":");
1571
+ docName = xtkschema.substring(index+1);
1572
+ }
1573
+ if (!docName) docName = paramName; // Use te parameter name as the XML root element
1532
1574
  }
1533
- if (!docName) docName = paramName; // Use te parameter name as the XML root element
1534
- var xmlValue = that._fromRepresentation(docName, paramValue, callContext.representation);
1575
+ var xmlValue = that._fromRepresentation(docName, paramValue, representation);
1535
1576
  if (type == "DOMDocument")
1536
1577
  soapCall.writeDocument(paramName, xmlValue);
1537
1578
  else
@@ -1564,6 +1605,8 @@ class Client {
1564
1605
  var returnValue;
1565
1606
  if (type == "string")
1566
1607
  returnValue = soapCall.getNextString();
1608
+ else if (type == "primarykey")
1609
+ returnValue = soapCall.getNextPrimaryKey();
1567
1610
  else if (type == "boolean")
1568
1611
  returnValue = soapCall.getNextBoolean();
1569
1612
  else if (type == "byte")
@@ -90,33 +90,44 @@ class MethodCache extends Cache {
90
90
  var impls = DomUtil.getAttributeAsString(schema, "implements");
91
91
  var root = DomUtil.getFirstChildElement(schema);
92
92
  while (root) {
93
- var schemaId;
94
- var soapUrn;
95
-
93
+ let schemaId;
96
94
  if (root.nodeName == "interface") {
97
- const itfName = namespace + ":" + DomUtil.getAttributeAsString(root, "name");
98
- if (impls === itfName) {
99
- schemaId = namespace + ":" + name;
100
- soapUrn = itfName;
101
- }
95
+ const nodeName = DomUtil.getAttributeAsString(root, "name");
96
+ schemaId = `${namespace}:${nodeName}`;
102
97
  }
103
98
  else if (root.nodeName == "methods") {
104
- schemaId = namespace + ":" + name;
105
- soapUrn = schemaId;
99
+ schemaId = `${namespace}:${name}`;
106
100
  }
107
-
108
101
  if (schemaId) {
109
102
  var child = DomUtil.getFirstChildElement(root, "method");
110
103
  while (child) {
111
104
  const methodName = DomUtil.getAttributeAsString(child, "name");
112
- const cached = { method: child, urn: soapUrn };
105
+ const cached = { method: child, urn: schemaId };
113
106
  super.put(schemaId, methodName, cached);
114
- super.put(soapUrn, methodName, cached); /// version 0.1.23: cache the method in both the schema id and interface id form compatibility reasons
115
107
  child = DomUtil.getNextSiblingElement(child, "method");
116
108
  }
117
109
  }
118
110
  root = DomUtil.getNextSiblingElement(root);
119
111
  }
112
+
113
+ // If the schema implements an interface, then add the interface methods to the schema
114
+ // methods in the cache, using the "<interface>|<schemaId>" urn
115
+ // example: xtk:session implements xtk:persist, and therefore will have xtk:persist methods
116
+ // under the urn "xtk:persist|xtk:session"
117
+ if (impls) {
118
+ const schemaId = `${namespace}:${name}`;
119
+ const prefix = `${impls}#`;
120
+ const urn = `${impls}|${schemaId}`;
121
+ const keys = Object.keys(this._cache);
122
+ for (const key of keys) {
123
+ if (key.startsWith(prefix)) {
124
+ let cached = this._cache[key].value;
125
+ cached = { method: cached.method, urn: urn };
126
+ const methodName = DomUtil.getAttributeAsString(cached.method, "name");
127
+ super.put(schemaId, methodName, cached);
128
+ }
129
+ }
130
+ }
120
131
  }
121
132
 
122
133
  /**
package/src/soap.js CHANGED
@@ -356,13 +356,25 @@ class SoapMethodCall {
356
356
  *
357
357
  * @returns {string} the string result value
358
358
  */
359
- getNextString() {
359
+ getNextString() {
360
360
  this._checkTypeMatch("xsd:string");
361
361
  var value = DomUtil.elementValue(this.elemCurrent);
362
362
  this.elemCurrent = DomUtil.getNextSiblingElement(this.elemCurrent);
363
363
  return value;
364
364
  }
365
365
 
366
+ /**
367
+ * Extracts the next result value as a primary key string
368
+ *
369
+ * @returns {string} the primary key string result value
370
+ */
371
+ getNextPrimaryKey() {
372
+ this._checkTypeMatch("xsd:primarykey");
373
+ var value = DomUtil.elementValue(this.elemCurrent);
374
+ this.elemCurrent = DomUtil.getNextSiblingElement(this.elemCurrent);
375
+ return value;
376
+ }
377
+
366
378
  /**
367
379
  * Extracts the next result value as a boolean
368
380
  *
package/src/xtkCaster.js CHANGED
@@ -58,6 +58,7 @@ const { Util } = require('./util.js');
58
58
  | timespan | 14 | number | A timespan, in seconds
59
59
  | boolean | 15 | boolean | boolean value, defaultint to false. Cannot be null |
60
60
  | array | | Array | a array or a collection
61
+ | primarykey | | string | A primary key is serialized with format <schemaId>|<keyField>[|<keyField>[...]]
61
62
 
62
63
  * @typedef {(0|''|6|'string'|'int64'|12|13|'memo'|'CDATA'|1|'byte'|2|'short'|3|'long'|15|'boolean'|4|5|'float'|'double'|7|'datetime'|'datetimetz'|'datetimenotz'|10|'date'|14|'timespan'|'array')} XtkType
63
64
  * @memberof Campaign
@@ -94,6 +95,7 @@ class XtkCaster {
94
95
  case "string":
95
96
  case "uuid":
96
97
  case "int64":
98
+ case "primarykey":
97
99
  return "stringValue";
98
100
  case 12: // FIELD_MEMO
99
101
  case 13: // FIELD_MEMOSHORT
@@ -198,6 +200,9 @@ class XtkCaster {
198
200
  case "timespan": {
199
201
  return this.asTimespan(value);
200
202
  }
203
+ case "primarykey": {
204
+ return this.asPrimaryKey(value);
205
+ }
201
206
  default: {
202
207
  throw CampaignException.BAD_PARAMETER("type", type, `Cannot convert value type='${type}', value='${value}'`);
203
208
  }
@@ -218,7 +223,21 @@ class XtkCaster {
218
223
  return ""; // Invalid JavaScript date
219
224
  return value.toISOString();
220
225
  }
221
- if ((typeof value) == "object") return "";
226
+ if ((typeof value) == "object") {
227
+ if (XtkCaster.isPrimaryKey(value)) {
228
+ let result = value.schemaId;
229
+ for (let i=0; i<value.values.length; i++) {
230
+ result = result + "|";
231
+ let item = value.values[i];
232
+ if (item === null || item === undefined) continue;
233
+ if (typeof item !== "string") item = XtkCaster.asString(item);
234
+ const escaped = item.replace(/\|/g, "\\|").replace(/\"/g, "\\\""); // escape | and " chars
235
+ result = result + escaped;
236
+ }
237
+ return result;
238
+ }
239
+ return "";
240
+ }
222
241
  return value.toString();
223
242
  }
224
243
 
@@ -430,6 +449,35 @@ class XtkCaster {
430
449
  var timespan = XtkCaster.asLong(value);
431
450
  return timespan;
432
451
  }
452
+
453
+ /**
454
+ * Convert a raw value into an primary key object. A primary key is serialized with format <schemaId>|<keyField>[|<keyField>[...]]
455
+ *
456
+ * @param {*} value is the raw value to convert
457
+ * @return {PrimaryKey} a primary key
458
+ */
459
+ static asPrimaryKey(value) {
460
+ if (value === null || value === undefined) return undefined;
461
+ if (this.isPrimaryKey(value)) return value;
462
+ value = value.toString();
463
+ let index = value.indexOf('|');
464
+ if (index <= 0) return undefined; // no schema id or empty schema id
465
+ const primaryKey = {
466
+ schemaId: value.substring(0, index),
467
+ values: []
468
+ };
469
+ value = value.substring(index+1) + "|";
470
+ let start = 0;
471
+ for(var i=0; i<value.length; i++) {
472
+ const c = value[i];
473
+ if (c === '\\') { i = i + 1; continue; } // escaped char
474
+ if (c === '|') {
475
+ primaryKey.values.push(value.substring(start, i).replace(/\\/g, ""));
476
+ start = i + 1;
477
+ }
478
+ }
479
+ return primaryKey;
480
+ }
433
481
 
434
482
  /**
435
483
  * Tests if a given type is a date or time type
@@ -445,7 +493,7 @@ class XtkCaster {
445
493
  * @param {string|number} type the type name
446
494
  * @returns {boolean} true if the type is a string type
447
495
  */
448
- static isStringType(type) {
496
+ static isStringType(type) {
449
497
  return type === "string" || type === "memo" || type === 6 || type === 12 || type === 13 || type === "blob" || type === "html" || type === "CDATA";
450
498
  }
451
499
 
@@ -454,9 +502,19 @@ class XtkCaster {
454
502
  * @param {string|number} type the type name
455
503
  * @returns {boolean} true if the type is a numeric type
456
504
  */
457
- static isNumericType(type) {
505
+ static isNumericType(type) {
458
506
  return type === "byte" || type === 1 || type === "short" || type === 2 || type === "int" || type === "long" || type === 3 || type === "float" || type === 4 || type === "double" || type === 5 || type === "timespan" || type === 14;
459
507
  }
508
+
509
+ /**
510
+ * Tests if an object is a primary key, i.e. has a schemaId and a array of values
511
+ * @param {*} value the object to test
512
+ * @returns true or false depending on whether the object is a primary key or not
513
+ */
514
+ static isPrimaryKey(value) {
515
+ if (!value) return false;
516
+ return !!(value.schemaId && value.values && Util.isArray(value.values));
517
+ }
460
518
  }
461
519
 
462
520
 
@@ -1270,6 +1270,29 @@ describe('Application', () => {
1270
1270
  });
1271
1271
  });
1272
1272
 
1273
+ describe("belongsTo", () => {
1274
+
1275
+ it("Should extract belongsTo", async () => {
1276
+ var xml = DomUtil.parse(`<schema namespace='nms' name='delivery'>
1277
+ <element name='delivery' label='Delivery'>
1278
+ <attribute belongsTo="nms:deliveryOperation" defOnDuplicate="true" enum="sandboxStatus" label="Status of inclusion in the provisional calendar" name="sandboxStatus" sqlname="iSandboxStatus" type="byte"/>
1279
+ <attribute belongsTo="cus:delivery" label="" length="255" name="hello" sqlname="sHello" type="string"/>
1280
+ <element advanced="true" desc="Memo field containing data stored as XML" label="XML memo" name="data" sqlname="mData" type="memo"/>
1281
+ </element>
1282
+ </schema>`);
1283
+ var schema = newSchema(xml);
1284
+
1285
+ var node = await schema.root.findNode("@sandboxStatus");
1286
+ expect(node).toMatchObject({ name:"@sandboxStatus", belongsTo:"nms:deliveryOperation", childrenCount:0 });
1287
+
1288
+ var node = await schema.root.findNode("@hello");
1289
+ expect(node).toMatchObject({ name:"@hello", belongsTo:"cus:delivery", childrenCount:0 });
1290
+
1291
+ var node = await schema.root.findNode("data");
1292
+ expect(node).toMatchObject({ name:"data", belongsTo:"", childrenCount:0 });
1293
+ });
1294
+ });
1295
+
1273
1296
  describe("Links", () => {
1274
1297
  it("Should find link node", async () => {
1275
1298
  var xml = DomUtil.parse(`<schema namespace='nms' name='recipient'>
@@ -267,7 +267,7 @@ describe('Caches', function() {
267
267
  assert.ok(found !== null && found !== undefined);
268
268
  assert.strictEqual(found.nodeName, "method");
269
269
  assert.strictEqual(found.getAttribute("name"), "Write");
270
- assert.strictEqual(urn, "xtk:persist");
270
+ assert.strictEqual(urn, "xtk:persist|xtk:session");
271
271
 
272
272
  // For compatibility reasons (SDK versions earlier than 0.1.23), keep the Write method on the interface too
273
273
  found = cache.get("xtk:persist", "Write");