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
JavaScript
;
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