node-opcua-address-space 2.181.1 → 2.182.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.
- package/api/helpers/ensure_secure_access.ts +53 -2
- package/api/index.ts +30 -1
- package/api/loader/generateAddressSpaceRaw.ts +44 -139
- package/api/loader/load_nodeset2.ts +5 -0
- package/api/loader/narrow_instance_datatypes.ts +264 -0
- package/api/loader/nodeset_builtin_formats.ts +143 -0
- package/api/loader/nodeset_format.ts +153 -0
- package/api/loader/nodeset_image.ts +37 -4
- package/api/loader/nodeset_image_codec.ts +153 -34
- package/api/loader/nodeset_record.ts +26 -2
- package/api/loader/nodeset_record_applier.ts +54 -3
- package/api/loader/nodeset_records_to_xml.ts +620 -0
- package/api/loader/nodeset_source.ts +185 -26
- package/api/loader/nodeset_to_image.ts +16 -5
- package/api/loader/nodeset_xml_header.ts +119 -0
- package/api/loader/nodeset_xml_primitives.ts +209 -0
- package/api/loader/nodeset_xml_producer.ts +61 -2
- package/api/loader/parsers/variant_parser.ts +9 -0
- package/bin/nodeset_equivalence.js +750 -0
- package/bin/nodeset_tool_common.js +72 -0
- package/bin/opcua-nodeset-image.js +93 -103
- package/bin/opcua-nodeset-live.js +139 -0
- package/dist/api/helpers/ensure_secure_access.d.ts +20 -0
- package/dist/api/helpers/ensure_secure_access.js +39 -0
- package/dist/api/helpers/ensure_secure_access.js.map +1 -1
- package/dist/api/index.d.ts +7 -2
- package/dist/api/index.js +21 -1
- package/dist/api/index.js.map +1 -1
- package/dist/api/loader/generateAddressSpaceRaw.d.ts +1 -7
- package/dist/api/loader/generateAddressSpaceRaw.js +32 -125
- package/dist/api/loader/generateAddressSpaceRaw.js.map +1 -1
- package/dist/api/loader/load_nodeset2.js +4 -0
- package/dist/api/loader/load_nodeset2.js.map +1 -1
- package/dist/api/loader/narrow_instance_datatypes.d.ts +30 -0
- package/dist/api/loader/narrow_instance_datatypes.js +217 -0
- package/dist/api/loader/narrow_instance_datatypes.js.map +1 -0
- package/dist/api/loader/nodeset_builtin_formats.d.ts +27 -0
- package/dist/api/loader/nodeset_builtin_formats.js +133 -0
- package/dist/api/loader/nodeset_builtin_formats.js.map +1 -0
- package/dist/api/loader/nodeset_format.d.ts +123 -0
- package/dist/api/loader/nodeset_format.js +63 -0
- package/dist/api/loader/nodeset_format.js.map +1 -0
- package/dist/api/loader/nodeset_image.d.ts +17 -1
- package/dist/api/loader/nodeset_image.js +32 -4
- package/dist/api/loader/nodeset_image.js.map +1 -1
- package/dist/api/loader/nodeset_image_codec.d.ts +4 -0
- package/dist/api/loader/nodeset_image_codec.js +142 -53
- package/dist/api/loader/nodeset_image_codec.js.map +1 -1
- package/dist/api/loader/nodeset_record.d.ts +26 -2
- package/dist/api/loader/nodeset_record.js +11 -2
- package/dist/api/loader/nodeset_record.js.map +1 -1
- package/dist/api/loader/nodeset_record_applier.js +52 -3
- package/dist/api/loader/nodeset_record_applier.js.map +1 -1
- package/dist/api/loader/nodeset_records_to_xml.d.ts +16 -0
- package/dist/api/loader/nodeset_records_to_xml.js +536 -0
- package/dist/api/loader/nodeset_records_to_xml.js.map +1 -0
- package/dist/api/loader/nodeset_source.d.ts +68 -7
- package/dist/api/loader/nodeset_source.js +157 -20
- package/dist/api/loader/nodeset_source.js.map +1 -1
- package/dist/api/loader/nodeset_to_image.d.ts +4 -4
- package/dist/api/loader/nodeset_to_image.js +14 -4
- package/dist/api/loader/nodeset_to_image.js.map +1 -1
- package/dist/api/loader/nodeset_xml_header.d.ts +22 -0
- package/dist/api/loader/nodeset_xml_header.js +113 -0
- package/dist/api/loader/nodeset_xml_header.js.map +1 -0
- package/dist/api/loader/nodeset_xml_primitives.d.ts +37 -0
- package/dist/api/loader/nodeset_xml_primitives.js +196 -0
- package/dist/api/loader/nodeset_xml_primitives.js.map +1 -0
- package/dist/api/loader/nodeset_xml_producer.js +56 -1
- package/dist/api/loader/nodeset_xml_producer.js.map +1 -1
- package/dist/api/loader/parsers/variant_parser.js +7 -0
- package/dist/api/loader/parsers/variant_parser.js.map +1 -1
- package/dist/impl/_instantiate_helpers.js +1 -0
- package/dist/impl/_instantiate_helpers.js.map +1 -1
- package/dist/impl/alarms_and_conditions/ua_alarm_condition_impl.js +4 -0
- package/dist/impl/alarms_and_conditions/ua_alarm_condition_impl.js.map +1 -1
- package/dist/impl/alarms_and_conditions/ua_certificate_expiration_alarm_impl.js +14 -0
- package/dist/impl/alarms_and_conditions/ua_certificate_expiration_alarm_impl.js.map +1 -1
- package/dist/impl/base_node_impl.d.ts +9 -0
- package/dist/impl/base_node_impl.js +11 -0
- package/dist/impl/base_node_impl.js.map +1 -1
- package/dist/impl/base_node_private.js +67 -97
- package/dist/impl/base_node_private.js.map +1 -1
- package/dist/impl/nodeset_tools/construct_namespace_dependency.js +5 -1
- package/dist/impl/nodeset_tools/construct_namespace_dependency.js.map +1 -1
- package/dist/impl/nodeset_tools/nodeset_to_records.js +61 -15
- package/dist/impl/nodeset_tools/nodeset_to_records.js.map +1 -1
- package/dist/impl/nodeset_tools/nodeset_to_xml.d.ts +1 -8
- package/dist/impl/nodeset_tools/nodeset_to_xml.js +174 -190
- package/dist/impl/nodeset_tools/nodeset_to_xml.js.map +1 -1
- package/dist/impl/ua_data_type_impl.d.ts +21 -0
- package/dist/impl/ua_data_type_impl.js +27 -2
- package/dist/impl/ua_data_type_impl.js.map +1 -1
- package/dist/impl/ua_object_type_impl.d.ts +9 -0
- package/dist/impl/ua_object_type_impl.js +12 -1
- package/dist/impl/ua_object_type_impl.js.map +1 -1
- package/dist/impl/ua_variable_impl.js +27 -10
- package/dist/impl/ua_variable_impl.js.map +1 -1
- package/dist/impl/ua_variable_impl_ext_obj.js +19 -2
- package/dist/impl/ua_variable_impl_ext_obj.js.map +1 -1
- package/dist/impl/ua_variable_type_impl.d.ts +8 -1
- package/dist/impl/ua_variable_type_impl.js +39 -2
- package/dist/impl/ua_variable_type_impl.js.map +1 -1
- package/dist/tsconfig_base.tsbuildinfo +1 -1
- package/distNodeJS/generate_address_space.js +4 -0
- package/distNodeJS/generate_address_space.js.map +1 -1
- package/impl/_instantiate_helpers.ts +3 -0
- package/impl/alarms_and_conditions/ua_alarm_condition_impl.ts +10 -0
- package/impl/alarms_and_conditions/ua_certificate_expiration_alarm_impl.ts +17 -0
- package/impl/base_node_impl.ts +15 -0
- package/impl/base_node_private.ts +97 -146
- package/impl/nodeset_tools/construct_namespace_dependency.ts +5 -1
- package/impl/nodeset_tools/nodeset_to_records.ts +66 -16
- package/impl/nodeset_tools/nodeset_to_xml.ts +127 -160
- package/impl/ua_data_type_impl.ts +40 -2
- package/impl/ua_object_type_impl.ts +16 -3
- package/impl/ua_variable_impl.ts +27 -10
- package/impl/ua_variable_impl_ext_obj.ts +20 -2
- package/impl/ua_variable_type_impl.ts +48 -2
- package/package.json +29 -27
- package/source_nodejs/generate_address_space.ts +7 -1
|
@@ -13,15 +13,21 @@
|
|
|
13
13
|
* - QualifiedName: `[namespaceIndex, name]`; LocalizedText: `{ locale, text }`
|
|
14
14
|
* - DateTime: ISO-8601 string, or `{ iso, picoseconds }` when sub-millisecond precision is carried
|
|
15
15
|
* - ByteString: base64; Guid, String, XmlElement: the string
|
|
16
|
-
* - Int64 and UInt64:
|
|
16
|
+
* - Int64 and UInt64: a decimal string; Float and Double: JSON numbers, `NaN`, `Infinity` and
|
|
17
17
|
* `-Infinity` as strings; StatusCode: the numeric code
|
|
18
18
|
* - the extension objects the XML reader decodes itself (Argument, EUInformation, Range,
|
|
19
19
|
* EnumValueType): `{ "$class": name, ...fields }` with the rules above; every other extension
|
|
20
20
|
* object is the XML fragment the reader captured: `{ "$xml": [typeId, body] }`
|
|
21
21
|
*
|
|
22
22
|
* A value is `{ dataType, arrayType?, dimensions?, value }`; `arrayType` is omitted for a scalar.
|
|
23
|
+
*
|
|
24
|
+
* Every key whose value is the one it would take anyway is left out; {@link NODE_DEFAULTS} is the
|
|
25
|
+
* table, and a reader that does not find a key applies the default from it. gzip already codes a
|
|
26
|
+
* key repeated verbatim in a couple of bits, so this is worth far less compressed than it looks
|
|
27
|
+
* on the raw text -- except for `displayName`, which repeats the browse name on 97% of the nodes
|
|
28
|
+
* of the published nodesets and is the one omission that carries real entropy away.
|
|
23
29
|
*/
|
|
24
|
-
import {
|
|
30
|
+
import type { Int64, UInt64 } from "node-opcua-basic-types";
|
|
25
31
|
import { Range } from "node-opcua-data-access";
|
|
26
32
|
import {
|
|
27
33
|
coerceLocalizedText,
|
|
@@ -47,6 +53,7 @@ import {
|
|
|
47
53
|
type NodesetReferenceRecord,
|
|
48
54
|
XmlExtensionObjectFragment
|
|
49
55
|
} from "./nodeset_record.js";
|
|
56
|
+
import { int64ToDecimalString } from "./nodeset_xml_primitives.js";
|
|
50
57
|
|
|
51
58
|
/**
|
|
52
59
|
* a NodeId: the number alone when numeric in namespace 0 (nearly every id of a nodeset),
|
|
@@ -146,39 +153,113 @@ function decodeFloat(json: Json): number {
|
|
|
146
153
|
return json as number;
|
|
147
154
|
}
|
|
148
155
|
|
|
156
|
+
// #region defaults
|
|
157
|
+
/**
|
|
158
|
+
* what a key of a node record means when the image leaves it out: the value the reader would have
|
|
159
|
+
* produced anyway. A writer omits a key whose encoded value is its default here; a reader that
|
|
160
|
+
* does not find a key applies the default. The XML reader synthesizes every one of these from an
|
|
161
|
+
* absent attribute (see `nodeset_xml_producer`) and the address space export already omitted most
|
|
162
|
+
* of them, so this table is also what makes the two record producers agree.
|
|
163
|
+
*
|
|
164
|
+
* `displayName` is not in the table: its default is the node's own browse name rather than a
|
|
165
|
+
* constant, and it is the only key a reader must put back rather than infer, since a consumer
|
|
166
|
+
* reads it directly. It is also the only omission worth much once gzipped -- a constant repeated
|
|
167
|
+
* verbatim on every line costs a couple of bits, a browse name repeated does not.
|
|
168
|
+
*/
|
|
169
|
+
const NODE_DEFAULTS: Readonly<Record<string, unknown>> = {
|
|
170
|
+
valueRank: -1,
|
|
171
|
+
arrayDimensions: null,
|
|
172
|
+
minimumSamplingInterval: 0,
|
|
173
|
+
historizing: false,
|
|
174
|
+
isAbstract: false,
|
|
175
|
+
symmetric: false,
|
|
176
|
+
containsNoLoops: false,
|
|
177
|
+
hasNoPermissions: false,
|
|
178
|
+
eventNotifier: 0,
|
|
179
|
+
parentNodeId: null,
|
|
180
|
+
dataType: null,
|
|
181
|
+
methodDeclarationId: null
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/** the same, for one field of a DataType definition; an absent DataType attribute is BaseDataType */
|
|
185
|
+
const FIELD_DEFAULT_VALUE_RANK = -1;
|
|
186
|
+
const FIELD_DEFAULT_ALLOW_SUBTYPES = false;
|
|
187
|
+
const FIELD_DEFAULT_DATA_TYPE: JsonNodeId = 24; // i=24, BaseDataType
|
|
188
|
+
|
|
189
|
+
/** an absent ValueRank on an Argument, as on any other node */
|
|
190
|
+
const ARGUMENT_DEFAULT_VALUE_RANK = -1;
|
|
191
|
+
|
|
192
|
+
/** drop from `out` every key whose value is the one a reader would assume anyway */
|
|
193
|
+
function omitDefaults(out: Record<string, unknown>): void {
|
|
194
|
+
for (const [key, fallback] of Object.entries(NODE_DEFAULTS)) {
|
|
195
|
+
if (key in out && out[key] === fallback) {
|
|
196
|
+
delete out[key];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** whether a localized text carries nothing: the reader's `{}` for an absent Description */
|
|
202
|
+
const isEmptyText = (text: JsonLocalizedText | null | undefined): boolean =>
|
|
203
|
+
text === null || text === undefined || (text.locale === undefined && text.text === undefined);
|
|
204
|
+
|
|
205
|
+
// #endregion
|
|
206
|
+
|
|
207
|
+
// #region 64-bit integers
|
|
208
|
+
//
|
|
209
|
+
// on the wire a 64-bit integer is a decimal string, the spelling Part 6 uses and the only one that
|
|
210
|
+
// says what it means. In memory it stays node-opcua's `[high, low]` pair, whose halves are
|
|
211
|
+
// unsigned: Int64 -1 and UInt64 18446744073709551615 are the *same* pair, so the pair cannot be
|
|
212
|
+
// read without knowing the signedness from somewhere else, and Int64 min has two equally defensible
|
|
213
|
+
// spellings ([2147483648,0] and [-2147483648,0]) which would give one value two documents.
|
|
214
|
+
|
|
215
|
+
const TWO_64 = 1n << 64n;
|
|
216
|
+
|
|
217
|
+
const encodeInt64 = int64ToDecimalString;
|
|
218
|
+
|
|
219
|
+
function decodeInt64(json: Json): Int64 {
|
|
220
|
+
let unsigned = BigInt(json as string);
|
|
221
|
+
if (unsigned < 0n) unsigned += TWO_64;
|
|
222
|
+
return [Number(unsigned >> 32n), Number(unsigned & 0xffffffffn)] as Int64;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// #endregion
|
|
226
|
+
|
|
149
227
|
function encodeExtensionObject(value: unknown): Json {
|
|
150
228
|
if (value instanceof XmlExtensionObjectFragment) {
|
|
151
229
|
return { $xml: [encodeNodeId(value.typeId), value.bodyXML] };
|
|
152
230
|
}
|
|
153
231
|
if (value instanceof Argument) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
232
|
+
const out: Record<string, Json> = { $class: "Argument", name: value.name, dataType: encodeNodeId(value.dataType) };
|
|
233
|
+
if (value.valueRank !== ARGUMENT_DEFAULT_VALUE_RANK) out.valueRank = value.valueRank;
|
|
234
|
+
// an empty arrayDimensions is what a scalar argument carries and is left out; a `null` one
|
|
235
|
+
// is not the same thing to the reader that put it there, and is written
|
|
236
|
+
if (value.arrayDimensions === null || value.arrayDimensions.length > 0) out.arrayDimensions = value.arrayDimensions;
|
|
237
|
+
const description = encodeLocalizedText(value.description);
|
|
238
|
+
if (!isEmptyText(description)) out.description = description;
|
|
239
|
+
return out;
|
|
162
240
|
}
|
|
163
241
|
if (value instanceof EUInformation) {
|
|
164
|
-
|
|
242
|
+
const out: Record<string, Json> = {
|
|
165
243
|
$class: "EUInformation",
|
|
166
244
|
namespaceUri: value.namespaceUri,
|
|
167
|
-
unitId: value.unitId
|
|
168
|
-
displayName: encodeLocalizedText(value.displayName),
|
|
169
|
-
description: encodeLocalizedText(value.description)
|
|
245
|
+
unitId: value.unitId
|
|
170
246
|
};
|
|
247
|
+
const displayName = encodeLocalizedText(value.displayName);
|
|
248
|
+
const description = encodeLocalizedText(value.description);
|
|
249
|
+
if (!isEmptyText(displayName)) out.displayName = displayName;
|
|
250
|
+
if (!isEmptyText(description)) out.description = description;
|
|
251
|
+
return out;
|
|
171
252
|
}
|
|
172
253
|
if (value instanceof Range) {
|
|
173
254
|
return { $class: "Range", low: encodeFloat(value.low), high: encodeFloat(value.high) };
|
|
174
255
|
}
|
|
175
256
|
if (value instanceof EnumValueType) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
257
|
+
const out: Record<string, Json> = { $class: "EnumValueType", value: encodeInt64(value.value, true) };
|
|
258
|
+
const displayName = encodeLocalizedText(value.displayName);
|
|
259
|
+
const description = encodeLocalizedText(value.description);
|
|
260
|
+
if (!isEmptyText(displayName)) out.displayName = displayName;
|
|
261
|
+
if (!isEmptyText(description)) out.description = description;
|
|
262
|
+
return out;
|
|
182
263
|
}
|
|
183
264
|
const name = (value as ExtensionObject).constructor?.name ?? typeof value;
|
|
184
265
|
throw new NodesetImageError(
|
|
@@ -197,8 +278,10 @@ function decodeExtensionObject(json: Json): ExtensionObject | XmlExtensionObject
|
|
|
197
278
|
const argument = new Argument({});
|
|
198
279
|
argument.name = j.name as string;
|
|
199
280
|
argument.dataType = decodeNodeId(j.dataType);
|
|
200
|
-
argument.valueRank = j.valueRank as number;
|
|
201
|
-
|
|
281
|
+
argument.valueRank = (j.valueRank as number | undefined) ?? ARGUMENT_DEFAULT_VALUE_RANK;
|
|
282
|
+
// an absent arrayDimensions is the empty one the reader leaves on a scalar argument;
|
|
283
|
+
// an explicit `null` is the one the address space export writes, and is kept apart
|
|
284
|
+
argument.arrayDimensions = j.arrayDimensions === undefined ? [] : (j.arrayDimensions as number[] | null);
|
|
202
285
|
argument.description = decodeLocalizedText(j.description);
|
|
203
286
|
return argument;
|
|
204
287
|
}
|
|
@@ -217,7 +300,7 @@ function decodeExtensionObject(json: Json): ExtensionObject | XmlExtensionObject
|
|
|
217
300
|
}
|
|
218
301
|
case "EnumValueType":
|
|
219
302
|
return new EnumValueType({
|
|
220
|
-
value:
|
|
303
|
+
value: decodeInt64(j.value as Json),
|
|
221
304
|
displayName: decodeLocalizedText(j.displayName),
|
|
222
305
|
description: decodeLocalizedText(j.description)
|
|
223
306
|
});
|
|
@@ -258,8 +341,12 @@ function encodeElement(dataType: DataType, value: unknown): Json {
|
|
|
258
341
|
case DataType.DataValue:
|
|
259
342
|
case DataType.DiagnosticInfo:
|
|
260
343
|
throw new NodesetImageError(`an image cannot carry a ${DataType[dataType]} value`);
|
|
344
|
+
case DataType.Int64:
|
|
345
|
+
return encodeInt64(value as Int64, true);
|
|
346
|
+
case DataType.UInt64:
|
|
347
|
+
return encodeInt64(value as UInt64, false);
|
|
261
348
|
default:
|
|
262
|
-
// Boolean, the integer families
|
|
349
|
+
// Boolean, the 32-bit-and-under integer families, String, Guid, XmlElement
|
|
263
350
|
return value;
|
|
264
351
|
}
|
|
265
352
|
}
|
|
@@ -293,9 +380,8 @@ function decodeElement(dataType: DataType, json: Json): unknown {
|
|
|
293
380
|
case DataType.Variant:
|
|
294
381
|
return new Variant(decodeValue(json as JsonValue));
|
|
295
382
|
case DataType.Int64:
|
|
296
|
-
return coerceInt64(json as Int64);
|
|
297
383
|
case DataType.UInt64:
|
|
298
|
-
return
|
|
384
|
+
return decodeInt64(json);
|
|
299
385
|
default:
|
|
300
386
|
return json;
|
|
301
387
|
}
|
|
@@ -362,6 +448,7 @@ export interface NodesetImageHeader {
|
|
|
362
448
|
accessRestrictions?: string;
|
|
363
449
|
}>;
|
|
364
450
|
aliases: Record<string, JsonNodeId>;
|
|
451
|
+
extensions?: string[];
|
|
365
452
|
}
|
|
366
453
|
|
|
367
454
|
export interface NodesetImageTrailer {
|
|
@@ -409,6 +496,7 @@ export function encodeHeader(record: NodesetHeaderRecord, options: EncodeHeaderO
|
|
|
409
496
|
}),
|
|
410
497
|
aliases
|
|
411
498
|
};
|
|
499
|
+
if (record.extensions?.length) header.extensions = record.extensions;
|
|
412
500
|
if (options.sourceLength !== undefined) header.sourceLength = options.sourceLength;
|
|
413
501
|
return header;
|
|
414
502
|
}
|
|
@@ -439,7 +527,9 @@ export function decodeHeader(json: NodesetImageHeader): NodesetHeaderRecord {
|
|
|
439
527
|
if (m.accessRestrictions !== undefined) model.accessRestrictions = m.accessRestrictions;
|
|
440
528
|
return model;
|
|
441
529
|
});
|
|
442
|
-
|
|
530
|
+
const header: NodesetHeaderRecord = { kind: "header", namespaceUris: json.namespaceUris || [], models, aliases };
|
|
531
|
+
if (json.extensions?.length) header.extensions = json.extensions;
|
|
532
|
+
return header;
|
|
443
533
|
}
|
|
444
534
|
|
|
445
535
|
interface JsonField extends Omit<NodesetDefinitionField, "dataType"> {
|
|
@@ -451,6 +541,8 @@ export interface NodesetImageNode {
|
|
|
451
541
|
browseName: JsonQualifiedName;
|
|
452
542
|
displayName?: string;
|
|
453
543
|
description?: string;
|
|
544
|
+
category?: string[];
|
|
545
|
+
documentation?: string;
|
|
454
546
|
references: JsonReference[];
|
|
455
547
|
releaseStatus?: "Draft" | "Deprecated";
|
|
456
548
|
symbolicName?: string;
|
|
@@ -472,12 +564,14 @@ export interface NodesetImageNode {
|
|
|
472
564
|
userAccessLevel?: string;
|
|
473
565
|
value?: JsonValue;
|
|
474
566
|
methodDeclarationId?: JsonNodeId | null;
|
|
475
|
-
definition?: { name?: string; isUnion?: boolean; fields: JsonField[] };
|
|
567
|
+
definition?: { name?: string; isUnion?: boolean; isOptionSet?: boolean; fields: JsonField[] };
|
|
476
568
|
}
|
|
477
569
|
|
|
478
570
|
const OPTIONAL_PLAIN: Array<keyof NodesetNodeRecord & keyof NodesetImageNode> = [
|
|
479
571
|
"displayName",
|
|
480
572
|
"description",
|
|
573
|
+
"category",
|
|
574
|
+
"documentation",
|
|
481
575
|
"releaseStatus",
|
|
482
576
|
"symbolicName",
|
|
483
577
|
"accessRestrictions",
|
|
@@ -521,15 +615,37 @@ export function encodeNode(record: NodesetNodeRecord): NodesetImageNode {
|
|
|
521
615
|
if (record.methodDeclarationId !== undefined) out.methodDeclarationId = encodeNodeIdOrNull(record.methodDeclarationId);
|
|
522
616
|
if (record.value !== undefined) out.value = encodeValue(record.value);
|
|
523
617
|
if (record.definition) {
|
|
524
|
-
out.definition = {
|
|
525
|
-
fields: record.definition.fields.map((f) => ({ ...f, dataType: encodeNodeIdOrNull(f.dataType) }))
|
|
526
|
-
};
|
|
618
|
+
out.definition = { fields: record.definition.fields.map(encodeDefinitionField) };
|
|
527
619
|
if (record.definition.name !== undefined) out.definition.name = record.definition.name;
|
|
528
620
|
if (record.definition.isUnion !== undefined) out.definition.isUnion = record.definition.isUnion;
|
|
621
|
+
if (record.definition.isOptionSet !== undefined) out.definition.isOptionSet = record.definition.isOptionSet;
|
|
622
|
+
}
|
|
623
|
+
if (out.displayName !== undefined && out.displayName === out.browseName[1]) {
|
|
624
|
+
// the browse name over again on nearly every node of a published nodeset; a reader puts it back
|
|
625
|
+
delete out.displayName;
|
|
529
626
|
}
|
|
627
|
+
omitDefaults(out as unknown as Record<string, unknown>);
|
|
628
|
+
return out;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function encodeDefinitionField(field: NodesetDefinitionField): JsonField {
|
|
632
|
+
const out = { ...field, dataType: encodeNodeIdOrNull(field.dataType) } as JsonField & Record<string, unknown>;
|
|
633
|
+
if (out.valueRank === FIELD_DEFAULT_VALUE_RANK) delete out.valueRank;
|
|
634
|
+
if (out.allowSubTypes === FIELD_DEFAULT_ALLOW_SUBTYPES) delete out.allowSubTypes;
|
|
635
|
+
if (out.dataType === FIELD_DEFAULT_DATA_TYPE) delete out.dataType;
|
|
530
636
|
return out;
|
|
531
637
|
}
|
|
532
638
|
|
|
639
|
+
function decodeDefinitionField(json: JsonField): NodesetDefinitionField {
|
|
640
|
+
const field = {
|
|
641
|
+
...json,
|
|
642
|
+
valueRank: json.valueRank ?? FIELD_DEFAULT_VALUE_RANK,
|
|
643
|
+
allowSubTypes: json.allowSubTypes ?? FIELD_DEFAULT_ALLOW_SUBTYPES,
|
|
644
|
+
dataType: decodeNodeIdOrNull(json.dataType ?? FIELD_DEFAULT_DATA_TYPE)
|
|
645
|
+
};
|
|
646
|
+
return field as unknown as NodesetDefinitionField;
|
|
647
|
+
}
|
|
648
|
+
|
|
533
649
|
export function decodeNode(json: NodesetImageNode): NodesetNodeRecord {
|
|
534
650
|
if (typeof json.nodeClass !== "number" || !(json.nodeClass in NodeClass)) {
|
|
535
651
|
throw new NodesetImageError(`not a node record: ${JSON.stringify(json).slice(0, 80)}`);
|
|
@@ -554,6 +670,10 @@ export function decodeNode(json: NodesetImageNode): NodesetNodeRecord {
|
|
|
554
670
|
(record as unknown as Record<string, unknown>)[key] = v;
|
|
555
671
|
}
|
|
556
672
|
}
|
|
673
|
+
// an absent displayName is the browse name: what the writer left out because it was the same
|
|
674
|
+
if (record.displayName === undefined) {
|
|
675
|
+
record.displayName = record.browseName.name ?? "";
|
|
676
|
+
}
|
|
557
677
|
if (json.rolePermissions) {
|
|
558
678
|
record.rolePermissions = json.rolePermissions.map((p) => ({ roleId: decodeNodeId(p.roleId), permissions: p.permissions }));
|
|
559
679
|
}
|
|
@@ -563,12 +683,11 @@ export function decodeNode(json: NodesetImageNode): NodesetNodeRecord {
|
|
|
563
683
|
if (json.value !== undefined) record.value = decodeValue(json.value);
|
|
564
684
|
if (json.definition) {
|
|
565
685
|
const definition: NodesetDataTypeDefinitionRecord = {
|
|
566
|
-
fields: json.definition.fields.map(
|
|
567
|
-
(f) => ({ ...f, dataType: decodeNodeIdOrNull(f.dataType) }) as NodesetDefinitionField
|
|
568
|
-
)
|
|
686
|
+
fields: json.definition.fields.map(decodeDefinitionField)
|
|
569
687
|
};
|
|
570
688
|
if (json.definition.name !== undefined) definition.name = json.definition.name;
|
|
571
689
|
if (json.definition.isUnion !== undefined) definition.isUnion = json.definition.isUnion;
|
|
690
|
+
if (json.definition.isOptionSet !== undefined) definition.isOptionSet = json.definition.isOptionSet;
|
|
572
691
|
record.definition = definition;
|
|
573
692
|
}
|
|
574
693
|
return record;
|
|
@@ -19,8 +19,17 @@ import type { NodeId } from "node-opcua-nodeid";
|
|
|
19
19
|
import type { VariantOptions } from "node-opcua-variant";
|
|
20
20
|
import type { EnumFieldOptions, StructureFieldOptions } from "node-opcua-xml2json";
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* bumped whenever the record shape changes; the precompiled image carries it, and it is part of
|
|
24
|
+
* the key an image is cached under, so a bump invalidates every stored image.
|
|
25
|
+
*
|
|
26
|
+
* It is bumped for a change in what the records *contain* as well as for a change in their shape:
|
|
27
|
+
* when the reader learns to read something it used to drop -- a DateTime value, say -- every image
|
|
28
|
+
* built before is still valid schema-N and still parses, but it is less faithful than the file it
|
|
29
|
+
* was built from, and nothing else would ever invalidate it. The source digest cannot: the source
|
|
30
|
+
* did not change. This number is the only lever.
|
|
31
|
+
*/
|
|
32
|
+
export const NODESET_RECORD_SCHEMA = 3;
|
|
24
33
|
|
|
25
34
|
export interface NodesetModelRecord {
|
|
26
35
|
modelUri: string;
|
|
@@ -38,6 +47,12 @@ export interface NodesetHeaderRecord {
|
|
|
38
47
|
models: NodesetModelRecord[];
|
|
39
48
|
/** alias name to the id it stands for, file-local; no record refers to an alias by name */
|
|
40
49
|
aliases: Record<string, NodeId>;
|
|
50
|
+
/**
|
|
51
|
+
* the `<Extension>` elements of the document's own `<Extensions>`, each as the XML it is.
|
|
52
|
+
* Their content is foreign to this schema by design -- a tool stamps whatever it likes there,
|
|
53
|
+
* such as UaModeler's `ModelInfo` -- so there is nothing to model and only the bytes to keep.
|
|
54
|
+
*/
|
|
55
|
+
extensions?: string[];
|
|
41
56
|
}
|
|
42
57
|
|
|
43
58
|
export interface NodesetReferenceRecord {
|
|
@@ -64,6 +79,11 @@ export type NodesetDefinitionField = Omit<StructureFieldOptions, "dataType"> & E
|
|
|
64
79
|
export interface NodesetDataTypeDefinitionRecord {
|
|
65
80
|
name?: string;
|
|
66
81
|
isUnion?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* the DataType is a bit mask: its fields are bit positions, not enumeration values. An
|
|
84
|
+
* OptionSet whose flag is lost is indistinguishable from an ordinary enumeration.
|
|
85
|
+
*/
|
|
86
|
+
isOptionSet?: boolean;
|
|
67
87
|
fields: NodesetDefinitionField[];
|
|
68
88
|
}
|
|
69
89
|
|
|
@@ -87,6 +107,10 @@ export interface NodesetNodeRecord {
|
|
|
87
107
|
browseName: QualifiedName;
|
|
88
108
|
displayName?: string;
|
|
89
109
|
description?: string;
|
|
110
|
+
/** the `<Category>` elements, in order; a node may declare several */
|
|
111
|
+
category?: string[];
|
|
112
|
+
/** the `<Documentation>` element: a URL into the specification text, at most one */
|
|
113
|
+
documentation?: string;
|
|
90
114
|
references: NodesetReferenceRecord[];
|
|
91
115
|
releaseStatus?: "Draft" | "Deprecated";
|
|
92
116
|
symbolicName?: string;
|
|
@@ -119,8 +119,16 @@ function makeDefaultVariant(
|
|
|
119
119
|
if (nodeDataType?.basicDataType) {
|
|
120
120
|
const basicDataType = nodeDataType.basicDataType;
|
|
121
121
|
if (basicDataType === DataType.Variant) {
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
// the DataType is BaseDataType, or an abstract type that resolves to it, so there is
|
|
123
|
+
// no concrete default to synthesise. That is the same situation as a Variable whose
|
|
124
|
+
// DataType the document never stated, and it now gets the same answer: a null value
|
|
125
|
+
// waiting for its first update.
|
|
126
|
+
//
|
|
127
|
+
// Returning undefined here left the variable on the constructor's
|
|
128
|
+
// UncertainInitialValue, so two documents that say the same thing disagreed about the
|
|
129
|
+
// status code of a value neither of them carries -- an omitted DataType against an
|
|
130
|
+
// explicit i=24, which is the XSD default for that very attribute.
|
|
131
|
+
return { dataType: DataType.Null };
|
|
124
132
|
}
|
|
125
133
|
|
|
126
134
|
if (basicDataType === DataType.ExtensionObject) {
|
|
@@ -185,11 +193,23 @@ function isUninitializedMatrix(value: VariantOptions): boolean {
|
|
|
185
193
|
return product > 0;
|
|
186
194
|
}
|
|
187
195
|
|
|
196
|
+
/**
|
|
197
|
+
* a <Definition> Name worth keeping on the node: one the document spelled bare. A "<n>:Name" is a
|
|
198
|
+
* namespace index into the writing document's own table, which means nothing in any other document,
|
|
199
|
+
* so it is dropped here and rebuilt from the browse name when a document is written back out.
|
|
200
|
+
*/
|
|
201
|
+
function definitionNameToKeep(name: string | undefined): string | undefined {
|
|
202
|
+
if (!name || /^[0-9]+:/.test(name)) return undefined;
|
|
203
|
+
return name;
|
|
204
|
+
}
|
|
205
|
+
|
|
188
206
|
const UA_NAMESPACE_URI = "http://opcfoundation.org/UA/";
|
|
189
207
|
const reg = /ns=([0-9]+);(.*)/;
|
|
190
208
|
|
|
191
209
|
interface INodePermissions {
|
|
192
210
|
accessRestrictions?: AccessRestrictionsFlag;
|
|
211
|
+
/** what the document declared, whatever this loader enforces; see BaseNode */
|
|
212
|
+
declaredAccessRestrictions?: string;
|
|
193
213
|
rolePermissions?: RolePermissionTypeOptions[];
|
|
194
214
|
}
|
|
195
215
|
|
|
@@ -491,6 +511,9 @@ export class NodesetRecordApplier implements NodesetRecordConsumer {
|
|
|
491
511
|
}
|
|
492
512
|
return {
|
|
493
513
|
accessRestrictions: this.applyAccessRestrictions ? convertAccessRestrictions(record.accessRestrictions) : undefined,
|
|
514
|
+
// kept whatever the option says: the option governs what is enforced, not what the
|
|
515
|
+
// document declared, and an exporter needs the latter
|
|
516
|
+
declaredAccessRestrictions: record.accessRestrictions,
|
|
494
517
|
rolePermissions
|
|
495
518
|
};
|
|
496
519
|
}
|
|
@@ -501,6 +524,8 @@ export class NodesetRecordApplier implements NodesetRecordConsumer {
|
|
|
501
524
|
nodeId: record.nodeId.isEmpty() ? null : this.translate(record.nodeId),
|
|
502
525
|
browseName: this.translateQualifiedName(record.browseName),
|
|
503
526
|
references: this.references(record),
|
|
527
|
+
// documentation, not behaviour, but the document said it and it must survive the trip
|
|
528
|
+
releaseStatus: record.releaseStatus,
|
|
504
529
|
...this.permissions(record)
|
|
505
530
|
};
|
|
506
531
|
}
|
|
@@ -560,6 +585,15 @@ export class NodesetRecordApplier implements NodesetRecordConsumer {
|
|
|
560
585
|
} // already translated
|
|
561
586
|
const namespace = this.addressSpace.getNamespace(params.nodeId.namespace);
|
|
562
587
|
const node = namespace.internalCreateNode(params) as BaseNode;
|
|
588
|
+
// Whether the document declared ArrayDimensions at all. node-opcua synthesizes [0] on the
|
|
589
|
+
// variables it generates itself -- EnumStrings, the variables it materializes from a
|
|
590
|
+
// structure's fields -- so [0] on a node means either "the document said 0" or "nobody
|
|
591
|
+
// said anything", and an exporter that cannot tell them apart must either invent the
|
|
592
|
+
// attribute everywhere or drop it everywhere. Recorded here, where the document is still
|
|
593
|
+
// in hand, rather than guessed at afterwards
|
|
594
|
+
if (record.arrayDimensions !== undefined && record.arrayDimensions !== null) {
|
|
595
|
+
(node as { arrayDimensionsWereDeclared?: boolean }).arrayDimensionsWereDeclared = true;
|
|
596
|
+
}
|
|
563
597
|
this.settle(node, record, params.references as UAReference[]);
|
|
564
598
|
return node;
|
|
565
599
|
}
|
|
@@ -659,7 +693,16 @@ export class NodesetRecordApplier implements NodesetRecordConsumer {
|
|
|
659
693
|
displayName: record.displayName ?? "",
|
|
660
694
|
description: record.description ?? "",
|
|
661
695
|
symbolicName: record.symbolicName,
|
|
662
|
-
partialDefinition: fields
|
|
696
|
+
partialDefinition: fields,
|
|
697
|
+
// the address space models no OptionSet, so the flag and the fields above are all that
|
|
698
|
+
// is left of the declaration; without them the DataType cannot be written back out
|
|
699
|
+
isOptionSet: record.definition?.isOptionSet,
|
|
700
|
+
isUnion: record.definition?.isUnion,
|
|
701
|
+
// the <Definition> Name, but only when the document spelled it bare. The catalogue uses
|
|
702
|
+
// both spellings, and a "<n>:Name" prefix is an index into the table of the document
|
|
703
|
+
// that wrote it -- carrying it into another document would name a different namespace,
|
|
704
|
+
// so a prefixed name is left to be recomputed from the browse name on the way out
|
|
705
|
+
definitionName: definitionNameToKeep(record.definition?.name)
|
|
663
706
|
} as unknown as CreateNodeOptions;
|
|
664
707
|
|
|
665
708
|
let capturedDataTypeNode: UADataType | undefined = this.createNode(params, record) as UADataType;
|
|
@@ -817,6 +860,12 @@ export class NodesetRecordApplier implements NodesetRecordConsumer {
|
|
|
817
860
|
} else {
|
|
818
861
|
(cv as UAVariableImpl)._setInitialDataValue(defaultValue, StatusCodes.Good);
|
|
819
862
|
}
|
|
863
|
+
// this value was made up here, not read from the document: the document said
|
|
864
|
+
// nothing, and a usable default is served rather than a Bad status. An exporter
|
|
865
|
+
// must not write it back out as though the document had declared it, and the
|
|
866
|
+
// status code cannot tell it apart from a value the document really did declare
|
|
867
|
+
// -- a declared empty string is Good too. So the fact is recorded on the node
|
|
868
|
+
(cv as { valueWasSynthesized?: boolean }).valueWasSynthesized = true;
|
|
820
869
|
}
|
|
821
870
|
capturedVariable = undefined;
|
|
822
871
|
};
|
|
@@ -860,6 +909,8 @@ export class NodesetRecordApplier implements NodesetRecordConsumer {
|
|
|
860
909
|
} else {
|
|
861
910
|
(variable as UAVariableImpl)._setInitialDataValue(defaultValue, StatusCodes.Good);
|
|
862
911
|
}
|
|
912
|
+
// made up here, not read from the document: see the note on the other synthesis site
|
|
913
|
+
(variable as { valueWasSynthesized?: boolean }).valueWasSynthesized = true;
|
|
863
914
|
}
|
|
864
915
|
} catch (err) {
|
|
865
916
|
errorLog(
|