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.
Files changed (51) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +361 -0
  3. package/dist/add_alias.d.ts +73 -0
  4. package/dist/add_alias.js +262 -0
  5. package/dist/add_alias.js.map +1 -0
  6. package/dist/address_space_alias_store.d.ts +130 -0
  7. package/dist/address_space_alias_store.js +360 -0
  8. package/dist/address_space_alias_store.js.map +1 -0
  9. package/dist/alias_hierarchy.d.ts +45 -0
  10. package/dist/alias_hierarchy.js +135 -0
  11. package/dist/alias_hierarchy.js.map +1 -0
  12. package/dist/alias_index.d.ts +60 -0
  13. package/dist/alias_index.js +119 -0
  14. package/dist/alias_index.js.map +1 -0
  15. package/dist/alias_name_archive.d.ts +45 -0
  16. package/dist/alias_name_archive.js +75 -0
  17. package/dist/alias_name_archive.js.map +1 -0
  18. package/dist/bind_alias_category.d.ts +173 -0
  19. package/dist/bind_alias_category.js +417 -0
  20. package/dist/bind_alias_category.js.map +1 -0
  21. package/dist/bind_configuration_methods.d.ts +44 -0
  22. package/dist/bind_configuration_methods.js +175 -0
  23. package/dist/bind_configuration_methods.js.map +1 -0
  24. package/dist/bind_find_alias.d.ts +61 -0
  25. package/dist/bind_find_alias.js +227 -0
  26. package/dist/bind_find_alias.js.map +1 -0
  27. package/dist/index.d.ts +18 -0
  28. package/dist/index.js +64 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/install_alias_names.d.ts +229 -0
  31. package/dist/install_alias_names.js +146 -0
  32. package/dist/install_alias_names.js.map +1 -0
  33. package/dist/last_change.d.ts +87 -0
  34. package/dist/last_change.js +174 -0
  35. package/dist/last_change.js.map +1 -0
  36. package/dist/well_known.d.ts +88 -0
  37. package/dist/well_known.js +93 -0
  38. package/dist/well_known.js.map +1 -0
  39. package/package.json +54 -0
  40. package/source/add_alias.ts +318 -0
  41. package/source/address_space_alias_store.ts +417 -0
  42. package/source/alias_hierarchy.ts +141 -0
  43. package/source/alias_index.ts +127 -0
  44. package/source/alias_name_archive.ts +84 -0
  45. package/source/bind_alias_category.ts +546 -0
  46. package/source/bind_configuration_methods.ts +219 -0
  47. package/source/bind_find_alias.ts +290 -0
  48. package/source/index.ts +74 -0
  49. package/source/install_alias_names.ts +342 -0
  50. package/source/last_change.ts +201 -0
  51. package/source/well_known.ts +101 -0
@@ -0,0 +1,219 @@
1
+ /**
2
+ * @module node-opcua-alias-name-server
3
+ *
4
+ * `AddAliasesToCategory` (clause 6.3.4) and `DeleteAliasesFromCategory`
5
+ * (clause 6.3.5) — the *AliasName Configuration Support* facet, CU 5874.
6
+ *
7
+ * Both report **per item**: the Method itself succeeds and an `ErrorCodes` array
8
+ * parallel to `AliasNames` says what happened to each one. A single bad entry
9
+ * does not fail the call, which is why the store interface returns
10
+ * `StatusCode[]` rather than throwing.
11
+ */
12
+
13
+ import type { ISessionContext, UAMethod, UAObject } from "node-opcua-address-space-base";
14
+ import type { AliasEntry, IAliasStore } from "node-opcua-alias-name-common";
15
+ import { type ExpandedNodeId, type NodeId, NodeId as NodeIdClass } from "node-opcua-nodeid";
16
+ import type { CallMethodResultOptions } from "node-opcua-service-call";
17
+ import { type StatusCode, StatusCodes } from "node-opcua-status-code";
18
+ import { DataType, type Variant, VariantArrayType } from "node-opcua-variant";
19
+ import { ALIAS_FOR } from "./well_known.js";
20
+
21
+ export interface ConfigurationBindingOptions {
22
+ /** The store that performs the mutation. Must implement `add` / `delete`. */
23
+ store: IAliasStore;
24
+ /**
25
+ * Write gate (clause 6.3.4 Table 11 / 6.3.5 Table 15). **Defaults to denying
26
+ * everyone** — the write surface is the one place a permissive default would
27
+ * be a security defect rather than a convenience.
28
+ */
29
+ isWriteAllowed?: (context: ISessionContext, categoryNodeId: NodeId) => boolean | Promise<boolean>;
30
+ /** Called after a successful mutation so `LastChange` can move. */
31
+ onChanged?: (categoryNodeId: NodeId) => void | Promise<void>;
32
+ }
33
+
34
+ /** Read a String[] argument, tolerating null for "empty". */
35
+ function readStringArray(inputArguments: Variant[], index: number): string[] | null {
36
+ const value = inputArguments?.[index]?.value;
37
+ if (value === null || value === undefined) {
38
+ return [];
39
+ }
40
+ if (!Array.isArray(value)) {
41
+ return null;
42
+ }
43
+ return value.map((v) => (typeof v === "string" ? v : String(v)));
44
+ }
45
+
46
+ /** Read an ExpandedNodeId[] argument, tolerating null for "empty". */
47
+ function readNodeIdArray(inputArguments: Variant[], index: number): ExpandedNodeId[] | null {
48
+ const value = inputArguments?.[index]?.value;
49
+ if (value === null || value === undefined) {
50
+ return [];
51
+ }
52
+ if (!Array.isArray(value)) {
53
+ return null;
54
+ }
55
+ return value as ExpandedNodeId[];
56
+ }
57
+
58
+ /** Build the OutputArguments Variant for an ErrorCodes array. */
59
+ function errorCodesResult(codes: StatusCode[]): CallMethodResultOptions {
60
+ return {
61
+ statusCode: StatusCodes.Good,
62
+ outputArguments: [
63
+ {
64
+ dataType: DataType.StatusCode,
65
+ arrayType: VariantArrayType.Array,
66
+ value: codes
67
+ }
68
+ ]
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Handler for `AddAliasesToCategory` (clause 6.3.4).
74
+ *
75
+ * Table 11 governs the *call*: `Bad_InvalidArgument` when an argument is the
76
+ * wrong type, when the arrays other than `TargetServers` differ in length, or
77
+ * when all arrays are empty. Table 10 governs each *item*.
78
+ */
79
+ export function makeAddAliasesToCategoryHandler(options: ConfigurationBindingOptions) {
80
+ return async function addAliasesToCategoryHandler(
81
+ this: UAMethod,
82
+ inputArguments: Variant[],
83
+ context: ISessionContext
84
+ ): Promise<CallMethodResultOptions> {
85
+ const category = this.parent as UAObject | null;
86
+ if (!category) {
87
+ return { statusCode: StatusCodes.BadInternalError };
88
+ }
89
+
90
+ // clause 6.3.4 Table 11
91
+ if (!(await isWriteAllowed(options, context, category.nodeId))) {
92
+ return { statusCode: StatusCodes.BadUserAccessDenied };
93
+ }
94
+
95
+ const aliasNames = readStringArray(inputArguments, 0);
96
+ const targetNodes = readNodeIdArray(inputArguments, 1);
97
+ const targetServers = readStringArray(inputArguments, 2);
98
+ const targetReferenceTypeRaw = inputArguments?.[3]?.value;
99
+
100
+ if (aliasNames === null || targetNodes === null || targetServers === null) {
101
+ return { statusCode: StatusCodes.BadInvalidArgument };
102
+ }
103
+ // "the size of the arrays for all arguments except TargetServers is not
104
+ // the same" - TargetServers is excluded because Table 9 lets it be null
105
+ // or empty to mean "all local"
106
+ if (aliasNames.length !== targetNodes.length) {
107
+ return { statusCode: StatusCodes.BadInvalidArgument };
108
+ }
109
+ // "or if all arrays are empty"
110
+ if (aliasNames.length === 0) {
111
+ return { statusCode: StatusCodes.BadInvalidArgument };
112
+ }
113
+ if (targetServers.length !== 0 && targetServers.length !== aliasNames.length) {
114
+ return { statusCode: StatusCodes.BadInvalidArgument };
115
+ }
116
+
117
+ // Table 9: "If null, it defaults to AliasFor."
118
+ const referenceTypeId =
119
+ targetReferenceTypeRaw instanceof NodeIdClass && !targetReferenceTypeRaw.isEmpty() ? targetReferenceTypeRaw : ALIAS_FOR;
120
+
121
+ const entries: AliasEntry[] = aliasNames.map((aliasName, i) => {
122
+ // Table 9: "The ServerIndex in the ExpandedNodeId shall be ignored
123
+ // and the TargetServers Uri shall be used."
124
+ const serverUri = targetServers[i] ? targetServers[i] : null;
125
+ return {
126
+ aliasName,
127
+ referencedNodes: [targetNodes[i]],
128
+ serverUris: [serverUri],
129
+ categoryNodeId: category.nodeId,
130
+ referenceTypeIds: [referenceTypeId]
131
+ };
132
+ });
133
+
134
+ if (!options.store.add) {
135
+ // a read-only store: every item is unsupported, but the call itself
136
+ // succeeded in reporting that
137
+ return errorCodesResult(entries.map(() => StatusCodes.BadNotSupported));
138
+ }
139
+
140
+ const codes = await options.store.add(category.nodeId, entries);
141
+ if (codes.some((code) => code.isGood())) {
142
+ await options.onChanged?.(category.nodeId);
143
+ }
144
+ return errorCodesResult(codes);
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Handler for `DeleteAliasesFromCategory` (clause 6.3.5).
150
+ *
151
+ * Table 15 governs the call: unlike Add, **every** array must be the same
152
+ * length — "the size of the arrays for all arguments is not the same", with no
153
+ * exception, because there is no `TargetServers` here.
154
+ */
155
+ export function makeDeleteAliasesFromCategoryHandler(options: ConfigurationBindingOptions) {
156
+ return async function deleteAliasesFromCategoryHandler(
157
+ this: UAMethod,
158
+ inputArguments: Variant[],
159
+ context: ISessionContext
160
+ ): Promise<CallMethodResultOptions> {
161
+ const category = this.parent as UAObject | null;
162
+ if (!category) {
163
+ return { statusCode: StatusCodes.BadInternalError };
164
+ }
165
+
166
+ if (!(await isWriteAllowed(options, context, category.nodeId))) {
167
+ return { statusCode: StatusCodes.BadUserAccessDenied };
168
+ }
169
+
170
+ const aliasNames = readStringArray(inputArguments, 0);
171
+ const targetNodes = readNodeIdArray(inputArguments, 1);
172
+
173
+ if (aliasNames === null || targetNodes === null) {
174
+ return { statusCode: StatusCodes.BadInvalidArgument };
175
+ }
176
+ if (aliasNames.length === 0) {
177
+ return { statusCode: StatusCodes.BadInvalidArgument };
178
+ }
179
+ // Table 13: "The length of each of the arrays shall be the same." A null
180
+ // TargetNodes array is the documented way to say "every target", so an
181
+ // empty array is accepted and expanded.
182
+ if (targetNodes.length !== 0 && targetNodes.length !== aliasNames.length) {
183
+ return { statusCode: StatusCodes.BadInvalidArgument };
184
+ }
185
+
186
+ const entries = aliasNames.map((aliasName, i) => {
187
+ // "If the TargetNodes array entry is null or empty, all AliasNames
188
+ // with the provided name are deleted from the AliasNameCategory."
189
+ const target = targetNodes[i];
190
+ const referencedNodes = target && !target.isEmpty() ? [target] : [];
191
+ return { aliasName, referencedNodes };
192
+ });
193
+
194
+ if (!options.store.delete) {
195
+ return errorCodesResult(entries.map(() => StatusCodes.BadNotSupported));
196
+ }
197
+
198
+ const codes = await options.store.delete(category.nodeId, entries);
199
+ if (codes.some((code) => code.isGood())) {
200
+ await options.onChanged?.(category.nodeId);
201
+ }
202
+ return errorCodesResult(codes);
203
+ };
204
+ }
205
+
206
+ /** The write gate, defaulting to deny. */
207
+ async function isWriteAllowed(
208
+ options: ConfigurationBindingOptions,
209
+ context: ISessionContext,
210
+ categoryNodeId: NodeId
211
+ ): Promise<boolean> {
212
+ if (!options.isWriteAllowed) {
213
+ // Writing is off unless the Server says who may do it. OPC 10000-17
214
+ // defines no security model at all, so there is no safe default rule to
215
+ // fall back on - only a safe default answer.
216
+ return false;
217
+ }
218
+ return await options.isWriteAllowed(context, categoryNodeId);
219
+ }
@@ -0,0 +1,290 @@
1
+ /**
2
+ * @module node-opcua-alias-name-server
3
+ *
4
+ * The `FindAlias` (clause 6.3.2) and `FindAliasVerbose` (clause 6.3.3) Method
5
+ * handlers.
6
+ *
7
+ * The two Methods are identical in every respect except the DataType they
8
+ * return, so both are produced from one implementation: whatever the FindAlias
9
+ * suite proves is proved for FindAliasVerbose too.
10
+ */
11
+
12
+ import type { ISessionContext, UAMethod, UAObject } from "node-opcua-address-space-base";
13
+ import { type AliasEntry, type AliasQuery, type IAliasStore, InvalidLikePatternError } from "node-opcua-alias-name-common";
14
+ import { type NodeId, NodeId as NodeIdClass } from "node-opcua-nodeid";
15
+ import type { CallMethodResultOptions } from "node-opcua-service-call";
16
+ import { StatusCodes } from "node-opcua-status-code";
17
+ import { AliasNameDataType, AliasNameVerboseDataType } from "node-opcua-types";
18
+ import { DataType, type Variant, VariantArrayType } from "node-opcua-variant";
19
+
20
+ /**
21
+ * Orders results before they are returned.
22
+ *
23
+ * Clause 6.3.2 requires the Server to return "what it recommends as the best
24
+ * match first", and says the criteria are Server specific — the examples it
25
+ * gives (ServerStatus of the Server holding the Node, load balancing) only mean
26
+ * anything once more than one Server is involved. A Server publishing its own
27
+ * aliases has no basis to prefer one of its own Nodes over another, so the
28
+ * default preserves discovery order, which is at least deterministic and
29
+ * therefore stable across calls. Replace it when the Server does have a basis.
30
+ */
31
+ export type AliasComparator = (a: AliasEntry, b: AliasEntry) => number;
32
+
33
+ /** The default: keep discovery order (a stable no-op comparator). */
34
+ export const insertionOrderComparator: AliasComparator = () => 0;
35
+
36
+ export interface FindAliasBindingOptions {
37
+ /** Where the aliases come from. */
38
+ store: IAliasStore;
39
+ /**
40
+ * Beyond this many results the call fails with `Bad_ResponseTooLarge`
41
+ * (clause 6.3.2 Table 4).
42
+ */
43
+ maxResults: number;
44
+ /** Result ordering; defaults to {@link insertionOrderComparator}. */
45
+ comparator?: AliasComparator;
46
+ /**
47
+ * Read gate. Return false to answer `Bad_UserAccessDenied`
48
+ * (clause 6.3.2 Table 4). Defaults to allowing everyone, matching a Server
49
+ * that publishes its aliases openly.
50
+ *
51
+ * Receives the category the Method was called on, so a Server with
52
+ * per-customer or per-tenant categories can answer "may this user see *this*
53
+ * category" rather than only "may this user read aliases at all". May return
54
+ * a Promise: the handler is async anyway, so a permission lookup that hits a
55
+ * database costs nothing structurally.
56
+ */
57
+ isReadAllowed?: (context: ISessionContext, categoryNodeId: NodeId) => boolean | Promise<boolean>;
58
+ }
59
+
60
+ /** Read `AliasNameSearchPattern` (argument 0). */
61
+ function readPattern(inputArguments: Variant[]): string | null {
62
+ const value = inputArguments?.[0]?.value;
63
+ if (value === null || value === undefined) {
64
+ // clause 6.3.2 gives no meaning to a null pattern; treat it as "match
65
+ // everything" would silently dump the whole hierarchy, so reject it
66
+ return null;
67
+ }
68
+ return typeof value === "string" ? value : null;
69
+ }
70
+
71
+ /** Read `ReferenceTypeFilter` (argument 1). */
72
+ function readReferenceTypeFilter(inputArguments: Variant[]): NodeId | undefined {
73
+ const value = inputArguments?.[1]?.value;
74
+ return value instanceof NodeIdClass ? value : undefined;
75
+ }
76
+
77
+ /**
78
+ * Merge entries that share an AliasName.
79
+ *
80
+ * `AliasNameDataType` is "an array of ExpandedNodeId for a single AliasName"
81
+ * (clause 7.2), so the non-verbose Method reports one entry per distinct name
82
+ * with all of its targets. The verbose form does *not* merge: its
83
+ * `AliasNameCategoryId` names the category that held the alias, which differs
84
+ * between entries.
85
+ */
86
+ function mergeByAliasName(entries: AliasEntry[]): AliasEntry[] {
87
+ const byName = new Map<string, AliasEntry>();
88
+ // Membership is kept in a Set per name rather than rescanning the growing
89
+ // array: with a linear scan, a name shared by many entries makes this
90
+ // quadratic in the size of the result set.
91
+ const seenTargets = new Map<string, Set<string>>();
92
+
93
+ for (const entry of entries) {
94
+ const existing = byName.get(entry.aliasName);
95
+ if (!existing) {
96
+ byName.set(entry.aliasName, { ...entry, referencedNodes: [...entry.referencedNodes] });
97
+ seenTargets.set(entry.aliasName, new Set(entry.referencedNodes.map((n) => n.toString())));
98
+ continue;
99
+ }
100
+ // written alongside byName above, so it is always present
101
+ const seen = seenTargets.get(entry.aliasName) ?? new Set<string>();
102
+ seenTargets.set(entry.aliasName, seen);
103
+ for (const node of entry.referencedNodes) {
104
+ const key = node.toString();
105
+ if (!seen.has(key)) {
106
+ seen.add(key);
107
+ existing.referencedNodes.push(node);
108
+ }
109
+ }
110
+ }
111
+ return [...byName.values()];
112
+ }
113
+
114
+ /**
115
+ * Resolve each entry's AliasName namespace index, once per call.
116
+ *
117
+ * The namespace reported is the one the alias was published in, taken from the
118
+ * store. Using the *category's* namespace instead would put every alias on the
119
+ * three well-known categories into namespace 0, which is reserved for the OPC
120
+ * Foundation and is not what clause 6.2 intends. A store that does not know its
121
+ * namespace falls back to the Server's own, never to 0.
122
+ *
123
+ * Clients ignore the namespace when comparing AliasNames (clause 6.2), so this
124
+ * is not a matching concern — it is a matter of reporting truthfully.
125
+ */
126
+ function makeNamespaceResolver(category: UAObject): (entry: AliasEntry) => number {
127
+ const addressSpace = category.addressSpace;
128
+ const ownNamespaceIndex = addressSpace.getOwnNamespace().index;
129
+ const cache = new Map<string, number>();
130
+
131
+ return (entry: AliasEntry): number => {
132
+ if (!entry.aliasNameNamespaceUri) {
133
+ return ownNamespaceIndex;
134
+ }
135
+ const cached = cache.get(entry.aliasNameNamespaceUri);
136
+ if (cached !== undefined) {
137
+ return cached;
138
+ }
139
+ const index = addressSpace.getNamespaceIndex(entry.aliasNameNamespaceUri);
140
+ // an unregistered URI resolves to -1; reporting the Server's own
141
+ // namespace is closer to the truth than reporting namespace 0
142
+ const resolved = index >= 0 ? index : ownNamespaceIndex;
143
+ cache.set(entry.aliasNameNamespaceUri, resolved);
144
+ return resolved;
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Build a handler for `FindAlias` or `FindAliasVerbose`.
150
+ *
151
+ * `this` is the Method node, so the category searched is the Method's parent —
152
+ * that is what makes one handler serve every instance of
153
+ * `AliasNameCategoryType`, including vendor subcategories created through the
154
+ * `<SubAliasNameCategories>` placeholder.
155
+ */
156
+ export function makeFindAliasHandler(options: FindAliasBindingOptions, verbose: boolean) {
157
+ const comparator = options.comparator ?? insertionOrderComparator;
158
+
159
+ return async function findAliasHandler(
160
+ this: UAMethod,
161
+ inputArguments: Variant[],
162
+ context: ISessionContext
163
+ ): Promise<CallMethodResultOptions> {
164
+ const category = this.parent as UAObject | null;
165
+ if (!category) {
166
+ return { statusCode: StatusCodes.BadInternalError };
167
+ }
168
+
169
+ // The gate is consulted per category, and each category at most once per
170
+ // call. Memoised because a recursive search reaches the same category
171
+ // through every alias it holds, and the rule may hit a database.
172
+ const gate = options.isReadAllowed;
173
+ const decisions = new Map<string, boolean>();
174
+ const mayRead = async (categoryNodeId: NodeId): Promise<boolean> => {
175
+ if (!gate) {
176
+ return true;
177
+ }
178
+ const key = categoryNodeId.toString();
179
+ const cached = decisions.get(key);
180
+ if (cached !== undefined) {
181
+ return cached;
182
+ }
183
+ const allowed = await gate(context, categoryNodeId);
184
+ decisions.set(key, allowed);
185
+ return allowed;
186
+ };
187
+
188
+ // A direct call on a category the caller may not read is
189
+ // Bad_UserAccessDenied: there is nothing left to filter, so silence
190
+ // would be a lie rather than a non-disclosure.
191
+ if (!(await mayRead(category.nodeId))) {
192
+ return { statusCode: StatusCodes.BadUserAccessDenied };
193
+ }
194
+
195
+ const pattern = readPattern(inputArguments);
196
+ if (pattern === null) {
197
+ return { statusCode: StatusCodes.BadInvalidArgument };
198
+ }
199
+
200
+ const query: AliasQuery = {
201
+ pattern,
202
+ referenceTypeFilter: readReferenceTypeFilter(inputArguments),
203
+ categoryNodeId: category.nodeId,
204
+ maxResults: options.maxResults,
205
+ // Handed the same memoised closure the filter below uses, so the
206
+ // rule is evaluated at most once per category per call however many
207
+ // times it is consulted. Passed only when a gate is configured, so
208
+ // an ungated Server takes exactly the path it took before.
209
+ isVisible: gate ? mayRead : undefined
210
+ };
211
+
212
+ let found: AliasEntry[];
213
+ try {
214
+ found = await options.store.find(query);
215
+ } catch (err) {
216
+ if (err instanceof InvalidLikePatternError) {
217
+ // clause 6.3.2 Table 4: "The input string is not a valid search string"
218
+ return { statusCode: StatusCodes.BadInvalidArgument };
219
+ }
220
+ throw err;
221
+ }
222
+
223
+ // clause 6.3.2 Table 4: too large to return, "try new filter and repeat find".
224
+ //
225
+ // Applied to what the store produced, *before* filtering and merging.
226
+ // The store stops collecting one entry past the cap, so a count that
227
+ // reaches it means "there may be more"; reducing the count first would
228
+ // report a truncated scan as a complete answer. The code names no
229
+ // category, so it discloses nothing a gated caller should not see.
230
+ if (found.length > options.maxResults) {
231
+ return { statusCode: StatusCodes.BadResponseTooLarge };
232
+ }
233
+
234
+ // A nested category the caller may not read is omitted, and the call
235
+ // still succeeds: an error, or a count that changed, would confirm the
236
+ // category exists. Absence is the only answer that discloses nothing.
237
+ //
238
+ // The store was given the same predicate and should already have skipped
239
+ // these, so this is normally a no-op. It stays as a backstop: an injected
240
+ // store written by someone else may ignore `isVisible`, and the cost of
241
+ // that must be a wasted scan, never a leak.
242
+ const visible: AliasEntry[] = [];
243
+ for (const entry of found) {
244
+ if (await mayRead(entry.categoryNodeId)) {
245
+ visible.push(entry);
246
+ }
247
+ }
248
+
249
+ // Filtering happens before the verbose/plain split, so FindAliasVerbose
250
+ // cannot reveal a ServerUri or an AliasNameCategoryId for a category
251
+ // that FindAlias would have hidden.
252
+ const results = verbose ? visible : mergeByAliasName(visible);
253
+
254
+ // sort() is stable in modern JavaScript, so a comparator that returns 0
255
+ // leaves discovery order untouched
256
+ const ordered = [...results].sort(comparator);
257
+
258
+ // No match is Good with an empty AliasNodeList (clause 6.3.2 Table 3),
259
+ // never an error.
260
+ const namespaceIndexOf = makeNamespaceResolver(category);
261
+ const value = verbose
262
+ ? ordered.map(
263
+ (entry) =>
264
+ new AliasNameVerboseDataType({
265
+ aliasName: { name: entry.aliasName, namespaceIndex: namespaceIndexOf(entry) },
266
+ referencedNodes: entry.referencedNodes,
267
+ serverUris: entry.serverUris,
268
+ aliasNameCategoryId: entry.categoryNodeId
269
+ })
270
+ )
271
+ : ordered.map(
272
+ (entry) =>
273
+ new AliasNameDataType({
274
+ aliasName: { name: entry.aliasName, namespaceIndex: namespaceIndexOf(entry) },
275
+ referencedNodes: entry.referencedNodes
276
+ })
277
+ );
278
+
279
+ return {
280
+ statusCode: StatusCodes.Good,
281
+ outputArguments: [
282
+ {
283
+ dataType: DataType.ExtensionObject,
284
+ arrayType: VariantArrayType.Array,
285
+ value
286
+ }
287
+ ]
288
+ };
289
+ };
290
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @module node-opcua-alias-name-server
3
+ *
4
+ * Server-side OPC 10000-17 (AliasNames).
5
+ *
6
+ * Publishes **this Server's own** AliasNames. Aggregating AliasNames collected
7
+ * from other Servers (Annex B, Annex C) and the Annex D PubSub change
8
+ * notification are out of scope.
9
+ */
10
+
11
+ export { type AddAliasOptions, AliasNameError, addAlias, findAlias, removeAlias } from "./add_alias.js";
12
+ export { AddressSpaceAliasStore, type AddressSpaceAliasStoreOptions } from "./address_space_alias_store.js";
13
+ export {
14
+ aliasesOf,
15
+ collectAllCategories,
16
+ collectCategories,
17
+ findAliasNameCategoryType,
18
+ findAliasNameType,
19
+ isAliasName,
20
+ isAliasNameCategory,
21
+ presentWellKnownCategories
22
+ } from "./alias_hierarchy.js";
23
+ export {
24
+ ALIAS_NAME_ARCHIVE_VERSION,
25
+ type AliasNameArchive,
26
+ readAliasNameArchive,
27
+ writeAliasNameArchive
28
+ } from "./alias_name_archive.js";
29
+ export {
30
+ type AddAliasCategoryOptions,
31
+ addAliasCategory,
32
+ type BindAliasCategoryOptions,
33
+ bindAliasCategory,
34
+ ensureLastChangeProperty,
35
+ ensureOptionalMethod,
36
+ findMethodByDeclaration,
37
+ getInstalledAliasNames,
38
+ type InstalledAliasNames,
39
+ removeAliasCategory
40
+ } from "./bind_alias_category.js";
41
+ export {
42
+ type AliasComparator,
43
+ type FindAliasBindingOptions,
44
+ insertionOrderComparator,
45
+ makeFindAliasHandler
46
+ } from "./bind_find_alias.js";
47
+ export {
48
+ ALIAS_SERVER_CAPABILITY_ID,
49
+ advertiseAliasCapability,
50
+ type CategoryProvider,
51
+ DEFAULT_MAX_RESULTS,
52
+ defaultCategoryProvider,
53
+ type InstallAliasNamesOptions,
54
+ type InstallAliasNamesResult,
55
+ type IServerForAliasNames,
56
+ installAliasNames,
57
+ installAliasNamesOnAddressSpace
58
+ } from "./install_alias_names.js";
59
+ export {
60
+ LAST_CHANGE_BROWSE_NAME,
61
+ LastChangeTracker,
62
+ type LastChangeTrackerOptions
63
+ } from "./last_change.js";
64
+ export {
65
+ ALIAS_FOR,
66
+ ALIAS_NAME_CATEGORY_TYPE,
67
+ ALIAS_NAME_TYPE,
68
+ ALIAS_SERVER_CAPABILITY,
69
+ MethodDeclarations,
70
+ PUBLISHED_DATA_SET_TYPE,
71
+ WellKnownCategories,
72
+ WellKnownLastChange,
73
+ WellKnownOptionalMethods
74
+ } from "./well_known.js";