UNPKG

json-fetchfy

Version:

A lightweight Node.js module to fetch, validate, and manipulate JSON data from various sources seamlessly.

483 lines (412 loc) 14.3 kB
// External dependencies import "./js/plugin.mjs"; import fileReader from "./js/fileReader.mjs"; import findInArray from "./js/findInArray.mjs"; import findInObject from "./js/findInObject.mjs"; import jsonGenerator from "./js/jsonGenerator.mjs"; import { validatePathSync } from "./js/validatePath.mjs"; // Node.js built-in modules import fs from "fs"; import path from "path"; /** * Fetchfy - A utility module for data processing and caching * @module fetchfy * @description Provides functionality for JSON data manipulation, searching, and caching * @version 1.0.0 */ // Constants const CONTENT_TYPES = { NUMBERS: "Numbers", STRINGS: "Strings", OBJECTS: "Objects", SINGLE: "Single", UNKNOWN: "Unknown", INVALID: "Invalid JSON", }; const DEFAULT_OPTIONS = { cache: true, deep: false, caseSensitive: true, limit: Infinity, single: false, }; // Private store for caching const _store = new Map(); // Main module object const fetchfy = {}; /** * Detects content type in a JSON string or parsed object. * @param {string|object} jsonInput - JSON string or parsed object to analyze * @returns {string} Detected content type from CONTENT_TYPES * @throws {Error} If input is null or undefined */ fetchfy.detectJsonContentType = (jsonInput) => { if (jsonInput == null) { throw new Error("JSON input cannot be null or undefined"); } try { const parsedData = typeof jsonInput === "string" ? JSON.parse(jsonInput) : jsonInput; if (Array.isArray(parsedData)) { if (parsedData.length === 0) return CONTENT_TYPES.UNKNOWN; const typeChecks = { [CONTENT_TYPES.NUMBERS]: (item) => typeof item === "number", [CONTENT_TYPES.STRINGS]: (item) => typeof item === "string", [CONTENT_TYPES.OBJECTS]: (item) => typeof item === "object" && item !== null && !Array.isArray(item), }; for (const [type, check] of Object.entries(typeChecks)) { if (parsedData.every(check)) return type; } return CONTENT_TYPES.UNKNOWN; } return typeof parsedData === "object" && parsedData !== null ? CONTENT_TYPES.SINGLE : CONTENT_TYPES.UNKNOWN; } catch (error) { return CONTENT_TYPES.INVALID; } }; /** * Manages module options. * @param {object|string} options - Configuration object or 'get' to retrieve current options * @throws {TypeError} If options parameter is invalid */ fetchfy.options = (options = {}) => { const OPTIONS_KEY = "options"; const getOptions = () => _store.get(OPTIONS_KEY) || null; const setOptions = (newOptions) => { const currentOptions = getOptions() || {}; _store.set(OPTIONS_KEY, { ...currentOptions, ...newOptions }); }; if (typeof options === "string" && options.toLowerCase() === "get") { return getOptions(); } if (typeof options === "object" && options !== null) { setOptions(options); return; } throw new TypeError( "Options parameter must be an object or the string 'get'." ); }; /** * Retrieves and processes data based on input. * @param {*} data - Input data (array or object) * @param {*} query - Search query * @param {string} [key=null] - Key to search within objects * @param {object} [options={}] - Processing options * @returns {*} Processed results */ fetchfy.get = function (data = null, query, key = null, options = {}) { // Validate and process file path if provided const pathValidation = validatePathSync(data); if (pathValidation.isValid && pathValidation.isFile) { data = fileReader.readFileSync(data); } // Validate input data const contentType = fetchfy.detectJsonContentType(data); if ( contentType === CONTENT_TYPES.UNKNOWN || contentType === CONTENT_TYPES.INVALID ) { console.error( "Invalid input: Must be an array of numbers/strings/objects or a single object" ); return null; } // Merge options const mergedOptions = { ..._store.get("options"), ...options, }; mergedOptions.removeKeys(["cache", "single", "update", "save"]); // Process based on content type if ( contentType === CONTENT_TYPES.OBJECTS || contentType === CONTENT_TYPES.NUMBERS ) { const results = findInArray(data, query, key, mergedOptions); return options.single ? (results.length > 0 ? results[0] : null) : results; } return findInObject(data, query, key, mergedOptions); }; /** * Updates JSON elements based on query. * @param {*} data - Input data to update * @param {*} query - Search query for items to update * @param {string} [key=null] - Key to search within objects * @param {object} [options={}] - Update options * @returns {boolean|object} Updated data or false if update fails */ fetchfy.update = function (data = null, query, key = null, options = {}) { let filePath = null; // Handle file input const pathValidation = validatePathSync(data); if (pathValidation.isValid && pathValidation.isFile) { filePath = data; data = fileReader.readFileSync(data); } // Validate input const contentType = fetchfy.detectJsonContentType(data); if ( contentType === CONTENT_TYPES.UNKNOWN || contentType === CONTENT_TYPES.INVALID ) { console.error( "Invalid input: Must be an array of numbers/strings/objects or a single object" ); return false; } // Process updates if ( contentType === CONTENT_TYPES.OBJECTS || contentType === CONTENT_TYPES.NUMBERS ) { const mergedOptions = { ..._store.get("options"), ...options }; mergedOptions.removeKeys(["cache", "single", "update", "save"]); const matches = fetchfy.get(data, query, key, mergedOptions); if (matches?.length > 0) { const updates = options.update || {}; matches.forEach((item) => { Object.keys(updates).forEach((prop) => { if (Object.prototype.hasOwnProperty.call(item, prop)) { item[prop] = updates[prop]; } }); }); if (options.save && filePath) { fetchfy.save(data, filePath); } return data; } console.log("No matching items found for update"); return false; } console.error("Update only supports arrays of objects or numbers"); return false; }; /** * Validates new item against existing data structure * @private * @param {Array|Object} data - Existing data * @param {*} newItem - Item to validate * @returns {boolean} Whether the item is valid for the data structure */ const _validateNewItem = (data, newItem) => { const dataType = fetchfy.detectJsonContentType(data); const itemType = fetchfy.detectJsonContentType(newItem); // For arrays, check if new item matches the type of existing items if (dataType === CONTENT_TYPES.NUMBERS) { return typeof newItem === "number"; } if (dataType === CONTENT_TYPES.STRINGS) { return typeof newItem === "string"; } if (dataType === CONTENT_TYPES.OBJECTS) { return ( itemType === CONTENT_TYPES.SINGLE || itemType === CONTENT_TYPES.OBJECTS ); } if (dataType === CONTENT_TYPES.SINGLE) { return itemType === CONTENT_TYPES.SINGLE; } return false; }; /** * Reorders object properties with ID first and optional alphabetical sorting * @private * @param {Object} obj - Object to reorder * @param {Object} options - Ordering options * @param {boolean} options.alphabetical - Whether to sort properties alphabetically * @param {string|null} options.idKey - Name of the ID field (null if no ID) * @returns {Object} New object with ordered properties */ const _orderProperties = (obj, { alphabetical = false, idKey = null }) => { // Create a new object to store ordered properties const ordered = {}; // Get all keys from the object let keys = Object.keys(obj); // If ID exists, remove it from keys array as we'll handle it separately if (idKey && obj.hasOwnProperty(idKey)) { keys = keys.filter(key => key !== idKey); // Add ID first ordered[idKey] = obj[idKey]; } // Sort remaining keys if alphabetical ordering is requested if (alphabetical) { keys.sort((a, b) => a.localeCompare(b)); } // Add remaining properties in order keys.forEach(key => { ordered[key] = obj[key]; }); return ordered; }; /** * Gets the next available ID from an array of objects * @private * @param {Array<Object>} data - Array of objects to check * @param {string} idKey - Name of the ID field * @returns {number} Next available ID */ const _getNextId = (data, idKey = 'id') => { if (!Array.isArray(data) || data.length === 0) return 1; const maxId = Math.max(...data.map(item => typeof item[idKey] === 'number' ? item[idKey] : 0 )); return maxId + 1; }; /** * Adds new items to the data structure * @param {Array|Object} data - Target data structure * @param {*} items - Single item or array of items to add * @param {Object} [options={}] - Addition options * @param {boolean} [options.validate=true] - Whether to validate items before adding * @param {boolean} [options.save=false] - Whether to save to file if data is from file * @param {boolean} [options.unique=false] - Prevent duplicate objects when adding * @param {string|string[]} [options.uniqueKeys=[]] - Keys to check for uniqueness * @param {string} [options.id] - Auto-increment ID field name (use "auto" to enable) * @param {boolean} [options.alphabetical=false] - Sort properties alphabetically * @returns {Object} Result object containing success status and updated data */ fetchfy.add = function(data = null, items, options = {}) { // Default options const defaultOptions = { validate: true, save: false, unique: false, uniqueKeys: [], id: null, alphabetical: false }; const opts = { ...defaultOptions, ...options }; let filePath = null; // Handle file input const pathValidation = validatePathSync(data); if (pathValidation.isValid && pathValidation.isFile) { filePath = data; data = fileReader.readFileSync(data); } // Validate input data const contentType = fetchfy.detectJsonContentType(data); if (contentType === CONTENT_TYPES.UNKNOWN || contentType === CONTENT_TYPES.INVALID) { throw new Error("Invalid target data structure"); } // Convert single item to array for consistent processing const itemsToAdd = Array.isArray(items) ? items : [items]; try { // Handle different data types if (contentType === CONTENT_TYPES.SINGLE) { if (itemsToAdd.length > 1) { throw new Error("Cannot add multiple items to a single object"); } if (opts.validate && !_validateNewItem(data, itemsToAdd[0])) { throw new Error("New item does not match target object structure"); } // Merge objects and ensure proper ordering const mergedData = { ...data, ...itemsToAdd[0] }; Object.assign(data, _orderProperties(mergedData, { alphabetical: opts.alphabetical, idKey: mergedData.hasOwnProperty('id') ? 'id' : null })); } else { if (opts.validate) { const invalidItems = itemsToAdd.filter(item => !_validateNewItem(data, item)); if (invalidItems.length > 0) { throw new Error(`${invalidItems.length} items do not match array structure`); } } // Handle auto-increment ID if specified let nextId = null; const useAutoId = opts.id === 'auto' && contentType === CONTENT_TYPES.OBJECTS; if (useAutoId) { nextId = _getNextId(data); } // Process each item if (opts.unique && contentType === CONTENT_TYPES.OBJECTS) { const uniqueKeys = Array.isArray(opts.uniqueKeys) ? opts.uniqueKeys : (opts.uniqueKeys ? [opts.uniqueKeys] : Object.keys(itemsToAdd[0] || {})); itemsToAdd.forEach(newItem => { // Create a new object for the item const processedItem = { ...newItem }; // Add auto-increment ID if enabled if (useAutoId) { processedItem.id = nextId++; } // Order properties const orderedItem = _orderProperties(processedItem, { alphabetical: opts.alphabetical, idKey: processedItem.hasOwnProperty('id') ? 'id' : null }); const isDuplicate = data.some(existingItem => uniqueKeys.every(key => (useAutoId && key === 'id') ? false : existingItem[key] === orderedItem[key] ) ); if (!isDuplicate) { data.push(orderedItem); } }); } else { // Process items without uniqueness check itemsToAdd.forEach(item => { const processedItem = { ...item }; if (useAutoId) { processedItem.id = nextId++; } const orderedItem = _orderProperties(processedItem, { alphabetical: opts.alphabetical, idKey: processedItem.hasOwnProperty('id') ? 'id' : null }); data.push(orderedItem); }); } } // Save if requested and file path exists if (opts.save && filePath) { fetchfy.save(data, filePath); } return { success: true, data: data, added: itemsToAdd.length, message: "Items added successfully" }; } catch (error) { return { success: false, data: data, added: 0, message: error.message }; } }; /** * Saves data to a file. * @param {*} data - Data to save * @param {string} filePath - Path to save the file * @returns {boolean} Success status * @throws {Error} If filePath is invalid */ fetchfy.save = function (data, filePath) { if (!filePath || typeof filePath !== "string") { throw new Error("Valid file path required"); } try { const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8"); return true; } catch (error) { console.error("Failed to save data:", error); return false; } }; // Initialize with default options fetchfy.options(DEFAULT_OPTIONS); // Export combined module export default { ...fetchfy, ...jsonGenerator };