node-opcua-alias-name-client 2.176.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/LICENSE +22 -0
- package/README.md +137 -0
- package/dist/client_alias_set.d.ts +144 -0
- package/dist/client_alias_set.js +223 -0
- package/dist/client_alias_set.js.map +1 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.js +54 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/server_index_resolver.d.ts +54 -0
- package/dist/server_index_resolver.js +82 -0
- package/dist/server_index_resolver.js.map +1 -0
- package/package.json +54 -0
- package/source/client_alias_set.ts +312 -0
- package/source/errors.ts +57 -0
- package/source/index.ts +23 -0
- package/source/server_index_resolver.ts +89 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module node-opcua-alias-name-client
|
|
4
|
+
*
|
|
5
|
+
* Client-side OPC 10000-17 (AliasNames).
|
|
6
|
+
*
|
|
7
|
+
* Resolves the AliasNames **a Server publishes about itself**. Aggregating
|
|
8
|
+
* AliasNames collected from other Servers (Annexes B and C) and the Annex D
|
|
9
|
+
* PubSub change notification are out of scope; {@link ServerIndexResolver}
|
|
10
|
+
* covers the one cross-Server step a Client still has to make, turning the
|
|
11
|
+
* `ServerIndex` of a returned `ExpandedNodeId` into a URI (Annex A).
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.ServerIndexResolver = exports.LOCAL_SERVER_INDEX = exports.AliasNameMethodNotSupportedError = exports.AliasNameCallError = exports.TOPICS = exports.TAG_VARIABLES = exports.ClientAliasSet = exports.ALIASES_ROOT = void 0;
|
|
15
|
+
var client_alias_set_js_1 = require("./client_alias_set.js");
|
|
16
|
+
Object.defineProperty(exports, "ALIASES_ROOT", { enumerable: true, get: function () { return client_alias_set_js_1.ALIASES_ROOT; } });
|
|
17
|
+
Object.defineProperty(exports, "ClientAliasSet", { enumerable: true, get: function () { return client_alias_set_js_1.ClientAliasSet; } });
|
|
18
|
+
Object.defineProperty(exports, "TAG_VARIABLES", { enumerable: true, get: function () { return client_alias_set_js_1.TAG_VARIABLES; } });
|
|
19
|
+
Object.defineProperty(exports, "TOPICS", { enumerable: true, get: function () { return client_alias_set_js_1.TOPICS; } });
|
|
20
|
+
var errors_js_1 = require("./errors.js");
|
|
21
|
+
Object.defineProperty(exports, "AliasNameCallError", { enumerable: true, get: function () { return errors_js_1.AliasNameCallError; } });
|
|
22
|
+
Object.defineProperty(exports, "AliasNameMethodNotSupportedError", { enumerable: true, get: function () { return errors_js_1.AliasNameMethodNotSupportedError; } });
|
|
23
|
+
var server_index_resolver_js_1 = require("./server_index_resolver.js");
|
|
24
|
+
Object.defineProperty(exports, "LOCAL_SERVER_INDEX", { enumerable: true, get: function () { return server_index_resolver_js_1.LOCAL_SERVER_INDEX; } });
|
|
25
|
+
Object.defineProperty(exports, "ServerIndexResolver", { enumerable: true, get: function () { return server_index_resolver_js_1.ServerIndexResolver; } });
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../source/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;AAEH,6DAQ+B;AAP3B,mHAAA,YAAY,OAAA;AAEZ,qHAAA,cAAc,OAAA;AAGd,oHAAA,aAAa,OAAA;AACb,6GAAA,MAAM,OAAA;AAEV,yCAAmF;AAA1E,+GAAA,kBAAkB,OAAA;AAAE,6HAAA,gCAAgC,OAAA;AAC7D,uEAAqF;AAA5E,8HAAA,kBAAkB,OAAA;AAAE,+HAAA,mBAAmB,OAAA"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-alias-name-client
|
|
3
|
+
*
|
|
4
|
+
* Turning the `ServerIndex` of a returned `ExpandedNodeId` into something a
|
|
5
|
+
* Client can act on.
|
|
6
|
+
*/
|
|
7
|
+
import { type ExpandedNodeId } from "node-opcua-nodeid";
|
|
8
|
+
import type { IBasicSessionAsync } from "node-opcua-pseudo-session";
|
|
9
|
+
/** The Server's own entry in the ServerArray is always index 0 (OPC 10000-5). */
|
|
10
|
+
export declare const LOCAL_SERVER_INDEX = 0;
|
|
11
|
+
/**
|
|
12
|
+
* Resolves `ServerIndex` values against a Server's `ServerArray`, caching it.
|
|
13
|
+
*
|
|
14
|
+
* `FindAlias` may return an `ExpandedNodeId` whose `serverIndex` is not 0, which
|
|
15
|
+
* says only "this Node is on a different Server" — the index is meaningless
|
|
16
|
+
* without the `ServerArray` that gives it a URI (OPC 10000-17 Annex A walks
|
|
17
|
+
* through exactly this). Every Client consuming AliasNames from a Server that
|
|
18
|
+
* aggregates has to do this step, so it lives here rather than in each caller.
|
|
19
|
+
*
|
|
20
|
+
* The `ServerArray` is read once and cached. It is not expected to change during
|
|
21
|
+
* a session; call {@link invalidate} if the Client has reason to believe it has.
|
|
22
|
+
*/
|
|
23
|
+
export declare class ServerIndexResolver {
|
|
24
|
+
private readonly session;
|
|
25
|
+
private serverArray?;
|
|
26
|
+
constructor(session: IBasicSessionAsync);
|
|
27
|
+
/** The Server's `ServerArray`, read once and cached. */
|
|
28
|
+
getServerArray(): Promise<string[]>;
|
|
29
|
+
/** Forget the cached `ServerArray`. */
|
|
30
|
+
invalidate(): void;
|
|
31
|
+
/**
|
|
32
|
+
* The URI for a `ServerIndex`, or `null` when the index is not in the
|
|
33
|
+
* `ServerArray`.
|
|
34
|
+
*
|
|
35
|
+
* An index the array does not cover is a Server defect, but a Client has to
|
|
36
|
+
* survive it, so it is reported as `null` rather than thrown.
|
|
37
|
+
*/
|
|
38
|
+
resolveServerIndex(serverIndex: number): Promise<string | null>;
|
|
39
|
+
/** True when the ExpandedNodeId names a Node on the Server that answered. */
|
|
40
|
+
isLocal(expandedNodeId: ExpandedNodeId): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Describe where a returned Node lives.
|
|
43
|
+
*
|
|
44
|
+
* `serverUri` is `null` for a Node on the Server that answered the call —
|
|
45
|
+
* which is every Node these packages publish, since aggregating other
|
|
46
|
+
* Servers is out of scope. It is non-null only when talking to a Server that
|
|
47
|
+
* does aggregate.
|
|
48
|
+
*/
|
|
49
|
+
locate(expandedNodeId: ExpandedNodeId): Promise<{
|
|
50
|
+
local: boolean;
|
|
51
|
+
serverIndex: number;
|
|
52
|
+
serverUri: string | null;
|
|
53
|
+
}>;
|
|
54
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module node-opcua-alias-name-client
|
|
4
|
+
*
|
|
5
|
+
* Turning the `ServerIndex` of a returned `ExpandedNodeId` into something a
|
|
6
|
+
* Client can act on.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.ServerIndexResolver = exports.LOCAL_SERVER_INDEX = void 0;
|
|
10
|
+
const node_opcua_constants_1 = require("node-opcua-constants");
|
|
11
|
+
const node_opcua_data_model_1 = require("node-opcua-data-model");
|
|
12
|
+
const node_opcua_nodeid_1 = require("node-opcua-nodeid");
|
|
13
|
+
/** The Server's own entry in the ServerArray is always index 0 (OPC 10000-5). */
|
|
14
|
+
exports.LOCAL_SERVER_INDEX = 0;
|
|
15
|
+
/**
|
|
16
|
+
* Resolves `ServerIndex` values against a Server's `ServerArray`, caching it.
|
|
17
|
+
*
|
|
18
|
+
* `FindAlias` may return an `ExpandedNodeId` whose `serverIndex` is not 0, which
|
|
19
|
+
* says only "this Node is on a different Server" — the index is meaningless
|
|
20
|
+
* without the `ServerArray` that gives it a URI (OPC 10000-17 Annex A walks
|
|
21
|
+
* through exactly this). Every Client consuming AliasNames from a Server that
|
|
22
|
+
* aggregates has to do this step, so it lives here rather than in each caller.
|
|
23
|
+
*
|
|
24
|
+
* The `ServerArray` is read once and cached. It is not expected to change during
|
|
25
|
+
* a session; call {@link invalidate} if the Client has reason to believe it has.
|
|
26
|
+
*/
|
|
27
|
+
class ServerIndexResolver {
|
|
28
|
+
session;
|
|
29
|
+
serverArray;
|
|
30
|
+
constructor(session) {
|
|
31
|
+
this.session = session;
|
|
32
|
+
}
|
|
33
|
+
/** The Server's `ServerArray`, read once and cached. */
|
|
34
|
+
async getServerArray() {
|
|
35
|
+
if (this.serverArray) {
|
|
36
|
+
return this.serverArray;
|
|
37
|
+
}
|
|
38
|
+
const dataValue = await this.session.read({
|
|
39
|
+
nodeId: (0, node_opcua_nodeid_1.resolveNodeId)(node_opcua_constants_1.VariableIds.Server_ServerArray),
|
|
40
|
+
attributeId: node_opcua_data_model_1.AttributeIds.Value
|
|
41
|
+
});
|
|
42
|
+
const value = dataValue.value?.value;
|
|
43
|
+
this.serverArray = Array.isArray(value) ? value : [];
|
|
44
|
+
return this.serverArray;
|
|
45
|
+
}
|
|
46
|
+
/** Forget the cached `ServerArray`. */
|
|
47
|
+
invalidate() {
|
|
48
|
+
this.serverArray = undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The URI for a `ServerIndex`, or `null` when the index is not in the
|
|
52
|
+
* `ServerArray`.
|
|
53
|
+
*
|
|
54
|
+
* An index the array does not cover is a Server defect, but a Client has to
|
|
55
|
+
* survive it, so it is reported as `null` rather than thrown.
|
|
56
|
+
*/
|
|
57
|
+
async resolveServerIndex(serverIndex) {
|
|
58
|
+
const serverArray = await this.getServerArray();
|
|
59
|
+
return serverArray[serverIndex] ?? null;
|
|
60
|
+
}
|
|
61
|
+
/** True when the ExpandedNodeId names a Node on the Server that answered. */
|
|
62
|
+
isLocal(expandedNodeId) {
|
|
63
|
+
return (expandedNodeId.serverIndex ?? exports.LOCAL_SERVER_INDEX) === exports.LOCAL_SERVER_INDEX;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Describe where a returned Node lives.
|
|
67
|
+
*
|
|
68
|
+
* `serverUri` is `null` for a Node on the Server that answered the call —
|
|
69
|
+
* which is every Node these packages publish, since aggregating other
|
|
70
|
+
* Servers is out of scope. It is non-null only when talking to a Server that
|
|
71
|
+
* does aggregate.
|
|
72
|
+
*/
|
|
73
|
+
async locate(expandedNodeId) {
|
|
74
|
+
const serverIndex = expandedNodeId.serverIndex ?? exports.LOCAL_SERVER_INDEX;
|
|
75
|
+
if (serverIndex === exports.LOCAL_SERVER_INDEX) {
|
|
76
|
+
return { local: true, serverIndex, serverUri: null };
|
|
77
|
+
}
|
|
78
|
+
return { local: false, serverIndex, serverUri: await this.resolveServerIndex(serverIndex) };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.ServerIndexResolver = ServerIndexResolver;
|
|
82
|
+
//# sourceMappingURL=server_index_resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server_index_resolver.js","sourceRoot":"","sources":["../source/server_index_resolver.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEH,+DAAmD;AACnD,iEAAqD;AACrD,yDAAuE;AAGvE,iFAAiF;AACpE,QAAA,kBAAkB,GAAG,CAAC,CAAC;AAEpC;;;;;;;;;;;GAWG;AACH,MAAa,mBAAmB;IACX,OAAO,CAAqB;IACrC,WAAW,CAAY;IAE/B,YAAY,OAA2B;QACnC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAC3B,CAAC;IAED,wDAAwD;IACjD,KAAK,CAAC,cAAc;QACvB,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACnB,OAAO,IAAI,CAAC,WAAW,CAAC;QAC5B,CAAC;QACD,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;YACtC,MAAM,EAAE,IAAA,iCAAa,EAAC,kCAAW,CAAC,kBAAkB,CAAC;YACrD,WAAW,EAAE,oCAAY,CAAC,KAAK;SAClC,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC;QACrC,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAE,KAAkB,CAAC,CAAC,CAAC,EAAE,CAAC;QACnE,OAAO,IAAI,CAAC,WAAW,CAAC;IAC5B,CAAC;IAED,uCAAuC;IAChC,UAAU;QACb,IAAI,CAAC,WAAW,GAAG,SAAS,CAAC;IACjC,CAAC;IAED;;;;;;OAMG;IACI,KAAK,CAAC,kBAAkB,CAAC,WAAmB;QAC/C,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;QAChD,OAAO,WAAW,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC;IAC5C,CAAC;IAED,6EAA6E;IACtE,OAAO,CAAC,cAA8B;QACzC,OAAO,CAAC,cAAc,CAAC,WAAW,IAAI,0BAAkB,CAAC,KAAK,0BAAkB,CAAC;IACrF,CAAC;IAED;;;;;;;OAOG;IACI,KAAK,CAAC,MAAM,CACf,cAA8B;QAE9B,MAAM,WAAW,GAAG,cAAc,CAAC,WAAW,IAAI,0BAAkB,CAAC;QACrE,IAAI,WAAW,KAAK,0BAAkB,EAAE,CAAC;YACrC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;QACzD,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC,kBAAkB,CAAC,WAAW,CAAC,EAAE,CAAC;IAChG,CAAC;CACJ;AA7DD,kDA6DC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-opcua-alias-name-client",
|
|
3
|
+
"version": "2.176.0",
|
|
4
|
+
"description": "pure nodejs OPCUA SDK - client-side alias names (OPC 10000-17)",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc -b",
|
|
7
|
+
"lint": "biome check .",
|
|
8
|
+
"format": "biome check --write .",
|
|
9
|
+
"clean": "npx rimraf -g node_modules dist *.tsbuildinfo",
|
|
10
|
+
"test": "mocha",
|
|
11
|
+
"test:check": "tsc --noEmit -p test/tsconfig.json"
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"devEngines": {
|
|
16
|
+
"node": ">=18.x",
|
|
17
|
+
"npm": ">=10.x"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"node-opcua-constants": "2.176.0",
|
|
21
|
+
"node-opcua-data-model": "2.176.0",
|
|
22
|
+
"node-opcua-nodeid": "2.176.0",
|
|
23
|
+
"node-opcua-pseudo-session": "2.176.0",
|
|
24
|
+
"node-opcua-service-translate-browse-path": "2.176.0",
|
|
25
|
+
"node-opcua-status-code": "2.176.0",
|
|
26
|
+
"node-opcua-types": "2.176.0",
|
|
27
|
+
"node-opcua-variant": "2.176.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"node-opcua-address-space": "2.176.0",
|
|
31
|
+
"node-opcua-alias-name-server": "2.176.0",
|
|
32
|
+
"node-opcua-leak-detector": "2.175.6",
|
|
33
|
+
"node-opcua-nodesets": "2.175.3"
|
|
34
|
+
},
|
|
35
|
+
"author": "Etienne Rossignon",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git://github.com/node-opcua/node-opcua.git"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"OPCUA",
|
|
43
|
+
"opcua",
|
|
44
|
+
"m2m",
|
|
45
|
+
"iot",
|
|
46
|
+
"opc ua",
|
|
47
|
+
"internet of things"
|
|
48
|
+
],
|
|
49
|
+
"homepage": "http://node-opcua.github.io/",
|
|
50
|
+
"files": [
|
|
51
|
+
"dist",
|
|
52
|
+
"source"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-alias-name-client
|
|
3
|
+
*
|
|
4
|
+
* Client-side API for OPC UA AliasNames (OPC 10000-17).
|
|
5
|
+
*
|
|
6
|
+
* All navigation goes through {@link IBasicSessionAsync2} — browse, read, call,
|
|
7
|
+
* translateBrowsePath — so the same code drives a remote `ClientSession` and an
|
|
8
|
+
* in-process `PseudoSession`. That is what lets these be tested without a
|
|
9
|
+
* transport, and what lets a Server-side tool resolve its own aliases through
|
|
10
|
+
* the same API a Client uses.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { ObjectIds } from "node-opcua-constants";
|
|
14
|
+
import { BrowseDirection, NodeClass, type QualifiedName } from "node-opcua-data-model";
|
|
15
|
+
import { type ExpandedNodeId, type NodeId, NodeId as NodeIdClass, resolveNodeId } from "node-opcua-nodeid";
|
|
16
|
+
import type { IBasicSessionAsync2 } from "node-opcua-pseudo-session";
|
|
17
|
+
import { makeBrowsePath } from "node-opcua-service-translate-browse-path";
|
|
18
|
+
import type { AliasNameDataType, AliasNameVerboseDataType } from "node-opcua-types";
|
|
19
|
+
import { DataType } from "node-opcua-variant";
|
|
20
|
+
import { AliasNameCallError, AliasNameMethodNotSupportedError } from "./errors.js";
|
|
21
|
+
import { ServerIndexResolver } from "./server_index_resolver.js";
|
|
22
|
+
|
|
23
|
+
/** `Aliases`, the root of the hierarchy (OPC 10000-17 clause 9.2). */
|
|
24
|
+
export const ALIASES_ROOT: NodeId = resolveNodeId(ObjectIds.Aliases);
|
|
25
|
+
/** `TagVariables` (clause 9.3). */
|
|
26
|
+
export const TAG_VARIABLES: NodeId = resolveNodeId(ObjectIds.TagVariables);
|
|
27
|
+
/** `Topics` (clause 9.4). */
|
|
28
|
+
export const TOPICS: NodeId = resolveNodeId(ObjectIds.Topics);
|
|
29
|
+
|
|
30
|
+
/** One resolved AliasName, as `FindAlias` reports it (clause 7.2). */
|
|
31
|
+
export interface ClientAliasEntry {
|
|
32
|
+
/** The string part of the AliasName. */
|
|
33
|
+
aliasName: string;
|
|
34
|
+
/**
|
|
35
|
+
* The namespace the AliasName was published in.
|
|
36
|
+
*
|
|
37
|
+
* Clause 6.2 requires a Client to **ignore this when comparing** AliasNames.
|
|
38
|
+
* It is reported for completeness, not for matching.
|
|
39
|
+
*/
|
|
40
|
+
namespaceIndex: number;
|
|
41
|
+
/** The Nodes the alias names, best match first (clause 6.3.2). */
|
|
42
|
+
referencedNodes: ExpandedNodeId[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One resolved AliasName, as `FindAliasVerbose` reports it (clause 7.3). */
|
|
46
|
+
export interface ClientAliasVerboseEntry extends ClientAliasEntry {
|
|
47
|
+
/**
|
|
48
|
+
* Parallel to {@link referencedNodes}: the ServerUri of each Node, `null`
|
|
49
|
+
* for one on the Server that answered.
|
|
50
|
+
*/
|
|
51
|
+
serverUris: (string | null)[];
|
|
52
|
+
/**
|
|
53
|
+
* The category that actually held the alias, which for a recursive search is
|
|
54
|
+
* the nested one rather than the one that was called.
|
|
55
|
+
*/
|
|
56
|
+
aliasNameCategoryId: NodeId;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface FindAliasOptions {
|
|
60
|
+
/**
|
|
61
|
+
* The category to search, recursively. Defaults to {@link ALIASES_ROOT},
|
|
62
|
+
* which covers everything the Server publishes.
|
|
63
|
+
*/
|
|
64
|
+
categoryNodeId?: NodeId;
|
|
65
|
+
/**
|
|
66
|
+
* Restrict to this ReferenceType and its subtypes (clause 6.3.2 Table 3).
|
|
67
|
+
* Omit for any.
|
|
68
|
+
*/
|
|
69
|
+
referenceTypeFilter?: NodeId;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The Methods a category may carry. */
|
|
73
|
+
interface CategoryMethods {
|
|
74
|
+
findAlias: NodeId | null;
|
|
75
|
+
findAliasVerbose: NodeId | null;
|
|
76
|
+
addAliasesToCategory: NodeId | null;
|
|
77
|
+
deleteAliasesFromCategory: NodeId | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Read a QualifiedName's parts defensively — a Server may send a bare string. */
|
|
81
|
+
function qualifiedName(value: QualifiedName | undefined): { name: string; namespaceIndex: number } {
|
|
82
|
+
return { name: value?.name ?? "", namespaceIndex: value?.namespaceIndex ?? 0 };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Client-side entry point for a Server's AliasNames.
|
|
87
|
+
*
|
|
88
|
+
* ```ts
|
|
89
|
+
* const aliases = new ClientAliasSet(session);
|
|
90
|
+
* const [entry] = await aliases.findAlias("TI101");
|
|
91
|
+
* const nodeId = entry.referencedNodes[0];
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* Construct one per session: Method NodeIds are resolved lazily, in a single
|
|
95
|
+
* `translateBrowsePath` round trip per category, and cached for the lifetime of
|
|
96
|
+
* the instance.
|
|
97
|
+
*
|
|
98
|
+
* This resolves the aliases **a Server publishes about itself**. Aggregating
|
|
99
|
+
* across Servers (OPC 10000-17 Annexes B and C) is out of scope; where a Server
|
|
100
|
+
* does aggregate, {@link serverIndexResolver} turns the `ServerIndex` of a
|
|
101
|
+
* returned `ExpandedNodeId` into a URI.
|
|
102
|
+
*/
|
|
103
|
+
export class ClientAliasSet {
|
|
104
|
+
public readonly session: IBasicSessionAsync2;
|
|
105
|
+
/** Resolves the `ServerIndex` of a returned Node (Annex A). */
|
|
106
|
+
public readonly serverIndexResolver: ServerIndexResolver;
|
|
107
|
+
|
|
108
|
+
private readonly methodCache = new Map<string, CategoryMethods>();
|
|
109
|
+
|
|
110
|
+
constructor(session: IBasicSessionAsync2) {
|
|
111
|
+
this.session = session;
|
|
112
|
+
this.serverIndexResolver = new ServerIndexResolver(session);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resolve an AliasName, or a `Like` pattern, to the Nodes it names.
|
|
117
|
+
*
|
|
118
|
+
* The pattern is an OPC 10000-4 `Like` pattern: `%` is any run of
|
|
119
|
+
* characters, `_` is exactly one, `[abc]` and `[^abc]` are lists, and `\`
|
|
120
|
+
* escapes. An exact name is simply a pattern with no wildcards.
|
|
121
|
+
*
|
|
122
|
+
* @throws {@link AliasNameCallError} when the Server answers a bad
|
|
123
|
+
* StatusCode — `Bad_InvalidArgument` for a malformed pattern,
|
|
124
|
+
* `Bad_ResponseTooLarge` when a narrower filter is needed,
|
|
125
|
+
* `Bad_UserAccessDenied` when the session may not read the category.
|
|
126
|
+
*/
|
|
127
|
+
public async findAlias(pattern: string, options?: FindAliasOptions): Promise<ClientAliasEntry[]> {
|
|
128
|
+
const categoryNodeId = options?.categoryNodeId ?? ALIASES_ROOT;
|
|
129
|
+
const methods = await this.resolveMethods(categoryNodeId);
|
|
130
|
+
if (!methods.findAlias) {
|
|
131
|
+
// FindAlias is MANDATORY, so this means the Node is not an
|
|
132
|
+
// AliasNameCategoryType instance, or the Server is non-conformant
|
|
133
|
+
throw new AliasNameMethodNotSupportedError("FindAlias", categoryNodeId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const extensionObjects = await this.callFind(
|
|
137
|
+
"FindAlias",
|
|
138
|
+
categoryNodeId,
|
|
139
|
+
methods.findAlias,
|
|
140
|
+
pattern,
|
|
141
|
+
options?.referenceTypeFilter
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return extensionObjects.map((raw) => {
|
|
145
|
+
const value = raw as AliasNameDataType;
|
|
146
|
+
const { name, namespaceIndex } = qualifiedName(value.aliasName);
|
|
147
|
+
return {
|
|
148
|
+
aliasName: name,
|
|
149
|
+
namespaceIndex,
|
|
150
|
+
referencedNodes: value.referencedNodes ?? []
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Resolve an AliasName and learn which category held it and which Server
|
|
157
|
+
* each Node is on (clause 6.3.3).
|
|
158
|
+
*
|
|
159
|
+
* @throws {@link AliasNameMethodNotSupportedError} when the Server exposes
|
|
160
|
+
* only the mandatory `FindAlias`. That is a conformant Server, so this is
|
|
161
|
+
* an expected outcome to handle, not a fault — use {@link supportsVerbose}
|
|
162
|
+
* to check first.
|
|
163
|
+
*/
|
|
164
|
+
public async findAliasVerbose(pattern: string, options?: FindAliasOptions): Promise<ClientAliasVerboseEntry[]> {
|
|
165
|
+
const categoryNodeId = options?.categoryNodeId ?? ALIASES_ROOT;
|
|
166
|
+
const methods = await this.resolveMethods(categoryNodeId);
|
|
167
|
+
if (!methods.findAliasVerbose) {
|
|
168
|
+
throw new AliasNameMethodNotSupportedError("FindAliasVerbose", categoryNodeId);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const extensionObjects = await this.callFind(
|
|
172
|
+
"FindAliasVerbose",
|
|
173
|
+
categoryNodeId,
|
|
174
|
+
methods.findAliasVerbose,
|
|
175
|
+
pattern,
|
|
176
|
+
options?.referenceTypeFilter
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return extensionObjects.map((raw) => {
|
|
180
|
+
const value = raw as AliasNameVerboseDataType;
|
|
181
|
+
const { name, namespaceIndex } = qualifiedName(value.aliasName);
|
|
182
|
+
return {
|
|
183
|
+
aliasName: name,
|
|
184
|
+
namespaceIndex,
|
|
185
|
+
referencedNodes: value.referencedNodes ?? [],
|
|
186
|
+
serverUris: value.serverUris ?? [],
|
|
187
|
+
aliasNameCategoryId: value.aliasNameCategoryId
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* True when the Server exposes `FindAliasVerbose` on this category.
|
|
194
|
+
*
|
|
195
|
+
* Cheaper than catching {@link AliasNameMethodNotSupportedError}, and reads
|
|
196
|
+
* better when the Client has a fallback path.
|
|
197
|
+
*/
|
|
198
|
+
public async supportsVerbose(categoryNodeId: NodeId = ALIASES_ROOT): Promise<boolean> {
|
|
199
|
+
return (await this.resolveMethods(categoryNodeId)).findAliasVerbose !== null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* True when the Server exposes the configuration Methods on this category
|
|
204
|
+
* (clauses 6.3.4 and 6.3.5). They are optional and off by default in most
|
|
205
|
+
* Servers.
|
|
206
|
+
*/
|
|
207
|
+
public async supportsConfiguration(categoryNodeId: NodeId = ALIASES_ROOT): Promise<boolean> {
|
|
208
|
+
const methods = await this.resolveMethods(categoryNodeId);
|
|
209
|
+
return methods.addAliasesToCategory !== null && methods.deleteAliasesFromCategory !== null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The `AliasNameCategoryType` instances Organized directly by a category.
|
|
214
|
+
*
|
|
215
|
+
* Browsing is not needed to *resolve* an alias — `FindAlias` on the root
|
|
216
|
+
* searches recursively — but a Client that wants to show the hierarchy, or
|
|
217
|
+
* to search one branch, needs it.
|
|
218
|
+
*/
|
|
219
|
+
public async browseSubCategories(
|
|
220
|
+
categoryNodeId: NodeId = ALIASES_ROOT
|
|
221
|
+
): Promise<Array<{ nodeId: NodeId; browseName: string }>> {
|
|
222
|
+
const result = await this.session.browse({
|
|
223
|
+
nodeId: categoryNodeId,
|
|
224
|
+
browseDirection: BrowseDirection.Forward,
|
|
225
|
+
referenceTypeId: resolveNodeId("Organizes"),
|
|
226
|
+
includeSubtypes: true,
|
|
227
|
+
nodeClassMask: NodeClass.Object,
|
|
228
|
+
resultMask: 0x3f
|
|
229
|
+
});
|
|
230
|
+
const references = result.references ?? [];
|
|
231
|
+
const out: Array<{ nodeId: NodeId; browseName: string }> = [];
|
|
232
|
+
for (const reference of references) {
|
|
233
|
+
// an AliasNameCategoryType instance carries FindAlias; an
|
|
234
|
+
// AliasNameType instance does not, which is how they are told apart
|
|
235
|
+
// without reading TypeDefinition for each
|
|
236
|
+
const methods = await this.resolveMethods(reference.nodeId);
|
|
237
|
+
if (methods.findAlias) {
|
|
238
|
+
out.push({ nodeId: reference.nodeId, browseName: reference.browseName.name ?? "" });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Forget every cached Method NodeId. */
|
|
245
|
+
public invalidate(): void {
|
|
246
|
+
this.methodCache.clear();
|
|
247
|
+
this.serverIndexResolver.invalidate();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Resolve every Method of a category in **one** `translateBrowsePath` round
|
|
252
|
+
* trip, and cache the result.
|
|
253
|
+
*
|
|
254
|
+
* Asking for all four at once costs no more than asking for one, and means a
|
|
255
|
+
* Client that later calls `findAliasVerbose` after `findAlias` makes no
|
|
256
|
+
* further round trip.
|
|
257
|
+
*/
|
|
258
|
+
private async resolveMethods(categoryNodeId: NodeId): Promise<CategoryMethods> {
|
|
259
|
+
const key = categoryNodeId.toString();
|
|
260
|
+
const cached = this.methodCache.get(key);
|
|
261
|
+
if (cached) {
|
|
262
|
+
return cached;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const names = ["FindAlias", "FindAliasVerbose", "AddAliasesToCategory", "DeleteAliasesFromCategory"] as const;
|
|
266
|
+
const results = await this.session.translateBrowsePath(names.map((name) => makeBrowsePath(categoryNodeId, `/${name}`)));
|
|
267
|
+
|
|
268
|
+
const pick = (index: number): NodeId | null => {
|
|
269
|
+
const result = results[index];
|
|
270
|
+
return result?.statusCode.isGood() && result.targets?.length ? result.targets[0].targetId : null;
|
|
271
|
+
};
|
|
272
|
+
const methods: CategoryMethods = {
|
|
273
|
+
findAlias: pick(0),
|
|
274
|
+
findAliasVerbose: pick(1),
|
|
275
|
+
addAliasesToCategory: pick(2),
|
|
276
|
+
deleteAliasesFromCategory: pick(3)
|
|
277
|
+
};
|
|
278
|
+
this.methodCache.set(key, methods);
|
|
279
|
+
return methods;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Call one of the two find Methods and return its ExtensionObject array. */
|
|
283
|
+
private async callFind(
|
|
284
|
+
methodName: string,
|
|
285
|
+
categoryNodeId: NodeId,
|
|
286
|
+
methodId: NodeId,
|
|
287
|
+
pattern: string,
|
|
288
|
+
referenceTypeFilter?: NodeId
|
|
289
|
+
): Promise<unknown[]> {
|
|
290
|
+
const result = await this.session.call({
|
|
291
|
+
objectId: categoryNodeId,
|
|
292
|
+
methodId,
|
|
293
|
+
inputArguments: [
|
|
294
|
+
{ dataType: DataType.String, value: pattern },
|
|
295
|
+
// an omitted filter is the null NodeId, not a null value
|
|
296
|
+
{ dataType: DataType.NodeId, value: referenceTypeFilter ?? NodeIdClass.nullNodeId }
|
|
297
|
+
]
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
if (!result.statusCode.isGood()) {
|
|
301
|
+
throw new AliasNameCallError(methodName, categoryNodeId, result.statusCode);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const output = result.outputArguments?.[0];
|
|
305
|
+
const value = output?.value;
|
|
306
|
+
// no match is Good with an empty list (clause 6.3.2 Table 3)
|
|
307
|
+
if (value === null || value === undefined) {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
return Array.isArray(value) ? value : [value];
|
|
311
|
+
}
|
|
312
|
+
}
|
package/source/errors.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-alias-name-client
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { NodeId } from "node-opcua-nodeid";
|
|
6
|
+
import type { StatusCode } from "node-opcua-status-code";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The Server does not expose the Method that was asked for.
|
|
10
|
+
*
|
|
11
|
+
* `FindAlias` is MANDATORY on every `AliasNameCategoryType` instance, but
|
|
12
|
+
* `FindAliasVerbose`, `AddAliasesToCategory` and `DeleteAliasesFromCategory` are
|
|
13
|
+
* all optional (OPC 10000-17 clause 6.3). A Server that implements only the
|
|
14
|
+
* mandatory Method is perfectly conformant, so a Client that wants the verbose
|
|
15
|
+
* form must be able to tell "this Server does not offer it" apart from "the call
|
|
16
|
+
* failed".
|
|
17
|
+
*
|
|
18
|
+
* Raised before any call is made — the Method's absence is discovered while
|
|
19
|
+
* resolving NodeIds — so it never surfaces as an unhandled `Bad_NotImplemented`
|
|
20
|
+
* from the wire.
|
|
21
|
+
*/
|
|
22
|
+
export class AliasNameMethodNotSupportedError extends Error {
|
|
23
|
+
/** The Method that is missing, by BrowseName. */
|
|
24
|
+
public readonly methodName: string;
|
|
25
|
+
/** The category it was looked for on. */
|
|
26
|
+
public readonly categoryNodeId: NodeId;
|
|
27
|
+
|
|
28
|
+
constructor(methodName: string, categoryNodeId: NodeId) {
|
|
29
|
+
super(
|
|
30
|
+
`the Server does not expose ${methodName} on ${categoryNodeId.toString()}. ` +
|
|
31
|
+
"Only FindAlias is mandatory in OPC 10000-17 clause 6.3; the rest are optional."
|
|
32
|
+
);
|
|
33
|
+
this.name = "AliasNameMethodNotSupportedError";
|
|
34
|
+
this.methodName = methodName;
|
|
35
|
+
this.categoryNodeId = categoryNodeId;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The Server answered a `FindAlias` call with a bad StatusCode.
|
|
41
|
+
*
|
|
42
|
+
* Carries the code so a caller can distinguish the cases clause 6.3.2 Table 4
|
|
43
|
+
* defines — `Bad_InvalidArgument` for a malformed search pattern,
|
|
44
|
+
* `Bad_ResponseTooLarge` for a result set that needs a narrower filter,
|
|
45
|
+
* `Bad_UserAccessDenied` for a category the session may not read.
|
|
46
|
+
*/
|
|
47
|
+
export class AliasNameCallError extends Error {
|
|
48
|
+
public readonly statusCode: StatusCode;
|
|
49
|
+
public readonly categoryNodeId: NodeId;
|
|
50
|
+
|
|
51
|
+
constructor(methodName: string, categoryNodeId: NodeId, statusCode: StatusCode) {
|
|
52
|
+
super(`${methodName} on ${categoryNodeId.toString()} failed with ${statusCode.toString()}`);
|
|
53
|
+
this.name = "AliasNameCallError";
|
|
54
|
+
this.statusCode = statusCode;
|
|
55
|
+
this.categoryNodeId = categoryNodeId;
|
|
56
|
+
}
|
|
57
|
+
}
|
package/source/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-alias-name-client
|
|
3
|
+
*
|
|
4
|
+
* Client-side OPC 10000-17 (AliasNames).
|
|
5
|
+
*
|
|
6
|
+
* Resolves the AliasNames **a Server publishes about itself**. Aggregating
|
|
7
|
+
* AliasNames collected from other Servers (Annexes B and C) and the Annex D
|
|
8
|
+
* PubSub change notification are out of scope; {@link ServerIndexResolver}
|
|
9
|
+
* covers the one cross-Server step a Client still has to make, turning the
|
|
10
|
+
* `ServerIndex` of a returned `ExpandedNodeId` into a URI (Annex A).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
ALIASES_ROOT,
|
|
15
|
+
type ClientAliasEntry,
|
|
16
|
+
ClientAliasSet,
|
|
17
|
+
type ClientAliasVerboseEntry,
|
|
18
|
+
type FindAliasOptions,
|
|
19
|
+
TAG_VARIABLES,
|
|
20
|
+
TOPICS
|
|
21
|
+
} from "./client_alias_set.js";
|
|
22
|
+
export { AliasNameCallError, AliasNameMethodNotSupportedError } from "./errors.js";
|
|
23
|
+
export { LOCAL_SERVER_INDEX, ServerIndexResolver } from "./server_index_resolver.js";
|