UNPKG

signalk-parquet

Version:

Vessel data Parquet file archive with automated value and geospatial triggers. History API compliant with cloud backups and queries.

147 lines 5.41 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RetentionRuleSet = void 0; exports.validatePathRetentionRules = validatePathRetentionRules; /** * Glob-to-regex with the simple `*` semantics described above. * * Multiple consecutive `*` characters are collapsed to a single `*` * before compilation. This both keeps the regex tidy and shuts the * door on ReDoS via patterns like `*****foo*****` — without the * collapse, runs of `.*` against a long non-matching input cause * catastrophic backtracking. */ function compilePattern(pattern) { const collapsed = pattern.replace(/\*+/g, '*'); // Escape every regex special character except `*`, then turn `*` into // `.*`. Anchor full-string. const escaped = collapsed.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); const regex = escaped.replace(/\*/g, '.*'); return new RegExp(`^${regex}$`); } /** * Specificity is the count of literal (non-`*`) characters in the * pattern. `environment.wind.speedApparent` (29 literal chars) beats * `environment.wind.*` (16 literal chars) which beats `*` (0 literal * chars). It's a rough heuristic, but it does the right thing for the * shapes operators actually write. */ function computeSpecificity(pattern) { let n = 0; for (const ch of pattern) if (ch !== '*') n++; return n; } class RetentionRuleSet { /** * Construct with a list of rules. Per-rule compile failures (from a * malformed pattern in a hand-edited config) are not fatal — the bad * rule is dropped and `onCompileError` is invoked so the caller can * surface it. This keeps a single typo from poisoning plugin start. */ constructor(rules = [], onCompileError) { const compiled = []; rules.forEach((rule, i) => { try { compiled.push({ rule, regex: compilePattern(rule.pattern), specificity: computeSpecificity(rule.pattern), declarationOrder: i, }); } catch (err) { if (onCompileError) onCompileError(rule, err); } }); this.rules = compiled; } /** * Resolve the matching rule for a given SignalK path, or null if none * matches. The caller decides whether to fall back to the global * retention default. */ match(signalkPath) { let best = null; for (const c of this.rules) { if (!c.regex.test(signalkPath)) continue; if (best === null || c.specificity > best.specificity || (c.specificity === best.specificity && c.declarationOrder < best.declarationOrder)) { best = c; } } return best ? best.rule : null; } /** * Resolve effective retention for a path. Falls back to the global * default when no rule matches. `null` means "keep forever". */ resolveRetentionDays(signalkPath, globalDefaultDays) { const matched = this.match(signalkPath); const days = matched ? matched.days : globalDefaultDays; return days > 0 ? days : null; } /** * True if aggregation should skip this path (because a matching rule * has skipAggregation set). */ shouldSkipAggregation(signalkPath) { return this.match(signalkPath)?.skipAggregation === true; } isEmpty() { return this.rules.length === 0; } } exports.RetentionRuleSet = RetentionRuleSet; /** * Validate a list of rules. Always returns the valid rules plus any * per-entry errors so callers can drop only the bad entries instead of * all of them. Omitted input (undefined / null) is valid and yields an * empty rule set; a present-but-non-array input returns `rules: []` with * one error. */ function validatePathRetentionRules(rules) { if (rules === undefined || rules === null) { return { rules: [], errors: [] }; } if (!Array.isArray(rules)) { return { rules: [], errors: ['pathRetentionOverrides must be an array'] }; } const errors = []; const out = []; rules.forEach((entry, i) => { if (typeof entry !== 'object' || entry === null) { errors.push(`pathRetentionOverrides[${i}] must be an object`); return; } const r = entry; if (typeof r.pattern !== 'string' || r.pattern.length === 0) { errors.push(`pathRetentionOverrides[${i}].pattern must be a non-empty string`); return; } if (typeof r.days !== 'number' || !Number.isFinite(r.days) || !Number.isInteger(r.days) || r.days < 0) { errors.push(`pathRetentionOverrides[${i}].days must be a non-negative integer`); return; } if (r.skipAggregation !== undefined && typeof r.skipAggregation !== 'boolean') { errors.push(`pathRetentionOverrides[${i}].skipAggregation must be a boolean`); return; } out.push({ pattern: r.pattern, days: r.days, skipAggregation: r.skipAggregation === true ? true : undefined, }); }); return { rules: out, errors }; } //# sourceMappingURL=retention-rules.js.map