probejs-core
Version:
A powerful tool for traversing and investigating nested objects
203 lines • 7.18 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Probe = void 0;
const validation_1 = require("./validation");
const errors_1 = require("./errors");
const timing_1 = require("./utils/timing");
class Probe {
constructor(data, options = {}) {
var _a, _b, _c, _d;
this.cache = new Map();
this.statistics = {
searches: 0,
cacheHits: 0,
cacheMisses: 0,
averageSearchTime: 0,
};
if (!data || (typeof data !== "object" && !Array.isArray(data))) {
throw new errors_1.ProbeError("Data must be a non-null object or array");
}
this.data = data;
this.options = {
caseSensitive: (_a = options.caseSensitive) !== null && _a !== void 0 ? _a : true,
maxDepth: (_b = options.maxDepth) !== null && _b !== void 0 ? _b : Infinity,
pathDelimiter: (_c = options.pathDelimiter) !== null && _c !== void 0 ? _c : ".",
caching: (_d = options.caching) !== null && _d !== void 0 ? _d : true,
};
(0, validation_1.validateOptions)(this.options);
(0, validation_1.detectCircular)(this.data);
}
find(key, occurrence = "first") {
(0, validation_1.validateKey)(key);
const startTime = (0, timing_1.getTime)();
let results = this.getCachedResults(key);
if (!results) {
results = this.findAll(key);
this.cacheResults(key, results);
}
this.trackSearch(startTime);
if (results.length === 0)
return undefined;
switch (occurrence) {
case "first":
return results[0];
case "last":
return results[results.length - 1];
case "all":
return results || [];
default:
throw new errors_1.ProbeError(`Invalid occurrence type: ${occurrence}`);
}
}
isProtectedKey(key) {
return key === "__proto__" || key === "constructor" || key === "prototype";
}
handleArrayAccess(current, key) {
const index = parseInt(key, 10);
if (isNaN(index) || index < 0 || index >= current.length) {
return undefined;
}
return current[index];
}
findByPath(path) {
(0, validation_1.validatePath)(path);
const startTime = (0, timing_1.getTime)();
try {
const keys = path.split(this.options.pathDelimiter);
let current = this.data;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
// Protect against prototype pollution
if (this.isProtectedKey(key)) {
return undefined;
}
if (current === null || typeof current !== "object") {
return undefined;
}
if (Array.isArray(current)) {
const result = this.handleArrayAccess(current, key);
if (result === undefined) {
return undefined;
}
current = result;
}
else {
if (!Object.prototype.hasOwnProperty.call(current, key)) {
// If the key doesn't exist, check if the remaining path forms a valid key
const remainingPath = keys.slice(i).join(this.options.pathDelimiter);
if (Object.prototype.hasOwnProperty.call(current, remainingPath)) {
current = current[remainingPath];
break; // Exit the loop since we've processed the full key
}
return undefined;
}
current = current[key];
}
}
return {
value: current,
path,
key: keys[keys.length - 1],
};
}
finally {
this.trackSearch(startTime);
}
}
findWhere(predicate) {
if (typeof predicate !== "function") {
throw new errors_1.ProbeError("Predicate must be a function");
}
const startTime = (0, timing_1.getTime)();
const results = [];
try {
this.traverse(this.data, [], (value, key, path) => {
if (predicate(value, key, path)) {
results.push({
value,
path: path.join(this.options.pathDelimiter),
key,
});
}
});
return results;
}
finally {
this.trackSearch(startTime);
}
}
getStatistics() {
return { ...this.statistics };
}
clearCache() {
this.cache.clear();
}
setCaching(enabled) {
if (!enabled) {
this.clearCache();
}
this.options.caching = enabled;
}
findAll(key) {
const results = [];
this.traverse(this.data, [], (value, currentKey, path) => {
const normalizedKey = this.options.caseSensitive
? currentKey
: currentKey.toLowerCase();
const searchKey = this.options.caseSensitive ? key : key.toLowerCase();
if (normalizedKey === searchKey) {
results.push({
value,
path: path.join(this.options.pathDelimiter),
key: currentKey,
});
}
});
return results;
}
traverse(obj, path, callback) {
if (!obj ||
typeof obj !== "object" ||
path.length >= this.options.maxDepth) {
return;
}
if (Array.isArray(obj)) {
obj.forEach((value, index) => {
callback(value, index.toString(), [...path, index.toString()]);
this.traverse(value, [...path, index.toString()], callback);
});
}
else {
for (const [key, value] of Object.entries(obj)) {
callback(value, key, [...path, key]);
this.traverse(value, [...path, key], callback);
}
}
}
getCachedResults(key) {
if (!this.options.caching)
return null;
const cached = this.cache.get(key);
if (cached) {
this.statistics.cacheHits++;
return cached;
}
this.statistics.cacheMisses++;
return null;
}
cacheResults(key, results) {
if (this.options.caching) {
this.cache.set(key, results);
}
}
trackSearch(startTime) {
const searchTime = (0, timing_1.getTime)() - startTime;
this.statistics.searches++;
this.statistics.averageSearchTime =
(this.statistics.averageSearchTime * (this.statistics.searches - 1) +
searchTime) /
this.statistics.searches;
}
}
exports.Probe = Probe;
//# sourceMappingURL=Probe.js.map