node-opcua-alias-name-server 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 +361 -0
- package/dist/add_alias.d.ts +73 -0
- package/dist/add_alias.js +262 -0
- package/dist/add_alias.js.map +1 -0
- package/dist/address_space_alias_store.d.ts +130 -0
- package/dist/address_space_alias_store.js +360 -0
- package/dist/address_space_alias_store.js.map +1 -0
- package/dist/alias_hierarchy.d.ts +45 -0
- package/dist/alias_hierarchy.js +135 -0
- package/dist/alias_hierarchy.js.map +1 -0
- package/dist/alias_index.d.ts +60 -0
- package/dist/alias_index.js +119 -0
- package/dist/alias_index.js.map +1 -0
- package/dist/alias_name_archive.d.ts +45 -0
- package/dist/alias_name_archive.js +75 -0
- package/dist/alias_name_archive.js.map +1 -0
- package/dist/bind_alias_category.d.ts +173 -0
- package/dist/bind_alias_category.js +417 -0
- package/dist/bind_alias_category.js.map +1 -0
- package/dist/bind_configuration_methods.d.ts +44 -0
- package/dist/bind_configuration_methods.js +175 -0
- package/dist/bind_configuration_methods.js.map +1 -0
- package/dist/bind_find_alias.d.ts +61 -0
- package/dist/bind_find_alias.js +227 -0
- package/dist/bind_find_alias.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/dist/install_alias_names.d.ts +229 -0
- package/dist/install_alias_names.js +146 -0
- package/dist/install_alias_names.js.map +1 -0
- package/dist/last_change.d.ts +87 -0
- package/dist/last_change.js +174 -0
- package/dist/last_change.js.map +1 -0
- package/dist/well_known.d.ts +88 -0
- package/dist/well_known.js +93 -0
- package/dist/well_known.js.map +1 -0
- package/package.json +54 -0
- package/source/add_alias.ts +318 -0
- package/source/address_space_alias_store.ts +417 -0
- package/source/alias_hierarchy.ts +141 -0
- package/source/alias_index.ts +127 -0
- package/source/alias_name_archive.ts +84 -0
- package/source/bind_alias_category.ts +546 -0
- package/source/bind_configuration_methods.ts +219 -0
- package/source/bind_find_alias.ts +290 -0
- package/source/index.ts +74 -0
- package/source/install_alias_names.ts +342 -0
- package/source/last_change.ts +201 -0
- package/source/well_known.ts +101 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2024 Sterfive SAS - 833264583 RCS ORLEANS - France (https://www.sterfive.com)
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2014-2022 Etienne Rossignon
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
8
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
9
|
+
the Software without restriction, including without limitation the rights to
|
|
10
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
11
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
12
|
+
subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
19
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
20
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
21
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
22
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# node-opcua-alias-name-server
|
|
2
|
+
|
|
3
|
+
Server-side OPC UA **AliasNames** (OPC 10000-17).
|
|
4
|
+
|
|
5
|
+
These packages let a Server publish **its own** AliasNames and let a Client resolve
|
|
6
|
+
them. They do **not** aggregate AliasNames collected from other Servers: Annex B
|
|
7
|
+
(aggregating Server) and Annex C (GDS) of OPC 10000-17, and the Annex D PubSub change
|
|
8
|
+
notification, are out of scope. Anything that requires knowing about more than one
|
|
9
|
+
Server is not implemented here.
|
|
10
|
+
|
|
11
|
+
see http://node-opcua.github.io/
|
|
12
|
+
|
|
13
|
+
## Why this exists
|
|
14
|
+
|
|
15
|
+
Every node-opcua Server that loads the standard nodeset already exposes the `Aliases`,
|
|
16
|
+
`TagVariables` and `Topics` Objects, each carrying a MANDATORY `FindAlias` Method that
|
|
17
|
+
is bound to nothing. A conformance tester therefore sees the SDK advertise the
|
|
18
|
+
AliasName feature and then fail its only required Method. This package binds them.
|
|
19
|
+
|
|
20
|
+
Part 17 is, in effect, DNS for an address space: a Client asks `FindAlias("TI101")` and
|
|
21
|
+
gets back the `ExpandedNodeId` values that name resolves to, instead of being configured
|
|
22
|
+
with a raw NodeId. It is the standard bridge between ISA-5.1 style plant tag names and
|
|
23
|
+
explicitly modelled OPC UA Nodes.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install node-opcua-alias-name-server
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Zero configuration
|
|
32
|
+
|
|
33
|
+
If your NodeSet2.xml already models `AliasNameType` instances, one call is the whole
|
|
34
|
+
integration:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { installAliasNames } from "node-opcua-alias-name-server";
|
|
38
|
+
|
|
39
|
+
await server.initialize();
|
|
40
|
+
await installAliasNames(server); // also declares the ALIAS capability
|
|
41
|
+
await server.start();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Call it between `initialize()` and `start()`.** The address space exists from
|
|
45
|
+
`initialize()` onwards, and `start()` performs the mDNS/LDS registration that reads the
|
|
46
|
+
capability list — so installing afterwards binds the Methods correctly but registers the
|
|
47
|
+
Server without `ALIAS`.
|
|
48
|
+
|
|
49
|
+
`FindAlias` now answers correctly on `Aliases`, `TagVariables`, `Topics` and every
|
|
50
|
+
vendor subcategory nested below them. The default store reads the address space
|
|
51
|
+
directly, so the model *is* the database — there is nothing to keep in sync.
|
|
52
|
+
|
|
53
|
+
Calling `installAliasNames` twice is a no-op, not a double binding.
|
|
54
|
+
|
|
55
|
+
There is also an address-space-level form, for tests and tools that have no Server:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { installAliasNamesOnAddressSpace } from "node-opcua-alias-name-server";
|
|
59
|
+
await installAliasNamesOnAddressSpace(addressSpace);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Declaring aliases from code
|
|
63
|
+
|
|
64
|
+
For a Server whose address space is built programmatically:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { addAlias, removeAlias, WellKnownCategories } from "node-opcua-alias-name-server";
|
|
68
|
+
|
|
69
|
+
addAlias(addressSpace, WellKnownCategories.TagVariables, "TI101", temperatureVariable);
|
|
70
|
+
removeAlias(addressSpace, WellKnownCategories.TagVariables, "TI101");
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`addAlias` enforces the rules of clause 6.2 so you cannot build a non-conformant alias:
|
|
74
|
+
the BrowseName's string part equals the DisplayName with an empty locale and no other
|
|
75
|
+
locale, and at least one `AliasFor` Reference exists. It also enforces the category
|
|
76
|
+
restrictions where the target is local and therefore checkable — `TagVariables` accepts
|
|
77
|
+
only Variables (clause 9.3), `Topics` only `PublishedDataSetType` instances or subtypes
|
|
78
|
+
(clause 9.4).
|
|
79
|
+
|
|
80
|
+
**There is deliberately no rename.** Clause 6.2 makes the BrowseName immutable: "If an
|
|
81
|
+
AliasName is to be changed, it shall be a deletion of the old AliasName and the addition
|
|
82
|
+
of the new AliasName", which yields a new NodeId. That is what lets an aggregating Server
|
|
83
|
+
notice the change, so a rename API would quietly break aggregation downstream.
|
|
84
|
+
|
|
85
|
+
Adding the same name twice adds a target to the existing alias rather than creating a
|
|
86
|
+
second node; an exact duplicate of (name, target) is ignored.
|
|
87
|
+
|
|
88
|
+
## Options
|
|
89
|
+
|
|
90
|
+
| Option | Default | Meaning |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `store` | `AddressSpaceAliasStore` | Where aliases come from. Inject your own to back them with a database or an existing tag dictionary. |
|
|
93
|
+
| `maxResults` | `1000` | Beyond this a call answers `Bad_ResponseTooLarge` (clause 6.3.2 Table 4). |
|
|
94
|
+
| `verbose` | `true` | Also bind `FindAliasVerbose` (clause 6.3.3). |
|
|
95
|
+
| `comparator` | insertion order | Result ordering (clause 6.3.2, "best match first"). |
|
|
96
|
+
| `isReadAllowed` | allow all | `(context, categoryNodeId) => boolean \| Promise<boolean>`, consulted per category. |
|
|
97
|
+
| `isWriteAllowed` | **deny all** | Same shape, for the configuration Methods. |
|
|
98
|
+
| `likeOptions` | case sensitive | Passed to the `Like` matcher. |
|
|
99
|
+
| `additionalCategoryRoots` | — | Categories modelled outside the `Aliases` hierarchy. |
|
|
100
|
+
| `categoryProvider` | `defaultCategoryProvider()` | Replace category discovery entirely; may be async. |
|
|
101
|
+
| `advertiseCapability` | `true` | Declare `ALIAS` in the Server's `capabilitiesForMDNS` (OPC 10000-12 Annex D). |
|
|
102
|
+
| `configurationMethods` | `false` | Expose `AddAliasesToCategory` / `DeleteAliasesFromCategory` (CU 5874). |
|
|
103
|
+
| `persistencePath` | — | File backing the persisted `LastChange` (clause 6.3.1). |
|
|
104
|
+
| `lastChangeOnAllCategories` | `true` | Add a `LastChange` Property to every category, not only the root. |
|
|
105
|
+
|
|
106
|
+
**Set `persistencePath` on any Server that Clients cache against.** Without it every
|
|
107
|
+
restart resets `LastChange` to zero, and clause 6.3.1 requires a Client seeing a value
|
|
108
|
+
older than its cache to *clear that cache* — so an unpersisted Server silently orders
|
|
109
|
+
every connected Client to discard a still-valid cache on every restart.
|
|
110
|
+
|
|
111
|
+
## Extending it
|
|
112
|
+
|
|
113
|
+
Everything installation does is available piecewise, so an advanced Server — a GDS, an
|
|
114
|
+
aggregating Server, a vendor Server with per-customer categories — does not have to
|
|
115
|
+
re-implement a private half.
|
|
116
|
+
|
|
117
|
+
### Categories created at runtime
|
|
118
|
+
|
|
119
|
+
`installAliasNames` binds what exists when it runs, and is a no-op if called again. A
|
|
120
|
+
category created afterwards would otherwise have an unbound MANDATORY `FindAlias` — the
|
|
121
|
+
exact defect this package removes, reappearing at runtime. Use `addAliasCategory`, which
|
|
122
|
+
creates *and* binds:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { addAliasCategory, WellKnownCategories } from "node-opcua-alias-name-server";
|
|
126
|
+
|
|
127
|
+
const wells = addAliasCategory(addressSpace, WellKnownCategories.TagVariables, "Wells");
|
|
128
|
+
// FindAlias and FindAliasVerbose are already bound, with the options
|
|
129
|
+
// installAliasNames was given
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
For a category built by hand, bind it with the options installation used:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { bindAliasCategory, getInstalledAliasNames } from "node-opcua-alias-name-server";
|
|
136
|
+
|
|
137
|
+
const installed = getInstalledAliasNames(addressSpace)!;
|
|
138
|
+
bindAliasCategory(addressSpace, myCategory, installed.bindingOptions);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`installAliasNamesOnAddressSpace` calls `bindAliasCategory` in its own loop, so there is
|
|
142
|
+
exactly one binding path and a late category cannot diverge from an installed one. This
|
|
143
|
+
matters most for `FindAliasVerbose`, whose clone-with-reserved-NodeId logic cannot
|
|
144
|
+
sensibly be hand-rolled.
|
|
145
|
+
|
|
146
|
+
### Dynamic category sets
|
|
147
|
+
|
|
148
|
+
`additionalCategoryRoots` only covers roots known at install time. When the set is
|
|
149
|
+
genuinely dynamic, replace discovery:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import { defaultCategoryProvider } from "node-opcua-alias-name-server";
|
|
153
|
+
|
|
154
|
+
await installAliasNames(server, {
|
|
155
|
+
categoryProvider: async (addressSpace) => [
|
|
156
|
+
...(await defaultCategoryProvider()(addressSpace)),
|
|
157
|
+
...(await myTenantCategories(addressSpace))
|
|
158
|
+
]
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`additionalCategoryRoots` is expressed through `defaultCategoryProvider`, so the two
|
|
163
|
+
compose rather than being parallel mechanisms.
|
|
164
|
+
|
|
165
|
+
### Per-category access control
|
|
166
|
+
|
|
167
|
+
`isReadAllowed` receives the category the Method was called on and may return a Promise,
|
|
168
|
+
so "may this user see *this* customer's category" is expressible, and a permission lookup
|
|
169
|
+
may hit a database:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
await installAliasNames(server, {
|
|
173
|
+
isReadAllowed: async (context, categoryNodeId) => tenantOf(context) === ownerOf(categoryNodeId)
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The gate is consulted **per category**, each at most once per call, and the outcome
|
|
178
|
+
differs by how the category was reached:
|
|
179
|
+
|
|
180
|
+
| Situation | Result |
|
|
181
|
+
|---|---|
|
|
182
|
+
| Direct call on a denied category | `Bad_UserAccessDenied` — nothing left to filter |
|
|
183
|
+
| Denied category reached by a recursive search | Omitted; the call still returns `Good` |
|
|
184
|
+
|
|
185
|
+
Absence is the only answer that discloses nothing: an error, or a count that changed,
|
|
186
|
+
would confirm the category exists. `FindAliasVerbose` filters at the same point as
|
|
187
|
+
`FindAlias`, so it cannot leak an `AliasNameCategoryId` or a `ServerUri` for a category
|
|
188
|
+
the plain form would have hidden.
|
|
189
|
+
|
|
190
|
+
`Bad_ResponseTooLarge` is still possible for a gated caller, since the cap is applied to
|
|
191
|
+
the raw scan to keep truncation detectable — but it names no category.
|
|
192
|
+
|
|
193
|
+
OPC 10000-17 defines no security model at all: four `Bad_UserAccessDenied` rows, no
|
|
194
|
+
Security clause, no Roles, and no `RolePermissions` on any Part 17 node in the standard
|
|
195
|
+
nodeset. Every Server has to supply its own rule, which is why this hook exists rather
|
|
196
|
+
than a built-in policy.
|
|
197
|
+
|
|
198
|
+
### Removing a category
|
|
199
|
+
|
|
200
|
+
The specification does not say what happens to a category's contents, so the rule is
|
|
201
|
+
explicit:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
removeAliasCategory(addressSpace, tenantCategory); // re-parent (default)
|
|
205
|
+
removeAliasCategory(addressSpace, tenantCategory, { orphans: "cascade" }); // delete with it
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`reparent` moves the aliases and subcategories to the parent first, so an alias keeps its
|
|
209
|
+
NodeId and a Client that resolved it keeps resolving it — clause 6.2 makes a NodeId change
|
|
210
|
+
mean "this is a different alias". `cascade` is right when the category itself is being
|
|
211
|
+
retired. The three well-known categories cannot be removed; clause 9 requires them.
|
|
212
|
+
|
|
213
|
+
### Result ordering
|
|
214
|
+
|
|
215
|
+
Clause 6.3.2 requires "what it recommends as the best match first", and says the criteria
|
|
216
|
+
are Server specific. The examples it gives — the ServerStatus of the Server holding the
|
|
217
|
+
Node, load balancing — only mean anything once more than one Server is involved. A Server
|
|
218
|
+
publishing its own aliases has no basis to prefer one of its own Nodes over another, so
|
|
219
|
+
the default preserves discovery order: deterministic, and therefore stable across calls.
|
|
220
|
+
Supply `comparator` when your Server does have a basis.
|
|
221
|
+
|
|
222
|
+
### Denial-of-service bounds
|
|
223
|
+
|
|
224
|
+
`FindAlias` is remotely callable, usually by an anonymous session, so both of its inputs
|
|
225
|
+
are bounded:
|
|
226
|
+
|
|
227
|
+
- **The search pattern.** Parsing allocates one element per character, and the transport
|
|
228
|
+
accepts a String up to 16 MB, so patterns over 2048 characters are refused with
|
|
229
|
+
`Bad_InvalidArgument` before anything is allocated. See the cost table in
|
|
230
|
+
`node-opcua-like-matcher`. Adjust with `likeOptions.maxPatternLength`.
|
|
231
|
+
- **The result set.** `maxResults` bounds the *work*, not just the response: the store
|
|
232
|
+
stops collecting one entry past the cap rather than walking the whole hierarchy and
|
|
233
|
+
discarding it. The cap is applied to the raw entries before they are merged by name,
|
|
234
|
+
because merging can reduce the count and would otherwise report a truncated scan as a
|
|
235
|
+
complete answer. A merge that trips the cap is therefore `Bad_ResponseTooLarge` even
|
|
236
|
+
though the merged list would have been short — conservative, and consistent with
|
|
237
|
+
"try new filter and repeat find".
|
|
238
|
+
|
|
239
|
+
### Category discovery
|
|
240
|
+
|
|
241
|
+
Categories are found by walking down from `Aliases`, which is where clause 9.1 puts them:
|
|
242
|
+
vendors "are free to add additional instances of AliasNameCategoryType under this
|
|
243
|
+
hierarchy". A category modelled anywhere else is not discovered — the address space keeps
|
|
244
|
+
no inverse `HasTypeDefinition` reference, so there is nothing to sweep. Name such a
|
|
245
|
+
category in `additionalCategoryRoots`, otherwise its MANDATORY `FindAlias` stays unbound.
|
|
246
|
+
|
|
247
|
+
### `FindAliasVerbose` NodeIds
|
|
248
|
+
|
|
249
|
+
The shipped `Opc.Ua.NodeSet2.xml` declares `FindAliasVerbose` on `AliasNameCategoryType`
|
|
250
|
+
but instantiates it on none of the three well-known categories. Upstream nonetheless
|
|
251
|
+
reserves fixed NodeIds for those instances (`i=24054`, `i=24063`, `i=24072`), so
|
|
252
|
+
installation uses them rather than server-assigned ones and an aggregating Server sees
|
|
253
|
+
the NodeId it expects. Vendor subcategories get a server-assigned NodeId, as they must.
|
|
254
|
+
|
|
255
|
+
## The `ALIAS` capability is declared for you
|
|
256
|
+
|
|
257
|
+
`installAliasNames` adds `ALIAS` to the Server's `capabilitiesForMDNS`
|
|
258
|
+
(OPC 10000-12 Annex D Table D.1). Declaring the capability and installing the feature are
|
|
259
|
+
the same decision, so they happen together — leaving it to each caller to remember means
|
|
260
|
+
it will sometimes be forgotten, and a Server that omits it is simply never discovered by
|
|
261
|
+
anything looking for alias-capable Servers, with nothing reporting the failure.
|
|
262
|
+
|
|
263
|
+
It is idempotent, case-insensitive, and replaces node-opcua's `NA` placeholder rather
|
|
264
|
+
than producing the meaningless `["NA", "ALIAS"]`:
|
|
265
|
+
|
|
266
|
+
| Before | After |
|
|
267
|
+
|---|---|
|
|
268
|
+
| `[]` | `["ALIAS"]` |
|
|
269
|
+
| `["NA"]` | `["ALIAS"]` |
|
|
270
|
+
| `["DA", "HD"]` | `["DA", "HD", "ALIAS"]` |
|
|
271
|
+
| `["Alias"]` | `["Alias"]` — already declared |
|
|
272
|
+
|
|
273
|
+
The normative identifier is `ALIAS`; Part 17's prose writes it `Alias`, and Part 12
|
|
274
|
+
Annex D is the normative source.
|
|
275
|
+
|
|
276
|
+
Pass `advertiseCapability: false` if the Server manages its own capability list, or use
|
|
277
|
+
the helper directly:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
import { advertiseAliasCapability } from "node-opcua-alias-name-server";
|
|
281
|
+
advertiseAliasCapability(server.capabilitiesForMDNS);
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## `LastChange` (clause 6.3.1)
|
|
285
|
+
|
|
286
|
+
A **`VersionTime`: a UInt32 count of seconds since 2000-01-01T00:00:00Z**, not a
|
|
287
|
+
`DateTime` — which is what every other "last changed" Property in the SDK is, and so the
|
|
288
|
+
easiest thing here to get wrong.
|
|
289
|
+
|
|
290
|
+
All three clause 6.3.1 triggers move it: an alias added or deleted, a category added or
|
|
291
|
+
deleted, and an alias's referenced Nodes changing. Nested categories roll up — a change
|
|
292
|
+
deep in the hierarchy moves every ancestor to the root — and the rollup is applied when
|
|
293
|
+
the change happens, not computed on read, so the Property a Client subscribes to actually
|
|
294
|
+
carries the value.
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
await installAliasNames(server, { persistencePath: "./aliases-lastchange.json" });
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
The archive is small JSON — a version and a map of category NodeId to VersionTime — and
|
|
301
|
+
is written atomically. A corrupt or future-versioned archive is **reported**, not silently
|
|
302
|
+
treated as "start from zero", because that is the same cache-clearing bug persistence
|
|
303
|
+
exists to prevent.
|
|
304
|
+
|
|
305
|
+
Two things worth designing around:
|
|
306
|
+
|
|
307
|
+
- **Resolution is one second.** Two changes inside the same second are indistinguishable.
|
|
308
|
+
A Client should treat an *equal* `LastChange` as "re-browse to be sure"; only a value
|
|
309
|
+
*older* than the cached one carries clause 6.3.1's "clear the cache" meaning.
|
|
310
|
+
- **Category NodeIds must be stable**, since the archive keys on them.
|
|
311
|
+
`addAliasCategory` therefore derives a string NodeId from the category's path
|
|
312
|
+
(`ns=1;s=Aliases/TagVariables/Unit200`) rather than taking the next free numeric id,
|
|
313
|
+
which would shift whenever an unrelated Node happened to be created first.
|
|
314
|
+
|
|
315
|
+
## The configuration Methods (clauses 6.3.4, 6.3.5)
|
|
316
|
+
|
|
317
|
+
Off by default. Turning them on exposes `AddAliasesToCategory` and
|
|
318
|
+
`DeleteAliasesFromCategory` on every category — but **every call is denied until
|
|
319
|
+
`isWriteAllowed` says otherwise**:
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
await installAliasNames(server, {
|
|
323
|
+
configurationMethods: true,
|
|
324
|
+
isWriteAllowed: async (context, categoryNodeId) => isEngineer(context)
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Read and write are gated independently, and only one of them is safe to open by default:
|
|
329
|
+
`isReadAllowed` allows everyone, `isWriteAllowed` denies everyone.
|
|
330
|
+
|
|
331
|
+
Both Methods report **per item**. The call succeeds and an `ErrorCodes` array parallel to
|
|
332
|
+
`AliasNames` says what happened to each, so one bad entry does not fail the batch. Only
|
|
333
|
+
the argument errors of Tables 11 and 15 — mismatched array sizes, an empty call, a denied
|
|
334
|
+
caller — fail the call itself.
|
|
335
|
+
|
|
336
|
+
`AddAliasesToCategory` follows Table 10: `Bad_NodeIdUnknown` for a missing local target,
|
|
337
|
+
`Bad_NotSupported` for a remote target unless `allowRemoteTargets` is set on the store,
|
|
338
|
+
and `Uncertain_ReferenceOutOfServer` for a remote target when it is — the clause is
|
|
339
|
+
explicit that the uncertain code applies *whether or not* a check was performed, and this
|
|
340
|
+
Server does not check, since that would mean being a Client of the other Server. An exact
|
|
341
|
+
duplicate of (AliasName, target, target Server) is `Good` and ignored, whether already
|
|
342
|
+
stored or repeated within the same call. A null `TargetReferenceType` defaults to
|
|
343
|
+
`AliasFor`, and the `ServerIndex` inside an incoming `ExpandedNodeId` is ignored —
|
|
344
|
+
`TargetServers` is authoritative (Table 9).
|
|
345
|
+
|
|
346
|
+
`DeleteAliasesFromCategory` follows Table 14: `Bad_NotFound` when the name is not there,
|
|
347
|
+
`Bad_InvalidState` when it is not owned by this Server. An entry with no target removes
|
|
348
|
+
every target of that name, removal is all-or-nothing per name, and removing the last
|
|
349
|
+
target removes the `AliasNameType` Object, since clause 7.2 gives it at least one
|
|
350
|
+
ReferencedNode.
|
|
351
|
+
|
|
352
|
+
## What is not here yet
|
|
353
|
+
|
|
354
|
+
- **Aggregation across Servers** (Annexes B, C) and the **Annex D PubSub change
|
|
355
|
+
notification** — out of scope by design, not pending.
|
|
356
|
+
- **UACTT has not been run.** The sample Server in `node-opcua-alias-name-test` is ready
|
|
357
|
+
for it.
|
|
358
|
+
|
|
359
|
+
## License
|
|
360
|
+
|
|
361
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-alias-name-server
|
|
3
|
+
*
|
|
4
|
+
* Creating and removing `AliasNameType` instances from code, for Servers whose
|
|
5
|
+
* address space is built programmatically rather than loaded from a NodeSet2.
|
|
6
|
+
*/
|
|
7
|
+
import type { BaseNode, IAddressSpace, UAObject } from "node-opcua-address-space-base";
|
|
8
|
+
import { type NodeId } from "node-opcua-nodeid";
|
|
9
|
+
/** Raised when an alias would break a rule of OPC 10000-17. */
|
|
10
|
+
export declare class AliasNameError extends Error {
|
|
11
|
+
constructor(message: string);
|
|
12
|
+
}
|
|
13
|
+
export interface AddAliasOptions {
|
|
14
|
+
/**
|
|
15
|
+
* ReferenceType linking the AliasName to its target. Defaults to `AliasFor`
|
|
16
|
+
* (clause 8.2); a subtype is allowed.
|
|
17
|
+
*/
|
|
18
|
+
referenceType?: NodeId | string;
|
|
19
|
+
/**
|
|
20
|
+
* Namespace index for the AliasName's BrowseName. Defaults to the Server's
|
|
21
|
+
* own namespace. Clause 6.2 allows namespace 1 or a vendor namespace, and
|
|
22
|
+
* requires Clients to ignore it when comparing.
|
|
23
|
+
*/
|
|
24
|
+
namespaceIndex?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Accept a target this Server cannot resolve.
|
|
27
|
+
*
|
|
28
|
+
* For a Node on **another** Server, which by definition is not in this
|
|
29
|
+
* address space: the existence check and the clause 9.3 / 9.4 category
|
|
30
|
+
* restrictions are both skipped, because neither can be evaluated without
|
|
31
|
+
* being a Client of that Server. Used by `AddAliasesToCategory` when
|
|
32
|
+
* `TargetServers` names a remote Server (clause 6.3.4).
|
|
33
|
+
*/
|
|
34
|
+
allowUnresolvedTarget?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Add an `AliasNameType` instance under `category`, naming `target`.
|
|
38
|
+
*
|
|
39
|
+
* Enforces clause 6.2:
|
|
40
|
+
*
|
|
41
|
+
* - the string part of the BrowseName equals the DisplayName, with an empty
|
|
42
|
+
* locale and no other locale;
|
|
43
|
+
* - the Object has at least one `AliasFor` Reference;
|
|
44
|
+
* - the BrowseName is immutable once created. There is deliberately no rename:
|
|
45
|
+
* "If an AliasName is to be changed, it shall be a deletion of the old
|
|
46
|
+
* AliasName and the addition of the new AliasName", which yields a new NodeId
|
|
47
|
+
* and is what lets an aggregating Server notice the change.
|
|
48
|
+
*
|
|
49
|
+
* and the category restrictions of clauses 9.3 and 9.4, where the target is
|
|
50
|
+
* local and therefore checkable.
|
|
51
|
+
*
|
|
52
|
+
* Adding the same (name, target) twice returns the existing node rather than
|
|
53
|
+
* creating a duplicate.
|
|
54
|
+
*/
|
|
55
|
+
export declare function addAlias(addressSpace: IAddressSpace, category: UAObject | NodeId, aliasName: string, target: BaseNode | NodeId, options?: AddAliasOptions): UAObject;
|
|
56
|
+
/**
|
|
57
|
+
* Remove one target from an alias, or the whole alias when no target is given.
|
|
58
|
+
*
|
|
59
|
+
* Removing the last target deletes the `AliasNameType` node: clause 7.2 says the
|
|
60
|
+
* ReferencedNodes array always has at least one entry, so an alias naming
|
|
61
|
+
* nothing cannot be represented.
|
|
62
|
+
*
|
|
63
|
+
* @returns true when something was removed.
|
|
64
|
+
*/
|
|
65
|
+
export declare function removeAlias(addressSpace: IAddressSpace, category: UAObject | NodeId, aliasName: string, target?: BaseNode | NodeId): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* The `AliasNameType` instance with this name Organized by `category`, if any.
|
|
68
|
+
*
|
|
69
|
+
* Compares the string part only, ignoring the namespace, as clause 6.2 requires.
|
|
70
|
+
* Backed by a per-category index — see {@link lookupAlias} for why a plain scan
|
|
71
|
+
* was not good enough.
|
|
72
|
+
*/
|
|
73
|
+
export declare function findAlias(addressSpace: IAddressSpace, category: UAObject, aliasName: string): UAObject | null;
|