avo-inspector 3.2.0 → 3.2.3
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/README.md +10 -0
- package/dist/AvoBatcher.d.ts +9 -3
- package/dist/AvoInspector.d.ts +4 -0
- package/dist/AvoNetworkCallsHandler.d.ts +80 -5
- package/dist/__tests__/UnboundPublicMethods_test.d.ts +1 -0
- package/dist/__tests__/constants.d.ts +2 -1
- package/dist/__tests__/helpers/internalGateway.d.ts +23 -0
- package/dist/eventSpec/AvoEventSpecCache.d.ts +7 -4
- package/dist/eventSpec/AvoEventSpecFetchTypes.d.ts +12 -1
- package/dist/eventSpec/AvoEventSpecFetcher.d.ts +6 -1
- package/dist/index.js +1 -1
- package/dist/lite/AvoBatcherLite.d.ts +9 -3
- package/dist/lite/AvoBatcherLite.js +46 -7
- package/dist/lite/AvoInspectorLite.d.ts +2 -0
- package/dist/lite/AvoInspectorLite.js +95 -15
- package/dist/lite/AvoNetworkCallsHandlerLite.d.ts +79 -5
- package/dist/lite/AvoNetworkCallsHandlerLite.js +300 -20
- package/package.json +10 -1
- package/dist/AvoBatcher.js +0 -87
- package/dist/AvoDeduplicator.js +0 -142
- package/dist/AvoEncryption.js +0 -176
- package/dist/AvoInspector.js +0 -435
- package/dist/AvoNetworkCallsHandler.js +0 -229
- package/dist/AvoSchemaParser.js +0 -95
- package/dist/AvoStreamId.js +0 -82
- package/dist/__mocks__/xhr.js +0 -14
- package/dist/__mocks__/xhrError.js +0 -14
- package/dist/__tests__/AvoEncryption.js +0 -272
- package/dist/__tests__/AvoEncryptionIntegration.js +0 -191
- package/dist/__tests__/AvoEventSpecFetcher.js +0 -377
- package/dist/__tests__/AvoInspector.js +0 -335
- package/dist/__tests__/Batcher.js +0 -214
- package/dist/__tests__/BatcherPublic.js +0 -139
- package/dist/__tests__/Deduplicator.js +0 -190
- package/dist/__tests__/EventSpecCache.js +0 -143
- package/dist/__tests__/EventValidator.js +0 -543
- package/dist/__tests__/InstallationId.js +0 -60
- package/dist/__tests__/NetworkCallsHandler.js +0 -222
- package/dist/__tests__/NetworkCallsHandlerModelA.js +0 -206
- package/dist/__tests__/Parsing.js +0 -115
- package/dist/__tests__/Sessions.d.ts +0 -1
- package/dist/__tests__/Sessions.js +0 -10
- package/dist/__tests__/Storage.d.ts +0 -1
- package/dist/__tests__/Storage.js +0 -173
- package/dist/__tests__/StreamId.d.ts +0 -1
- package/dist/__tests__/StreamId.js +0 -153
- package/dist/__tests__/constants.js +0 -48
- package/dist/esm/AvoBatcher.js +0 -75
- package/dist/esm/AvoDeduplicator.js +0 -137
- package/dist/esm/AvoEncryption.js +0 -277
- package/dist/esm/AvoGuid.js +0 -9
- package/dist/esm/AvoInspector.js +0 -470
- package/dist/esm/AvoInspectorEnv.js +0 -5
- package/dist/esm/AvoNetworkCallsHandler.js +0 -181
- package/dist/esm/AvoSchemaParser.js +0 -128
- package/dist/esm/AvoStorage.js +0 -176
- package/dist/esm/AvoStreamId.js +0 -140
- package/dist/esm/eventSpec/AvoEventSpecCache.js +0 -165
- package/dist/esm/eventSpec/AvoEventSpecFetchTypes.js +0 -6
- package/dist/esm/eventSpec/AvoEventSpecFetcher.js +0 -203
- package/dist/esm/eventSpec/EventValidator.js +0 -687
- package/dist/esm/eventSpec/index.js +0 -4
- package/dist/esm/index.js +0 -3
- package/dist/esm/utils.js +0 -38
- package/dist/esm/version.js +0 -2
- package/dist/eventSpec/AvoEventSpecCache.js +0 -181
- package/dist/eventSpec/AvoEventSpecFetcher.js +0 -290
- package/dist/eventSpec/EventValidator.js +0 -846
- package/dist/eventSpec/index.js +0 -20
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -5
- /package/dist/__tests__/{AvoEncryption.d.ts → ApiKeyHeaderInjection_test.d.ts} +0 -0
- /package/dist/__tests__/{AvoEncryptionIntegration.d.ts → BrowserBootstrap_test.d.ts} +0 -0
- /package/dist/__tests__/{AvoEventSpecFetcher.d.ts → EventSpecGatewayCoordinates_test.d.ts} +0 -0
- /package/dist/__tests__/{AvoInspector.d.ts → InFlightQueueGaps_test.d.ts} +0 -0
- /package/dist/__tests__/{Batcher.d.ts → InFlightQueue_test.d.ts} +0 -0
- /package/dist/__tests__/{BatcherPublic.d.ts → NetworkCallsHandlerLite_test.d.ts} +0 -0
- /package/dist/__tests__/{Deduplicator.d.ts → PublicSurface_test.d.ts} +0 -0
- /package/dist/__tests__/{EventSpecCache.d.ts → TrackOptionsIntegration_test.d.ts} +0 -0
- /package/dist/__tests__/{EventValidator.d.ts → TrackOptionsTransport_test.d.ts} +0 -0
- /package/dist/__tests__/{InstallationId.d.ts → TrackingHeaders_test.d.ts} +0 -0
- /package/dist/__tests__/{NetworkCallsHandler.d.ts → V1WireBaseline_test.d.ts} +0 -0
- /package/dist/__tests__/{NetworkCallsHandlerModelA.d.ts → V2QueuedAttribution_test.d.ts} +0 -0
- /package/dist/__tests__/{Parsing.d.ts → V2StopSending_test.d.ts} +0 -0
|
@@ -1,687 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* EventValidator - Client-side validation of tracking events against the Avo Tracking Plan.
|
|
3
|
-
*
|
|
4
|
-
* This module validates property values against constraints:
|
|
5
|
-
* - Pinned values (exact match required)
|
|
6
|
-
* - Allowed values (must be in list)
|
|
7
|
-
* - Regex patterns (must match pattern)
|
|
8
|
-
* - Min/max ranges (numeric values must be in range)
|
|
9
|
-
*
|
|
10
|
-
* No schema validation (types/required) is performed - only value constraints.
|
|
11
|
-
* Validation runs against ALL events/variants in the response.
|
|
12
|
-
*/
|
|
13
|
-
import safe from 'safe-regex2';
|
|
14
|
-
// =============================================================================
|
|
15
|
-
// HELPER FUNCTIONS FOR NESTED PROPERTIES
|
|
16
|
-
// =============================================================================
|
|
17
|
-
/**
|
|
18
|
-
* Deep copies a constraint mapping (pinnedValues, allowedValues, etc.),
|
|
19
|
-
* including the arrays inside to avoid shared references.
|
|
20
|
-
*/
|
|
21
|
-
function deepCopyConstraintMapping(mapping) {
|
|
22
|
-
const result = {};
|
|
23
|
-
for (const [key, arr] of Object.entries(mapping)) {
|
|
24
|
-
result[key] = [...arr];
|
|
25
|
-
}
|
|
26
|
-
return result;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Deep copies children constraints recursively.
|
|
30
|
-
*/
|
|
31
|
-
function deepCopyChildren(children) {
|
|
32
|
-
const result = {};
|
|
33
|
-
for (const [propName, constraints] of Object.entries(children)) {
|
|
34
|
-
result[propName] = {
|
|
35
|
-
type: constraints.type,
|
|
36
|
-
required: constraints.required,
|
|
37
|
-
pinnedValues: constraints.pinnedValues
|
|
38
|
-
? deepCopyConstraintMapping(constraints.pinnedValues)
|
|
39
|
-
: undefined,
|
|
40
|
-
allowedValues: constraints.allowedValues
|
|
41
|
-
? deepCopyConstraintMapping(constraints.allowedValues)
|
|
42
|
-
: undefined,
|
|
43
|
-
regexPatterns: constraints.regexPatterns
|
|
44
|
-
? deepCopyConstraintMapping(constraints.regexPatterns)
|
|
45
|
-
: undefined,
|
|
46
|
-
minMaxRanges: constraints.minMaxRanges
|
|
47
|
-
? deepCopyConstraintMapping(constraints.minMaxRanges)
|
|
48
|
-
: undefined,
|
|
49
|
-
children: constraints.children
|
|
50
|
-
? deepCopyChildren(constraints.children)
|
|
51
|
-
: undefined
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
return result;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Merges children constraints from source into target recursively.
|
|
58
|
-
*/
|
|
59
|
-
function mergeChildren(target, source) {
|
|
60
|
-
for (const [propName, sourceConstraints] of Object.entries(source)) {
|
|
61
|
-
if (!target[propName]) {
|
|
62
|
-
// New child property - deep copy it
|
|
63
|
-
target[propName] = {
|
|
64
|
-
type: sourceConstraints.type,
|
|
65
|
-
required: sourceConstraints.required,
|
|
66
|
-
pinnedValues: sourceConstraints.pinnedValues
|
|
67
|
-
? deepCopyConstraintMapping(sourceConstraints.pinnedValues)
|
|
68
|
-
: undefined,
|
|
69
|
-
allowedValues: sourceConstraints.allowedValues
|
|
70
|
-
? deepCopyConstraintMapping(sourceConstraints.allowedValues)
|
|
71
|
-
: undefined,
|
|
72
|
-
regexPatterns: sourceConstraints.regexPatterns
|
|
73
|
-
? deepCopyConstraintMapping(sourceConstraints.regexPatterns)
|
|
74
|
-
: undefined,
|
|
75
|
-
minMaxRanges: sourceConstraints.minMaxRanges
|
|
76
|
-
? deepCopyConstraintMapping(sourceConstraints.minMaxRanges)
|
|
77
|
-
: undefined,
|
|
78
|
-
children: sourceConstraints.children
|
|
79
|
-
? deepCopyChildren(sourceConstraints.children)
|
|
80
|
-
: undefined
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
// Merge into existing child property
|
|
85
|
-
const targetConstraints = target[propName];
|
|
86
|
-
mergeConstraintMappings(targetConstraints, sourceConstraints);
|
|
87
|
-
// Recursively merge nested children
|
|
88
|
-
if (sourceConstraints.children) {
|
|
89
|
-
if (!targetConstraints.children) {
|
|
90
|
-
targetConstraints.children = deepCopyChildren(sourceConstraints.children);
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
mergeChildren(targetConstraints.children, sourceConstraints.children);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Merges constraint mappings (pinnedValues, allowedValues, etc.) from source into target.
|
|
101
|
-
*/
|
|
102
|
-
function mergeConstraintMappings(target, source) {
|
|
103
|
-
if (source.pinnedValues) {
|
|
104
|
-
if (!target.pinnedValues) {
|
|
105
|
-
target.pinnedValues = {};
|
|
106
|
-
}
|
|
107
|
-
for (const key of Object.keys(source.pinnedValues)) {
|
|
108
|
-
if (target.pinnedValues[key]) {
|
|
109
|
-
const merged = new Set(target.pinnedValues[key]);
|
|
110
|
-
for (const id of source.pinnedValues[key]) {
|
|
111
|
-
merged.add(id);
|
|
112
|
-
}
|
|
113
|
-
target.pinnedValues[key] = Array.from(merged);
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
target.pinnedValues[key] = [...source.pinnedValues[key]];
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (source.allowedValues) {
|
|
121
|
-
if (!target.allowedValues) {
|
|
122
|
-
target.allowedValues = {};
|
|
123
|
-
}
|
|
124
|
-
for (const key of Object.keys(source.allowedValues)) {
|
|
125
|
-
if (target.allowedValues[key]) {
|
|
126
|
-
const merged = new Set(target.allowedValues[key]);
|
|
127
|
-
for (const id of source.allowedValues[key]) {
|
|
128
|
-
merged.add(id);
|
|
129
|
-
}
|
|
130
|
-
target.allowedValues[key] = Array.from(merged);
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
target.allowedValues[key] = [...source.allowedValues[key]];
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
if (source.regexPatterns) {
|
|
138
|
-
if (!target.regexPatterns) {
|
|
139
|
-
target.regexPatterns = {};
|
|
140
|
-
}
|
|
141
|
-
for (const key of Object.keys(source.regexPatterns)) {
|
|
142
|
-
if (target.regexPatterns[key]) {
|
|
143
|
-
const merged = new Set(target.regexPatterns[key]);
|
|
144
|
-
for (const id of source.regexPatterns[key]) {
|
|
145
|
-
merged.add(id);
|
|
146
|
-
}
|
|
147
|
-
target.regexPatterns[key] = Array.from(merged);
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
target.regexPatterns[key] = [...source.regexPatterns[key]];
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
if (source.minMaxRanges) {
|
|
155
|
-
if (!target.minMaxRanges) {
|
|
156
|
-
target.minMaxRanges = {};
|
|
157
|
-
}
|
|
158
|
-
for (const key of Object.keys(source.minMaxRanges)) {
|
|
159
|
-
if (target.minMaxRanges[key]) {
|
|
160
|
-
const merged = new Set(target.minMaxRanges[key]);
|
|
161
|
-
for (const id of source.minMaxRanges[key]) {
|
|
162
|
-
merged.add(id);
|
|
163
|
-
}
|
|
164
|
-
target.minMaxRanges[key] = Array.from(merged);
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
target.minMaxRanges[key] = [...source.minMaxRanges[key]];
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
// =============================================================================
|
|
173
|
-
// CACHES
|
|
174
|
-
// =============================================================================
|
|
175
|
-
/**
|
|
176
|
-
* Cache for compiled regex objects to avoid recompilation on every event.
|
|
177
|
-
* Patterns are expected to be stable per session.
|
|
178
|
-
* null values indicate patterns that were rejected (unsafe or invalid).
|
|
179
|
-
*/
|
|
180
|
-
const regexCache = new Map();
|
|
181
|
-
/**
|
|
182
|
-
* Gets a compiled regex from cache or compiles and caches it.
|
|
183
|
-
* Validates patterns with safe-regex2 before compilation to prevent ReDoS.
|
|
184
|
-
* Returns null if the pattern is unsafe or invalid.
|
|
185
|
-
* Null results are cached to avoid retrying bad patterns.
|
|
186
|
-
*/
|
|
187
|
-
function getOrCompileRegex(pattern) {
|
|
188
|
-
if (regexCache.has(pattern)) {
|
|
189
|
-
return regexCache.get(pattern);
|
|
190
|
-
}
|
|
191
|
-
if (!safe(pattern)) {
|
|
192
|
-
console.warn(`[Avo Inspector] Potentially unsafe regex pattern rejected, skipping constraint: ${pattern}`);
|
|
193
|
-
regexCache.set(pattern, null);
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
try {
|
|
197
|
-
const regex = new RegExp(pattern);
|
|
198
|
-
regexCache.set(pattern, regex);
|
|
199
|
-
return regex;
|
|
200
|
-
}
|
|
201
|
-
catch (e) {
|
|
202
|
-
console.warn(`[Avo Inspector] Invalid regex pattern, skipping constraint: ${pattern}`);
|
|
203
|
-
regexCache.set(pattern, null);
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Cache for parsed allowed values JSON.
|
|
209
|
-
* Key: JSON string, Value: Set of allowed values for O(1) lookup.
|
|
210
|
-
*/
|
|
211
|
-
const allowedValuesCache = new Map();
|
|
212
|
-
/**
|
|
213
|
-
* Parses allowed values JSON string and returns a Set for O(1) lookup.
|
|
214
|
-
* Results are cached to avoid repeated JSON.parse calls.
|
|
215
|
-
* @returns Set of allowed values, or null if JSON is invalid
|
|
216
|
-
*/
|
|
217
|
-
function getOrParseAllowedValues(jsonString) {
|
|
218
|
-
let allowedSet = allowedValuesCache.get(jsonString);
|
|
219
|
-
if (!allowedSet) {
|
|
220
|
-
try {
|
|
221
|
-
const allowedArray = JSON.parse(jsonString);
|
|
222
|
-
allowedSet = new Set(allowedArray);
|
|
223
|
-
allowedValuesCache.set(jsonString, allowedSet);
|
|
224
|
-
}
|
|
225
|
-
catch (e) {
|
|
226
|
-
// Invalid JSON - return null
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return allowedSet;
|
|
231
|
-
}
|
|
232
|
-
// =============================================================================
|
|
233
|
-
// MAIN VALIDATION FUNCTION
|
|
234
|
-
// =============================================================================
|
|
235
|
-
/**
|
|
236
|
-
* Validates runtime properties against all events in the EventSpecResponse.
|
|
237
|
-
*
|
|
238
|
-
* For each property:
|
|
239
|
-
* - If property not in spec: no validation needed (empty result)
|
|
240
|
-
* - If property in spec: check constraints and collect failed/passed eventIds
|
|
241
|
-
* - Return whichever list is smaller for bandwidth optimization
|
|
242
|
-
*
|
|
243
|
-
* @param properties - The properties observed at runtime
|
|
244
|
-
* @param specResponse - The EventSpecResponse from the backend
|
|
245
|
-
* @returns ValidationResult with baseEventId, metadata, and per-property results
|
|
246
|
-
*/
|
|
247
|
-
export function validateEvent(properties, specResponse) {
|
|
248
|
-
// Collect all eventIds from all events
|
|
249
|
-
const allEventIds = collectAllEventIds(specResponse.events);
|
|
250
|
-
// Build lookup table: propertyName -> constraints (with embedded eventId mappings)
|
|
251
|
-
const constraintsByProperty = collectConstraintsByPropertyName(specResponse.events);
|
|
252
|
-
// Validate each runtime property against its constraints
|
|
253
|
-
const propertyResults = {};
|
|
254
|
-
for (const propName of Object.keys(properties)) {
|
|
255
|
-
const value = properties[propName];
|
|
256
|
-
const constraints = constraintsByProperty[propName];
|
|
257
|
-
if (!constraints) {
|
|
258
|
-
// Property not in spec - no constraints to fail
|
|
259
|
-
propertyResults[propName] = {};
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
const result = validatePropertyConstraints(value, constraints, allEventIds);
|
|
263
|
-
propertyResults[propName] = result;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return {
|
|
267
|
-
metadata: specResponse.metadata,
|
|
268
|
-
propertyResults
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
// =============================================================================
|
|
272
|
-
// HELPER FUNCTIONS
|
|
273
|
-
// =============================================================================
|
|
274
|
-
/**
|
|
275
|
-
* Collects all eventIds (baseEventId + variantIds) from all events.
|
|
276
|
-
*/
|
|
277
|
-
function collectAllEventIds(events) {
|
|
278
|
-
const ids = [];
|
|
279
|
-
for (const event of events) {
|
|
280
|
-
ids.push(event.baseEventId);
|
|
281
|
-
for (let j = 0; j < event.variantIds.length; j++) {
|
|
282
|
-
ids.push(event.variantIds[j]);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
return ids;
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* Collects all property constraints from all events into a single lookup table.
|
|
289
|
-
*
|
|
290
|
-
* This is purely for lookup efficiency - each constraint already contains
|
|
291
|
-
* its applicable eventIds, so no conflict resolution is needed.
|
|
292
|
-
*
|
|
293
|
-
* Example:
|
|
294
|
-
* Event1.props.method = { pinnedValues: { "email": ["evt_1"] } }
|
|
295
|
-
* Event2.props.method = { pinnedValues: { "phone": ["evt_2"] } }
|
|
296
|
-
*
|
|
297
|
-
* Result: { method: { pinnedValues: { "email": ["evt_1"], "phone": ["evt_2"] } } }
|
|
298
|
-
*/
|
|
299
|
-
function collectConstraintsByPropertyName(events) {
|
|
300
|
-
// Fast path: no events
|
|
301
|
-
if (events.length === 0) {
|
|
302
|
-
return {};
|
|
303
|
-
}
|
|
304
|
-
// Fast path: single event, return props directly (no aggregation needed)
|
|
305
|
-
if (events.length === 1) {
|
|
306
|
-
return events[0].props;
|
|
307
|
-
}
|
|
308
|
-
// Multiple events: aggregate constraints from all events
|
|
309
|
-
const result = {};
|
|
310
|
-
for (const event of events) {
|
|
311
|
-
for (const [propName, constraints] of Object.entries(event.props)) {
|
|
312
|
-
if (!result[propName]) {
|
|
313
|
-
// First time seeing this property - initialize with copies
|
|
314
|
-
result[propName] = {
|
|
315
|
-
type: constraints.type,
|
|
316
|
-
required: constraints.required,
|
|
317
|
-
pinnedValues: constraints.pinnedValues
|
|
318
|
-
? { ...constraints.pinnedValues }
|
|
319
|
-
: undefined,
|
|
320
|
-
allowedValues: constraints.allowedValues
|
|
321
|
-
? { ...constraints.allowedValues }
|
|
322
|
-
: undefined,
|
|
323
|
-
regexPatterns: constraints.regexPatterns
|
|
324
|
-
? { ...constraints.regexPatterns }
|
|
325
|
-
: undefined,
|
|
326
|
-
minMaxRanges: constraints.minMaxRanges
|
|
327
|
-
? { ...constraints.minMaxRanges }
|
|
328
|
-
: undefined,
|
|
329
|
-
children: constraints.children
|
|
330
|
-
? deepCopyChildren(constraints.children)
|
|
331
|
-
: undefined
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
// Aggregate constraint mappings from additional events
|
|
336
|
-
const existing = result[propName];
|
|
337
|
-
mergeConstraintMappings(existing, constraints);
|
|
338
|
-
// Recursively merge nested children
|
|
339
|
-
if (constraints.children) {
|
|
340
|
-
if (!existing.children) {
|
|
341
|
-
existing.children = deepCopyChildren(constraints.children);
|
|
342
|
-
}
|
|
343
|
-
else {
|
|
344
|
-
mergeChildren(existing.children, constraints.children);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
return result;
|
|
351
|
-
}
|
|
352
|
-
/**
|
|
353
|
-
* Maximum nesting depth for recursive value validation.
|
|
354
|
-
* We validate prop (depth 0), prop.child1 (depth 1), but NOT prop.child1.child2 (depth 2+).
|
|
355
|
-
* At depth 2+, we know there's an object but don't dive into its children for value validation.
|
|
356
|
-
* This matches the behavior of schema validation.
|
|
357
|
-
*/
|
|
358
|
-
const MAX_CHILD_DEPTH = 2;
|
|
359
|
-
/**
|
|
360
|
-
* Validates a property value against its constraints.
|
|
361
|
-
* Returns the validation result with either failedEventIds or passedEventIds
|
|
362
|
-
* (whichever is smaller for bandwidth optimization).
|
|
363
|
-
*
|
|
364
|
-
* For object properties with children:
|
|
365
|
-
* - Skip value-level validation (pinned/allowed/regex/minmax)
|
|
366
|
-
* - Recursively validate child properties (up to MAX_CHILD_DEPTH)
|
|
367
|
-
*
|
|
368
|
-
* For list properties (isList=true):
|
|
369
|
-
* - If list of objects with children: validate each array item's children
|
|
370
|
-
* - If list of primitives: validate each array item against constraints
|
|
371
|
-
*
|
|
372
|
-
* Depth limiting: We validate prop (depth 0), prop.child1 (depth 1), but stop
|
|
373
|
-
* at prop.child1.child2 (depth 2+). This matches schema validation behavior.
|
|
374
|
-
*
|
|
375
|
-
* @param depth - Current recursion depth (internal use)
|
|
376
|
-
*/
|
|
377
|
-
function validatePropertyConstraints(value, constraints, allEventIds, depth = 0) {
|
|
378
|
-
const result = {};
|
|
379
|
-
// Stop recursion at depth 2+ - we don't validate child2 and deeper
|
|
380
|
-
// This matches schema validation which shows prop.child1.child2: object without diving in
|
|
381
|
-
if (depth >= MAX_CHILD_DEPTH) {
|
|
382
|
-
return result;
|
|
383
|
-
}
|
|
384
|
-
// Handle list types (isList=true)
|
|
385
|
-
if (constraints.isList) {
|
|
386
|
-
return validateListProperty(value, constraints, allEventIds, depth);
|
|
387
|
-
}
|
|
388
|
-
// Handle nested object properties with children (single object, not list)
|
|
389
|
-
if (constraints.children) {
|
|
390
|
-
return validateObjectProperty(value, constraints, allEventIds, depth);
|
|
391
|
-
}
|
|
392
|
-
// For primitive properties only: skip validation for null/undefined on non-required properties.
|
|
393
|
-
// For optional properties, null/undefined means "not sent" which is valid.
|
|
394
|
-
// Only required properties should fail validation when value is null/undefined.
|
|
395
|
-
// Note: We check this AFTER checking for children, because object/list properties
|
|
396
|
-
// need to validate their children even when the parent value is null/undefined.
|
|
397
|
-
if ((value === null || value === undefined) && !constraints.required) {
|
|
398
|
-
return result;
|
|
399
|
-
}
|
|
400
|
-
// Validate value constraints for primitive properties
|
|
401
|
-
return validatePrimitiveProperty(value, constraints, allEventIds);
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Validates a list property (array of items).
|
|
405
|
-
* For list of objects: validates each item's children.
|
|
406
|
-
* For list of primitives: validates each item against constraints.
|
|
407
|
-
* Any item failing causes the eventId to fail for that constraint.
|
|
408
|
-
*/
|
|
409
|
-
function validateListProperty(value, constraints, allEventIds, depth) {
|
|
410
|
-
const result = {};
|
|
411
|
-
// If value is not an array, we can't validate list items
|
|
412
|
-
if (!Array.isArray(value)) {
|
|
413
|
-
// Non-array value for a list property - return empty result (type mismatch not validated here)
|
|
414
|
-
return result;
|
|
415
|
-
}
|
|
416
|
-
// List of objects with children
|
|
417
|
-
if (constraints.children) {
|
|
418
|
-
const childrenResults = {};
|
|
419
|
-
// For each child property, collect failures across ALL array items
|
|
420
|
-
for (const [childName, childConstraints] of Object.entries(constraints.children)) {
|
|
421
|
-
const aggregatedFailedIds = new Set();
|
|
422
|
-
// Validate this child property in each array item
|
|
423
|
-
for (const item of value) {
|
|
424
|
-
const itemObj = (typeof item === "object" && item !== null && !Array.isArray(item))
|
|
425
|
-
? item
|
|
426
|
-
: {};
|
|
427
|
-
const childValue = itemObj[childName];
|
|
428
|
-
const childResult = validatePropertyConstraints(childValue, childConstraints, allEventIds, depth + 1);
|
|
429
|
-
// Collect failed IDs from this item
|
|
430
|
-
if (childResult.failedEventIds) {
|
|
431
|
-
for (const id of childResult.failedEventIds) {
|
|
432
|
-
aggregatedFailedIds.add(id);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
// If passedEventIds is returned, failed = allEventIds - passedEventIds
|
|
436
|
-
// Use Set for O(1) lookup instead of O(n) .includes()
|
|
437
|
-
if (childResult.passedEventIds) {
|
|
438
|
-
const passedSet = new Set(childResult.passedEventIds);
|
|
439
|
-
for (const id of allEventIds) {
|
|
440
|
-
if (!passedSet.has(id)) {
|
|
441
|
-
aggregatedFailedIds.add(id);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
// Build result for this child property
|
|
447
|
-
if (aggregatedFailedIds.size > 0) {
|
|
448
|
-
const failedArray = Array.from(aggregatedFailedIds);
|
|
449
|
-
const passedIds = allEventIds.filter((id) => !aggregatedFailedIds.has(id));
|
|
450
|
-
if (passedIds.length < failedArray.length && passedIds.length > 0) {
|
|
451
|
-
childrenResults[childName] = { passedEventIds: passedIds };
|
|
452
|
-
}
|
|
453
|
-
else {
|
|
454
|
-
childrenResults[childName] = { failedEventIds: failedArray };
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
if (Object.keys(childrenResults).length > 0) {
|
|
459
|
-
result.children = childrenResults;
|
|
460
|
-
}
|
|
461
|
-
return result;
|
|
462
|
-
}
|
|
463
|
-
// List of primitives - validate each item against constraints
|
|
464
|
-
const failedIds = new Set();
|
|
465
|
-
for (const item of value) {
|
|
466
|
-
// Check pinned values for this item
|
|
467
|
-
if (constraints.pinnedValues) {
|
|
468
|
-
checkPinnedValues(item, constraints.pinnedValues, failedIds);
|
|
469
|
-
}
|
|
470
|
-
// Check allowed values for this item
|
|
471
|
-
if (constraints.allowedValues) {
|
|
472
|
-
checkAllowedValues(item, constraints.allowedValues, failedIds);
|
|
473
|
-
}
|
|
474
|
-
// Check regex patterns for this item
|
|
475
|
-
if (constraints.regexPatterns) {
|
|
476
|
-
checkRegexPatterns(item, constraints.regexPatterns, failedIds);
|
|
477
|
-
}
|
|
478
|
-
// Check min/max ranges for this item
|
|
479
|
-
if (constraints.minMaxRanges) {
|
|
480
|
-
checkMinMaxRanges(item, constraints.minMaxRanges, failedIds);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
return buildValidationResult(failedIds, allEventIds);
|
|
484
|
-
}
|
|
485
|
-
/**
|
|
486
|
-
* Validates an object property (single object with children).
|
|
487
|
-
*/
|
|
488
|
-
function validateObjectProperty(value, constraints, allEventIds, depth) {
|
|
489
|
-
const result = {};
|
|
490
|
-
const childrenResults = {};
|
|
491
|
-
const valueObj = (typeof value === "object" && value !== null && !Array.isArray(value))
|
|
492
|
-
? value
|
|
493
|
-
: {};
|
|
494
|
-
for (const [childName, childConstraints] of Object.entries(constraints.children)) {
|
|
495
|
-
const childValue = valueObj[childName];
|
|
496
|
-
const childResult = validatePropertyConstraints(childValue, childConstraints, allEventIds, depth + 1);
|
|
497
|
-
// Only include non-empty results
|
|
498
|
-
if (childResult.failedEventIds || childResult.passedEventIds || childResult.children) {
|
|
499
|
-
childrenResults[childName] = childResult;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
if (Object.keys(childrenResults).length > 0) {
|
|
503
|
-
result.children = childrenResults;
|
|
504
|
-
}
|
|
505
|
-
return result;
|
|
506
|
-
}
|
|
507
|
-
/**
|
|
508
|
-
* Validates a primitive property (not list, not object with children).
|
|
509
|
-
*/
|
|
510
|
-
function validatePrimitiveProperty(value, constraints, allEventIds) {
|
|
511
|
-
const failedIds = new Set();
|
|
512
|
-
// Check pinned values
|
|
513
|
-
if (constraints.pinnedValues) {
|
|
514
|
-
checkPinnedValues(value, constraints.pinnedValues, failedIds);
|
|
515
|
-
}
|
|
516
|
-
// Check allowed values
|
|
517
|
-
if (constraints.allowedValues) {
|
|
518
|
-
checkAllowedValues(value, constraints.allowedValues, failedIds);
|
|
519
|
-
}
|
|
520
|
-
// Check regex patterns
|
|
521
|
-
if (constraints.regexPatterns) {
|
|
522
|
-
checkRegexPatterns(value, constraints.regexPatterns, failedIds);
|
|
523
|
-
}
|
|
524
|
-
// Check min/max ranges
|
|
525
|
-
if (constraints.minMaxRanges) {
|
|
526
|
-
checkMinMaxRanges(value, constraints.minMaxRanges, failedIds);
|
|
527
|
-
}
|
|
528
|
-
return buildValidationResult(failedIds, allEventIds);
|
|
529
|
-
}
|
|
530
|
-
/**
|
|
531
|
-
* Builds the validation result from failed IDs, returning whichever list is smaller.
|
|
532
|
-
*/
|
|
533
|
-
function buildValidationResult(failedIds, allEventIds) {
|
|
534
|
-
const passedIds = allEventIds.filter((id) => !failedIds.has(id));
|
|
535
|
-
const failedArray = Array.from(failedIds);
|
|
536
|
-
// Return whichever list is smaller for bandwidth optimization
|
|
537
|
-
// If both are empty, return empty object (no constraints)
|
|
538
|
-
if (failedArray.length === 0 && passedIds.length === 0) {
|
|
539
|
-
return {};
|
|
540
|
-
}
|
|
541
|
-
// Prefer passedEventIds only when strictly smaller than failedEventIds
|
|
542
|
-
// When equal, prefer failedEventIds (more intuitive to see what failed)
|
|
543
|
-
if (passedIds.length < failedArray.length && passedIds.length > 0) {
|
|
544
|
-
return { passedEventIds: passedIds };
|
|
545
|
-
}
|
|
546
|
-
else if (failedArray.length > 0) {
|
|
547
|
-
return { failedEventIds: failedArray };
|
|
548
|
-
}
|
|
549
|
-
else {
|
|
550
|
-
return {};
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
// =============================================================================
|
|
554
|
-
// CONSTRAINT VALIDATION FUNCTIONS
|
|
555
|
-
// =============================================================================
|
|
556
|
-
/**
|
|
557
|
-
* Adds all IDs from the array to the set.
|
|
558
|
-
* Helper to reduce code duplication in constraint check functions.
|
|
559
|
-
*/
|
|
560
|
-
function addIdsToSet(ids, set) {
|
|
561
|
-
for (const id of ids) {
|
|
562
|
-
set.add(id);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
/**
|
|
566
|
-
* Converts runtime value to string for comparison.
|
|
567
|
-
* - Primitives (null, undefined, boolean, number, string) -> String(value)
|
|
568
|
-
* - Objects/Arrays -> JSON.stringify(value)
|
|
569
|
-
*/
|
|
570
|
-
function convertValueToString(value) {
|
|
571
|
-
if (value === null ||
|
|
572
|
-
value === undefined ||
|
|
573
|
-
typeof value === "boolean" ||
|
|
574
|
-
typeof value === "number" ||
|
|
575
|
-
typeof value === "string") {
|
|
576
|
-
return String(value);
|
|
577
|
-
}
|
|
578
|
-
if (Array.isArray(value) || typeof value === "object") {
|
|
579
|
-
try {
|
|
580
|
-
return JSON.stringify(value);
|
|
581
|
-
}
|
|
582
|
-
catch (e) {
|
|
583
|
-
// Circular reference or other serialization error
|
|
584
|
-
console.warn(`[Avo Inspector] Failed to stringify value: ${e}`);
|
|
585
|
-
return String(value);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
return String(value);
|
|
589
|
-
}
|
|
590
|
-
/**
|
|
591
|
-
* Checks pinned values constraint.
|
|
592
|
-
* For each pinnedValue -> eventIds entry, if runtime value !== pinnedValue, those eventIds FAIL.
|
|
593
|
-
*/
|
|
594
|
-
function checkPinnedValues(value, pinnedValues, failedIds) {
|
|
595
|
-
const stringValue = convertValueToString(value);
|
|
596
|
-
for (const [pinnedValue, eventIds] of Object.entries(pinnedValues)) {
|
|
597
|
-
if (stringValue !== pinnedValue) {
|
|
598
|
-
// Value doesn't match this pinned value, so these eventIds fail
|
|
599
|
-
addIdsToSet(eventIds, failedIds);
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
/**
|
|
604
|
-
* Checks allowed values constraint.
|
|
605
|
-
* For each "[...array]" -> eventIds entry, if runtime value NOT in array, those eventIds FAIL.
|
|
606
|
-
* Uses cached Set for O(1) lookup instead of O(n) .includes().
|
|
607
|
-
*/
|
|
608
|
-
function checkAllowedValues(value, allowedValues, failedIds) {
|
|
609
|
-
const stringValue = convertValueToString(value);
|
|
610
|
-
for (const [allowedArrayJson, eventIds] of Object.entries(allowedValues)) {
|
|
611
|
-
const allowedSet = getOrParseAllowedValues(allowedArrayJson);
|
|
612
|
-
if (allowedSet === null) {
|
|
613
|
-
// Invalid JSON - skip this constraint
|
|
614
|
-
console.warn(`[Avo Inspector] Invalid allowed values JSON: ${allowedArrayJson}`);
|
|
615
|
-
continue;
|
|
616
|
-
}
|
|
617
|
-
if (!allowedSet.has(stringValue)) {
|
|
618
|
-
// Value not in allowed list, so these eventIds fail
|
|
619
|
-
addIdsToSet(eventIds, failedIds);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
/**
|
|
624
|
-
* Checks regex pattern constraint.
|
|
625
|
-
* For each pattern -> eventIds entry, if runtime value doesn't match pattern, those eventIds FAIL.
|
|
626
|
-
*/
|
|
627
|
-
function checkRegexPatterns(value, regexPatterns, failedIds) {
|
|
628
|
-
// Only check regex for string values
|
|
629
|
-
if (typeof value !== "string") {
|
|
630
|
-
// Non-string values fail all regex constraints
|
|
631
|
-
for (const eventIds of Object.values(regexPatterns)) {
|
|
632
|
-
addIdsToSet(eventIds, failedIds);
|
|
633
|
-
}
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
for (const [pattern, eventIds] of Object.entries(regexPatterns)) {
|
|
637
|
-
const regex = getOrCompileRegex(pattern);
|
|
638
|
-
if (regex === null) {
|
|
639
|
-
// Unsafe or invalid pattern - skip constraint (fail-open)
|
|
640
|
-
continue;
|
|
641
|
-
}
|
|
642
|
-
if (!regex.test(value)) {
|
|
643
|
-
// Value doesn't match pattern, so these eventIds fail
|
|
644
|
-
addIdsToSet(eventIds, failedIds);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
/**
|
|
649
|
-
* Checks min/max range constraint.
|
|
650
|
-
* For each "min,max" -> eventIds entry, if runtime value < min OR > max, those eventIds FAIL.
|
|
651
|
-
* Empty bounds are supported: "0," means min=0 with no max, ",100" means no min with max=100.
|
|
652
|
-
*/
|
|
653
|
-
function checkMinMaxRanges(value, minMaxRanges, failedIds) {
|
|
654
|
-
// Only check min/max for numeric values
|
|
655
|
-
if (typeof value !== "number") {
|
|
656
|
-
// Non-numeric values fail all min/max constraints
|
|
657
|
-
for (const eventIds of Object.values(minMaxRanges)) {
|
|
658
|
-
addIdsToSet(eventIds, failedIds);
|
|
659
|
-
}
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
// NaN values fail all min/max constraints (comparisons with NaN are always false)
|
|
663
|
-
if (Number.isNaN(value)) {
|
|
664
|
-
console.warn(`[Avo Inspector] NaN value fails min/max constraint`);
|
|
665
|
-
for (const eventIds of Object.values(minMaxRanges)) {
|
|
666
|
-
addIdsToSet(eventIds, failedIds);
|
|
667
|
-
}
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
for (const [rangeStr, eventIds] of Object.entries(minMaxRanges)) {
|
|
671
|
-
const [minStr, maxStr] = rangeStr.split(",");
|
|
672
|
-
// Handle empty bounds: empty string means no constraint on that side
|
|
673
|
-
const hasMin = minStr !== "" && minStr !== undefined;
|
|
674
|
-
const hasMax = maxStr !== "" && maxStr !== undefined;
|
|
675
|
-
const min = hasMin ? parseFloat(minStr) : -Infinity;
|
|
676
|
-
const max = hasMax ? parseFloat(maxStr) : Infinity;
|
|
677
|
-
// Only check for invalid format if a bound was specified but couldn't be parsed
|
|
678
|
-
if ((hasMin && isNaN(min)) || (hasMax && isNaN(max))) {
|
|
679
|
-
console.warn(`[Avo Inspector] Invalid min/max range: ${rangeStr}`);
|
|
680
|
-
continue;
|
|
681
|
-
}
|
|
682
|
-
if (value < min || value > max) {
|
|
683
|
-
// Value out of range, so these eventIds fail
|
|
684
|
-
addIdsToSet(eventIds, failedIds);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
}
|
package/dist/esm/index.js
DELETED