jsonion
Version:
A lightweight JSON file-based database with nested data access and manipulation capabilities.
310 lines • 9.62 kB
JavaScript
import { db } from "./db.js";
import { dbError } from "./types.js";
/**
* Parse dot notation path into array of keys
* Example: 'user.profile.address[0].city' => ['user', 'profile', 'address', '0', 'city']
*/
function parsePath(path) {
return path
.replace(/\[(\w+)\]/g, '.$1')
.replace(/^\./, '')
.split('.')
.filter(key => key.length > 0);
}
/**
* Get value at deep path
*/
db.prototype.getPath = function (path) {
const keys = parsePath(path);
let current = this.all();
for (const key of keys) {
if (current === undefined || current === null) {
return undefined;
}
current = current[key];
}
return current;
};
/**
* Set value at deep path (creates path if it doesn't exist)
*/
db.prototype.setPath = function (path, value) {
const keys = parsePath(path);
if (keys.length === 0) {
throw new dbError('set', 'Invalid path');
}
const data = this.all();
const lastKey = keys[keys.length - 1];
let current = data;
// Navigate to parent object
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
const nextKey = keys[i + 1];
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
// Create object or array based on next key
current[key] = /^\d+$/.test(nextKey) ? [] : {};
}
current = current[key];
}
current[lastKey] = value;
// Save all data
const rootKey = keys[0];
this.set(rootKey, data[rootKey]);
};
/**
* Check if path exists
*/
db.prototype.hasPath = function (path) {
const keys = parsePath(path);
let current = this.all();
for (const key of keys) {
if (current === undefined || current === null || !(key in current)) {
return false;
}
current = current[key];
}
return true;
};
/**
* Delete value at path
*/
db.prototype.deletePath = function (path) {
const keys = parsePath(path);
if (keys.length === 0)
return false;
const data = this.all();
const lastKey = keys[keys.length - 1];
let current = data;
// Navigate to parent
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!(key in current)) {
return false;
}
current = current[key];
}
if (!(lastKey in current)) {
return false;
}
delete current[lastKey];
// Save root object
const rootKey = keys[0];
this.set(rootKey, data[rootKey]);
return true;
};
/**
* Update value at path (only if it exists)
*/
db.prototype.updatePath = function (path, value) {
if (!this.hasPath(path)) {
return false;
}
const oldValue = this.getPath(path);
const newValue = typeof value === 'function' ? value(oldValue) : value;
this.setPath(path, newValue);
return true;
};
/**
* Find all occurrences of a key-value pair in nested data
*/
db.prototype.findDeep = function (key, value) {
const results = [];
function search(obj, currentPath = '') {
if (obj === null || typeof obj !== 'object')
return;
if (Array.isArray(obj)) {
obj.forEach((item, index) => {
search(item, `${currentPath}[${index}]`);
});
}
else {
for (const [k, v] of Object.entries(obj)) {
const newPath = currentPath ? `${currentPath}.${k}` : k;
if (k === key && v === value) {
results.push({ path: newPath, value: v });
}
if (typeof v === 'object' && v !== null) {
search(v, newPath);
}
}
}
}
search(this.all());
return results;
};
/**
* Get all values matching a wildcard path
* Example: pluckPath('users[*].name') returns all user names
*/
db.prototype.pluckPath = function (path) {
const results = [];
const parts = path.split('[*]');
if (parts.length === 1) {
// No wildcard, just get the value
const value = this.getPath(path);
return value !== undefined ? [value] : [];
}
// Get base path
const basePath = parts[0];
const baseData = basePath ? this.getPath(basePath) : this.all();
if (!Array.isArray(baseData)) {
return [];
}
// Get remaining path after [*]
const remainingPath = parts.slice(1).join('[*]').replace(/^\./, '');
for (let i = 0; i < baseData.length; i++) {
if (remainingPath) {
const fullPath = basePath ? `${basePath}[${i}].${remainingPath}` : `[${i}].${remainingPath}`;
const subResults = this.pluckPath(fullPath);
results.push(...subResults);
}
else {
results.push(baseData[i]);
}
}
return results;
};
/**
* Find items in array at path matching predicate
*/
db.prototype.findInPath = function (path, predicate) {
const data = this.getPath(path);
if (!Array.isArray(data)) {
return [];
}
if (typeof predicate === 'function') {
return data.filter(predicate);
}
// Object predicate
return data.filter(item => {
if (typeof item !== 'object' || item === null)
return false;
return Object.entries(predicate).every(([key, value]) => {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
// Handle operators like { $lt: 100 }
const operators = value;
const itemValue = item[key];
for (const [op, opValue] of Object.entries(operators)) {
switch (op) {
case '$lt':
if (!(itemValue < opValue))
return false;
break;
case '$lte':
if (!(itemValue <= opValue))
return false;
break;
case '$gt':
if (!(itemValue > opValue))
return false;
break;
case '$gte':
if (!(itemValue >= opValue))
return false;
break;
case '$ne':
if (!(itemValue !== opValue))
return false;
break;
case '$in':
if (!Array.isArray(opValue) || !opValue.includes(itemValue))
return false;
break;
case '$nin':
if (!Array.isArray(opValue) || opValue.includes(itemValue))
return false;
break;
}
}
return true;
}
return item[key] === value;
});
});
};
/**
* Push value to array at path
*/
db.prototype.pushToPath = function (path, value) {
const data = this.getPath(path);
if (!Array.isArray(data)) {
// Create array if doesn't exist
this.setPath(path, [value]);
return;
}
data.push(value);
this.setPath(path, data);
};
/**
* Remove items from array at path matching predicate
*/
db.prototype.pullFromPath = function (path, predicate) {
const data = this.getPath(path);
if (!Array.isArray(data)) {
return 0;
}
const originalLength = data.length;
const filtered = data.filter(item => !predicate(item));
this.setPath(path, filtered);
return originalLength - filtered.length;
};
/**
* Update item in array at path
*/
db.prototype.updateInPath = function (arrayPath, findCondition, updateData) {
const data = this.getPath(arrayPath);
if (!Array.isArray(data)) {
return false;
}
let found = false;
const predicate = typeof findCondition === 'function'
? findCondition
: (item) => {
if (typeof item !== 'object' || item === null)
return false;
return Object.entries(findCondition).every(([key, value]) => item[key] === value);
};
const updated = data.map(item => {
if (predicate(item)) {
found = true;
return { ...item, ...updateData };
}
return item;
});
if (found) {
this.setPath(arrayPath, updated);
}
return found;
};
/**
* Merge data at path without removing existing properties
*/
db.prototype.mergePath = function (path, data) {
const existing = this.getPath(path);
if (typeof existing === 'object' && existing !== null && !Array.isArray(existing)) {
this.setPath(path, { ...existing, ...data });
}
else {
this.setPath(path, data);
}
};
/**
* Copy data from one path to another
*/
db.prototype.copyPath = function (sourcePath, destPath) {
const data = this.getPath(sourcePath);
if (data === undefined) {
throw new dbError('read', `Source path does not exist: ${sourcePath}`);
}
// Deep clone to avoid reference issues
const cloned = JSON.parse(JSON.stringify(data));
this.setPath(destPath, cloned);
};
/**
* Move data from one path to another
*/
db.prototype.movePath = function (sourcePath, destPath) {
this.copyPath(sourcePath, destPath);
this.deletePath(sourcePath);
};
//# sourceMappingURL=DeepAccess.js.map