sehawq.db 3.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Storage Layer - Handles all file I/O with performance optimizations
3
+ *
4
+ * Because reading/writing files should be fast, not frustrating
5
+ * Added some tricks I learned the hard way 🎯
6
+ */
7
+
8
+ const fs = require('fs').promises;
9
+ const path = require('path');
10
+ const { performance } = require('perf_hooks');
11
+
12
+ class Storage {
13
+ constructor(filePath, options = {}) {
14
+ this.filePath = filePath;
15
+ this.options = {
16
+ compression: false,
17
+ backupOnWrite: true,
18
+ backupRetention: 5, // Keep last 5 backups
19
+ maxFileSize: 50 * 1024 * 1024, // 50MB limit
20
+ ...options
21
+ };
22
+
23
+ this.writeQueue = [];
24
+ this.isWriting = false;
25
+ this.stats = {
26
+ reads: 0,
27
+ writes: 0,
28
+ backups: 0,
29
+ errors: 0,
30
+ totalReadTime: 0,
31
+ totalWriteTime: 0
32
+ };
33
+
34
+ this._ensureDirectory();
35
+ }
36
+
37
+ /**
38
+ * Make sure the directory exists
39
+ * Learned this the hard way - files don't create their own folders! 😅
40
+ */
41
+ async _ensureDirectory() {
42
+ const dir = path.dirname(this.filePath);
43
+ try {
44
+ await fs.access(dir);
45
+ } catch (error) {
46
+ await fs.mkdir(dir, { recursive: true });
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Read data with performance tracking and caching
52
+ */
53
+ async read() {
54
+ const startTime = performance.now();
55
+
56
+ try {
57
+ // Check if file exists first
58
+ try {
59
+ await fs.access(this.filePath);
60
+ } catch (error) {
61
+ // File doesn't exist - return empty data
62
+ return {};
63
+ }
64
+
65
+ const data = await fs.readFile(this.filePath, 'utf8');
66
+
67
+ // Performance tracking
68
+ const readTime = performance.now() - startTime;
69
+ this.stats.reads++;
70
+ this.stats.totalReadTime += readTime;
71
+
72
+ if (this.options.debug) {
73
+ console.log(`📖 Read ${data.length} bytes in ${readTime.toFixed(2)}ms`);
74
+ }
75
+
76
+ return JSON.parse(data);
77
+ } catch (error) {
78
+ this.stats.errors++;
79
+ console.error('🚨 Storage read error:', error);
80
+
81
+ // Try to recover from backup if main file is corrupted
82
+ return await this._recoverFromBackup();
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Write data with queuing and atomic operations
88
+ * Prevents corruption and handles concurrent writes
89
+ */
90
+ async write(data) {
91
+ return new Promise((resolve, reject) => {
92
+ // Queue the write operation
93
+ this.writeQueue.push({ data, resolve, reject });
94
+
95
+ if (!this.isWriting) {
96
+ this._processWriteQueue();
97
+ }
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Process write queue one by one
103
+ * Prevents race conditions and file corruption
104
+ */
105
+ async _processWriteQueue() {
106
+ if (this.writeQueue.length === 0 || this.isWriting) {
107
+ return;
108
+ }
109
+
110
+ this.isWriting = true;
111
+ const startTime = performance.now();
112
+
113
+ try {
114
+ const { data, resolve, reject } = this.writeQueue.shift();
115
+
116
+ // Check file size limit
117
+ const dataSize = Buffer.byteLength(JSON.stringify(data), 'utf8');
118
+ if (dataSize > this.options.maxFileSize) {
119
+ throw new Error(`File size limit exceeded: ${dataSize} > ${this.options.maxFileSize}`);
120
+ }
121
+
122
+ // Create backup before writing
123
+ if (this.options.backupOnWrite) {
124
+ await this._createBackup();
125
+ }
126
+
127
+ // Atomic write - write to temp file then rename
128
+ const tempPath = this.filePath + '.tmp';
129
+ const serializedData = JSON.stringify(data, null, 2);
130
+
131
+ await fs.writeFile(tempPath, serializedData, 'utf8');
132
+ await fs.rename(tempPath, this.filePath);
133
+
134
+ // Performance tracking
135
+ const writeTime = performance.now() - startTime;
136
+ this.stats.writes++;
137
+ this.stats.totalWriteTime += writeTime;
138
+
139
+ if (this.options.debug) {
140
+ console.log(`💾 Written ${serializedData.length} bytes in ${writeTime.toFixed(2)}ms`);
141
+ }
142
+
143
+ resolve();
144
+ } catch (error) {
145
+ this.stats.errors++;
146
+ console.error('🚨 Storage write error:', error);
147
+ this.writeQueue[0]?.reject(error);
148
+ } finally {
149
+ this.isWriting = false;
150
+
151
+ // Process next item in queue
152
+ if (this.writeQueue.length > 0) {
153
+ setImmediate(() => this._processWriteQueue());
154
+ }
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Create backup of current data file
160
+ * Saved my data more times than I can count! 💾
161
+ */
162
+ async _createBackup() {
163
+ try {
164
+ // Check if source file exists
165
+ try {
166
+ await fs.access(this.filePath);
167
+ } catch (error) {
168
+ // No file to backup - that's fine
169
+ return;
170
+ }
171
+
172
+ const timestamp = new Date().toISOString()
173
+ .replace(/[:.]/g, '-')
174
+ .replace('T', '_')
175
+ .split('.')[0];
176
+
177
+ const backupPath = `${this.filePath}.backup_${timestamp}`;
178
+
179
+ await fs.copyFile(this.filePath, backupPath);
180
+ this.stats.backups++;
181
+
182
+ // Clean up old backups
183
+ await this._cleanupOldBackups();
184
+
185
+ if (this.options.debug) {
186
+ console.log(`🔐 Backup created: ${backupPath}`);
187
+ }
188
+ } catch (error) {
189
+ console.error('🚨 Backup creation failed:', error);
190
+ // Don't throw - backup failure shouldn't block main write
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Keep only the most recent backups
196
+ */
197
+ async _cleanupOldBackups() {
198
+ try {
199
+ const dir = path.dirname(this.filePath);
200
+ const fileName = path.basename(this.filePath);
201
+
202
+ const files = await fs.readdir(dir);
203
+ const backupFiles = files
204
+ .filter(file => file.startsWith(fileName + '.backup_'))
205
+ .sort()
206
+ .reverse();
207
+
208
+ // Remove old backups beyond retention limit
209
+ for (const file of backupFiles.slice(this.options.backupRetention)) {
210
+ await fs.unlink(path.join(dir, file));
211
+
212
+ if (this.options.debug) {
213
+ console.log(`🗑️ Cleaned up old backup: ${file}`);
214
+ }
215
+ }
216
+ } catch (error) {
217
+ console.error('🚨 Backup cleanup failed:', error);
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Try to recover data from backup if main file is corrupted
223
+ */
224
+ async _recoverFromBackup() {
225
+ try {
226
+ const dir = path.dirname(this.filePath);
227
+ const fileName = path.basename(this.filePath);
228
+
229
+ const files = await fs.readdir(dir);
230
+ const backupFiles = files
231
+ .filter(file => file.startsWith(fileName + '.backup_'))
232
+ .sort()
233
+ .reverse();
234
+
235
+ for (const backupFile of backupFiles) {
236
+ try {
237
+ const backupPath = path.join(dir, backupFile);
238
+ const data = await fs.readFile(backupPath, 'utf8');
239
+ const parsed = JSON.parse(data);
240
+
241
+ console.log(`🔧 Recovered data from backup: ${backupFile}`);
242
+
243
+ // Restore the backup to main file
244
+ await this.write(parsed);
245
+
246
+ return parsed;
247
+ } catch (error) {
248
+ // This backup is also corrupted, try next one
249
+ continue;
250
+ }
251
+ }
252
+
253
+ throw new Error('No valid backup found for recovery');
254
+ } catch (error) {
255
+ console.error('🚨 Recovery from backup failed:', error);
256
+ return {}; // Return empty data as last resort
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Get storage statistics
262
+ */
263
+ getStats() {
264
+ return {
265
+ ...this.stats,
266
+ avgReadTime: this.stats.reads > 0
267
+ ? (this.stats.totalReadTime / this.stats.reads).toFixed(2) + 'ms'
268
+ : '0ms',
269
+ avgWriteTime: this.stats.writes > 0
270
+ ? (this.stats.totalWriteTime / this.stats.writes).toFixed(2) + 'ms'
271
+ : '0ms',
272
+ queueLength: this.writeQueue.length,
273
+ isWriting: this.isWriting
274
+ };
275
+ }
276
+
277
+ /**
278
+ * Manual backup creation
279
+ */
280
+ async createBackup() {
281
+ return await this._createBackup();
282
+ }
283
+
284
+ /**
285
+ * Get list of available backups
286
+ */
287
+ async listBackups() {
288
+ try {
289
+ const dir = path.dirname(this.filePath);
290
+ const fileName = path.basename(this.filePath);
291
+
292
+ const files = await fs.readdir(dir);
293
+ return files
294
+ .filter(file => file.startsWith(fileName + '.backup_'))
295
+ .sort()
296
+ .reverse();
297
+ } catch (error) {
298
+ return [];
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Restore from specific backup
304
+ */
305
+ async restoreBackup(backupName) {
306
+ const backupPath = path.join(path.dirname(this.filePath), backupName);
307
+
308
+ try {
309
+ const data = await fs.readFile(backupPath, 'utf8');
310
+ const parsed = JSON.parse(data);
311
+
312
+ await this.write(parsed);
313
+ console.log(`✅ Restored from backup: ${backupName}`);
314
+
315
+ return parsed;
316
+ } catch (error) {
317
+ throw new Error(`Failed to restore backup ${backupName}: ${error.message}`);
318
+ }
319
+ }
320
+ }
321
+
322
+ module.exports = Storage;
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Data Validator - Keeps your data clean and proper 🧼
3
+ *
4
+ * Because garbage in, garbage out is a real thing
5
+ * Validates everything from emails to custom business rules
6
+ */
7
+
8
+ class Validator {
9
+ constructor() {
10
+ this.rules = new Map();
11
+ this.customValidators = new Map();
12
+
13
+ this._setupBuiltinRules();
14
+ }
15
+
16
+ /**
17
+ * Setup built-in validation rules
18
+ */
19
+ _setupBuiltinRules() {
20
+ // Type validators
21
+ this.rules.set('string', value => typeof value === 'string');
22
+ this.rules.set('number', value => typeof value === 'number' && !isNaN(value));
23
+ this.rules.set('boolean', value => typeof value === 'boolean');
24
+ this.rules.set('array', value => Array.isArray(value));
25
+ this.rules.set('object', value => value && typeof value === 'object' && !Array.isArray(value));
26
+ this.rules.set('function', value => typeof value === 'function');
27
+ this.rules.set('null', value => value === null);
28
+ this.rules.set('undefined', value => value === undefined);
29
+
30
+ // Common format validators
31
+ this.rules.set('email', value =>
32
+ typeof value === 'string' &&
33
+ /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
34
+ );
35
+
36
+ this.rules.set('url', value => {
37
+ if (typeof value !== 'string') return false;
38
+ try {
39
+ new URL(value);
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ });
45
+
46
+ this.rules.set('uuid', value =>
47
+ typeof value === 'string' &&
48
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)
49
+ );
50
+
51
+ this.rules.set('date', value =>
52
+ value instanceof Date && !isNaN(value.getTime())
53
+ );
54
+
55
+ this.rules.set('hexColor', value =>
56
+ typeof value === 'string' &&
57
+ /^#?([0-9A-F]{3}|[0-9A-F]{6})$/i.test(value)
58
+ );
59
+
60
+ // Comparison validators
61
+ this.rules.set('min', (value, min) => {
62
+ if (typeof value === 'number') return value >= min;
63
+ if (typeof value === 'string' || Array.isArray(value)) return value.length >= min;
64
+ return false;
65
+ });
66
+
67
+ this.rules.set('max', (value, max) => {
68
+ if (typeof value === 'number') return value <= max;
69
+ if (typeof value === 'string' || Array.isArray(value)) return value.length <= max;
70
+ return false;
71
+ });
72
+
73
+ this.rules.set('range', (value, [min, max]) => {
74
+ if (typeof value !== 'number') return false;
75
+ return value >= min && value <= max;
76
+ });
77
+
78
+ this.rules.set('minLength', (value, min) =>
79
+ (typeof value === 'string' || Array.isArray(value)) && value.length >= min
80
+ );
81
+
82
+ this.rules.set('maxLength', (value, max) =>
83
+ (typeof value === 'string' || Array.isArray(value)) && value.length <= max
84
+ );
85
+
86
+ this.rules.set('length', (value, length) =>
87
+ (typeof value === 'string' || Array.isArray(value)) && value.length === length
88
+ );
89
+
90
+ // Pattern validators
91
+ this.rules.set('pattern', (value, pattern) =>
92
+ typeof value === 'string' && pattern.test(value)
93
+ );
94
+
95
+ this.rules.set('alphanumeric', value =>
96
+ typeof value === 'string' && /^[a-zA-Z0-9]+$/.test(value)
97
+ );
98
+
99
+ this.rules.set('numeric', value =>
100
+ typeof value === 'string' && /^\d+$/.test(value)
101
+ );
102
+
103
+ // Collection validators
104
+ this.rules.set('in', (value, allowed) =>
105
+ Array.isArray(allowed) && allowed.includes(value)
106
+ );
107
+
108
+ this.rules.set('notIn', (value, disallowed) =>
109
+ Array.isArray(disallowed) && !disallowed.includes(value)
110
+ );
111
+
112
+ // Special validators
113
+ this.rules.set('required', value =>
114
+ value !== null && value !== undefined && value !== ''
115
+ );
116
+
117
+ this.rules.set('optional', () => true);
118
+ }
119
+
120
+ /**
121
+ * Add custom validator
122
+ */
123
+ addRule(name, validatorFn) {
124
+ if (this.rules.has(name) || this.customValidators.has(name)) {
125
+ throw new Error(`Validator '${name}' already exists`);
126
+ }
127
+
128
+ this.customValidators.set(name, validatorFn);
129
+ return this;
130
+ }
131
+
132
+ /**
133
+ * Remove validator
134
+ */
135
+ removeRule(name) {
136
+ this.rules.delete(name);
137
+ this.customValidators.delete(name);
138
+ return this;
139
+ }
140
+
141
+ /**
142
+ * Validate single value against rules
143
+ */
144
+ validateValue(value, rules) {
145
+ const errors = [];
146
+
147
+ for (const rule of rules) {
148
+ const [ruleName, ...ruleArgs] = Array.isArray(rule) ? rule : [rule];
149
+
150
+ let isValid = false;
151
+
152
+ // Check built-in rules first
153
+ if (this.rules.has(ruleName)) {
154
+ const validator = this.rules.get(ruleName);
155
+ isValid = validator(value, ...ruleArgs);
156
+ }
157
+ // Check custom rules
158
+ else if (this.customValidators.has(ruleName)) {
159
+ const validator = this.customValidators.get(ruleName);
160
+ isValid = validator(value, ...ruleArgs);
161
+ }
162
+ else {
163
+ errors.push(`Unknown validation rule: ${ruleName}`);
164
+ continue;
165
+ }
166
+
167
+ if (!isValid) {
168
+ errors.push(this._formatError(ruleName, ruleArgs, value));
169
+ }
170
+ }
171
+
172
+ return {
173
+ isValid: errors.length === 0,
174
+ errors,
175
+ value
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Validate object against schema
181
+ */
182
+ validateObject(obj, schema) {
183
+ const errors = {};
184
+ let isValid = true;
185
+
186
+ for (const [field, fieldRules] of Object.entries(schema)) {
187
+ const value = obj[field];
188
+ const result = this.validateValue(value, fieldRules);
189
+
190
+ if (!result.isValid) {
191
+ errors[field] = result.errors;
192
+ isValid = false;
193
+ }
194
+ }
195
+
196
+ return {
197
+ isValid,
198
+ errors,
199
+ data: obj
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Create schema validator
205
+ */
206
+ schema(schemaDef) {
207
+ return (data) => this.validateObject(data, schemaDef);
208
+ }
209
+
210
+ /**
211
+ * Format validation error
212
+ */
213
+ _formatError(rule, args, value) {
214
+ const valueStr = typeof value === 'string' ? `"${value}"` : JSON.stringify(value);
215
+
216
+ switch (rule) {
217
+ case 'required':
218
+ return 'Field is required';
219
+ case 'min':
220
+ return `Value must be at least ${args[0]}`;
221
+ case 'max':
222
+ return `Value must be at most ${args[0]}`;
223
+ case 'minLength':
224
+ return `Length must be at least ${args[0]} characters`;
225
+ case 'maxLength':
226
+ return `Length must be at most ${args[0]} characters`;
227
+ case 'length':
228
+ return `Length must be exactly ${args[0]} characters`;
229
+ case 'range':
230
+ return `Value must be between ${args[0][0]} and ${args[0][1]}`;
231
+ case 'email':
232
+ return 'Must be a valid email address';
233
+ case 'url':
234
+ return 'Must be a valid URL';
235
+ case 'uuid':
236
+ return 'Must be a valid UUID';
237
+ case 'pattern':
238
+ return 'Value does not match required pattern';
239
+ case 'in':
240
+ return `Value must be one of: ${args[0].join(', ')}`;
241
+ case 'notIn':
242
+ return `Value must not be one of: ${args[0].join(', ')}`;
243
+ default:
244
+ return `Failed validation: ${rule}`;
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Quick validators (static methods)
250
+ */
251
+ static isEmail(value) {
252
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
253
+ }
254
+
255
+ static isURL(value) {
256
+ try {
257
+ new URL(value);
258
+ return true;
259
+ } catch {
260
+ return false;
261
+ }
262
+ }
263
+
264
+ static isDate(value) {
265
+ return value instanceof Date && !isNaN(value.getTime());
266
+ }
267
+
268
+ static isNumber(value) {
269
+ return typeof value === 'number' && !isNaN(value);
270
+ }
271
+
272
+ static isString(value) {
273
+ return typeof value === 'string';
274
+ }
275
+
276
+ static isArray(value) {
277
+ return Array.isArray(value);
278
+ }
279
+
280
+ static isObject(value) {
281
+ return value && typeof value === 'object' && !Array.isArray(value);
282
+ }
283
+
284
+ static isEmpty(value) {
285
+ if (value === null || value === undefined) return true;
286
+ if (typeof value === 'string') return value.trim().length === 0;
287
+ if (Array.isArray(value)) return value.length === 0;
288
+ if (typeof value === 'object') return Object.keys(value).length === 0;
289
+ return false;
290
+ }
291
+
292
+ /**
293
+ * Sanitize functions
294
+ */
295
+ static trim(value) {
296
+ return typeof value === 'string' ? value.trim() : value;
297
+ }
298
+
299
+ static toLowerCase(value) {
300
+ return typeof value === 'string' ? value.toLowerCase() : value;
301
+ }
302
+
303
+ static toUpperCase(value) {
304
+ return typeof value === 'string' ? value.toUpperCase() : value;
305
+ }
306
+
307
+ static toNumber(value) {
308
+ if (typeof value === 'number') return value;
309
+ const num = parseFloat(value);
310
+ return isNaN(num) ? value : num;
311
+ }
312
+
313
+ static toBoolean(value) {
314
+ if (typeof value === 'boolean') return value;
315
+ if (typeof value === 'string') {
316
+ return value.toLowerCase() === 'true' || value === '1';
317
+ }
318
+ return !!value;
319
+ }
320
+ }
321
+
322
+ // Export singleton instance
323
+ const validator = new Validator();
324
+ module.exports = validator;
325
+ module.exports.Validator = Validator;