• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

node-opcua / node-opcua / 28389643215

29 Jun 2026 05:10PM UTC coverage: 92.054%. Remained the same
28389643215

push

github

erossignon
fix(filter): return a StatusCode when a selectClause typeDefinitionId does not resolve

checkSelectClause now returns BadNodeIdUnknown when the SimpleAttributeOperand
typeDefinitionId does not reference an existing node, and BadTypeMismatch when it
references a node that is not an ObjectType, instead of dereferencing the result
of findEventType() (which returns null for an unknown NodeId).

Adds a unit test (checkSelectClauses with an unresolved typeDefinitionId) and an
end-to-end test verifying the selectClause result is reported per-clause.

18511 of 21825 branches covered (84.82%)

5 of 6 new or added lines in 1 file covered. (83.33%)

340 existing lines in 8 files now uncovered.

164590 of 178797 relevant lines covered (92.05%)

434180.06 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.74
/packages/node-opcua-server/source/validate_filter.ts
1
/**
124✔
2
 * @module node-opcua-server
2✔
3
 */
2✔
4

2✔
5
import type { BaseNode, UAVariable } from "node-opcua-address-space";
2✔
6
import { assert } from "node-opcua-assert";
2✔
7
import { DataType } from "node-opcua-basic-types";
2✔
8
import { AttributeIds, NodeClass } from "node-opcua-data-model";
2✔
9
import { make_warningLog } from "node-opcua-debug";
2✔
10
import type { ExtensionObject } from "node-opcua-extension-object";
2✔
11
import { type INodeId, NodeId, NodeIdType } from "node-opcua-nodeid";
2✔
12
import { type ContentFilter, DataChangeFilter, EventFilter, validateContentFilter } from "node-opcua-service-filter";
2✔
13
import { DeadbandType } from "node-opcua-service-subscription";
2✔
14
import { type StatusCode, StatusCodes } from "node-opcua-status-code";
2✔
15
import type { ReadValueIdOptions } from "node-opcua-types";
2✔
16

2✔
17
const warningLog = make_warningLog(__filename);
2✔
18

2✔
19
function isNumberDataType(node: UAVariable): boolean {
27✔
20
    const n = node.dataType as INodeId;
27✔
21
    if (n.namespace === 0 && n.identifierType === NodeIdType.NUMERIC && n.value < 22) {
27✔
22
        switch (n.value) {
27✔
23
            case DataType.Float:
27✔
24
            case DataType.Double:
27✔
25
            case DataType.Byte:
27✔
26
            case DataType.SByte:
27✔
27
            case DataType.Int16:
27✔
28
            case DataType.Int32:
27✔
29
            case DataType.Int64:
27✔
30
            case DataType.UInt16:
27✔
31
            case DataType.UInt32:
27✔
32
            case DataType.UInt64:
27✔
33
                return true;
20✔
34
            default:
27✔
35
                return false;
7✔
36
        }
27✔
37
    }
27✔
38
    const dataType = node.addressSpace.findDataType(node.dataType)!;
19!
39
    const dataTypeNumber = node.addressSpace.findDataType("Number")!;
×
40
    return dataType.isSubtypeOf(dataTypeNumber);
×
41
}
19✔
42

2✔
43
function __validateDataChangeFilter(filter: DataChangeFilter, itemToMonitor: ReadValueIdOptions, node: UAVariable): StatusCode {
53✔
44
    assert(itemToMonitor.attributeId === AttributeIds.Value);
53✔
45
    if (node.nodeClass !== NodeClass.Variable) {
53!
46
        return StatusCodes.BadNodeIdInvalid;
×
47
    }
×
48

53✔
49
    if (filter.deadbandType !== DeadbandType.None) {
53✔
50
        // if node is not Numerical=> DataChangeFilter
27✔
51
        assert(node.dataType instanceof NodeId);
27✔
52
        if (!isNumberDataType(node)) {
27✔
53
            return StatusCodes.BadFilterNotAllowed;
7✔
54
        }
7✔
55
    }
27✔
56

49✔
57
    if (filter.deadbandType === DeadbandType.Percent) {
53✔
58
        if (filter.deadbandValue < 0 || filter.deadbandValue > 100) {
7✔
59
            return StatusCodes.BadDeadbandFilterInvalid;
2✔
60
        }
2✔
61

7✔
62
        // node must also have a valid euRange
7✔
63
        if (!node.getChildByName("EURange", 0)) {
7✔
64
            warningLog(" node has no euRange ! Dead band Percent cannot be used on node " + node.nodeId.toString());
1✔
65
            return StatusCodes.BadMonitoredItemFilterUnsupported;
1✔
66
        }
1✔
67
    }
7✔
68
    return StatusCodes.Good;
49✔
69
}
49✔
70

2✔
71
export interface ValidateFilterOptions {
2✔
72
    // ServerCapabilities.MaxWhereClauseParameters: maximum number of whereClause operands (OPC UA Part 5)
2✔
73
    maxWhereClauseParameters?: number;
2✔
74
    // ServerCapabilities.MaxSelectClauseParameters: maximum number of selectClauses (OPC UA Part 5)
2✔
75
    maxSelectClauseParameters?: number;
2✔
76
}
2✔
77

2✔
78
function __countWhereClauseParameters(whereClause: ContentFilter): number {
27✔
79
    let count = 0;
27✔
80
    for (const element of whereClause.elements || []) {
27!
81
        count += element?.filterOperands?.length || 0;
22!
82
    }
22✔
83
    return count;
27✔
84
}
27✔
85

2✔
86
function __validateEventFilter(filter: EventFilter, options?: ValidateFilterOptions): StatusCode {
38✔
87
    // Enforce the ServerCapabilities limits on the size of the filter (OPC UA Part 5). This bounds the
38✔
88
    // amount of work the filter can require and the depth of the whereClause evaluation.
38✔
89
    const maxSelectClauseParameters = options?.maxSelectClauseParameters;
38✔
90
    if (maxSelectClauseParameters !== undefined && (filter.selectClauses?.length || 0) > maxSelectClauseParameters) {
38!
91
        return StatusCodes.BadEventFilterInvalid;
1✔
92
    }
1✔
93
    const maxWhereClauseParameters = options?.maxWhereClauseParameters;
38✔
94
    if (
38✔
95
        maxWhereClauseParameters !== undefined &&
38✔
96
        filter.whereClause &&
27✔
97
        __countWhereClauseParameters(filter.whereClause) > maxWhereClauseParameters
27✔
98
    ) {
38✔
99
        return StatusCodes.BadEventFilterInvalid;
3✔
100
    }
3✔
101

36✔
102
    // Validate the structure of the whereClause ContentFilter (operand counts, in-bounds and acyclic
36✔
103
    // ElementOperand references) up front, instead of accepting it and only resolving it to false
36✔
104
    // when an event is evaluated. See OPC UA Part 4 - 7.7.
36✔
105
    if (filter.whereClause) {
36✔
106
        return validateContentFilter(filter.whereClause);
34✔
107
    }
34✔
108
    return StatusCodes.Good;
13!
109
}
13✔
110

2✔
111
export function validateFilter(
14,661✔
112
    filter: ExtensionObject | null,
14,783✔
113
    itemToMonitor: ReadValueIdOptions,
14,783✔
114
    node: BaseNode,
14,783✔
115
    options?: ValidateFilterOptions
14,783✔
116
): StatusCode {
14,783✔
117
    // handle filter information
14,783✔
118
    if (filter && filter instanceof EventFilter && itemToMonitor.attributeId !== AttributeIds.EventNotifier) {
14,783✔
119
        // invalid filter on Event
2✔
120
        return StatusCodes.BadFilterNotAllowed;
2✔
121
    }
2✔
122

14,782✔
123
    if (filter && filter instanceof DataChangeFilter && itemToMonitor.attributeId !== AttributeIds.Value) {
14,783✔
124
        // invalid DataChange filter on non Value Attribute
7✔
125
        return StatusCodes.BadFilterNotAllowed;
7✔
126
    }
7✔
127

14,782✔
128
    if (filter && itemToMonitor.attributeId !== AttributeIds.EventNotifier && itemToMonitor.attributeId !== AttributeIds.Value) {
14,783!
UNCOV
129
        return StatusCodes.BadFilterNotAllowed;
×
UNCOV
130
    }
×
131

14,782✔
132
    if (filter instanceof DataChangeFilter) {
14,783✔
133
        return __validateDataChangeFilter(filter, itemToMonitor, node as UAVariable);
53✔
134
    }
53✔
135

14,767✔
136
    if (filter instanceof EventFilter) {
14,783✔
137
        return __validateEventFilter(filter, options);
38✔
138
    }
38✔
139

14,742✔
140
    return StatusCodes.Good;
14,742✔
141
}
14,742✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc