├── .gitignore ├── .travis.yml ├── CONTRIBUTE.md ├── LICENSE ├── README.md ├── _config.yml ├── index.js ├── package-lock.json ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | collections/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "7" 5 | 6 | before_install: 7 | - npm install jsonfile 8 | - npm install 9 | -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | Please use this guidelines in creating an issue and contributing to the project. Remember to always be polite to others in the comments. Thank you. 3 | 4 | ## Opening an Issue 5 | 6 | ### I. Bug 7 | To create an issue for a bug, please put in the Title: 8 | BUG [BUG_TITLE] 9 | Then on the Leave a comment, just be clear in the description of the issue and be direct. 10 | 11 | For the [BUG_TITLE], it can be anything descriptive and appropriate. 12 | 13 | ### II. Feature 14 | To create an issue for a feature, please put in the title: 15 | FEATURE [FEATURE_TITLE] 16 | Then on the Leave a comment, just be clear in the description of the issue and be direct. 17 | 18 | For the [FEATURE_TITLE], it can be anything descriptive and appropriate. 19 | 20 | For other inquiries, please don't hesitate to contact me at: alexius.academia@gmail.com or visit my webpage at https://alexiusacademia.com -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alexius Academia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # electron-db 2 | --- 3 | [![Build Status](https://travis-ci.org/alexiusacademia/electron-db.svg?branch=master)](https://travis-ci.org/alexiusacademia/electron-db) 4 | [![NPM version](https://img.shields.io/npm/v/electron-db.svg)](https://npmjs.org/package/electron-db "View this project on NPM") 5 | [![NPM downloads](https://img.shields.io/npm/dm/electron-db.svg)](https://npmjs.org/package/electron-db "View this project on NPM") 6 | > Flat file database solution for electron and other Nodejs apps. 7 | 8 | **electron-db** is an npm library that let you simplify database creation and operation on a json file. 9 | 10 | The json file is saved on the application folder or you can specify the location for the database to be created. From version 0.10.0, the user has the option to save the database table anywhere they chose. 11 | 12 | The only difference with the default location is that, the user have to pass the string location as the second argument to any function to be used (this is optional if you want to control the database location). 13 | 14 | The table format contained in the `table_name.json` should be in the form of 15 | ``` 16 | { 17 | "table_name": [ 18 | { 19 | "field1": "Value of field 1", 20 | "field2": "Value of field 2", 21 | ... 22 | } 23 | ] 24 | } 25 | ``` 26 | 27 | **Important:** The script that uses this library should be run with electron command first in order to create the directory on the user data folder (when not using a custom directory for the database). The name that will be used for the app directory will be what was indicated in the `package.json` as name. If this is not set, the name property will be used. 28 | 29 | ### **Installation** 30 | The preferred way of installation is to install it locally on the application. 31 | ```javascript 32 | npm install electron-db --save 33 | ``` 34 | 35 | ### **Creating Table** 36 | Creates a json file `[table-name].js` inside the application `userData` folder. 37 | 38 | In Windows, the application folder should be in `C:\Users\[username]\AppData\Roaming\[application name]` 39 | 40 | ```javascript 41 | 42 | const db = require('electron-db'); 43 | const { app, BrowserWindow } = require("electron"); 44 | 45 | db.createTable('customers', (succ, msg) => { 46 | // succ - boolean, tells if the call is successful 47 | console.log("Success: " + succ); 48 | console.log("Message: " + msg); 49 | }) 50 | 51 | /* 52 | Output: 53 | Success: true 54 | Message: Success! 55 | 56 | Result file (customers.json): 57 | { 58 | "customers": [] 59 | } 60 | */ 61 | ``` 62 | 63 | ### **Creating Table specifying the Location** 64 | The custom location, if desired, shall be passed as the second argument and the remaining arguments are the same (if any) on a specific function. 65 | ```javascript 66 | const path = require('path') 67 | 68 | // This will save the database in the same directory as the application. 69 | const location = path.join(__dirname, '') 70 | 71 | db.createTable('customers', location, (succ, msg) => { 72 | // succ - boolean, tells if the call is successful 73 | if (succ) { 74 | console.log(msg) 75 | } else { 76 | console.log('An error has occured. ' + msg) 77 | } 78 | }) 79 | ``` 80 | 81 | ### **Inserting Object/Data to Table** 82 | Insert an object into the list of row/data of the table. 83 | 84 | To insert to a custom location, pass the custom location as the second argument 85 | as shown in the sample above. But do not forget to check if the database is valid. 86 | 87 | ```javascript 88 | let obj = new Object(); 89 | 90 | obj.name = "Alexius Academia"; 91 | obj.address = "Paco, Botolan, Zambales"; 92 | 93 | if (db.valid('customers')) { 94 | db.insertTableContent('customers', obj, (succ, msg) => { 95 | // succ - boolean, tells if the call is successful 96 | console.log("Success: " + succ); 97 | console.log("Message: " + msg); 98 | }) 99 | } 100 | 101 | /* 102 | Output: 103 | Success: true 104 | Message: Object written successfully! 105 | 106 | Result file (customers.json): 107 | { 108 | "customers": [ 109 | { 110 | "name": "Alexius Academia", 111 | "address": "Paco, Botolan, Zambales" 112 | } 113 | ] 114 | } 115 | 116 | */ 117 | ``` 118 | 119 | ### For the database table at custom location 120 | For the implementation of this new feature, always put the location string as second argument for all the functions. (The directory string must end with appropriate slashes, forward slash for unix and back slash with escape string for Windows) (e.g. Windows: ```'C:\\databases\\'```, Unix: ```'/Users//Desktop/'```). For good practice, use the ```path.join``` method to let the OS apply its directory separator automatically. 121 | 122 | 159 | ### **Get all rows** 160 | Get all the rows for a given table by using the callback function. 161 | ```javascript 162 | 163 | const db = require('electron-db'); 164 | const electron = require('electron'); 165 | 166 | const app = electron.app || electron.remote.app; 167 | 168 | db.getAll('customers', (succ, data) => { 169 | // succ - boolean, tells if the call is successful 170 | // data - array of objects that represents the rows. 171 | }) 172 | ``` 173 | ### **Get Row(s) from the table** 174 | Get row or rows that matched the given condition(s) in WHERE argument 175 | 176 | ```javascript 177 | const db = require('electron-db'); 178 | const electron = require('electron'); 179 | 180 | const app = electron.app || electron.remote.app; 181 | 182 | db.getRows('customers', { 183 | address: "Paco, Botolan, Zambales", 184 | name: 'Alexius Academia' 185 | }, (succ, result) => { 186 | // succ - boolean, tells if the call is successful 187 | console.log("Success: " + succ); 188 | console.log(result); 189 | }) 190 | 191 | /* 192 | Output: 193 | Success: true 194 | [ { name: 'Alexius Academia', 195 | address: 'Paco, Botolan, Zambales', 196 | id: 1508419374272 } ] 197 | */ 198 | ``` 199 | 200 | ### **Update Row** 201 | Updates a specific row or rows from a table/json file using a WHERE clause. 202 | 203 | ```javascript 204 | const db = require('electron-db'); 205 | const electron = require('electron'); 206 | 207 | const app = electron.app || electron.remote.app; 208 | 209 | let where = { 210 | "name": "Alexius Academia" 211 | }; 212 | 213 | let set = { 214 | "address": "Paco, Botolan, Zambales" 215 | } 216 | 217 | db.updateRow('customers', where, set, (succ, msg) => { 218 | // succ - boolean, tells if the call is successful 219 | console.log("Success: " + succ); 220 | console.log("Message: " + msg); 221 | }); 222 | ``` 223 | 224 | ### **Search Records** 225 | Search a specific record with a given key/field of the table. This method can search part of a string from a value. 226 | 227 | In this example, I have a table named 'customers', each row has a 'name' property. We are now trying to search for a name in the rows that has the substring 'oh' in it. 228 | 229 | ```javascript 230 | const db = require('electron-db'); 231 | const electron = require('electron'); 232 | 233 | const app = electron.app || electron.remote.app; 234 | 235 | let term = "oh"; 236 | 237 | db.search('customers', 'name', term, (succ, data) => { 238 | if (succ) { 239 | console.log(data); 240 | } 241 | }); 242 | 243 | // Output 244 | /* 245 | [ { name: 'John John Academia', 246 | address: 'Paco, Botolan, Zambales', 247 | id: 1508419430491 } ] 248 | */ 249 | ``` 250 | 251 | ### **Delete Records** 252 | Delete a specific record with a given key-value pair from the table. 253 | 254 | ```javascript 255 | 256 | const db = require('electron-db'); 257 | const electron = require('electron'); 258 | 259 | db.deleteRow('customers', {'id': 1508419374272}, (succ, msg) => { 260 | console.log(msg); 261 | }); 262 | 263 | ``` 264 | 265 | ### **Get data for specific field** 266 | Get all the field given in a specific key. 267 | This will return all values on each row that has the key given in the parameter. 268 | ```javascript 269 | const key = 'name' 270 | 271 | db.getField(dbName, dbLocation, key, (succ, data) => { 272 | if (succ) { 273 | console.log(data) 274 | } 275 | }) 276 | ``` 277 | 278 | ### **Clear all Records** 279 | Clear all the records in the specified table. 280 | ```javascript 281 | // Delete all the data 282 | db.clearTable(dbName, dbLocation, (succ, msg) => { 283 | if (succ) { 284 | console.log(msg) 285 | 286 | // Show the content now 287 | db.getAll(dbName, dbLocation, (succ, data) => { 288 | if (succ) { 289 | console.log(data); 290 | } 291 | }); 292 | } 293 | }) 294 | ``` 295 | 296 | ### **Count Records** 297 | Count the number of rows for a given table. 298 | ``` 299 | db.count(dbName, dbLocation, (succ, data) => { 300 | if (succ) { 301 | console.log(data) 302 | } else { 303 | console.log('An error has occured.') 304 | console.log(data) 305 | } 306 | }) 307 | ``` 308 | 309 | For contributions, please see the `CONTRIBUTE.md` file. Thank you. 310 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // const electron = require('electron'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const os = require('os'); // Corrected: no 'new' for os module 5 | 6 | let userData; 7 | 8 | try { 9 | const electron = require('electron'); 10 | // Attempt to get the app object. This might be electron.app or electron.remote.app 11 | // depending on the context (main vs renderer) and Electron version. 12 | // electron.remote is deprecated in newer versions. 13 | // A more robust solution for renderers would be to use ipcRenderer to get path from main. 14 | // For this library's general purpose, we try common patterns. 15 | const app = electron.app || (electron.remote ? electron.remote.app : undefined); 16 | 17 | if (!app || typeof app.getPath !== 'function') { 18 | // This error will be caught by the main try...catch block 19 | throw new Error("Electron app object or app.getPath method not available."); 20 | } 21 | userData = app.getPath('userData'); 22 | // Optional: create a subfolder within userData if desired, e.g.: 23 | // userData = path.join(app.getPath('userData'), 'electron-db-data'); 24 | } catch (e) { 25 | // Log the error for debugging if needed, but console.warn is for user feedback. 26 | // console.error("Electron-specific path retrieval failed:", e.message); 27 | console.warn("electron-db: Electron module not found or Electron app object not available. Using OS-specific default path. For robust behavior, explicitly provide a storage location for your tables, or ensure Electron's 'app' module is accessible if running in an Electron environment."); 28 | 29 | const defaultDirName = 'electron-db-data'; // Changed from 'electron-db-tables' for clarity 30 | const homeDir = os.homedir(); 31 | const currentPlatform = os.platform(); // Renamed to avoid conflict with 'platform' variable if it existed from old code 32 | 33 | if (currentPlatform === 'win32') { 34 | // process.env.APPDATA is the roaming app data folder. 35 | // process.env.LOCALAPPDATA would be for local app data, often preferred. 36 | // However, to align with Electron's default userData behavior which is typically roaming: 37 | userData = path.join(process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'), defaultDirName); 38 | } else if (currentPlatform === 'darwin') { 39 | userData = path.join(homeDir, 'Library', 'Application Support', defaultDirName); 40 | } else { // Linux and other POSIX-like 41 | // Use ~/.config/app-name as per XDG Base Directory Specification 42 | userData = path.join(homeDir, '.config', defaultDirName); 43 | } 44 | } 45 | 46 | /** 47 | * Create a table | a json file 48 | * The second argument is optional, if ommitted, the file 49 | * will be created at the default location. 50 | * @param {[string]} arguments[0] [Table name] 51 | * @param {[string]} arguments[1] [Location of the database file] (Optional) 52 | * @param {[function]} arguments[2] [Callbak ] 53 | */ 54 | // function createTable(tableName, callback) { 55 | // function createTable() { 56 | function createTable(tableName, locationOrCallback, callbackIfLocationProvided) { 57 | let location; 58 | let callback; 59 | 60 | if (typeof tableName !== 'string' || !tableName.trim()) { 61 | // Determine the actual callback function to use for error reporting early. 62 | let cb = null; 63 | if (typeof locationOrCallback === 'function') cb = locationOrCallback; 64 | else if (typeof callbackIfLocationProvided === 'function') cb = callbackIfLocationProvided; 65 | 66 | if (typeof tableName !== 'string' || !tableName.trim()) { 67 | if (cb) cb(new Error("Table name must be a non-empty string.")); 68 | return; 69 | } 70 | } 71 | 72 | if (typeof locationOrCallback === 'string') { 73 | location = locationOrCallback; 74 | callback = callbackIfLocationProvided; 75 | if (typeof callback !== 'function') { 76 | // No reliable callback to notify, and this is a programming error. 77 | // console.error("Error: createTable called with location but no valid callback."); 78 | // For a library, throwing might be too disruptive. Silently returning is one option. 79 | // Or, if a pseudo-callback was passed that's not a function, try to use it (already handled by initial cb check). 80 | // This case means callbackIfLocationProvided was not a function. 81 | return; 82 | } 83 | } else if (typeof locationOrCallback === 'function') { 84 | location = userData; // Default location 85 | callback = locationOrCallback; 86 | } else { 87 | // Invalid arguments if neither of the above. 88 | // cb might have been identified if callbackIfLocationProvided was a function but locationOrCallback was not a string/function. 89 | if (callbackIfLocationProvided && typeof callbackIfLocationProvided === 'function') { // Check if it was a function initially 90 | callbackIfLocationProvided(new Error("Invalid arguments for createTable: Expected location string or callback function as second argument.")); 91 | } else if (locationOrCallback && typeof locationOrCallback === 'function') { 92 | locationOrCallback(new Error("Invalid arguments for createTable: Expected callback function as third argument when location is provided.")); 93 | } 94 | return; 95 | } 96 | 97 | // Final check for callback 98 | if (typeof callback !== 'function') { 99 | // This should ideally not be reached if logic above is correct. 100 | // console.error("Error: createTable could not determine a valid callback function."); 101 | return; 102 | } 103 | 104 | const fname = path.join(location, tableName + '.json'); 105 | 106 | // Ensure the directory exists first 107 | fs.mkdir(location, { recursive: true }, (mkdirErr) => { 108 | if (mkdirErr) { 109 | callback(mkdirErr); // Propagate fs error 110 | return; 111 | } 112 | 113 | // Check if the file with the tablename.json exists 114 | fs.access(fname, fs.constants.F_OK, (accessErr) => { 115 | if (accessErr === null) { 116 | // File exists 117 | callback(new Error(`Table '${tableName}' already exists at ${location}.`)); 118 | return; 119 | } else if (accessErr.code === 'ENOENT') { 120 | // File does not exist, proceed with creation 121 | let obj = new Object(); 122 | obj[tableName] = []; 123 | 124 | fs.writeFile(fname, JSON.stringify(obj, null, 2), (writeErr) => { 125 | if (writeErr) { 126 | callback(writeErr); // Propagate fs error 127 | } else { 128 | callback(null, `Table '${tableName}' created successfully at ${location}.`); 129 | } 130 | }); 131 | } else { 132 | // Other access error 133 | callback(accessErr); // Propagate fs error 134 | } 135 | }); 136 | }); 137 | } 138 | 139 | /** 140 | * Checks if a json file contains valid JSON string 141 | */ 142 | // function valid(dbName, location) { 143 | function valid(dbName, location, callback) { 144 | if (typeof callback !== 'function') { 145 | // console.error("Error: valid called without a valid callback function."); 146 | return; 147 | } 148 | if (typeof dbName !== 'string' || !dbName.trim()) { 149 | callback(new Error("DB name must be a non-empty string for valid().")); 150 | return; 151 | } 152 | const dbPath = location || userData; 153 | const fName = path.join(dbPath, dbName + '.json'); 154 | 155 | fs.readFile(fName, 'utf-8', (err, content) => { 156 | if (err) { 157 | if (err.code === 'ENOENT') { // File does not exist, so not valid in this context 158 | callback(null, false); 159 | } else { // Other fs error 160 | callback(err); 161 | } 162 | return; 163 | } 164 | try { 165 | JSON.parse(content); 166 | callback(null, true); // Valid JSON 167 | } catch (e_parse) { 168 | // If it's a parse error, the file content is not valid JSON. 169 | callback(null, false); 170 | } 171 | }); 172 | } 173 | 174 | /** 175 | * Insert object to table. The object will be appended with the property, id 176 | * which uses timestamp as value. 177 | * There are 3 required arguments. 178 | * @param {string} arguments[0] [Table name] 179 | * @param {string} arguments[1] [Location of the database file] (Optional) 180 | * @param {string} arguments[2] [Row object] 181 | * @param {Function} arguments[3] [Callback function] 182 | * @returns {(number|undefined)} [ID of the inserted row] 183 | */ 184 | // function insertTableContent(tableName, tableRow, callback) { 185 | // function insertTableContent() { 186 | function insertTableContent(tableName, locationOrTableRow, tableRowOrCallback, callbackIfLocationProvided) { 187 | let location; 188 | let tableRow; 189 | // Removed duplicate declarations of location and tableRow 190 | let callback; 191 | 192 | // Try to identify the callback first for consistent error reporting 193 | if (typeof callbackIfLocationProvided === 'function') { 194 | callback = callbackIfLocationProvided; 195 | } else if (typeof tableRowOrCallback === 'function' && (typeof locationOrTableRow === 'string' || typeof locationOrTableRow === 'object')) { 196 | // If 3 args: (tableName, location/tableRow, callback) 197 | // or 4 args: (tableName, location, tableRow, callback) - this case is caught by callbackIfLocationProvided being the callback 198 | // This specifically targets the (tableName, tableRow, callback) scenario for tableRowOrCallback 199 | if(typeof locationOrTableRow === 'object' && locationOrTableRow !== null) callback = tableRowOrCallback; 200 | } else if (typeof locationOrTableRow === 'function'){ 201 | //This case implies (tableName, callback) which is not a valid signature for insert. 202 | //but if it was the only function, it might be the callback. 203 | } 204 | 205 | 206 | if (typeof tableName !== 'string' || !tableName.trim()) { 207 | if (callback && typeof callback === 'function') callback(new Error("Table name must be a non-empty string.")); 208 | else if (typeof locationOrTableRow === 'function') locationOrTableRow(new Error("Table name must be a non-empty string.")); 209 | else if (typeof tableRowOrCallback === 'function') tableRowOrCallback(new Error("Table name must be a non-empty string.")); 210 | return; 211 | } 212 | 213 | if (typeof locationOrTableRow === 'string') { // Location is provided: (tableName, location, tableRow, callback) 214 | location = locationOrTableRow; 215 | tableRow = tableRowOrCallback; 216 | // callback is already callbackIfLocationProvided 217 | if (typeof callback !== 'function') { /* console.error("Callback missing for insertTableContent with location"); */ return; } 218 | if (typeof tableRow !== 'object' || tableRow === null) { 219 | callback(new Error("tableRow must be an object when location is provided.")); 220 | return; 221 | } 222 | } else if (typeof locationOrTableRow === 'object' && locationOrTableRow !== null) { // Location is NOT provided: (tableName, tableRow, callback) 223 | location = userData; 224 | tableRow = locationOrTableRow; 225 | callback = tableRowOrCallback; // This is the callback 226 | if (typeof callback !== 'function') { /* console.error("Callback missing for insertTableContent"); */ return; } 227 | } else { 228 | const msg = "Invalid arguments for insertTableContent. Expected (tableName, [location], tableRow, callback)."; 229 | if (callback && typeof callback === 'function') callback(new Error(msg)); 230 | // Attempt to call other potential callbacks if the main one isn't a function yet. 231 | else if (typeof tableRowOrCallback === 'function') tableRowOrCallback(new Error(msg + " Check tableRow.")); 232 | else if (typeof locationOrTableRow === 'function') locationOrTableRow(new Error(msg + " Check location or tableRow.")); 233 | return; 234 | } 235 | 236 | // Final validation of callback and tableRow 237 | if (typeof callback !== 'function') { /* console.error("Unable to determine callback for insertTableContent"); */ return; } 238 | if (typeof tableRow !== 'object' || tableRow === null) { // This might be redundant if covered above but good for safety 239 | callback(new Error("tableRow must be a valid object.")); 240 | return; 241 | } 242 | 243 | const fname = path.join(location, tableName + '.json'); 244 | 245 | fs.mkdir(location, { recursive: true }, (mkdirErr) => { 246 | if (mkdirErr) { 247 | callback(mkdirErr); 248 | return; 249 | } 250 | 251 | fs.access(fname, fs.constants.F_OK, (accessErr) => { 252 | if (accessErr) { 253 | callback(new Error(`Table/json file '${fname}' doesn't exist or is not accessible.`)); 254 | return; 255 | } 256 | 257 | fs.readFile(fname, 'utf-8', (readErr, fileContent) => { 258 | if (readErr) { 259 | callback(readErr); 260 | return; 261 | } 262 | 263 | let table; 264 | try { 265 | table = JSON.parse(fileContent); 266 | } catch (parseErr) { 267 | callback(parseErr); 268 | return; 269 | } 270 | 271 | if (!table[tableName] || !Array.isArray(table[tableName])) { 272 | table[tableName] = []; 273 | } 274 | 275 | let id; 276 | if (!tableRow['id']) { 277 | let date = new Date(); 278 | id = date.getTime(); 279 | tableRow['id'] = id; 280 | } else { 281 | id = tableRow['id']; 282 | } 283 | 284 | table[tableName].push(tableRow); 285 | 286 | fs.writeFile(fname, JSON.stringify(table, null, 2), (writeErr) => { 287 | if (writeErr) { 288 | callback(writeErr); 289 | } else { 290 | callback(null, { message: "Object written successfully!", id: id }); 291 | } 292 | }); 293 | }); 294 | }); 295 | }); 296 | } 297 | 298 | /** 299 | * Get all contents of the table/json file object 300 | * @param {string} arguments[0] [Table name] 301 | * @param {string} arguments[1] [Location of the database file] (Optional) 302 | * @param {Function} arguments[2] [callback function] 303 | */ 304 | // function getAll(tableName, callback) { 305 | // function getAll() { 306 | function getAll(tableName, locationOrCallback, callbackIfLocationProvided) { 307 | let location; 308 | // Removed duplicate declaration of location 309 | let callback; 310 | 311 | // Argument parsing and callback identification 312 | if (typeof locationOrCallback === 'string') { 313 | location = locationOrCallback; 314 | callback = callbackIfLocationProvided; 315 | if (typeof callback !== 'function') { /* console.error("Callback missing for getAll with location"); */ return; } 316 | } else if (typeof locationOrCallback === 'function') { 317 | location = userData; 318 | callback = locationOrCallback; 319 | } else { // Invalid second argument type 320 | if (typeof callbackIfLocationProvided === 'function') { // If 3rd arg is a func, maybe it was the intended cb 321 | callbackIfLocationProvided(new Error("Invalid arguments for getAll: Second argument must be location (string) or callback (function).")); 322 | } else if (typeof locationOrCallback === 'function') { //This should have been caught by the elseif above 323 | locationOrCallback(new Error("Invalid arguments for getAll: Second argument must be location (string) or callback (function).")); 324 | } 325 | return; 326 | } 327 | 328 | if (typeof callback !== 'function') { /* console.error("Unable to determine callback for getAll"); */ return; } 329 | 330 | if (typeof tableName !== 'string' || !tableName.trim()) { 331 | callback(new Error("Table name must be a non-empty string.")); 332 | return; 333 | } 334 | 335 | const fname = path.join(location, tableName + '.json'); 336 | 337 | fs.access(fname, fs.constants.F_OK, (accessErr) => { 338 | if (accessErr) { 339 | // Distinguish between ENOENT and other errors if necessary, or just propagate. 340 | // For getAll, if file not found, it's a clear case of not being able to get the data. 341 | callback(new Error(`Table file '${fname}' does not exist or is not accessible. ${accessErr.message}`)); 342 | return; 343 | } 344 | 345 | fs.readFile(fname, 'utf-8', (readErr, fileContent) => { 346 | if (readErr) { 347 | callback(readErr); // Propagate fs error 348 | return; 349 | } 350 | 351 | try { 352 | let table = JSON.parse(fileContent); 353 | if (table && table.hasOwnProperty(tableName)) { 354 | callback(null, table[tableName]); 355 | } else { 356 | callback(new Error(`Table '${tableName}' not found in file or file structure is invalid.`)); 357 | } 358 | } catch (parseErr) { 359 | callback(parseErr); // Propagate JSON parsing error 360 | } 361 | }); 362 | }); 363 | } 364 | 365 | /** 366 | * Find rows of a given field/key. 367 | * @param {string} arguments[0] Table name 368 | * @param {string} arguments[1] Location of the database file (Optional) 369 | * @param {string} arguments[2] They fey/field to retrieve. 370 | */ 371 | // function getField() { 372 | function getField(tableName, locationOrKey, keyOrCallback, callbackIfLocationProvided) { 373 | let location; 374 | let key; 375 | // Removed duplicate declarations of location and key 376 | let callback; 377 | 378 | // Identify callback first 379 | if (typeof callbackIfLocationProvided === 'function') callback = callbackIfLocationProvided; 380 | else if (typeof keyOrCallback === 'function' && typeof locationOrKey === 'string') callback = keyOrCallback; // (tableName, key, callback) 381 | // else if (typeof locationOrKey === 'function') callback = locationOrKey; // This would be (tableName, callback), invalid signature 382 | 383 | if (typeof tableName !== 'string' || !tableName.trim()) { 384 | if (callback && typeof callback === 'function') callback(new Error("Table name must be a non-empty string.")); 385 | // Attempt to call other potential callbacks if main one not ID'd 386 | else if (typeof locationOrKey === 'function') locationOrKey(new Error("Table name must be a non-empty string.")); 387 | else if (typeof keyOrCallback === 'function') keyOrCallback(new Error("Table name must be a non-empty string.")); 388 | return; 389 | } 390 | 391 | if (typeof locationOrKey === 'string') { 392 | if (typeof keyOrCallback === 'string' && typeof callbackIfLocationProvided === 'function') { // (tableName, location, key, callback) 393 | location = locationOrKey; 394 | key = keyOrCallback; 395 | callback = callbackIfLocationProvided; // already set 396 | } else if (typeof keyOrCallback === 'function') { // (tableName, key, callback) 397 | location = userData; 398 | key = locationOrKey; 399 | callback = keyOrCallback; // already set 400 | } else { // Invalid combination like (tableName, string, string, not_a_function) 401 | if (callback && typeof callback === 'function') callback(new Error("Invalid arguments for getField: Check key and callback parameters.")); 402 | else if (typeof callbackIfLocationProvided === 'function') callbackIfLocationProvided(new Error("Invalid arguments for getField.")); 403 | else if (typeof keyOrCallback === 'function') keyOrCallback(new Error("Invalid arguments for getField.")); 404 | return; 405 | } 406 | } else { // locationOrKey is not a string, implies invalid arguments 407 | if (callback && typeof callback === 'function') callback(new Error("Invalid arguments for getField: Second argument must be location (string) or key (string).")); 408 | else if (typeof keyOrCallback === 'function') keyOrCallback(new Error("Invalid arguments for getField: Second argument must be location (string) or key (string).")); 409 | else if (typeof locationOrKey === 'function') locationOrKey(new Error("Invalid arguments for getField: Second argument must be location (string) or key (string).")); 410 | return; 411 | } 412 | 413 | if (typeof callback !== 'function') { /* console.error("Unable to determine callback for getField"); */ return; } 414 | if (typeof key !== 'string' || !key.trim()) { 415 | callback(new Error("Key must be a non-empty string.")); 416 | return; 417 | } 418 | 419 | const fname = path.join(location, tableName + '.json'); 420 | 421 | fs.access(fname, fs.constants.F_OK, (accessErr) => { 422 | if (accessErr) { 423 | callback(new Error(`The table file '${fname}' does not exist or is not accessible. ${accessErr.message}`)); 424 | return; 425 | } 426 | 427 | fs.readFile(fname, 'utf-8', (readErr, fileContent) => { 428 | if (readErr) { 429 | callback(readErr); 430 | return; 431 | } 432 | 433 | try { 434 | let table = JSON.parse(fileContent); 435 | if (!table || !table.hasOwnProperty(tableName) || !Array.isArray(table[tableName])) { 436 | callback(new Error(`Table '${tableName}' not found or is invalid in file.`)); 437 | return; 438 | } 439 | const rows = table[tableName]; 440 | let data = []; 441 | let hasMatch = false; 442 | 443 | for (let i = 0; i < rows.length; i++) { 444 | if (rows[i] && rows[i].hasOwnProperty(key)) { 445 | data.push(rows[i][key]); 446 | hasMatch = true; 447 | } 448 | } 449 | 450 | if (!hasMatch) { 451 | callback(new Error(`The key/field '${key}' does not exist in any row of table '${tableName}'.`)); 452 | } else { 453 | callback(null, data); 454 | } 455 | } catch (parseErr) { 456 | callback(parseErr); 457 | } 458 | }); 459 | }); 460 | } 461 | 462 | /** 463 | * Clears an existing table leaving an empty list in the json file. 464 | * @param {string} arguments[0] [Table name] 465 | * @param {string} arguments[1] [Location of the database file] (Optional) 466 | * @param {Function} arguments[2] [callback function] 467 | */ 468 | // function clearTable() { 469 | function clearTable(tableName, locationOrCallback, callbackIfLocationProvided) { 470 | let location; 471 | // Removed duplicate declaration of location 472 | let callback; 473 | 474 | // Argument parsing and callback identification 475 | if (typeof locationOrCallback === 'string') { 476 | location = locationOrCallback; 477 | callback = callbackIfLocationProvided; 478 | if (typeof callback !== 'function') { /* console.error("Callback missing for clearTable with location"); */ return; } 479 | } else if (typeof locationOrCallback === 'function') { 480 | location = userData; 481 | callback = locationOrCallback; 482 | } else { 483 | if (typeof callbackIfLocationProvided === 'function') { 484 | callbackIfLocationProvided(new Error("Invalid arguments for clearTable: Second argument must be location (string) or callback (function).")); 485 | } else if (typeof locationOrCallback === 'function') { // Should be caught by above 486 | locationOrCallback(new Error("Invalid arguments for clearTable: Second argument must be location (string) or callback (function).")); 487 | } 488 | return; 489 | } 490 | 491 | if (typeof callback !== 'function') { /* console.error("Unable to determine callback for clearTable"); */ return; } 492 | 493 | if (typeof tableName !== 'string' || !tableName.trim()) { 494 | callback(new Error("Table name must be a non-empty string.")); 495 | return; 496 | } 497 | 498 | const fname = path.join(location, tableName + '.json'); 499 | 500 | fs.mkdir(location, { recursive: true }, (mkdirErr) => { 501 | if (mkdirErr) { 502 | callback(mkdirErr); 503 | return; 504 | } 505 | 506 | fs.access(fname, fs.constants.F_OK, (accessErr) => { 507 | if (accessErr) { 508 | callback(new Error(`The table file '${fname}' you are trying to clear does not exist or is not accessible. ${accessErr.message}`)); 509 | return; 510 | } 511 | 512 | let obj = new Object(); 513 | obj[tableName] = []; 514 | 515 | fs.writeFile(fname, JSON.stringify(obj, null, 2), (writeErr) => { 516 | if (writeErr) { 517 | callback(writeErr); 518 | } else { 519 | callback(null, `Table '${tableName}' cleared successfully at ${location}.`); 520 | } 521 | }); 522 | }); 523 | }); 524 | } 525 | 526 | /** 527 | * Count the number of rows for a given table. 528 | * @param {string} FirstArgument Table name 529 | * @param {string} SecondArgument Location of the database file (Optional) 530 | * @param {callback} ThirdArgument Function callback 531 | */ 532 | // function count() { 533 | function count(tableName, locationOrCallback, callbackIfLocationProvided) { 534 | let location; 535 | let callback; 536 | 537 | if (typeof tableName !== 'string' || !tableName.trim()) { 538 | if (typeof locationOrCallback === 'function') locationOrCallback(false, "Table name must be a non-empty string."); 539 | else if (typeof callbackIfLocationProvided === 'function') callbackIfLocationProvided(false, "Table name must be a non-empty string."); 540 | return; 541 | } 542 | 543 | // count(tableName, callback) 544 | // count(tableName, location, callback) 545 | if (typeof locationOrCallback === 'string') { // location provided 546 | location = locationOrCallback; 547 | callback = callbackIfLocationProvided; 548 | if (typeof callback !== 'function') { /* console.error("Callback missing for count with location"); */ return; } 549 | } else if (typeof locationOrCallback === 'function') { // location not provided 550 | // location = userData; // userData will be handled by getAll if location is not passed 551 | callback = locationOrCallback; 552 | } else { 553 | if (typeof callbackIfLocationProvided === 'function') { 554 | callbackIfLocationProvided(new Error('Invalid arguments for count. Second argument must be location (string) or callback (function).')); 555 | } else if (typeof locationOrCallback === 'function'){ // Should be caught above 556 | locationOrCallback(new Error('Invalid arguments for count. Second argument must be location (string) or callback (function).')); 557 | } 558 | return; 559 | } 560 | 561 | if (typeof callback !== 'function') { /* console.error("Unable to determine callback for count"); */ return; } 562 | 563 | if (typeof tableName !== 'string' || !tableName.trim()) { 564 | callback(new Error("Table name must be a non-empty string.")); 565 | return; 566 | } 567 | 568 | const getAllCallback = (err, data) => { 569 | if (err) { 570 | // If err is already an Error object from getAll, just pass it. 571 | // If it was a string message (legacy), wrap it. 572 | // Based on current getAll refactor, it should be an Error object. 573 | callback(err); 574 | } else { 575 | callback(null, data.length); 576 | } 577 | }; 578 | 579 | if (typeof location === 'string') { // location was determined to be a string 580 | getAll(tableName, location, getAllCallback); 581 | } else { // location was not provided (or determined to be userData by default path in prior logic) 582 | getAll(tableName, getAllCallback); // Calls getAll(tableName, callback) version 583 | } 584 | } 585 | 586 | /** 587 | * Get row or rows that matched the given condition(s) in WHERE argument 588 | * @param {string} FirstArgument Table name 589 | * @param {string} SecondArgument Location of the database file (Optional) 590 | * @param {object} ThirdArgument Collection of conditions to be met 591 | ``` 592 | { 593 | key1: value1, 594 | key2: value2, 595 | ... 596 | } 597 | ``` 598 | * @param {callback} FourthArgument Function callback 599 | */ 600 | // function getRows() { 601 | function getRows(tableName, locationOrWhere, whereOrCallback, callbackIfLocationProvided) { 602 | let location; 603 | let where; 604 | // Removed duplicate declarations of location and where 605 | let callback; 606 | 607 | // Identify callback 608 | if(typeof callbackIfLocationProvided === 'function') callback = callbackIfLocationProvided; 609 | else if(typeof whereOrCallback === 'function' && (typeof locationOrWhere === 'string' || typeof locationOrWhere === 'object')) callback = whereOrCallback; 610 | // else if (typeof locationOrWhere === 'function') // invalid signature 611 | 612 | if (typeof tableName !== 'string' || !tableName.trim()) { 613 | if (callback && typeof callback === 'function') callback(new Error("Table name must be a non-empty string.")); 614 | else if(typeof locationOrWhere === 'function') locationOrWhere(new Error("Table name must be a non-empty string.")); 615 | else if(typeof whereOrCallback === 'function') whereOrCallback(new Error("Table name must be a non-empty string.")); 616 | return; 617 | } 618 | 619 | if (typeof locationOrWhere === 'string') { 620 | location = locationOrWhere; 621 | where = whereOrCallback; 622 | // callback = callbackIfLocationProvided; // already set 623 | } else if (typeof locationOrWhere === 'object' && locationOrWhere !== null) { 624 | location = userData; 625 | where = locationOrWhere; 626 | // callback = whereOrCallback; // already set 627 | } else { 628 | const msg = "Invalid arguments for getRows: Second argument must be location (string) or where (object)."; 629 | if (callback && typeof callback === 'function') callback(new Error(msg)); 630 | else if (typeof whereOrCallback === 'function') whereOrCallback(new Error(msg)); 631 | else if (typeof locationOrWhere === 'function') locationOrWhere(new Error(msg)); 632 | return; 633 | } 634 | 635 | if (typeof callback !== 'function') { /* console.error("Unable to determine callback for getRows"); */ return; } 636 | if (typeof where !== 'object' || where === null) { 637 | callback(new Error("WHERE clause must be an object.")); 638 | return; 639 | } 640 | 641 | const whereKeys = Object.keys(where); 642 | if (whereKeys.length === 0) { 643 | callback(new Error("There are no conditions passed to the WHERE clause.")); 644 | return; 645 | } 646 | 647 | const fname = path.join(location, tableName + '.json'); 648 | 649 | fs.access(fname, fs.constants.F_OK, (accessErr) => { 650 | if (accessErr) { 651 | callback(new Error(`Table file '${fname}' does not exist or is not accessible. ${accessErr.message}`)); 652 | return; 653 | } 654 | 655 | fs.readFile(fname, 'utf-8', (readErr, fileContent) => { 656 | if (readErr) { 657 | callback(readErr); 658 | return; 659 | } 660 | 661 | try { 662 | let table = JSON.parse(fileContent); 663 | if (!table || !table.hasOwnProperty(tableName) || !Array.isArray(table[tableName])) { 664 | callback(new Error(`Table '${tableName}' not found or is invalid in file.`)); 665 | return; 666 | } 667 | const rows = table[tableName]; 668 | let objs = []; 669 | 670 | for (let i = 0; i < rows.length; i++) { 671 | let matchedCount = 0; 672 | if(rows[i]){ 673 | for (let j = 0; j < whereKeys.length; j++) { 674 | const currentKey = whereKeys[j]; 675 | if (rows[i].hasOwnProperty(currentKey) && rows[i][currentKey] === where[currentKey]) { 676 | matchedCount++; 677 | } 678 | } 679 | } 680 | if (matchedCount === whereKeys.length) { 681 | objs.push(rows[i]); 682 | } 683 | } 684 | callback(null, objs); 685 | } catch (parseErr) { 686 | callback(parseErr); 687 | } 688 | }); 689 | }); 690 | } 691 | 692 | /** 693 | * Update a row or record which satisfies the where clause 694 | * @param {[string]} arguments[0] [Table name] 695 | * @param {string} arguments[1] [Location of the database file] (Optional) 696 | * @param {[object]} arguments[2] [Objet for WHERE clause] 697 | * @param {[object]} arguments[3] [Object for SET clause] 698 | * @param {Function} arguments[4] [Callback function] 699 | */ 700 | // function updateRow(tableName, where, set, callback) { 701 | // function updateRow() { 702 | function updateRow(tableName, locationOrWhere, whereOrSet, setOrCallback, callbackIfLocationProvided) { 703 | let location; 704 | let where; 705 | let set; 706 | // Removed duplicate declarations of location, where, and set 707 | let callback; 708 | 709 | // Identify callback 710 | if(typeof callbackIfLocationProvided === 'function') callback = callbackIfLocationProvided; 711 | else if(typeof setOrCallback === 'function' && (typeof whereOrSet === 'object' || typeof whereOrSet === 'string') && typeof locationOrWhere === 'object') callback = setOrCallback; // (tableName, where, set, callback) 712 | // More complex cases for callback identification might be needed if signatures are very flexible 713 | 714 | if (typeof tableName !== 'string' || !tableName.trim()) { 715 | if(callback && typeof callback === 'function') callback(new Error("Table name must be a non-empty string.")); 716 | // ... attempt other potential callbacks ... 717 | else if (typeof locationOrWhere === 'function') locationOrWhere(new Error("Table name must be a non-empty string.")); 718 | return; 719 | } 720 | 721 | if (typeof locationOrWhere === 'string') { 722 | location = locationOrWhere; 723 | where = whereOrSet; 724 | set = setOrCallback; 725 | // callback = callbackIfLocationProvided; // already set 726 | } else if (typeof locationOrWhere === 'object' && locationOrWhere !== null) { 727 | location = userData; 728 | where = locationOrWhere; 729 | set = whereOrSet; 730 | callback = setOrCallback; 731 | } else { 732 | const msg = "Invalid arguments for updateRow: Check location, where, or set parameters."; 733 | if (callback && typeof callback === 'function') callback(new Error(msg)); 734 | // ... attempt other potential callbacks ... 735 | return; 736 | } 737 | 738 | if (typeof callback !== 'function') { /* console.error("Unable to determine callback for updateRow"); */ return; } 739 | if (typeof where !== 'object' || where === null) { 740 | callback(new Error("WHERE clause must be an object.")); 741 | return; 742 | } 743 | const whereKeys = Object.keys(where); // Define whereKeys early for the check 744 | if (whereKeys.length === 0) { 745 | callback(new Error("Aborting update: WHERE clause is empty. Updating all rows is not permitted by this function.")); 746 | return; 747 | } 748 | if (typeof set !== 'object' || set === null) { 749 | callback(new Error("SET clause must be an object.")); 750 | return; 751 | } 752 | 753 | const fname = path.join(location, tableName + '.json'); 754 | 755 | fs.mkdir(location, { recursive: true }, (mkdirErr) => { 756 | if (mkdirErr) { 757 | callback(mkdirErr); 758 | return; 759 | } 760 | 761 | fs.access(fname, fs.constants.F_OK, (accessErr) => { 762 | if (accessErr) { 763 | callback(new Error(`Table file '${fname}' does not exist or is not accessible. ${accessErr.message}`)); 764 | return; 765 | } 766 | 767 | fs.readFile(fname, 'utf-8', (readErr, fileContent) => { 768 | if (readErr) { 769 | callback(readErr); 770 | return; 771 | } 772 | 773 | let table; 774 | try { 775 | table = JSON.parse(fileContent); 776 | } catch (parseErr) { 777 | callback(parseErr); 778 | return; 779 | } 780 | 781 | if (!table || !table.hasOwnProperty(tableName) || !Array.isArray(table[tableName])) { 782 | callback(new Error(`Table '${tableName}' not found or is invalid in file.`)); 783 | return; 784 | } 785 | 786 | let rows = table[tableName]; 787 | // const whereKeys = Object.keys(where); // Moved up 788 | const setKeys = Object.keys(set); 789 | let recordsUpdatedCount = 0; 790 | 791 | rows.forEach(row => { 792 | if (!row) return; // Skip if row is null or undefined 793 | 794 | let allConditionsMet = true; 795 | for (const key of whereKeys) { 796 | if (!row.hasOwnProperty(key) || row[key] !== where[key]) { 797 | allConditionsMet = false; 798 | break; 799 | } 800 | } 801 | 802 | if (allConditionsMet) { 803 | for (const keyToSet of setKeys) { 804 | row[keyToSet] = set[keyToSet]; 805 | } 806 | recordsUpdatedCount++; 807 | } 808 | }); 809 | 810 | if (recordsUpdatedCount > 0) { 811 | table[tableName] = rows; 812 | fs.writeFile(fname, JSON.stringify(table, null, 2), (writeErr) => { 813 | if (writeErr) { 814 | callback(writeErr); 815 | } else { 816 | callback(null, { message: `Successfully updated ${recordsUpdatedCount} row(s) in table '${tableName}'.`, count: recordsUpdatedCount }); 817 | } 818 | }); 819 | } else { 820 | // No rows matched the criteria, so no file write needed. 821 | callback(null, { message: `No rows matched the criteria in table '${tableName}'. Nothing updated.`, count: 0 }); 822 | } 823 | }); 824 | }); 825 | }); 826 | } 827 | 828 | /** 829 | * Searching function 830 | * @param {string} arguments[0] Name of the table to search for 831 | * @param {string} arguments[1] [Location of the database file] (Optional) 832 | * @param {string} arguments[2] Name of the column/key to match 833 | * @param {object} arguments[3] The part of the value of the key that is being lookup 834 | * @param {function} arguments[4] Callback function 835 | */ 836 | // function search(tableName, field, keyword, callback) { 837 | // function search() { 838 | function search(tableName, locationOrField, fieldOrKeyword, keywordOrCallback, callbackIfLocationProvided) { 839 | let location; 840 | let field; 841 | let keyword; 842 | // Removed duplicate declarations of location, field, and keyword 843 | let callback; 844 | 845 | // Identify callback 846 | if(typeof callbackIfLocationProvided === 'function') callback = callbackIfLocationProvided; 847 | else if(typeof keywordOrCallback === 'function') { // Covers (tableName, field, keyword, callback) and (tableName, location, field, callback) - needs more check 848 | if(typeof fieldOrKeyword === 'string' && typeof locationOrField === 'string') callback = keywordOrCallback; // (tableName, location, field, callback) 849 | else if (typeof fieldOrKeyword !== 'function' && typeof locationOrField === 'string') callback = keywordOrCallback; // (tableName, field, keyword, callback) 850 | } 851 | 852 | if (typeof tableName !== 'string' || !tableName.trim()) { 853 | if(callback && typeof callback === 'function') callback(new Error("Table name must be a non-empty string.")); 854 | // ... other attempts 855 | return; 856 | } 857 | 858 | if (typeof locationOrField === 'string') { 859 | if (typeof fieldOrKeyword === 'string' && typeof keywordOrCallback !== 'function' && typeof callbackIfLocationProvided === 'function') { // (tableName, location, field, keyword, callback) 860 | location = locationOrField; 861 | field = fieldOrKeyword; 862 | keyword = keywordOrCallback; 863 | // callback = callbackIfLocationProvided; // already set 864 | } else if (typeof fieldOrKeyword !== 'function' && typeof keywordOrCallback === 'function') { // (tableName, field, keyword, callback) 865 | location = userData; 866 | field = locationOrField; 867 | keyword = fieldOrKeyword; 868 | callback = keywordOrCallback; 869 | } else { 870 | const msg = "Invalid arguments for search: Structure doesn't match expected (tableName, [location], field, keyword, callback)."; 871 | if (callback && typeof callback === 'function') callback(new Error(msg)); 872 | // ... other attempts 873 | return; 874 | } 875 | } else { 876 | const msg = "Invalid arguments for search: Second argument (location or field) must be a string."; 877 | if (callback && typeof callback === 'function') callback(new Error(msg)); 878 | // ... other attempts 879 | return; 880 | } 881 | 882 | if (typeof callback !== 'function') { /* console.error("Unable to determine callback for search"); */ return; } 883 | if (typeof field !== 'string' || !field.trim()) { 884 | callback(new Error("Field must be a non-empty string.")); 885 | return; 886 | } 887 | // Keyword can be any type, will be converted to string for search. 888 | 889 | const fname = path.join(location, tableName + '.json'); 890 | 891 | fs.access(fname, fs.constants.F_OK, (accessErr) => { 892 | if (accessErr) { 893 | callback(new Error(`Table file '${fname}' does not exist or is not accessible. ${accessErr.message}`)); 894 | return; 895 | } 896 | 897 | fs.readFile(fname, 'utf-8', (readErr, fileContent) => { 898 | if (readErr) { 899 | callback(readErr); 900 | return; 901 | } 902 | 903 | try { 904 | let table = JSON.parse(fileContent); 905 | if (!table || !table.hasOwnProperty(tableName) || !Array.isArray(table[tableName])) { 906 | callback(new Error(`Table '${tableName}' not found or is invalid in file.`)); 907 | return; 908 | } 909 | const rows = table[tableName]; 910 | let foundRows = []; 911 | let fieldMissingInRow = false; 912 | 913 | if (rows.length > 0) { 914 | for (let i = 0; i < rows.length; i++) { 915 | if (rows[i] && rows[i].hasOwnProperty(field)) { 916 | const value = String(rows[i][field]).toLowerCase(); 917 | const searchKeyword = String(keyword).toLowerCase(); 918 | if (value.includes(searchKeyword)) { 919 | foundRows.push(rows[i]); 920 | } 921 | } else { 922 | // Field is missing in at least one row. 923 | fieldMissingInRow = true; 924 | break; 925 | } 926 | } 927 | } 928 | 929 | if (fieldMissingInRow) { 930 | callback(new Error(`Field '${field}' not found in one or more rows during search in table '${tableName}'.`)); 931 | } else { 932 | callback(null, foundRows); 933 | } 934 | 935 | } catch (parseErr) { 936 | callback(parseErr); 937 | } 938 | }); 939 | }); 940 | } 941 | 942 | /** 943 | * Delete a row specified. 944 | * @param {*} tableName 945 | * @param {string} arguments[1] [Location of the database file] (Optional) 946 | * @param {*} where 947 | * @param {*} callback 948 | */ 949 | // function deleteRow(tableName, where, callback) { 950 | // function deleteRow() { 951 | function deleteRow(tableName, locationOrWhere, whereOrCallback, callbackIfLocationProvided) { 952 | let location; 953 | let where; 954 | // Removed duplicate declarations of location and where 955 | let callback; 956 | 957 | // Identify callback 958 | if(typeof callbackIfLocationProvided === 'function') callback = callbackIfLocationProvided; 959 | else if(typeof whereOrCallback === 'function' && (typeof locationOrWhere === 'string' || typeof locationOrWhere === 'object')) callback = whereOrCallback; 960 | 961 | if (typeof tableName !== 'string' || !tableName.trim()) { 962 | if(callback && typeof callback === 'function') callback(new Error("Table name must be a non-empty string.")); 963 | // ... other attempts 964 | return; 965 | } 966 | 967 | if (typeof locationOrWhere === 'string') { 968 | location = locationOrWhere; 969 | where = whereOrCallback; 970 | // callback = callbackIfLocationProvided; // already set 971 | } else if (typeof locationOrWhere === 'object' && locationOrWhere !== null) { 972 | location = userData; 973 | where = locationOrWhere; 974 | // callback = whereOrCallback; // already set 975 | } else { 976 | const msg = "Invalid arguments for deleteRow: Second argument must be location (string) or where (object)."; 977 | if(callback && typeof callback === 'function') callback(new Error(msg)); 978 | // ... other attempts 979 | return; 980 | } 981 | 982 | if (typeof callback !== 'function') { /* console.error("Unable to determine callback for deleteRow"); */ return; } 983 | if (typeof where !== 'object' || where === null) { 984 | callback(new Error("WHERE clause must be an object.")); 985 | return; 986 | } 987 | if (Object.keys(where).length === 0) { // Prevent deleting all rows if where is {} 988 | callback(new Error("WHERE clause cannot be empty for deleteRow operation. Provide conditions or use clearTable.")); 989 | return; 990 | } 991 | 992 | 993 | const fname = path.join(location, tableName + '.json'); 994 | 995 | fs.mkdir(location, { recursive: true }, (mkdirErr) => { 996 | if (mkdirErr) { 997 | callback(mkdirErr); 998 | return; 999 | } 1000 | 1001 | fs.access(fname, fs.constants.F_OK, (accessErr) => { 1002 | if (accessErr) { 1003 | callback(new Error(`Table file '${fname}' does not exist or is not accessible. ${accessErr.message}`)); 1004 | return; 1005 | } 1006 | 1007 | fs.readFile(fname, 'utf-8', (readErr, fileContent) => { 1008 | if (readErr) { 1009 | callback(readErr); 1010 | return; 1011 | } 1012 | 1013 | let table; 1014 | try { 1015 | table = JSON.parse(fileContent); 1016 | } catch (parseErr) { 1017 | callback(parseErr); 1018 | return; 1019 | } 1020 | 1021 | if (!table || !table.hasOwnProperty(tableName) || !Array.isArray(table[tableName])) { 1022 | callback(new Error(`Table '${tableName}' not found or is invalid in file.`)); 1023 | return; 1024 | } 1025 | 1026 | let rows = table[tableName]; 1027 | const whereKeys = Object.keys(where); 1028 | let originalRowCount = rows.length; 1029 | 1030 | const newRows = rows.filter(row => { 1031 | if (!row) return true; 1032 | for (const key of whereKeys) { 1033 | if (!row.hasOwnProperty(key) || row[key] !== where[key]) { 1034 | return true; 1035 | } 1036 | } 1037 | return false; 1038 | }); 1039 | 1040 | if (newRows.length < originalRowCount) { 1041 | table[tableName] = newRows; 1042 | fs.writeFile(fname, JSON.stringify(table, null, 2), (writeErr) => { 1043 | if (writeErr) { 1044 | callback(writeErr); 1045 | } else { 1046 | const deletedCount = originalRowCount - newRows.length; 1047 | callback(null, { message: `Successfully deleted ${deletedCount} row(s) from table '${tableName}'.`, count: deletedCount }); 1048 | } 1049 | }); 1050 | } else { 1051 | // No rows matched the criteria, so no file write needed. 1052 | callback(null, { message: `No rows matched the criteria in table '${tableName}'. Nothing deleted.`, count: 0 }); 1053 | } 1054 | }); 1055 | }); 1056 | }); 1057 | } 1058 | 1059 | /** 1060 | * Check table existence 1061 | * @param {String} dbName - Table name 1062 | * @param {String} dbLocation - Table location path 1063 | * @return {Boolean} checking result 1064 | */ 1065 | // function tableExists() { 1066 | // function tableExists(dbName, location) { 1067 | function tableExists(dbName, location, callback) { 1068 | if (typeof callback !== 'function') { 1069 | // console.error("Error: tableExists called without a valid callback function."); 1070 | return; // Cannot proceed or report error without a callback 1071 | } 1072 | if (typeof dbName !== 'string' || !dbName.trim()) { 1073 | callback(new Error("DB name must be a non-empty string for tableExists().")); 1074 | return; 1075 | } 1076 | const dbPath = location || userData; 1077 | const fName = path.join(dbPath, dbName + '.json'); 1078 | 1079 | fs.access(fName, fs.constants.F_OK, (err) => { 1080 | if (err) { 1081 | if (err.code === 'ENOENT') { // File does not exist 1082 | callback(null, false); 1083 | } else { // Other error, like permission issue 1084 | callback(err); 1085 | } 1086 | } else { // File exists 1087 | callback(null, true); 1088 | } 1089 | }); 1090 | } 1091 | 1092 | 1093 | /** 1094 | * Insert an array of objects into a table. Each object will be appended with an 'id' property 1095 | * if it doesn't already have one. The 'id' uses a timestamp plus an index to ensure uniqueness within the batch. 1096 | * @param {string} tableName - The name of the table. 1097 | * @param {string} [locationOrRows] - Optional. The directory location of the table file OR the array of row objects if location is default. 1098 | * @param {Array} [rowsOrCb] - The array of row objects to insert OR the callback function if location was provided. 1099 | * @param {function} [callbackIfLocation] - The callback function if location and rows array were provided. 1100 | */ 1101 | function insertTableContents(tableName, locationOrRows, rowsOrCb, callbackIfLocation) { 1102 | let location; 1103 | let tableRowsArray; 1104 | let callback; 1105 | 1106 | // Argument parsing 1107 | if (typeof locationOrRows === 'string') { 1108 | location = locationOrRows; 1109 | if (!Array.isArray(rowsOrCb)) { // rowsOrCb should be the array 1110 | if (typeof callbackIfLocation === 'function') { 1111 | return callbackIfLocation(new Error("Rows argument must be an array when location is specified.")); 1112 | } else if (typeof rowsOrCb === 'function') { // Maybe rowsOrCb was intended as callback 1113 | return rowsOrCb(new Error("Rows argument must be an array when location is specified.")); 1114 | } 1115 | return; // No valid callback to report error 1116 | } 1117 | tableRowsArray = rowsOrCb; 1118 | callback = callbackIfLocation; 1119 | } else if (Array.isArray(locationOrRows)) { 1120 | location = userData; 1121 | tableRowsArray = locationOrRows; 1122 | callback = rowsOrCb; 1123 | } else { 1124 | // Attempt to find a callback for error reporting if arguments are wrong from the start 1125 | let potentialCb = callbackIfLocation || rowsOrCb || locationOrRows; 1126 | if (typeof potentialCb === 'function') { 1127 | potentialCb(new Error("Invalid arguments for insertTableContents. Expected (tableName, [location], rowsArray, callback).")); 1128 | } 1129 | return; 1130 | } 1131 | 1132 | if (typeof callback !== 'function') { 1133 | // console.error("Callback function is not defined after parsing arguments for insertTableContents."); 1134 | return; 1135 | } 1136 | 1137 | if (typeof tableName !== 'string' || !tableName.trim()) { 1138 | return callback(new Error("Table name must be a non-empty string.")); 1139 | } 1140 | if (!Array.isArray(tableRowsArray) || tableRowsArray.length === 0) { 1141 | return callback(new Error("Input must be a non-empty array of objects.")); 1142 | } 1143 | if (!tableRowsArray.every(item => typeof item === 'object' && item !== null)) { 1144 | return callback(new Error("All items in the input array must be objects.")); 1145 | } 1146 | 1147 | const fname = path.join(location, tableName + '.json'); 1148 | 1149 | fs.mkdir(location, { recursive: true }, (mkdirErr) => { 1150 | if (mkdirErr) { 1151 | return callback(mkdirErr); 1152 | } 1153 | 1154 | fs.access(fname, fs.constants.F_OK, (accessErr) => { 1155 | if (accessErr) { // Assuming table must exist, like insertTableContent 1156 | return callback(new Error(`Table file '${fname}' does not exist or is not accessible. ${accessErr.message}`)); 1157 | } 1158 | 1159 | fs.readFile(fname, 'utf-8', (readErr, fileContent) => { 1160 | if (readErr) { 1161 | return callback(readErr); 1162 | } 1163 | 1164 | let table; 1165 | try { 1166 | table = JSON.parse(fileContent); 1167 | } catch (parseErr) { 1168 | return callback(parseErr); 1169 | } 1170 | 1171 | if (!table[tableName] || !Array.isArray(table[tableName])) { 1172 | return callback(new Error(`Table '${tableName}' structure is invalid or not found in file.`)); 1173 | } 1174 | 1175 | const insertedIds = []; 1176 | const baseTimestamp = new Date().getTime(); 1177 | 1178 | tableRowsArray.forEach((row, index) => { 1179 | let currentId = row.id; 1180 | if (currentId === undefined || currentId === null || String(currentId).trim() === "") { 1181 | currentId = `${baseTimestamp}-${index}`; 1182 | row.id = currentId; 1183 | } 1184 | table[tableName].push(row); // Add to existing table data 1185 | insertedIds.push(currentId); 1186 | }); 1187 | 1188 | fs.writeFile(fname, JSON.stringify(table, null, 2), (writeErr) => { 1189 | if (writeErr) { 1190 | return callback(writeErr); 1191 | } 1192 | callback(null, { message: `Successfully inserted ${insertedIds.length} object(s) into table '${tableName}'.`, ids: insertedIds }); 1193 | }); 1194 | }); 1195 | }); 1196 | }); 1197 | } 1198 | 1199 | 1200 | // Export the public available functions 1201 | module.exports = { 1202 | createTable, 1203 | insertTableContent, 1204 | getAll, 1205 | getRows, 1206 | updateRow, 1207 | search, 1208 | deleteRow, 1209 | valid, 1210 | clearTable, 1211 | getField, 1212 | count, 1213 | tableExists, 1214 | _getInternals: () => ({ userData }), // Added for testing 1215 | insertTableContents // Added new function 1216 | }; 1217 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-db", 3 | "version": "0.15.5", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-db", 3 | "version": "0.15.5", 4 | "description": "Electron module that simulates database table operations which really just uses json file to store tables in the Application Folder.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "node test.js" 9 | }, 10 | "author": "Alexius Academia (https://alexiusacademia.com)", 11 | "license": "MIT", 12 | "dependencies": {}, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/alexiusacademia/electron-db" 16 | }, 17 | "devDependencies": {}, 18 | "keywords": [ 19 | "electron", 20 | "database", 21 | "json database", 22 | "json table", 23 | "electron database", 24 | "electron simple database" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const db = require('./index'); 2 | const fs = require('fs').promises; 3 | const fsSync = require('fs'); // For specific sync operations if needed during test setup 4 | const path = require('path'); 5 | const os = require('os'); // Required for replicating default path logic if _getInternals is not used 6 | 7 | const TEST_DIR_CUSTOM = path.join(__dirname, 'test_db_collections_custom'); 8 | // Default path will be retrieved using db._getInternals() 9 | let DEFAULT_USER_DATA_PATH; 10 | // Specific table names for default path tests to allow targeted cleanup 11 | const DEFAULT_PATH_TABLE_1 = 'test_default_table_1'; 12 | const DEFAULT_PATH_TABLE_2_FOR_VALID_TEST = 'test_default_table_2_for_valid_test'; 13 | 14 | let testsPassed = 0; 15 | let testsFailed = 0; 16 | 17 | // Helper to create a promise-based version of db functions 18 | function promisifyDb(fnName) { 19 | return (...args) => { 20 | return new Promise((resolve, reject) => { 21 | db[fnName](...args, (err, data) => { 22 | if (err) { 23 | return reject(err); 24 | } 25 | resolve(data); 26 | }); 27 | }); 28 | }; 29 | } 30 | 31 | const createTableAsync = promisifyDb('createTable'); 32 | const tableExistsAsync = promisifyDb('tableExists'); 33 | const validAsync = promisifyDb('valid'); 34 | const insertTableContentAsync = promisifyDb('insertTableContent'); 35 | const getAllAsync = promisifyDb('getAll'); 36 | const getRowsAsync = promisifyDb('getRows'); 37 | const updateRowAsync = promisifyDb('updateRow'); 38 | const deleteRowAsync = promisifyDb('deleteRow'); 39 | const searchAsync = promisifyDb('search'); 40 | const getFieldAsync = promisifyDb('getField'); 41 | const countAsync = promisifyDb('count'); 42 | const clearTableAsync = promisifyDb('clearTable'); 43 | const insertTableContentsAsync = promisifyDb('insertTableContents'); 44 | 45 | 46 | async function runTest(description, testFn) { 47 | console.log(`\nRunning: ${description}`); 48 | try { 49 | await testFn(); 50 | console.log(` PASS: ${description}`); 51 | testsPassed++; 52 | } catch (error) { 53 | console.error(` FAIL: ${description}`); 54 | console.error(' Error:', error.message || error); 55 | if (error.stack) { 56 | // console.error(' Stack:', error.stack.split('\n').slice(1).join('\n')); 57 | } 58 | testsFailed++; 59 | } 60 | } 61 | 62 | async function setup() { 63 | console.log('Setting up test environment...'); 64 | await fs.mkdir(TEST_DIR_CUSTOM, { recursive: true }); 65 | 66 | // Get default userData path from the module itself for cleanup 67 | if (db._getInternals && typeof db._getInternals === 'function') { 68 | try { 69 | DEFAULT_USER_DATA_PATH = db._getInternals().userData; 70 | console.log(` Default userData path for cleanup: ${DEFAULT_USER_DATA_PATH}`); 71 | } catch(e) { 72 | console.warn(" Could not get userData path from db._getInternals(). Default path cleanup might be incomplete."); 73 | } 74 | } else { 75 | console.warn(" db._getInternals() not available. Default path cleanup might be incomplete."); 76 | } 77 | console.log('Setup complete.'); 78 | } 79 | 80 | async function cleanup() { 81 | console.log('\nCleaning up test environment...'); 82 | try { 83 | await fs.rm(TEST_DIR_CUSTOM, { recursive: true, force: true }); 84 | console.log(` Removed custom test directory: ${TEST_DIR_CUSTOM}`); 85 | } catch (e) { 86 | console.error(` Error removing custom test directory: ${TEST_DIR_CUSTOM}`, e.message); 87 | } 88 | 89 | if (DEFAULT_USER_DATA_PATH) { 90 | try { 91 | const defaultTable1Path = path.join(DEFAULT_USER_DATA_PATH, DEFAULT_PATH_TABLE_1 + '.json'); 92 | if (fsSync.existsSync(defaultTable1Path)) { 93 | await fs.unlink(defaultTable1Path); 94 | console.log(` Cleaned up default path table: ${defaultTable1Path}`); 95 | } 96 | const defaultTable2Path = path.join(DEFAULT_USER_DATA_PATH, DEFAULT_PATH_TABLE_2_FOR_VALID_TEST + '.json'); 97 | if (fsSync.existsSync(defaultTable2Path)) { 98 | await fs.unlink(defaultTable2Path); 99 | console.log(` Cleaned up default path table: ${defaultTable2Path}`); 100 | } 101 | } catch (e) { 102 | console.error(` Error cleaning default path tables: `, e.message); 103 | } 104 | } 105 | console.log('Cleanup complete.'); 106 | } 107 | 108 | async function main() { 109 | await setup(); 110 | 111 | // --- createTable --- 112 | await runTest("createTable: successfully creates a table at custom location", async () => { 113 | const tableName = "usersCustom"; 114 | const result = await createTableAsync(tableName, TEST_DIR_CUSTOM); 115 | if (!result || !result.toLowerCase().includes("success")) throw new Error("Success message not received."); 116 | const tablePath = path.join(TEST_DIR_CUSTOM, tableName + '.json'); 117 | if (!fsSync.existsSync(tablePath)) throw new Error(`Table file not found at ${tablePath}`); 118 | }); 119 | 120 | await runTest("createTable: successfully creates a table at default location", async () => { 121 | // This test relies on DEFAULT_USER_DATA_PATH being set for cleanup. 122 | if (!DEFAULT_USER_DATA_PATH) { 123 | console.warn(" Skipping default path test for createTable as DEFAULT_USER_DATA_PATH is not set."); 124 | testsPassed++; // Count as passed to not fail the suite due to setup issue outside test's control 125 | return; 126 | } 127 | const result = await createTableAsync(DEFAULT_PATH_TABLE_1); // No location, uses default 128 | if (!result || !result.toLowerCase().includes("success")) throw new Error("Success message not received for default path."); 129 | const tablePath = path.join(DEFAULT_USER_DATA_PATH, DEFAULT_PATH_TABLE_1 + '.json'); 130 | if (!fsSync.existsSync(tablePath)) throw new Error(`Default table file not found at ${tablePath}`); 131 | }); 132 | 133 | await runTest("createTable: fails if table already exists", async () => { 134 | const tableName = "usersCustomExists"; 135 | await createTableAsync(tableName, TEST_DIR_CUSTOM); // Create first time 136 | try { 137 | await createTableAsync(tableName, TEST_DIR_CUSTOM); // Attempt to create again 138 | throw new Error("createTable should have failed for existing table."); 139 | } catch (error) { 140 | if (!error.message.includes("already exists")) { 141 | throw new Error(`Expected 'already exists' error, got: ${error.message}`); 142 | } 143 | } 144 | }); 145 | 146 | // --- tableExists --- 147 | await runTest("tableExists: returns true for an existing table (custom)", async () => { 148 | const tableName = "checkExistsCustom"; 149 | await createTableAsync(tableName, TEST_DIR_CUSTOM); 150 | const exists = await tableExistsAsync(tableName, TEST_DIR_CUSTOM); 151 | if (!exists) throw new Error("tableExists returned false for an existing table."); 152 | }); 153 | 154 | await runTest("tableExists: returns false for non-existing table", async () => { 155 | const exists = await tableExistsAsync("nonExistentTable", TEST_DIR_CUSTOM); 156 | if (exists) throw new Error("tableExists returned true for a non-existing table."); 157 | }); 158 | 159 | // --- valid --- 160 | await runTest("valid: returns true for a valid table file", async () => { 161 | const tableName = "validTableCustom"; 162 | await createTableAsync(tableName, TEST_DIR_CUSTOM); 163 | const isValid = await validAsync(tableName, TEST_DIR_CUSTOM); 164 | if (!isValid) throw new Error("valid returned false for a valid table file."); 165 | }); 166 | 167 | await runTest("valid: returns false for a non-existent table file", async () => { 168 | const isValid = await validAsync("nonExistentForValid", TEST_DIR_CUSTOM); 169 | if (isValid) throw new Error("valid returned true for a non-existent table."); 170 | }); 171 | 172 | await runTest("valid: returns false for a corrupted JSON file", async () => { 173 | const tableName = "corruptedTable"; 174 | const tablePath = path.join(TEST_DIR_CUSTOM, tableName + ".json"); 175 | // Ensure dir exists before writing corrupted file 176 | await fs.mkdir(path.dirname(tablePath), { recursive: true }); 177 | await fs.writeFile(tablePath, "{name: 'test', age: 30, invalidJson: }"); // Write invalid JSON 178 | const isValid = await validAsync(tableName, TEST_DIR_CUSTOM); 179 | if (isValid) throw new Error("valid returned true for a corrupted JSON file."); 180 | }); 181 | 182 | // --- insertTableContent --- 183 | const insertTableNameCustom = "insertCustom"; 184 | await runTest("insertTableContent: (setup) create table for insert tests", async () => { 185 | await createTableAsync(insertTableNameCustom, TEST_DIR_CUSTOM); 186 | }); 187 | 188 | await runTest("insertTableContent: inserts record with generated ID (custom)", async () => { 189 | const record = { name: "Alice", age: 30 }; 190 | const result = await insertTableContentAsync(insertTableNameCustom, TEST_DIR_CUSTOM, record); 191 | if (!result.id) throw new Error("Generated ID not returned in callback."); 192 | if (result.message.toLowerCase().indexOf("success") === -1) throw new Error("Success message not found."); 193 | }); 194 | 195 | await runTest("insertTableContent: inserts record with provided ID (custom)", async () => { 196 | const record = { id: "custom123", name: "Bob", occupation: "Builder" }; 197 | const result = await insertTableContentAsync(insertTableNameCustom, TEST_DIR_CUSTOM, record); 198 | if (result.id !== "custom123") throw new Error("Provided ID not used or returned correctly."); 199 | }); 200 | 201 | // --- getAll --- 202 | await runTest("getAll: retrieves all records (custom)", async () => { 203 | const records = await getAllAsync(insertTableNameCustom, TEST_DIR_CUSTOM); 204 | if (!Array.isArray(records)) throw new Error("getAll did not return an array."); 205 | if (records.length !== 2) throw new Error(`Expected 2 records, got ${records.length}`); 206 | }); 207 | 208 | // --- getRows --- 209 | await runTest("getRows: retrieves rows matching criteria (custom)", async () => { 210 | const rows = await getRowsAsync(insertTableNameCustom, TEST_DIR_CUSTOM, { name: "Alice" }); 211 | if (rows.length !== 1 || rows[0].name !== "Alice") throw new Error("getRows did not retrieve correct row."); 212 | }); 213 | await runTest("getRows: returns empty array for no match (custom)", async () => { 214 | const rows = await getRowsAsync(insertTableNameCustom, TEST_DIR_CUSTOM, { name: "NonExistent" }); 215 | if (rows.length !== 0) throw new Error("getRows did not return empty array for no match."); 216 | }); 217 | 218 | // --- updateRow --- 219 | const updateTableName = "updateTestCustom"; 220 | await runTest("updateRow: (setup) create and populate table for update tests", async () => { 221 | await createTableAsync(updateTableName, TEST_DIR_CUSTOM); 222 | await insertTableContentAsync(updateTableName, TEST_DIR_CUSTOM, { id: 1, name: "Charlie", version: 1 }); 223 | await insertTableContentAsync(updateTableName, TEST_DIR_CUSTOM, { id: 2, name: "Carol", version: 1 }); 224 | await insertTableContentAsync(updateTableName, TEST_DIR_CUSTOM, { id: 3, name: "Charlie", version: 1 }); // Another Charlie 225 | }); 226 | 227 | await runTest("updateRow: updates a single row and verifies count/change", async () => { 228 | const result = await updateRowAsync(updateTableName, TEST_DIR_CUSTOM, { id: 1 }, { version: 2, status: "active" }); 229 | if (result.count !== 1) throw new Error(`Expected 1 record updated, got ${result.count}`); 230 | const rows = await getRowsAsync(updateTableName, TEST_DIR_CUSTOM, { id: 1 }); 231 | if (rows[0].version !== 2 || rows[0].status !== "active") throw new Error("Row not updated correctly."); 232 | }); 233 | 234 | await runTest("updateRow: updates multiple rows and verifies count", async () => { 235 | const result = await updateRowAsync(updateTableName, TEST_DIR_CUSTOM, { name: "Charlie" }, { status: "verified" }); 236 | if (result.count !== 2) throw new Error(`Expected 2 records updated for name Charlie, got ${result.count}`); 237 | const rows = await getRowsAsync(updateTableName, TEST_DIR_CUSTOM, { name: "Charlie" }); 238 | if (rows.some(r => r.status !== "verified")) throw new Error("Not all Charlie rows updated."); 239 | }); 240 | 241 | await runTest("updateRow: returns count 0 for no matching rows", async () => { 242 | const result = await updateRowAsync(updateTableName, TEST_DIR_CUSTOM, { name: "NonExistent" }, { status: "ghost" }); 243 | if (result.count !== 0) throw new Error("Expected 0 records updated for non-matching criteria."); 244 | }); 245 | 246 | await runTest("updateRow: fails for empty WHERE clause", async () => { 247 | try { 248 | await updateRowAsync(updateTableName, TEST_DIR_CUSTOM, {}, { status: "DANGER" }); 249 | throw new Error("updateRow should have failed for empty WHERE clause."); 250 | } catch (error) { 251 | if (!error.message.toLowerCase().includes("where clause is empty")) { 252 | throw new Error(`Expected 'empty where' error, got: ${error.message}`); 253 | } 254 | } 255 | }); 256 | 257 | // --- deleteRow --- 258 | const deleteTableName = "deleteTestCustom"; 259 | await runTest("deleteRow: (setup) create and populate for delete tests", async () => { 260 | await createTableAsync(deleteTableName, TEST_DIR_CUSTOM); 261 | await insertTableContentAsync(deleteTableName, TEST_DIR_CUSTOM, { id: 'd1', name: "Dave", category: "A" }); 262 | await insertTableContentAsync(deleteTableName, TEST_DIR_CUSTOM, { id: 'd2', name: "Diana", category: "B" }); 263 | await insertTableContentAsync(deleteTableName, TEST_DIR_CUSTOM, { id: 'd3', name: "Dave", category: "C" }); 264 | }); 265 | 266 | await runTest("deleteRow: deletes a single row and verifies count", async () => { 267 | const result = await deleteRowAsync(deleteTableName, TEST_DIR_CUSTOM, { id: 'd2' }); 268 | if (result.count !== 1) throw new Error(`Expected 1 record deleted, got ${result.count}`); 269 | const rows = await getAllAsync(deleteTableName, TEST_DIR_CUSTOM); 270 | if (rows.length !== 2 || rows.find(r => r.id === 'd2')) throw new Error("Row not deleted or incorrect row deleted."); 271 | }); 272 | 273 | await runTest("deleteRow: deletes multiple rows and verifies count", async () => { 274 | const result = await deleteRowAsync(deleteTableName, TEST_DIR_CUSTOM, { name: "Dave" }); 275 | if (result.count !== 2) throw new Error(`Expected 2 records deleted for name Dave, got ${result.count}`); 276 | const rows = await getAllAsync(deleteTableName, TEST_DIR_CUSTOM); 277 | if (rows.length !== 0) throw new Error("Not all Dave rows deleted, or table not empty."); 278 | }); 279 | 280 | await runTest("deleteRow: returns count 0 for no matching rows", async () => { 281 | // Repopulate for this test 282 | await insertTableContentAsync(deleteTableName, TEST_DIR_CUSTOM, { id: 'd4', name: "Derek" }); 283 | const result = await deleteRowAsync(deleteTableName, TEST_DIR_CUSTOM, { name: "NonExistent" }); 284 | if (result.count !== 0) throw new Error("Expected 0 records deleted for non-matching criteria."); 285 | const rows = await getAllAsync(deleteTableName, TEST_DIR_CUSTOM); 286 | if (rows.length !== 1) throw new Error("Table should have 1 record after no-match delete."); 287 | }); 288 | 289 | await runTest("deleteRow: fails for empty WHERE clause", async () => { 290 | try { 291 | await deleteRowAsync(deleteTableName, TEST_DIR_CUSTOM, {}); 292 | throw new Error("deleteRow should have failed for empty WHERE clause."); 293 | } catch (error) { 294 | if (!error.message.toLowerCase().includes("where clause cannot be empty")) { 295 | throw new Error(`Expected 'empty where' error, got: ${error.message}`); 296 | } 297 | } 298 | }); 299 | 300 | // --- count --- 301 | const countTableName = "countTestCustom"; 302 | await runTest("count: (setup) create table for count tests", async () => { 303 | await createTableAsync(countTableName, TEST_DIR_CUSTOM); 304 | }); 305 | await runTest("count: returns 0 for an empty table", async () => { 306 | const num = await countAsync(countTableName, TEST_DIR_CUSTOM); 307 | if (num !== 0) throw new Error(`Expected count 0 for empty table, got ${num}`); 308 | }); 309 | await runTest("count: returns correct count for non-empty table", async () => { 310 | await insertTableContentAsync(countTableName, TEST_DIR_CUSTOM, { name: "Eve" }); 311 | await insertTableContentAsync(countTableName, TEST_DIR_CUSTOM, { name: "Enoch" }); 312 | const num = await countAsync(countTableName, TEST_DIR_CUSTOM); 313 | if (num !== 2) throw new Error(`Expected count 2, got ${num}`); 314 | }); 315 | 316 | // --- clearTable --- 317 | await runTest("clearTable: clears all records from a table", async () => { 318 | await clearTableAsync(countTableName, TEST_DIR_CUSTOM); 319 | const num = await countAsync(countTableName, TEST_DIR_CUSTOM); 320 | if (num !== 0) throw new Error(`Table not cleared, count is ${num}`); 321 | }); 322 | 323 | // --- insertTableContents --- 324 | const bulkInsertTableName = "bulkInsertTestCustom"; 325 | await runTest("insertTableContents: (setup) create table for bulk insert tests", async () => { 326 | await createTableAsync(bulkInsertTableName, TEST_DIR_CUSTOM); 327 | }); 328 | 329 | await runTest("insertTableContents: successfully inserts multiple records", async () => { 330 | const recordsToInsert = [ 331 | { name: "Bulk User 1", type: "bulk" }, 332 | { name: "Bulk User 2", type: "bulk" }, 333 | { id: "customBulkId1", name: "Bulk User 3 with ID", type: "bulk" } 334 | ]; 335 | const result = await insertTableContentsAsync(bulkInsertTableName, TEST_DIR_CUSTOM, recordsToInsert); 336 | if (!result || result.ids.length !== 3) throw new Error("insertTableContents did not return correct ID count."); 337 | if (!result.ids.includes("customBulkId1")) throw new Error("Custom ID not found in returned IDs."); 338 | 339 | const allRecords = await getAllAsync(bulkInsertTableName, TEST_DIR_CUSTOM); 340 | if (allRecords.length !== 3) throw new Error(`Expected 3 records after bulk insert, got ${allRecords.length}`); 341 | if (!allRecords.find(r => r.id === "customBulkId1")) throw new Error("Record with custom ID not found after bulk insert."); 342 | if (allRecords.filter(r => r.type === "bulk").length !== 3) throw new Error("Not all bulk records seem to be inserted or have correct type."); 343 | }); 344 | 345 | await runTest("insertTableContents: inserts more records, some with ID, some without", async () => { 346 | const recordsToInsert = [ 347 | { name: "Bulk User 4", type: "bulk-more" }, // Will get auto ID 348 | { id: "customBulkId2", name: "Bulk User 5 with ID", type: "bulk-more" } 349 | ]; 350 | const result = await insertTableContentsAsync(bulkInsertTableName, TEST_DIR_CUSTOM, recordsToInsert); 351 | if (!result || result.ids.length !== 2) throw new Error("insertTableContents did not return correct ID count for second batch."); 352 | if (!result.ids.includes("customBulkId2")) throw new Error("Custom ID from second batch not found in returned IDs."); 353 | 354 | const allRecords = await getAllAsync(bulkInsertTableName, TEST_DIR_CUSTOM); 355 | // Previous test had 3, this adds 2 = 5 total 356 | if (allRecords.length !== 5) throw new Error(`Expected 5 records after second bulk insert, got ${allRecords.length}`); 357 | if (allRecords.filter(r => r.type === "bulk-more").length !== 2) throw new Error("Not all 'bulk-more' records inserted."); 358 | }); 359 | 360 | await runTest("insertTableContents: fails when inserting an empty array", async () => { 361 | try { 362 | await insertTableContentsAsync(bulkInsertTableName, TEST_DIR_CUSTOM, []); 363 | throw new Error("insertTableContents should have failed for empty array."); 364 | } catch (error) { 365 | if (!error.message.includes("non-empty array")) { 366 | throw new Error(`Expected 'non-empty array' error, got: ${error.message}`); 367 | } 368 | } 369 | }); 370 | 371 | await runTest("insertTableContents: fails when inserting an array of non-objects", async () => { 372 | try { 373 | await insertTableContentsAsync(bulkInsertTableName, TEST_DIR_CUSTOM, [1, 2, "string"]); 374 | throw new Error("insertTableContents should have failed for array of non-objects."); 375 | } catch (error) { 376 | if (!error.message.includes("must be objects")) { 377 | throw new Error(`Expected 'must be objects' error, got: ${error.message}`); 378 | } 379 | } 380 | }); 381 | 382 | await runTest("insertTableContents: fails if table does not exist", async () => { 383 | const nonExistentTable = "ghostTableForBulkInsert"; 384 | try { 385 | await insertTableContentsAsync(nonExistentTable, TEST_DIR_CUSTOM, [{ name: "Ghost" }]); 386 | throw new Error("insertTableContents should have failed for non-existent table."); 387 | } catch (error) { 388 | if (!error.message.includes("does not exist or is not accessible")) { 389 | throw new Error(`Expected 'does not exist' error, got: ${error.message}`); 390 | } 391 | } 392 | }); 393 | 394 | 395 | // Final Summary 396 | console.log(`\n--- Test Summary ---`); 397 | console.log(` Total Tests: ${testsPassed + testsFailed}`); 398 | console.log(` Passed: ${testsPassed}`); 399 | console.log(` Failed: ${testsFailed}`); 400 | 401 | await cleanup(); 402 | 403 | if (testsFailed > 0) { 404 | console.error("\nSome tests failed. Exiting with error code 1."); 405 | process.exit(1); 406 | } else { 407 | console.log("\nAll tests passed!"); 408 | } 409 | } 410 | 411 | main().catch(err => { 412 | console.error("Critical error during test execution:", err); 413 | process.exit(1); 414 | }); 415 | --------------------------------------------------------------------------------