UNPKG

@odion-cloud/rockdex-db

Version:

🗄️ Lightweight, zero-dependency JavaScript database with manual file control. Features schema validation, transactions, triggers, and advanced queries. Works seamlessly in Node.js and browsers with optional persistence via .rdb files you create and manag

1 lines 19.5 kB
;(function (root, factory) {if (typeof define === 'function' && define.amd) {define([], factory);} else if (typeof module === 'object' && module.exports) {module.exports = factory();} else {root.RockdexDB = factory();}}(typeof self !== 'undefined' ? self : this, function () {'use strict';class RockdexDB {constructor(config = {}) {this._storageMode = config.storageMode || 'memory';this._storagePath = config.storagePath || './';this._storageTable = config.storageTable || [];this._defaultData = config.defaultData || {};this._tables = new Map();this._triggers = new Map();this._schemas = new Map();this._relationships = new Map();this._isNode = typeof window === 'undefined' && typeof global !== 'undefined';this._isBrowser = typeof window !== 'undefined';this._whereConditions = [];this._operator = 'AND';this._orderBy = null;this._limit = null;this._offset = 0;this._searchConditions = [];this._logger = config.logging || false;this._timestamps = config.timestamps || false;this._softDelete = config.softDelete || false;this._lastError = null;this._lastInsertId = null;this._storageReady = this._initializeStorage();} async ready() {await this._storageReady;return this;} async _initializeStorage() {if (this._storageMode === 'memory') {if (this._defaultData && Object.keys(this._defaultData).length > 0) {for (const [tableName, data] of Object.entries(this._defaultData)) {this.setTable(tableName, data);}} return;} if (this._storageMode === 'file') {await this._initializeManualTables();}} async _initializeManualTables() {try {if (!this._storageTable || this._storageTable.length === 0) {this._log('no_tables_configured', {message: 'No tables configured in storageTable array'});return;} for (const tableFile of this._storageTable) {const tableName = this._extractTableName(tableFile);this._tables.set(tableName, []);this._log('table_initialized', {tableName, tableFile, storagePath: this._storagePath});}} catch (error) {this._lastError = error;this._log('manual_table_init_error', {error: error.message});}} _extractTableName(tableFile) {return tableFile.replace(/\.rdb$/, '');} _generateSecureId() {const timestamp = Date.now().toString(36);const random = Math.random().toString(36).substr(2, 9);return `${timestamp}-${random}`.substring(0, 16);} _downloadTableFile(tableName) {if (!this._isBrowser) {throw new Error('Download only available in browser');} const tableData = {[tableName]: {rows: this._tables.get(tableName) || [], schema: this._schemas.get(tableName) || null, metadata: {lastModified: new Date().toISOString(), recordCount: (this._tables.get(tableName) || []).length, version: '1.0.0'}}};const jsonData = JSON.stringify(tableData, null, 2);const blob = new Blob([jsonData], {type: 'application/json'});const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = `${tableName}.rdb`;document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);this._log('table_downloaded', {tableName, fileName: `${tableName}.rdb`});} _exportTableData(tableName) {const tableData = {[tableName]: {rows: this._tables.get(tableName) || [], schema: this._schemas.get(tableName) || null, metadata: {lastModified: new Date().toISOString(), recordCount: (this._tables.get(tableName) || []).length, version: '1.0.0'}}};return JSON.stringify(tableData, null, 2);} _importTableData(jsonData, tableName) {try {const data = JSON.parse(jsonData);if (data[tableName]) {this._tables.set(tableName, data[tableName].rows || []);if (data[tableName].schema) {this._schemas.set(tableName, data[tableName].schema);} this._log('table_imported', {tableName, recordCount: data[tableName].rows?.length || 0});} else {throw new Error(`Table '${tableName}' not found in data`);}} catch (error) {this._lastError = error;throw new Error(`Failed to import table data: ${error.message}`);}} setLogging(enable) {this._logger = enable;return this;} _log(operation, details) {if (typeof this._logger === 'function') {this._logger(`[${new Date().toISOString()}] ${operation}: ${JSON.stringify(details)}`);} else if (this._logger) {console.log(`[${new Date().toISOString()}] ${operation}:`, details);}} _trigger(tableName, operation, OLD = null, NEW = null) {let shouldCommit = true;for (const [triggerName, trigger] of (this._triggers.get(tableName)?.entries() || [])) {try {if (trigger({operation, OLD, NEW}) === false) shouldCommit = false;} catch (err) {this._log(`${operation} trigger`, {tableName, triggerName, error: err, OLD, NEW});}} return shouldCommit;} setTable(tableName, data = [], schema = null) {if (!Array.isArray(data)) {throw new Error('Data must be an array');} if (schema) {this._validateSchema(data, schema);} if (this._timestamps) {data = data.map(row => ({...row, created_at: row.created_at || new Date().toISOString(), updated_at: row.updated_at || new Date().toISOString()}));} this._tables.set(tableName, data);this._log('setTable', {tableName, rowCount: data.length});return this;} createTrigger(tableName, triggerName, trigger) {if (!this._tables.has(tableName)) {throw new Error(`Table '${tableName}' does not exist`);} if (!this._triggers.has(tableName)) this._triggers.set(tableName, new Map());if (this._triggers.get(tableName).has(triggerName)) {throw new Error(`Trigger '${triggerName}' already exists`);} this._triggers.get(tableName).set(triggerName, trigger);return this;} dropTrigger(tableName, triggerName) {if (!this._triggers.has(tableName) || !this._triggers.get(tableName).has(triggerName)) {throw new Error(`Trigger '${triggerName}' on table '${tableName}' does not exist`);} this._triggers.get(tableName).delete(triggerName);return this;} createTable(tableName, schema = null) {if (this._tables.has(tableName)) {throw new Error(`Table '${tableName}' already exists`);} this._tables.set(tableName, []);if (schema) {this._schemas.set(tableName, schema);} this._log('createTable', {tableName, hasSchema: !!schema, storageMode: this._storageMode});return this;} dropTable(tableName) {if (!this._tables.has(tableName)) {throw new Error(`Table '${tableName}' does not exist`);} this._tables.delete(tableName);this._triggers.delete(tableName);this._schemas.delete(tableName);this._relationships.delete(tableName);this._log('dropTable', {tableName});return this;} exists(tableName) {return this.count(tableName) > 0;} addColumn(tableName, columnName, defaultValue = null) {if (!this._tables.has(tableName)) {throw new Error(`Table '${tableName}' does not exist`);} const table = this._tables.get(tableName);const updatedTable = table.map(row => ({...row, [columnName]: defaultValue}));this._tables.set(tableName, updatedTable);this._log('addColumn', {tableName, columnName});return this;} dropColumn(tableName, columnName) {if (!this._tables.has(tableName)) {throw new Error(`Table '${tableName}' does not exist`);} const table = this._tables.get(tableName);const updatedTable = table.map(row => {const {[columnName]: removed, ...rest} = row;return rest;});this._tables.set(tableName, updatedTable);this._log('dropColumn', {tableName, columnName});return this;} setRelation(tableName, relatedTable, type, foreignKey) {if (!this._relationships.has(tableName)) {this._relationships.set(tableName, []);} this._relationships.get(tableName).push({table: relatedTable, type, foreignKey});return this;} get(tableName) {if (!this._tables.has(tableName)) {throw new Error(`Table '${tableName}' does not exist`);} let results = [...this._tables.get(tableName)];if (this._softDelete) {results = results.filter(row => !row.deleted_at);} results = this._applyConditions(results);this._resetConditions();this._log('get', {tableName, resultCount: results.length, storageMode: this._storageMode});return results;} getOne(tableName) {const results = this.get(tableName);return results.length > 0 ? results[0] : null;} search(conditions) {this._searchConditions = Object.entries(conditions);return this;} orderBy(column, direction = 'ASC') {this._orderBy = {column, direction: direction.toUpperCase()};return this;} limit(limit, offset = 0) {this._limit = limit;this._offset = offset;return this;} whereOperator(field, operator, value) {this._whereConditions.push({field, operator, value});return this;} where(field, value, operator = 'AND') {this._whereConditions.push({field, value, operator});return this;} orWhere(field, value) {return this.where(field, value, 'OR');} whereIn(field, values) {if (!Array.isArray(values)) {throw new Error('Values must be an array');} this._whereConditions.push({field, operator: 'IN', value: values});return this;} whereLike(field, pattern) {this._whereConditions.push({field, operator: 'LIKE', value: pattern});return this;} count(tableName) {return this.get(tableName).length;} distinct(tableName, column) {const results = this.get(tableName);return [...new Set(results.map(row => row[column]))];} avg(tableName, column) {const results = this.get(tableName);if (results.length === 0) return 0;return results.reduce((sum, row) => sum + (row[column] || 0), 0) / results.length;} sum(tableName, column) {const results = this.get(tableName);return results.reduce((sum, row) => sum + (row[column] || 0), 0);} min(tableName, column) {const results = this.get(tableName);if (results.length === 0) return null;return Math.min(...results.map(row => row[column]));} max(tableName, column) {const results = this.get(tableName);if (results.length === 0) return null;return Math.max(...results.map(row => row[column]));} groupBy(tableName, column) {const results = this.get(tableName);return results.reduce((groups, row) => {const key = row[column];if (!groups[key]) groups[key] = [];groups[key].push(row);return groups;}, {});} _applyConditions(data) {let results = [...data];if (this._whereConditions.length > 0) {results = results.filter(row => {return this._whereConditions.every(condition => {switch (condition.operator) {case 'IN': return condition.value.includes(row[condition.field]);case 'LIKE': return String(row[condition.field]).includes(condition.value.replace(/%/g, ''));case '>': return row[condition.field] > condition.value;case '<': return row[condition.field] < condition.value;case '>=': return row[condition.field] >= condition.value;case '<=': return row[condition.field] <= condition.value;case '!=': return row[condition.field] !== condition.value;default: return row[condition.field] === condition.value;}});});} if (this._searchConditions.length > 0) {results = results.filter(row => {return this._searchConditions.some(([column, term]) => {return String(row[column]).toLowerCase().includes(String(term).toLowerCase());});});} if (this._orderBy) {results.sort((a, b) => {if (this._orderBy.direction === 'ASC') {return a[this._orderBy.column] > b[this._orderBy.column] ? 1 : -1;} return a[this._orderBy.column] < b[this._orderBy.column] ? 1 : -1;});} if (this._limit !== null) {results = results.slice(this._offset, this._offset + this._limit);} return results;} insert(tableName, data) {if (!this._tables.has(tableName)) {throw new Error(`Table '${tableName}' does not exist`);} const table = this._tables.get(tableName);const newData = {...data};if (data.id === 'AUTO_INCREMENT') {newData.id = this._generateSecureId();} else if (!data.id) {newData.id = this._generateSecureId();} this._lastInsertId = newData.id;if (this._timestamps) {newData.created_at = new Date().toISOString();newData.updated_at = new Date().toISOString();} const schema = this._schemas.get(tableName);if (schema) {this._validateRecord(newData, schema);} const shouldInsert = this._trigger(tableName, 'insert', null, newData);if (shouldInsert) {table.push(newData);} this._log('insert', {tableName, id: newData.id, storageMode: this._storageMode});return this;} getLastInsertId() {this._log('getLastInsertId', {id: this._lastInsertId});return this._lastInsertId;} bulkInsert(tableName, dataArray) {for (const data of dataArray) {this.insert(tableName, data);} return this;} update(tableName, data) {if (!this._tables.has(tableName)) {throw new Error(`Table '${tableName}' does not exist`);} const updateData = {...data};if (this._timestamps) {updateData.updated_at = new Date().toISOString();} const schema = this._schemas.get(tableName);if (schema) {const partialSchema = {};for (const field of Object.keys(updateData)) {if (schema[field]) {partialSchema[field] = schema[field];}} if (Object.keys(partialSchema).length > 0) {this._validateRecord(updateData, partialSchema);}} const table = this._tables.get(tableName);let updatedCount = 0;const updatedTable = table.map(row => {let shouldUpdate = this._whereConditions.every(condition => row[condition.field] === condition.value );let newData = shouldUpdate ? {...row, ...updateData} : row;shouldUpdate = shouldUpdate && this._trigger(tableName, 'update', row, newData);if (shouldUpdate) updatedCount++;return shouldUpdate ? newData : row;});this._tables.set(tableName, updatedTable);this._log('update', {tableName, updatedCount, storageMode: this._storageMode});this._resetConditions();return this;} delete(tableName) {if (!this._tables.has(tableName)) {throw new Error(`Table '${tableName}' does not exist`);} const table = this._tables.get(tableName);let deletedCount = 0;if (this._softDelete) {const updatedTable = table.map(row => {let shouldDelete = this._whereConditions.every(condition => row[condition.field] === condition.value );shouldDelete = shouldDelete && this._trigger(tableName, 'delete', row, null);if (shouldDelete) deletedCount++;return shouldDelete ? {...row, deleted_at: new Date().toISOString()} : row;});this._tables.set(tableName, updatedTable);} else {const filteredTable = table.filter(row => {let shouldDelete = this._whereConditions.every(condition => row[condition.field] === condition.value );shouldDelete = shouldDelete && this._trigger(tableName, 'delete', row, null);if (shouldDelete) deletedCount++;return !shouldDelete;});this._tables.set(tableName, filteredTable);} this._log('delete', {tableName, deletedCount, softDelete: this._softDelete, storageMode: this._storageMode});this._resetConditions();return this;} toJSON(tableName) {const data = this.get(tableName);return JSON.stringify(data, null, 2);} fromJSON(tableName, jsonData) {try {const data = JSON.parse(jsonData);this.setTable(tableName, data);return this;} catch (error) {this._lastError = error;throw new Error('Invalid JSON data');}} getLastError() {return this._lastError;} _getCurrentTimestamp() {return new Date().toISOString().slice(0, 19).replace('T', ' ');} _validateSchema(data, schema) {for (const row of data) {this._validateRecord(row, schema);}} _validateRecord(record, schema) {for (const [field, rules] of Object.entries(schema)) {if (rules.required && (record[field] === undefined || record[field] === null)) {throw new Error(`Field '${field}' is required`);} if (rules.type && record[field] !== undefined && typeof record[field] !== rules.type) {throw new Error(`Field '${field}' must be of type ${rules.type}`);} if (rules.min && record[field] < rules.min) {throw new Error(`Field '${field}' must be at least ${rules.min}`);} if (rules.max && record[field] > rules.max) {throw new Error(`Field '${field}' must be at most ${rules.max}`);} if (rules.length && String(record[field]).length !== rules.length) {throw new Error(`Field '${field}' must be exactly ${rules.length} characters long`);} if (rules.pattern && !rules.pattern.test(String(record[field]))) {throw new Error(`Field '${field}' does not match required pattern`);}}} backup() {const backup = {timestamp: this._getCurrentTimestamp(), data: {}, metadata: {tables: [], relationships: {}}};for (const [tableName, data] of this._tables.entries()) {backup.data[tableName] = data;backup.metadata.tables.push({name: tableName, count: data.length});} backup.metadata.relationships = Object.fromEntries(this._relationships);return backup;} restore(backup) {try {this._tables = new Map(Object.entries(backup.data));this._relationships = new Map(Object.entries(backup.metadata.relationships));this._log('restore', {timestamp: backup.timestamp});return this;} catch (error) {this._lastError = error;throw new Error('Invalid backup data');}} join(table1, table2, key1, key2) {const data1 = this._tables.get(table1);const data2 = this._tables.get(table2);if (!data1 || !data2) {throw new Error('One or both tables do not exist');} return data1.map(row1 => {const matching = data2.find(row2 => row2[key2] === row1[key1]);if (matching) {const joined = {};Object.keys(row1).forEach(key => {joined[`${table1}_${key}`] = row1[key];});Object.keys(matching).forEach(key => {joined[`${table2}_${key}`] = matching[key];});return joined;} return row1;});} transaction(callback) {const backup = this.backup();try {callback(this);this._log('transaction', {status: 'committed'});return this;} catch (error) {this.restore(backup);this._log('transaction', {status: 'rollback', error: error.message});throw error;}} paginate(tableName, page = 1, perPage = 10) {const total = this.count(tableName);const totalPages = Math.ceil(total / perPage);const offset = (page - 1) * perPage;const results = this.limit(perPage, offset).get(tableName);return {data: results, pagination: {total, perPage, currentPage: page, totalPages, hasNextPage: page < totalPages, hasPrevPage: page > 1}};} raw(tableName, filterFn) {if (!this._tables.has(tableName)) {throw new Error(`Table '${tableName}' does not exist`);} const table = this._tables.get(tableName);return table.filter(filterFn);} truncate(tableName) {if (!this._tables.has(tableName)) {throw new Error(`Table '${tableName}' does not exist`);} this._tables.set(tableName, []);this._log('truncate', {tableName});return this;} getSchema(tableName) {return this._schemas.get(tableName) || null;} updateSchema(tableName, schema) {if (!this._tables.has(tableName)) {throw new Error(`Table '${tableName}' does not exist`);} this._schemas.set(tableName, schema);const table = this._tables.get(tableName);this._validateSchema(table, schema);this._log('updateSchema', {tableName});return this;} _resetConditions() {this._whereConditions = [];this._operator = 'AND';this._orderBy = null;this._limit = null;this._offset = 0;this._searchConditions = [];} exportTable(tableName) {if (!this._tables.has(tableName)) {throw new Error(`Table '${tableName}' does not exist`);} if (this._isBrowser) {this._downloadTableFile(tableName);} else {throw new Error('File download only available in browser. Use getTableExport() for Node.js.');} return this;} getTableExport(tableName) {if (!this._tables.has(tableName)) {throw new Error(`Table '${tableName}' does not exist`);} return this._exportTableData(tableName);} importTable(tableName, fileContent) {this._importTableData(fileContent, tableName);return this;} getStorageStats() {const stats = {storageMode: this._storageMode, storagePath: this._storagePath, storageTable: this._storageTable, tables: {}, totalRecords: 0, memoryUsage: 0};for (const [tableName, rows] of this._tables.entries()) {const activeRecords = this._softDelete ? rows.filter(row => !row.deleted_at).length : rows.length;stats.tables[tableName] = {totalRecords: rows.length, activeRecords, deletedRecords: rows.length - activeRecords, hasSchema: this._schemas.has(tableName)};stats.totalRecords += activeRecords;} const dataString = JSON.stringify(Object.fromEntries(this._tables));stats.memoryUsage = Math.round((dataString.length * 2) / 1024 / 1024 * 100) / 100;return stats;}} RockdexDB.AUTO_INCREMENT = 'AUTO_INCREMENT';RockdexDB.OPERATORS = {EQ: '=', GT: '>', LT: '<', GTE: '>=', LTE: '<=', NEQ: '!=', LIKE: 'LIKE', IN: 'IN'};return RockdexDB;}));