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/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
+ }
@@ -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
+ }
@@ -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";