node-opcua-address-space 2.181.1 → 2.182.2

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.
Files changed (121) hide show
  1. package/api/helpers/ensure_secure_access.ts +53 -2
  2. package/api/index.ts +30 -1
  3. package/api/loader/generateAddressSpaceRaw.ts +44 -139
  4. package/api/loader/load_nodeset2.ts +5 -0
  5. package/api/loader/narrow_instance_datatypes.ts +264 -0
  6. package/api/loader/nodeset_builtin_formats.ts +143 -0
  7. package/api/loader/nodeset_format.ts +153 -0
  8. package/api/loader/nodeset_image.ts +37 -4
  9. package/api/loader/nodeset_image_codec.ts +153 -34
  10. package/api/loader/nodeset_record.ts +26 -2
  11. package/api/loader/nodeset_record_applier.ts +54 -3
  12. package/api/loader/nodeset_records_to_xml.ts +620 -0
  13. package/api/loader/nodeset_source.ts +185 -26
  14. package/api/loader/nodeset_to_image.ts +16 -5
  15. package/api/loader/nodeset_xml_header.ts +119 -0
  16. package/api/loader/nodeset_xml_primitives.ts +209 -0
  17. package/api/loader/nodeset_xml_producer.ts +61 -2
  18. package/api/loader/parsers/variant_parser.ts +9 -0
  19. package/bin/nodeset_equivalence.js +750 -0
  20. package/bin/nodeset_tool_common.js +72 -0
  21. package/bin/opcua-nodeset-image.js +93 -103
  22. package/bin/opcua-nodeset-live.js +139 -0
  23. package/dist/api/helpers/ensure_secure_access.d.ts +20 -0
  24. package/dist/api/helpers/ensure_secure_access.js +39 -0
  25. package/dist/api/helpers/ensure_secure_access.js.map +1 -1
  26. package/dist/api/index.d.ts +7 -2
  27. package/dist/api/index.js +21 -1
  28. package/dist/api/index.js.map +1 -1
  29. package/dist/api/loader/generateAddressSpaceRaw.d.ts +1 -7
  30. package/dist/api/loader/generateAddressSpaceRaw.js +32 -125
  31. package/dist/api/loader/generateAddressSpaceRaw.js.map +1 -1
  32. package/dist/api/loader/load_nodeset2.js +4 -0
  33. package/dist/api/loader/load_nodeset2.js.map +1 -1
  34. package/dist/api/loader/narrow_instance_datatypes.d.ts +30 -0
  35. package/dist/api/loader/narrow_instance_datatypes.js +217 -0
  36. package/dist/api/loader/narrow_instance_datatypes.js.map +1 -0
  37. package/dist/api/loader/nodeset_builtin_formats.d.ts +27 -0
  38. package/dist/api/loader/nodeset_builtin_formats.js +133 -0
  39. package/dist/api/loader/nodeset_builtin_formats.js.map +1 -0
  40. package/dist/api/loader/nodeset_format.d.ts +123 -0
  41. package/dist/api/loader/nodeset_format.js +63 -0
  42. package/dist/api/loader/nodeset_format.js.map +1 -0
  43. package/dist/api/loader/nodeset_image.d.ts +17 -1
  44. package/dist/api/loader/nodeset_image.js +32 -4
  45. package/dist/api/loader/nodeset_image.js.map +1 -1
  46. package/dist/api/loader/nodeset_image_codec.d.ts +4 -0
  47. package/dist/api/loader/nodeset_image_codec.js +142 -53
  48. package/dist/api/loader/nodeset_image_codec.js.map +1 -1
  49. package/dist/api/loader/nodeset_record.d.ts +26 -2
  50. package/dist/api/loader/nodeset_record.js +11 -2
  51. package/dist/api/loader/nodeset_record.js.map +1 -1
  52. package/dist/api/loader/nodeset_record_applier.js +52 -3
  53. package/dist/api/loader/nodeset_record_applier.js.map +1 -1
  54. package/dist/api/loader/nodeset_records_to_xml.d.ts +16 -0
  55. package/dist/api/loader/nodeset_records_to_xml.js +536 -0
  56. package/dist/api/loader/nodeset_records_to_xml.js.map +1 -0
  57. package/dist/api/loader/nodeset_source.d.ts +68 -7
  58. package/dist/api/loader/nodeset_source.js +157 -20
  59. package/dist/api/loader/nodeset_source.js.map +1 -1
  60. package/dist/api/loader/nodeset_to_image.d.ts +4 -4
  61. package/dist/api/loader/nodeset_to_image.js +14 -4
  62. package/dist/api/loader/nodeset_to_image.js.map +1 -1
  63. package/dist/api/loader/nodeset_xml_header.d.ts +22 -0
  64. package/dist/api/loader/nodeset_xml_header.js +113 -0
  65. package/dist/api/loader/nodeset_xml_header.js.map +1 -0
  66. package/dist/api/loader/nodeset_xml_primitives.d.ts +37 -0
  67. package/dist/api/loader/nodeset_xml_primitives.js +196 -0
  68. package/dist/api/loader/nodeset_xml_primitives.js.map +1 -0
  69. package/dist/api/loader/nodeset_xml_producer.js +56 -1
  70. package/dist/api/loader/nodeset_xml_producer.js.map +1 -1
  71. package/dist/api/loader/parsers/variant_parser.js +7 -0
  72. package/dist/api/loader/parsers/variant_parser.js.map +1 -1
  73. package/dist/impl/_instantiate_helpers.js +1 -0
  74. package/dist/impl/_instantiate_helpers.js.map +1 -1
  75. package/dist/impl/alarms_and_conditions/ua_alarm_condition_impl.js +4 -0
  76. package/dist/impl/alarms_and_conditions/ua_alarm_condition_impl.js.map +1 -1
  77. package/dist/impl/alarms_and_conditions/ua_certificate_expiration_alarm_impl.js +14 -0
  78. package/dist/impl/alarms_and_conditions/ua_certificate_expiration_alarm_impl.js.map +1 -1
  79. package/dist/impl/base_node_impl.d.ts +9 -0
  80. package/dist/impl/base_node_impl.js +11 -0
  81. package/dist/impl/base_node_impl.js.map +1 -1
  82. package/dist/impl/base_node_private.js +67 -97
  83. package/dist/impl/base_node_private.js.map +1 -1
  84. package/dist/impl/nodeset_tools/construct_namespace_dependency.js +5 -1
  85. package/dist/impl/nodeset_tools/construct_namespace_dependency.js.map +1 -1
  86. package/dist/impl/nodeset_tools/nodeset_to_records.js +61 -15
  87. package/dist/impl/nodeset_tools/nodeset_to_records.js.map +1 -1
  88. package/dist/impl/nodeset_tools/nodeset_to_xml.d.ts +1 -8
  89. package/dist/impl/nodeset_tools/nodeset_to_xml.js +174 -190
  90. package/dist/impl/nodeset_tools/nodeset_to_xml.js.map +1 -1
  91. package/dist/impl/ua_data_type_impl.d.ts +21 -0
  92. package/dist/impl/ua_data_type_impl.js +27 -2
  93. package/dist/impl/ua_data_type_impl.js.map +1 -1
  94. package/dist/impl/ua_object_type_impl.d.ts +9 -0
  95. package/dist/impl/ua_object_type_impl.js +12 -1
  96. package/dist/impl/ua_object_type_impl.js.map +1 -1
  97. package/dist/impl/ua_variable_impl.js +27 -10
  98. package/dist/impl/ua_variable_impl.js.map +1 -1
  99. package/dist/impl/ua_variable_impl_ext_obj.js +19 -2
  100. package/dist/impl/ua_variable_impl_ext_obj.js.map +1 -1
  101. package/dist/impl/ua_variable_type_impl.d.ts +8 -1
  102. package/dist/impl/ua_variable_type_impl.js +39 -2
  103. package/dist/impl/ua_variable_type_impl.js.map +1 -1
  104. package/dist/tsconfig_base.tsbuildinfo +1 -1
  105. package/distNodeJS/generate_address_space.js +4 -0
  106. package/distNodeJS/generate_address_space.js.map +1 -1
  107. package/impl/_instantiate_helpers.ts +3 -0
  108. package/impl/alarms_and_conditions/ua_alarm_condition_impl.ts +10 -0
  109. package/impl/alarms_and_conditions/ua_certificate_expiration_alarm_impl.ts +17 -0
  110. package/impl/base_node_impl.ts +15 -0
  111. package/impl/base_node_private.ts +97 -146
  112. package/impl/nodeset_tools/construct_namespace_dependency.ts +5 -1
  113. package/impl/nodeset_tools/nodeset_to_records.ts +66 -16
  114. package/impl/nodeset_tools/nodeset_to_xml.ts +127 -160
  115. package/impl/ua_data_type_impl.ts +40 -2
  116. package/impl/ua_object_type_impl.ts +16 -3
  117. package/impl/ua_variable_impl.ts +27 -10
  118. package/impl/ua_variable_impl_ext_obj.ts +20 -2
  119. package/impl/ua_variable_type_impl.ts +48 -2
  120. package/package.json +29 -27
  121. package/source_nodejs/generate_address_space.ts +7 -1
@@ -23,12 +23,29 @@ import { type LocalizedText, makeAccessLevelFlag, NodeClass, QualifiedName } fro
23
23
  import { make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
24
24
  import type { ExtensionObject } from "node-opcua-extension-object";
25
25
  import { NodeId, NodeIdType } from "node-opcua-nodeid";
26
+ import { StatusCodes } from "node-opcua-status-code";
26
27
  import { EnumDefinition, StructureDefinition, StructureType } from "node-opcua-types";
27
28
  import { isNullOrUndefined, lowerFirstLetter } from "node-opcua-utils";
28
29
  import { DataType, Variant, VariantArrayType } from "node-opcua-variant";
29
30
  import XMLWriter from "xml-writer";
30
31
  import { makeDefinitionMap } from "../../api/loader/decode_xml_extension_object.js";
31
32
  import type { DefinitionMap2 } from "../../api/loader/make_xml_extension_object_parser.js";
33
+ import {
34
+ _dumpLocalizedText,
35
+ _dumpNodeId,
36
+ _dumpQualifiedName,
37
+ _dumpXmlElement,
38
+ b,
39
+ coerceInt64ToInt32,
40
+ findXsdNamespaceUri,
41
+ getPrefix,
42
+ initXmlWriterEx,
43
+ int64ToDecimalString,
44
+ makeTypeXsd,
45
+ n,
46
+ restoreDefaultNamespace,
47
+ startElementEx
48
+ } from "../../api/loader/nodeset_xml_primitives.js";
32
49
  import { SessionContext } from "../../api/session_context.js";
33
50
  import type { XmlWriter } from "../../api/xml_writer.js";
34
51
  import { BaseNodeImpl, getReferenceType } from "../base_node_impl.js";
@@ -46,6 +63,8 @@ import {
46
63
  } from "./construct_namespace_dependency.js";
47
64
  import { type NodesetWalkEvent, namespaceToWalkEvents, nodeToWalkEvents, sortByNodeId } from "./nodeset_to_records.js";
48
65
 
66
+ export { coerceInt64ToInt32, initXmlWriterEx, makeTypeXsd } from "../../api/loader/nodeset_xml_primitives.js";
67
+
49
68
  const debugLog = make_debugLog("nodeset_to_xml");
50
69
  const warningLog = make_warningLog("nodeset_to_xml");
51
70
  const errorLog = make_errorLog("nodeset_to_xml");
@@ -70,28 +89,6 @@ function _dumpDescription(xw: XmlWriter, node: { description?: LocalizedText }):
70
89
  }
71
90
  }
72
91
 
73
- function translateNodeId(xw: XmlWriter, nodeId: NodeId): NodeId {
74
- assert(nodeId instanceof NodeId);
75
- const nn = xw.translationTable.get(nodeId.namespace);
76
- const translatedNode = new NodeId(nodeId.identifierType, nodeId.value, nn);
77
- return translatedNode;
78
- }
79
-
80
- function n(xw: XmlWriter, nodeId: NodeId): string {
81
- return translateNodeId(xw, nodeId).toString().replace("ns=0;", "");
82
- }
83
-
84
- function translateBrowseName(xw: XmlWriter, browseName: QualifiedName): QualifiedName {
85
- assert(browseName instanceof QualifiedName);
86
- const nn = xw.translationTable.get(browseName.namespaceIndex);
87
- const translatedBrowseName = new QualifiedName({ namespaceIndex: nn, name: browseName.name });
88
- return translatedBrowseName;
89
- }
90
-
91
- function b(xw: XmlWriter, browseName: QualifiedName): string {
92
- return translateBrowseName(xw, browseName).toString().replace("ns=0;", "");
93
- }
94
-
95
92
  function hasHigherPriorityThan(namespaceIndex1: number, namespaceIndex2: number, priorityTable: number[]) {
96
93
  const order1 = priorityTable[namespaceIndex1];
97
94
  const order2 = priorityTable[namespaceIndex2];
@@ -129,6 +126,7 @@ function _dumpReferences(xw: XmlWriter, node: BaseNode) {
129
126
  // below already tolerates it being absent.
130
127
  const connectsToReferenceType = addressSpace.findReferenceType("ConnectsTo");
131
128
  const hasEventSourceReferenceType = _mustFindReferenceType(addressSpace, "HasEventSource");
129
+ const hierarchicalReferencesType = _mustFindReferenceType(addressSpace, "HierarchicalReferences");
132
130
 
133
131
  function referenceToKeep(reference: UAReference): boolean {
134
132
  // resolved here: a reference nobody has looked at since the load carries only the NodeId of its type
@@ -165,10 +163,26 @@ function _dumpReferences(xw: XmlWriter, node: BaseNode) {
165
163
  } else if (referenceType.isSubtypeOf(organizesReferencesType) && !reference.isForward) {
166
164
  // Organizes inverse — the current node is organized by an external folder
167
165
  return true;
166
+ } else if (referenceType.isSubtypeOf(organizesReferencesType) && reference.isForward) {
167
+ // Organizes forward into another namespace: the target lives in a document this export
168
+ // does not write, so nothing else will ever declare this reference and dropping it
169
+ // loses it outright. Within our own namespace the target declares the inverse and this
170
+ // end stays silent, exactly as the Aggregates branch above decides it
171
+ return targetedNamespaceIndex !== node.nodeId.namespace;
168
172
  } else if (connectsToReferenceType && referenceType.isSubtypeOf(connectsToReferenceType) && reference.isForward) {
169
173
  return true;
170
174
  } else if (referenceType.isSubtypeOf(hasEventSourceReferenceType) && reference.isForward) {
171
175
  return true;
176
+ } else if (referenceType.isSubtypeOf(hierarchicalReferencesType)) {
177
+ // a hierarchical reference type the branches above do not name: a companion
178
+ // specification's own, such as Robotics' Requires, Controls and HasSafetyStates. Those
179
+ // fell through to `return false` and were dropped in both directions, so a nodeset that
180
+ // defines its own hierarchical reference type lost every reference that used it.
181
+ // Decided the way Aggregates is decided just above: this end writes the reference
182
+ // unless the other end has the higher priority and will write it instead
183
+ return reference.isForward
184
+ ? !_hasHigherPriorityThan(xw, targetedNamespaceIndex, node.nodeId.namespace)
185
+ : targetedNamespaceIndex !== node.nodeId.namespace;
172
186
  }
173
187
  return false;
174
188
  }
@@ -192,36 +206,6 @@ function _dumpReferences(xw: XmlWriter, node: BaseNode) {
192
206
  }
193
207
  xw.endElement();
194
208
  }
195
- function _dumpLocalizedText(xw: XmlWriter, v: LocalizedText) {
196
- const uax = getPrefix(xw, "http://opcfoundation.org/UA/2008/02/Types.xsd");
197
- if (v.locale?.length) {
198
- xw.startElement(`${uax}Locale`);
199
- xw.text(v.locale);
200
- xw.endElement();
201
- }
202
- xw.startElement(`${uax}Text`);
203
- if (v.text) {
204
- xw.text(v.text);
205
- }
206
- xw.endElement();
207
- }
208
- function _dumpQualifiedName(xw: XmlWriter, v: QualifiedName) {
209
- const uax = getPrefix(xw, "http://opcfoundation.org/UA/2008/02/Types.xsd");
210
- const t = translateBrowseName(xw, v);
211
- if (t.name) {
212
- xw.startElement(`${uax}Name`);
213
- xw.text(t.name);
214
- xw.endElement();
215
- }
216
- if (t.namespaceIndex) {
217
- xw.startElement(`${uax}NamespaceIndex`);
218
- xw.text(t.namespaceIndex.toString());
219
- xw.endElement();
220
- }
221
- }
222
- function _dumpXmlElement(xw: XmlWriter, v: string) {
223
- xw.text(v);
224
- }
225
209
  /*
226
210
  <uax:ExtensionObject>
227
211
  <uax:TypeId>
@@ -239,78 +223,6 @@ function _dumpXmlElement(xw: XmlWriter, v: string) {
239
223
  </uax:Body>
240
224
  </uax:ExtensionObject>
241
225
  */
242
- type XmlNamespaceUri = string;
243
- type NamespaceUri = string;
244
- type XmlNs = string;
245
- interface XmlWriterEx extends XmlWriter {
246
- map: Record<XmlNamespaceUri, XmlNs>;
247
- stackMap: Record<XmlNamespaceUri, XmlNs>[];
248
- namespaceArray: NamespaceUri[];
249
- }
250
- export function initXmlWriterEx(xw: XmlWriter, map: Record<XmlNamespaceUri, XmlNs>, namespaceArray: NamespaceUri[]): void {
251
- const xwe = xw as XmlWriterEx;
252
- xwe.map = map;
253
- xwe.stackMap = [];
254
- xwe.namespaceArray = namespaceArray;
255
- }
256
- function findXsdNamespaceUri(xw: XmlWriter, nodeId: NodeId): string {
257
- const xwe = xw as XmlWriterEx;
258
- if (!xwe.namespaceArray) {
259
- return "";
260
- }
261
- const namespace = xwe.namespaceArray[nodeId.namespace];
262
- if (namespace === "http://opcfoundation.org/UA/") {
263
- return "http://opcfoundation.org/UA/2008/02/Types.xsd";
264
- }
265
- // c8 ignore next
266
- if (!namespace) {
267
- return "";
268
- }
269
- return `${namespace.replace(/\/$/, "")}/Types.xsd`;
270
- }
271
-
272
- function getPrefix(xw: XmlWriter, namespace: XmlNamespaceUri): XmlNs {
273
- const xwe = xw as XmlWriterEx;
274
- if (!xwe.map) return "";
275
- const p = xwe.map[namespace] || "";
276
- return p ? `${p}:` : "";
277
- }
278
-
279
- function restoreDefaultNamespace(xw: XmlWriter) {
280
- const xwe = xw as XmlWriterEx;
281
- if (!xwe.map) return;
282
- const previousMap = xwe.stackMap.pop();
283
- if (previousMap) {
284
- xwe.map = previousMap;
285
- }
286
- }
287
-
288
- function setDefaultNamespace(xw: XmlWriter, namespace: XmlNamespaceUri): void {
289
- const xwe = xw as XmlWriterEx;
290
- if (!xwe.map) return;
291
- if (xwe.map[namespace] !== "") {
292
- xw.writeAttribute("xmlns", namespace);
293
- }
294
-
295
- xwe.stackMap.push({
296
- ...xwe.map
297
- });
298
- xwe.map[namespace] = "";
299
- }
300
-
301
- function startElementEx(xw: XmlWriter, _ns: XmlNs, name: string, defaultNamespace: XmlNamespaceUri) {
302
- const _xwe = xw as XmlWriterEx;
303
- xw.startElement(name);
304
- setDefaultNamespace(xw, defaultNamespace);
305
- }
306
-
307
- function _dumpNodeId(xw: XmlWriter, v: NodeId) {
308
- const xmlns = getPrefix(xw, "http://opcfoundation.org/UA/2008/02/Types.xsd");
309
- xw.startElement(`${xmlns}Identifier`);
310
- xw.text(n(xw, v));
311
- xw.endElement();
312
- }
313
-
314
226
  function _dumpVariantValue(xw: XmlWriter, dataTypeNodeId: NodeId, dataType: DataType, addressSpace: IAddressSpace, value: unknown) {
315
227
  if (value === undefined || value === null) {
316
228
  return;
@@ -449,7 +361,7 @@ function _dumpVariantInnerValue(
449
361
  break;
450
362
  case DataType.Int64:
451
363
  case DataType.UInt64:
452
- xw.text((value as [number, number])[1].toString());
364
+ xw.text(int64ToDecimalString(value as Int64, dataType === DataType.Int64));
453
365
  break;
454
366
  case DataType.Boolean:
455
367
  case DataType.SByte:
@@ -662,7 +574,6 @@ function _dumpValue(xw: XmlWriter, node: UAVariable | UAVariableType, variant: V
662
574
  return;
663
575
  }
664
576
 
665
- const dataTypeName = dataTypeNode.browseName.name?.toString();
666
577
  const baseDataTypeName = DataType[variant.dataType];
667
578
 
668
579
  if (baseDataTypeName === "Null") {
@@ -673,9 +584,13 @@ function _dumpValue(xw: XmlWriter, node: UAVariable | UAVariableType, variant: V
673
584
  // determine if dataTypeName is a ExtensionObject
674
585
  const isExtensionObject = variant.dataType === DataType.ExtensionObject;
675
586
 
676
- if (_isDefaultValue(variant)) {
587
+ // nothing to serialize: a scalar with no value, or an array that is null rather than empty.
588
+ // This is the one thing _isDefaultValue used to catch here that still needs catching -- it is
589
+ // a test for the absence of a value, not for a value that happens to equal a type's default
590
+ if (variant.value === null || variant.value === undefined) {
677
591
  return;
678
592
  }
593
+
679
594
  xw.startElement("Value");
680
595
 
681
596
  const uax = getPrefix(xw, "http://opcfoundation.org/UA/2008/02/Types.xsd");
@@ -702,7 +617,11 @@ function _dumpValue(xw: XmlWriter, node: UAVariable | UAVariableType, variant: V
702
617
  switch (variant.arrayType) {
703
618
  case VariantArrayType.Matrix:
704
619
  case VariantArrayType.Array:
705
- startElementEx(xw, uax, `ListOf${dataTypeName}`, "http://opcfoundation.org/UA/2008/02/Types.xsd");
620
+ // the built-in type, not the DataType's browse name: the uax schema defines
621
+ // ListOfInt32 and ListOfString, and nothing named after a nodeset's own DataType.
622
+ // A `ListOfIdType` is not an element any reader knows, so the reader that meets it
623
+ // walks straight past into the first child and takes the array for a scalar
624
+ startElementEx(xw, uax, `ListOf${baseDataTypeName}`, "http://opcfoundation.org/UA/2008/02/Types.xsd");
706
625
  variant.value.forEach(encodeXml);
707
626
  restoreDefaultNamespace(xw);
708
627
  xw.endElement();
@@ -720,12 +639,15 @@ function _dumpValue(xw: XmlWriter, node: UAVariable | UAVariableType, variant: V
720
639
  }
721
640
 
722
641
  function _dumpArrayDimensionsAttribute(xw: XmlWriter, node: UAVariableType | UAVariable) {
723
- if (node.arrayDimensions) {
724
- if (node.valueRank === -1 || (node.arrayDimensions.length === 1 && node.arrayDimensions[0] === 0)) {
725
- return;
726
- }
727
- xw.writeAttribute("ArrayDimensions", node.arrayDimensions.join(","));
728
- }
642
+ // ArrayDimensions="0" is written only when the document declared it: node-opcua synthesizes [0]
643
+ // on the variables it generates, so writing it unconditionally would invent the attribute on
644
+ // every generated node. See arrayDimensionsWereDeclared in nodeset_record_applier
645
+ if (!node.arrayDimensions || node.valueRank <= 0) return;
646
+ // all-zero dimensions are what node-opcua synthesizes when nobody declared any, at whatever
647
+ // rank: [0] for a one-dimensional value, [0,0] for a matrix
648
+ const degenerate = node.arrayDimensions.every((d) => d === 0);
649
+ if (degenerate && !(node as unknown as { arrayDimensionsWereDeclared?: boolean }).arrayDimensionsWereDeclared) return;
650
+ xw.writeAttribute("ArrayDimensions", node.arrayDimensions.join(","));
729
651
  }
730
652
 
731
653
  function getParent(node: BaseNode): BaseNode | null {
@@ -749,6 +671,10 @@ function dumpCommonAttributes(xw: XmlWriter, node: BaseNode) {
749
671
  if (Object.hasOwn(node, "symbolicName")) {
750
672
  xw.writeAttribute("SymbolicName", (node as unknown as { symbolicName: string }).symbolicName);
751
673
  }
674
+ // Released is the default and is left unwritten; the other two the document declared
675
+ if (node.releaseStatus) {
676
+ xw.writeAttribute("ReleaseStatus", node.releaseStatus);
677
+ }
752
678
  if (Object.hasOwn(node, "isAbstract")) {
753
679
  const isAbstract = (node as unknown as { isAbstract: boolean }).isAbstract;
754
680
  if (isAbstract) {
@@ -773,8 +699,11 @@ function dumpCommonAttributes(xw: XmlWriter, node: BaseNode) {
773
699
  }
774
700
  // access policy: undefined means "inherit from the namespace", an empty rolePermissions array
775
701
  // means "this node deliberately grants nothing".
776
- if (node.accessRestrictions !== undefined) {
777
- xw.writeAttribute("AccessRestrictions", node.accessRestrictions.toString());
702
+ // what the document declared, which survives whatever the loader was asked to enforce; the
703
+ // enforced flag is the fallback for a node built in code rather than loaded
704
+ const accessRestrictions = node.declaredAccessRestrictions ?? node.accessRestrictions?.toString();
705
+ if (accessRestrictions !== undefined) {
706
+ xw.writeAttribute("AccessRestrictions", accessRestrictions);
778
707
  }
779
708
  if (node.rolePermissions !== undefined && node.rolePermissions.length === 0) {
780
709
  xw.writeAttribute("HasNoPermissions", "true");
@@ -785,6 +714,19 @@ function dumpCommonAttributes(xw: XmlWriter, node: BaseNode) {
785
714
  xw.writeAttribute("MinimumSamplingInterval", minimumSamplingInterval);
786
715
  }
787
716
  }
717
+ // EventNotifier lives on UAObject, UAObjectType and UAView in the XSD. 0 is the default and is
718
+ // left unwritten; anything else the document declared. It was written nowhere at all until now,
719
+ // so every node that declared one lost it on the way out
720
+ if (node.nodeClass === NodeClass.Object || node.nodeClass === NodeClass.ObjectType || node.nodeClass === NodeClass.View) {
721
+ // typeof, not truthiness: a node with a child called "EventNotifier" resolves the name to
722
+ // that child through the shared child accessors, so `node.eventNotifier` is a BaseNode
723
+ // there and not the attribute. PublishedEventsType is one, and stringifying it wrote a
724
+ // whole node dump into the attribute
725
+ const eventNotifier = (node as unknown as { eventNotifier?: unknown }).eventNotifier;
726
+ if (typeof eventNotifier === "number" && eventNotifier !== 0) {
727
+ xw.writeAttribute("EventNotifier", eventNotifier.toString());
728
+ }
729
+ }
788
730
  // Historizing exists in the XSD on UAVariable only: UAVariableType is restricted to
789
731
  // DataType/ValueRank/ArrayDimensions. UAVariableTypeImpl nevertheless owns a `historizing`
790
732
  // property, so the node class must be tested here rather than the presence of the property.
@@ -819,19 +761,6 @@ function dumpCommonElements(xw: XmlWriter, node: BaseNode) {
819
761
  _dumpRolePermissions(xw, node);
820
762
  }
821
763
 
822
- export function coerceInt64ToInt32(int64: Int64): number {
823
- if (typeof int64 === "number") {
824
- return int64 as number;
825
- }
826
- if (int64[0] === 0xffffffff && int64[1] === 0xffffffff) {
827
- return 0xffffffff;
828
- }
829
- if (int64[0] !== 0) {
830
- warningLog("coerceInt64ToInt32 , loosing high word in conversion");
831
- }
832
- return int64[1];
833
- }
834
-
835
764
  function _dumpEnumDefinition(xw: XmlWriter, enumDefinition: EnumDefinition) {
836
765
  enumDefinition.fields = enumDefinition.fields || [];
837
766
 
@@ -860,6 +789,12 @@ function _dumpStructureDefinition(
860
789
  // do not repeat elements that are already defined in base structure in the xml ouput!
861
790
  const fields = structureDefinition.fields || [];
862
791
  const nbFieldsInBase: number = baseStructureDefinition ? baseStructureDefinition.fields?.length || 0 : 0;
792
+ // Part 3 8.51: in a *WithSubtypedValues structure, isOptional is where "subtyping allowed" is
793
+ // held, and the document spells that AllowSubTypes. Written as IsOptional it said the opposite
794
+ // of what it meant, and the AllowSubTypes the document had was simply gone
795
+ const subtypedValues =
796
+ structureDefinition.structureType === StructureType.StructureWithSubtypedValues ||
797
+ structureDefinition.structureType === StructureType.UnionWithSubtypedValues;
863
798
 
864
799
  for (let index = nbFieldsInBase; index < fields.length; index++) {
865
800
  const defItem = fields[index];
@@ -872,8 +807,8 @@ function _dumpStructureDefinition(
872
807
  if (defItem.valueRank !== undefined && defItem.valueRank !== -1) {
873
808
  xw.writeAttribute("ValueRank", defItem.valueRank);
874
809
  }
875
- if (defItem.isOptional /* && defItem.isOptional !== false */) {
876
- xw.writeAttribute("IsOptional", defItem.isOptional.toString());
810
+ if (defItem.isOptional) {
811
+ xw.writeAttribute(subtypedValues ? "AllowSubTypes" : "IsOptional", "true");
877
812
  }
878
813
  if (defItem.maxStringLength !== undefined && defItem.maxStringLength !== 0) {
879
814
  xw.writeAttribute("MaxStringLength", defItem.maxStringLength);
@@ -892,9 +827,37 @@ function _dumpStructureDefinition(
892
827
  function _dumpUADataTypeDefinition(xw: XmlWriter, uaDataType: UADataType) {
893
828
  const uaDataTypeBase = uaDataType.subtypeOfObj;
894
829
 
830
+ // an OptionSet first: node-opcua builds neither an enumeration nor a structure for one, so
831
+ // neither branch below would fire and the whole <Definition> would be dropped. The fields are
832
+ // the ones the loader kept, see UADataTypeImpl.isOptionSetDataType
833
+ const asImpl = uaDataType as unknown as {
834
+ isOptionSetDataType?: boolean;
835
+ declaredDefinitionFields?: Array<{ name?: string | null; value?: number; description?: unknown }>;
836
+ declaredDefinitionName?: string;
837
+ };
838
+ // what the document called this definition, or -- for a DataType built here, which never had a
839
+ // declaration -- the browse name, which is the only name available
840
+ const definitionName = asImpl.declaredDefinitionName ?? b(xw, uaDataType.browseName);
841
+ if (asImpl.isOptionSetDataType && asImpl.declaredDefinitionFields) {
842
+ xw.startElement("Definition");
843
+ xw.writeAttribute("Name", definitionName);
844
+ xw.writeAttribute("IsOptionSet", "true");
845
+ for (const field of asImpl.declaredDefinitionFields) {
846
+ xw.startElement("Field");
847
+ xw.writeAttribute("Name", field.name || "");
848
+ if (field.value !== undefined && field.value !== null) {
849
+ xw.writeAttribute("Value", field.value);
850
+ }
851
+ _dumpDescription(xw, field as { description?: LocalizedText });
852
+ xw.endElement();
853
+ }
854
+ xw.endElement();
855
+ return;
856
+ }
857
+
895
858
  if (uaDataType.isEnumeration()) {
896
859
  xw.startElement("Definition");
897
- xw.writeAttribute("Name", b(xw, uaDataType.browseName));
860
+ xw.writeAttribute("Name", definitionName);
898
861
  _dumpEnumDefinition(xw, uaDataType.getEnumDefinition());
899
862
  xw.endElement();
900
863
  return;
@@ -909,9 +872,11 @@ function _dumpUADataTypeDefinition(xw: XmlWriter, uaDataType: UADataType) {
909
872
  const t = true;
910
873
  if (t || dataValue.statusCode.isGood()) {
911
874
  const definition = uaDataType.getStructureDefinition();
912
- const baseDefinition = uaDataTypeBase ? uaDataTypeBase.getStructureDefinition() : null;
875
+ // a structure's supertype need not be a structure: `Structure` itself subtypes BaseDataType,
876
+ // which has no definition at all. Asking it for one threw, and took the whole export with it
877
+ const baseDefinition = uaDataTypeBase?.isStructure() ? uaDataTypeBase.getStructureDefinition() : null;
913
878
  xw.startElement("Definition");
914
- xw.writeAttribute("Name", b(xw, uaDataType.browseName));
879
+ xw.writeAttribute("Name", definitionName);
915
880
  if (definition.structureType === StructureType.Union) {
916
881
  xw.writeAttribute("IsUnion", "true");
917
882
  }
@@ -993,8 +958,14 @@ function dumpUAVariable(xw: XmlWriter, node: UAVariable) {
993
958
  // sub elements
994
959
  dumpCommonElements(xw, node);
995
960
 
996
- const value = (node as UAVariableImpl).$dataValue.value;
997
- if (value) {
961
+ // a value the loader made up for a variable the document left unvalued is not written
962
+ // back out; see valueWasSynthesized in nodeset_record_applier. What used to stand here was
963
+ // _isDefaultValue, which dropped every zero and every empty string alike -- suppressing the
964
+ // made-up ones at the cost of the ones the document really did declare
965
+ const synthesized = (node as unknown as { valueWasSynthesized?: boolean }).valueWasSynthesized;
966
+ const dataValue = (node as UAVariableImpl).$dataValue;
967
+ const value = dataValue.value;
968
+ if (value && !synthesized && !dataValue.statusCode.equals(StatusCodes.BadWaitingForInitialData)) {
998
969
  _dumpValue(xw, node, value);
999
970
  }
1000
971
  }
@@ -1236,10 +1207,6 @@ BaseNodeImpl.prototype.dumpXML = function (this: BaseNodeImpl, xw: XmlWriter) {
1236
1207
  writeWalkEvents(xw, this.addressSpace, translationTable, events);
1237
1208
  };
1238
1209
 
1239
- export function makeTypeXsd(namespaceUri: string): string {
1240
- return `${namespaceUri.replace(/\/$/, "")}/Type.xsd`;
1241
- }
1242
-
1243
1210
  NamespaceImpl.prototype.toNodeset2XML = function (this: NamespaceImpl) {
1244
1211
  const namespaceArrayNode = this.addressSpace.findNode(VariableIds.Server_NamespaceArray);
1245
1212
  const namespaceArray: string[] = namespaceArrayNode
@@ -52,6 +52,10 @@ export interface EnumerationInfo {
52
52
  export interface UADataTypeOptions extends InternalBaseNodeOptions {
53
53
  partialDefinition: StructureFieldOptions[] | EnumFieldOptions[];
54
54
  isUnion?: boolean;
55
+ /** the DataType is a bit mask, not an enumeration; see UADataTypeImpl.isOptionSet */
56
+ isOptionSet?: boolean;
57
+ /** the <Definition> Name as declared; see UADataTypeImpl.declaredDefinitionName */
58
+ definitionName?: string;
55
59
  isAbstract?: boolean;
56
60
  symbolicName?: string;
57
61
  }
@@ -94,11 +98,37 @@ export class UADataTypeImpl extends BaseNodeImpl implements UADataType {
94
98
  private $partialDefinition?: StructureFieldOptionsEx[] | EnumFieldOptions[];
95
99
  private $fullDefinition?: StructureDefinition | EnumDefinition;
96
100
 
101
+ /**
102
+ * whether the nodeset declared this DataType with `IsOptionSet="true"`: a bit mask whose fields
103
+ * are bit positions rather than an enumeration whose fields are values. node-opcua builds no
104
+ * enumeration and no structure for one -- it is a plain UInt16 or UInt32 subtype -- so without
105
+ * this flag and the fields kept in $partialDefinition, an OptionSet that goes into the address
106
+ * space cannot come out again as the DataType the document declared.
107
+ */
108
+ public isOptionSetDataType = false;
109
+
110
+ /**
111
+ * the Name attribute of the <Definition> element as the nodeset spelled it. The published
112
+ * catalogue uses both spellings -- 467 bare names and 211 namespace-prefixed ones -- so there
113
+ * is no form to normalise to: recomputing it from the browse name rewrote every bare one.
114
+ * Undefined for a DataType this address space built itself, which has no declaration to keep.
115
+ */
116
+ public declaredDefinitionName?: string;
117
+
118
+ /** the fields of the <Definition> as declared, for a DataType the address space models no further */
119
+ public get declaredDefinitionFields(): StructureFieldOptionsEx[] | EnumFieldOptions[] | undefined {
120
+ return this.$partialDefinition;
121
+ }
122
+
97
123
  constructor(options: UADataTypeOptions) {
98
124
  super(options);
99
125
  if (options.partialDefinition) {
100
126
  this.$partialDefinition = options.partialDefinition;
101
127
  }
128
+ this.isOptionSetDataType = !!options.isOptionSet;
129
+ if (options.definitionName) {
130
+ this.declaredDefinitionName = options.definitionName;
131
+ }
102
132
  this.isAbstract = options.isAbstract === undefined || options.isAbstract === null ? false : options.isAbstract;
103
133
  this.symbolicName = options.symbolicName || this.browseName.name || "";
104
134
  }
@@ -261,7 +291,12 @@ export class UADataTypeImpl extends BaseNodeImpl implements UADataType {
261
291
  }
262
292
  getStructureDefinition(): StructureDefinition {
263
293
  const definition = this._getDefinition();
264
- assert(definition instanceof StructureDefinition);
294
+ // named: a bare assert here threw an Error with an empty message, which told a caller
295
+ // nothing at all about which DataType had refused and what it had instead
296
+ assert(
297
+ definition instanceof StructureDefinition,
298
+ `${this.browseName.toString()} (${this.nodeId.toString()}) has no StructureDefinition: ${definition ? definition.constructor.name : "none"}`
299
+ );
265
300
  return definition as StructureDefinition;
266
301
  }
267
302
 
@@ -271,7 +306,10 @@ export class UADataTypeImpl extends BaseNodeImpl implements UADataType {
271
306
  }
272
307
  getEnumDefinition(): EnumDefinition {
273
308
  const definition = this._getDefinition();
274
- assert(definition instanceof EnumDefinition);
309
+ assert(
310
+ definition instanceof EnumDefinition,
311
+ `${this.browseName.toString()} (${this.nodeId.toString()}) has no EnumDefinition: ${definition ? definition.constructor.name : "none"}`
312
+ );
275
313
  return definition as EnumDefinition;
276
314
  }
277
315
 
@@ -4,12 +4,12 @@
4
4
 
5
5
  import type {
6
6
  AddObjectOptions,
7
+ AddReferenceOpts,
7
8
  BaseNodeEvents,
8
9
  InstantiateObjectOptions,
9
10
  ISessionContext,
10
11
  UAObject,
11
- UAObjectType,
12
- UAReference
12
+ UAObjectType
13
13
  } from "node-opcua-address-space-base";
14
14
  import { assert } from "node-opcua-assert";
15
15
  import { AttributeIds, type LocalizedTextLike, NodeClass } from "node-opcua-data-model";
@@ -28,11 +28,20 @@ import { assertUnusedChildBrowseName, topMostParentIsObjectTypeOrVariableType }
28
28
 
29
29
  export interface UAObjectTypeOptions extends InternalBaseNodeOptions {
30
30
  isAbstract?: boolean | null;
31
+ /** the EventNotifier the nodeset declared; see UAObjectTypeImpl.eventNotifier */
32
+ eventNotifier?: number;
31
33
  }
32
34
 
33
35
  export class UAObjectTypeImpl extends BaseNodeImpl<BaseNodeEvents> implements UAObjectType {
34
36
  public readonly nodeClass = NodeClass.ObjectType;
35
37
  public readonly isAbstract: boolean;
38
+ /**
39
+ * the EventNotifier the nodeset declared. The XSD allows it on a UAObjectType and nodesets use
40
+ * it, but nothing here held it: the attribute was dropped at load, and -- because the name was
41
+ * then free -- a child called EventNotifier claimed `eventNotifier` through the shared child
42
+ * accessors, so reading it handed back a BaseNode. PublishedEventsType is such a type.
43
+ */
44
+ public readonly eventNotifier: number;
36
45
  /**
37
46
  * returns true if the object has some opcua methods
38
47
  */
@@ -54,6 +63,7 @@ export class UAObjectTypeImpl extends BaseNodeImpl<BaseNodeEvents> implements UA
54
63
  constructor(options: UAObjectTypeOptions) {
55
64
  super(options);
56
65
  this.isAbstract = options.isAbstract ?? false;
66
+ this.eventNotifier = options.eventNotifier ?? 0;
57
67
  }
58
68
 
59
69
  public readAttribute(context: ISessionContext, attributeId: AttributeIds): DataValue {
@@ -118,7 +128,10 @@ export class UAObjectTypeImpl extends BaseNodeImpl<BaseNodeEvents> implements UA
118
128
  const baseObjectType = addressSpace.findObjectType("BaseObjectType") as UAObjectType;
119
129
  assert(baseObjectType, "BaseObjectType must be defined in the address space");
120
130
 
121
- const references: UAReference[] = [];
131
+ // caller-supplied references (typically an inverse aggregate reference to a
132
+ // parent that has no dedicated option, e.g. HasOrderedComponent) are created
133
+ // with the node, so the NodeIdManager sees the parent when naming it.
134
+ const references: AddReferenceOpts[] = options.references ? [...options.references] : [];
122
135
 
123
136
  const copyAlsoModellingRules = topMostParentIsObjectTypeOrVariableType(addressSpace, options);
124
137
 
@@ -283,8 +283,19 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
283
283
 
284
284
  this.historizing = !!options.historizing; // coerced to boolean"
285
285
 
286
- // a Variant built from null takes the constructor's short path; the options form costs twice as much
287
- this.$dataValue = new DataValue({ statusCode: StatusCodes.UncertainInitialValue, value: new Variant(null) });
286
+ // a Variant built from null takes the constructor's short path; the options form costs twice as much.
287
+ // Stamped with the clock of the construction: a variable nobody gives a value to (a nodeset
288
+ // Variable of an abstract data type has no default to make up) is read as it is, and the
289
+ // requested timestamps are expected on that Uncertain result too (FEAT-34)
290
+ const now = getCurrentClock();
291
+ this.$dataValue = new DataValue({
292
+ statusCode: StatusCodes.UncertainInitialValue,
293
+ value: new Variant(null),
294
+ sourceTimestamp: now.timestamp,
295
+ sourcePicoseconds: now.picoseconds,
296
+ serverTimestamp: now.timestamp,
297
+ serverPicoseconds: now.picoseconds
298
+ });
288
299
 
289
300
  if (options.value) {
290
301
  this.bindVariable(options.value);
@@ -405,23 +416,27 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
405
416
  context = SessionContext.defaultContext;
406
417
  }
407
418
 
419
+ // A Bad result the variable makes up here carries the requested timestamps: a
420
+ // DataValue may carry them whatever its status, and the CTT expects them on every
421
+ // result of a Read with TimestampsToReturn Source or Both (Attribute Read 037 for
422
+ // the encoding, Base Info Core Structure 2 001 for the arguments of a method the
423
+ // nodeset reserves to a role nobody holds, FEAT-34). The clock of the denial is
424
+ // the honest source timestamp: the value behind it stays undisclosed.
408
425
  if (context.isAccessRestricted(this)) {
409
- return new DataValue({ statusCode: StatusCodes.BadSecurityModeInsufficient });
426
+ return makeNowDataValue({ statusCode: StatusCodes.BadSecurityModeInsufficient });
410
427
  }
411
428
 
412
429
  if (!this.isReadable(context)) {
413
- return new DataValue({ statusCode: StatusCodes.BadNotReadable });
430
+ return makeNowDataValue({ statusCode: StatusCodes.BadNotReadable });
414
431
  }
415
432
  if (!this.checkPermissionPrivate(context, PermissionType.Read)) {
416
- return new DataValue({ statusCode: StatusCodes.BadUserAccessDenied });
433
+ return makeNowDataValue({ statusCode: StatusCodes.BadUserAccessDenied });
417
434
  }
418
435
  if (!this.isUserReadable(context)) {
419
- return new DataValue({ statusCode: StatusCodes.BadNotReadable });
436
+ return makeNowDataValue({ statusCode: StatusCodes.BadNotReadable });
420
437
  }
421
438
  if (!isValidDataEncoding(dataEncoding)) {
422
- // Table 51: no encoding can be applied to a non-Structure value. The CTT
423
- // (Attribute Read 037) still expects the requested timestamps on this Bad
424
- // result, and a DataValue may carry them whatever its status.
439
+ // Table 51: no encoding can be applied to a non-Structure value
425
440
  return makeNowDataValue({ statusCode: StatusCodes.BadDataEncodingInvalid });
426
441
  }
427
442
 
@@ -1307,8 +1322,10 @@ export class UAVariableImpl<T extends UAVariableEvents & ListenerSignature<T> =
1307
1322
 
1308
1323
  let func: (innerCallback: (err: Error | null, dataValue: DataValue) => void) => void;
1309
1324
 
1325
+ // stamped like the same gates of readValue(): the requested timestamps are expected
1326
+ // on a Bad result too (FEAT-34)
1310
1327
  const answerStatusOnly = (statusCode: StatusCode) => (innerCallback: (err: Error | null, dataValue: DataValue) => void) =>
1311
- innerCallback(null, new DataValue({ statusCode }));
1328
+ innerCallback(null, makeNowDataValue({ statusCode }));
1312
1329
 
1313
1330
  if (context.isAccessRestricted(this)) {
1314
1331
  // same gate as readValue(): a variable bound with a simple get()