waengine 1.7.3 → 1.7.4

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.
package/src/storage.js CHANGED
@@ -8,7 +8,7 @@ export class WAStorage {
8
8
  this.cache = new Map();
9
9
  this.ensureBaseDir();
10
10
 
11
- console.log(`💾 WAStorage initialisiert: ${baseDir}`);
11
+ // Schöne Console ist jetzt Standard - keine Logs mehr hier
12
12
  }
13
13
 
14
14
  // ===== SETUP =====
@@ -21,11 +21,33 @@ export class WAStorage {
21
21
  }
22
22
 
23
23
  getFilePath(fileName) {
24
+ // Sicherheitsvalidierung gegen Path Traversal
25
+ if (!fileName || typeof fileName !== 'string') {
26
+ throw new Error('❌ Ungültiger Dateiname');
27
+ }
28
+
29
+ // Entferne gefährliche Zeichen und Pfad-Traversal-Versuche
30
+ let sanitizedFileName = fileName
31
+ .replace(/\.\./g, '') // Entferne ../
32
+ .replace(/[<>:"|?*]/g, '') // Entferne Windows-ungültige Zeichen
33
+ .replace(/^\/+/, '') // Entferne führende Slashes
34
+ .trim();
35
+
36
+ if (!sanitizedFileName) {
37
+ throw new Error('❌ Dateiname ist nach Bereinigung leer');
38
+ }
39
+
24
40
  // Automatisch .json hinzufügen falls nicht vorhanden
25
- if (!fileName.endsWith('.json')) {
26
- fileName += '.json';
41
+ if (!sanitizedFileName.endsWith('.json')) {
42
+ sanitizedFileName += '.json';
27
43
  }
28
- return path.join(this.baseDir, fileName);
44
+
45
+ // Prüfe ob Dateiname zu lang ist (Windows: 255 Zeichen)
46
+ if (sanitizedFileName.length > 200) {
47
+ throw new Error('❌ Dateiname zu lang');
48
+ }
49
+
50
+ return path.join(this.baseDir, sanitizedFileName);
29
51
  }
30
52
 
31
53
  // ===== WRITE SYSTEM =====
@@ -49,14 +71,9 @@ export class WAStorage {
49
71
  return this.setKey(fileName, key, value);
50
72
  },
51
73
 
52
- // write.in("datei").push(value) - Für Arrays
53
- push: (value) => {
54
- return this.pushToArray(fileName, value);
55
- },
56
-
57
- // write.in("datei").increment(key, amount)
58
- increment: (key, amount = 1) => {
59
- return this.incrementKey(fileName, key, amount);
74
+ // write.in("datei").push(item)
75
+ push: (item) => {
76
+ return this.pushItem(fileName, item);
60
77
  }
61
78
  };
62
79
  }
@@ -65,7 +82,7 @@ export class WAStorage {
65
82
  // ===== READ SYSTEM =====
66
83
 
67
84
  read = {
68
- // read.from("datei")
85
+ // read.from("datei") - Liest Daten aus Datei
69
86
  from: (fileName) => {
70
87
  return {
71
88
  // read.from("datei").all()
@@ -78,33 +95,24 @@ export class WAStorage {
78
95
  return this.getKey(fileName, key);
79
96
  },
80
97
 
81
- // read.from("datei").keys()
82
- keys: () => {
83
- const data = this.readData(fileName);
84
- return data ? Object.keys(data) : [];
98
+ // read.from("datei").find(condition)
99
+ find: (condition) => {
100
+ return this.findItem(fileName, condition);
85
101
  },
86
102
 
87
- // read.from("datei").values()
88
- values: () => {
89
- const data = this.readData(fileName);
90
- return data ? Object.values(data) : [];
103
+ // read.from("datei").filter(condition)
104
+ filter: (condition) => {
105
+ return this.filterItems(fileName, condition);
91
106
  },
92
107
 
93
- // read.from("datei").length() - Für Arrays
94
- length: () => {
108
+ // read.from("datei").count()
109
+ count: () => {
95
110
  const data = this.readData(fileName);
96
- return Array.isArray(data) ? data.length : 0;
97
- },
98
-
99
- // read.from("datei").exists()
100
- exists: () => {
101
- return this.fileExists(fileName);
111
+ return Array.isArray(data) ? data.length : (data ? Object.keys(data).length : 0);
102
112
  }
103
113
  };
104
114
  }
105
115
  };
106
-
107
- // ===== DELETE SYSTEM =====
108
116
 
109
117
  delete = {
110
118
  // delete.from("datei")
@@ -133,12 +141,39 @@ export class WAStorage {
133
141
  writeData(fileName, data) {
134
142
  try {
135
143
  const filePath = this.getFilePath(fileName);
136
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
137
- this.cache.set(fileName, data);
138
- // Stille Speicherung - keine Console-Ausgabe
144
+
145
+ // Validiere Daten
146
+ if (data === undefined) {
147
+ throw new Error('Daten sind undefined');
148
+ }
149
+
150
+ // Atomare Schreibung: Erst in temporäre Datei, dann umbenennen
151
+ const tempFilePath = `${filePath}.tmp`;
152
+ const jsonData = JSON.stringify(data, null, 2);
153
+
154
+ // Schreibe in temporäre Datei
155
+ fs.writeFileSync(tempFilePath, jsonData);
156
+
157
+ // Atomare Umbenennung (verhindert korrupte Dateien)
158
+ fs.renameSync(tempFilePath, filePath);
159
+
160
+ // Cache mit Größenlimit aktualisieren
161
+ this.updateCache(fileName, data);
162
+
139
163
  return true;
140
164
  } catch (error) {
141
165
  console.error(`❌ Fehler beim Schreiben in ${fileName}:`, error);
166
+
167
+ // Cleanup temporäre Datei
168
+ try {
169
+ const tempFilePath = `${this.getFilePath(fileName)}.tmp`;
170
+ if (fs.existsSync(tempFilePath)) {
171
+ fs.unlinkSync(tempFilePath);
172
+ }
173
+ } catch (cleanupError) {
174
+ // Stille Behandlung
175
+ }
176
+
142
177
  return false;
143
178
  }
144
179
  }
@@ -156,11 +191,72 @@ export class WAStorage {
156
191
  return null;
157
192
  }
158
193
 
159
- const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
160
- this.cache.set(fileName, data);
194
+ // Robuste JSON-Lesung mit Validierung
195
+ const fileContent = fs.readFileSync(filePath, 'utf8');
196
+
197
+ // Prüfe auf leere oder zu kurze Dateien
198
+ if (!fileContent || fileContent.trim().length < 2) {
199
+ console.warn(`⚠️ Datei ${fileName} ist leer oder zu kurz`);
200
+ return null;
201
+ }
202
+
203
+ // Prüfe auf unvollständige JSON
204
+ const trimmed = fileContent.trim();
205
+ if (!((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
206
+ (trimmed.startsWith('[') && trimmed.endsWith(']')))) {
207
+ console.warn(`⚠️ Datei ${fileName} hat ungültiges JSON-Format`);
208
+ return null;
209
+ }
210
+
211
+ const data = JSON.parse(fileContent);
212
+
213
+ // Cache mit Größenlimit aktualisieren
214
+ this.updateCache(fileName, data);
215
+
161
216
  return data;
162
217
  } catch (error) {
163
- console.error(`❌ Fehler beim Lesen von ${fileName}:`, error);
218
+ console.error(`❌ Fehler beim Lesen von ${fileName}:`, error.message);
219
+
220
+ // Versuche Backup-Recovery
221
+ const backupData = this.tryRecoverFromBackup(fileName);
222
+ if (backupData) {
223
+ console.log(`✅ ${fileName} aus Backup wiederhergestellt`);
224
+ return backupData;
225
+ }
226
+
227
+ return null;
228
+ }
229
+ }
230
+
231
+ // Cache-Management mit Größenlimit
232
+ updateCache(fileName, data) {
233
+ // Begrenze Cache-Größe auf 100 Einträge
234
+ if (this.cache.size >= 100) {
235
+ // Entferne älteste Einträge (FIFO)
236
+ const firstKey = this.cache.keys().next().value;
237
+ this.cache.delete(firstKey);
238
+ }
239
+
240
+ this.cache.set(fileName, data);
241
+ }
242
+
243
+ // Backup-Recovery für korrupte Dateien
244
+ tryRecoverFromBackup(fileName) {
245
+ try {
246
+ const filePath = this.getFilePath(fileName);
247
+ const backupPath = `${filePath}.backup`;
248
+
249
+ if (fs.existsSync(backupPath)) {
250
+ const backupContent = fs.readFileSync(backupPath, 'utf8');
251
+ const backupData = JSON.parse(backupContent);
252
+
253
+ // Backup ist gültig - stelle wieder her
254
+ fs.copyFileSync(backupPath, filePath);
255
+ return backupData;
256
+ }
257
+
258
+ return null;
259
+ } catch (error) {
164
260
  return null;
165
261
  }
166
262
  }
@@ -170,34 +266,29 @@ export class WAStorage {
170
266
 
171
267
  if (Array.isArray(existingData)) {
172
268
  existingData.push(newData);
269
+ return this.writeData(fileName, existingData);
173
270
  } else {
174
- // Wenn Object, merge
175
- Object.assign(existingData, newData);
271
+ // Für Objekte: Merge
272
+ const merged = { ...existingData, ...newData };
273
+ return this.writeData(fileName, merged);
176
274
  }
177
-
178
- return this.writeData(fileName, existingData);
179
275
  }
180
276
 
181
277
  setKey(fileName, key, value) {
182
- let data = this.readData(fileName) || {};
278
+ const data = this.readData(fileName) || {};
183
279
 
184
- // Nested keys unterstützen (z.B. "user.settings.theme")
185
- if (key.includes('.')) {
186
- const keys = key.split('.');
187
- let current = data;
188
-
189
- for (let i = 0; i < keys.length - 1; i++) {
190
- if (!current[keys[i]]) {
191
- current[keys[i]] = {};
192
- }
193
- current = current[keys[i]];
280
+ // Unterstütze nested keys mit Punkt-Notation (z.B. "user.name")
281
+ const keys = key.split('.');
282
+ let current = data;
283
+
284
+ for (let i = 0; i < keys.length - 1; i++) {
285
+ if (!current[keys[i]] || typeof current[keys[i]] !== 'object') {
286
+ current[keys[i]] = {};
194
287
  }
195
-
196
- current[keys[keys.length - 1]] = value;
197
- } else {
198
- data[key] = value;
288
+ current = current[keys[i]];
199
289
  }
200
290
 
291
+ current[keys[keys.length - 1]] = value;
201
292
  return this.writeData(fileName, data);
202
293
  }
203
294
 
@@ -205,42 +296,44 @@ export class WAStorage {
205
296
  const data = this.readData(fileName);
206
297
  if (!data) return null;
207
298
 
208
- // Nested keys unterstützen
209
- if (key.includes('.')) {
210
- const keys = key.split('.');
211
- let current = data;
212
-
213
- for (const k of keys) {
214
- if (current[k] === undefined) {
215
- return null;
216
- }
217
- current = current[k];
299
+ // Unterstütze nested keys mit Punkt-Notation
300
+ const keys = key.split('.');
301
+ let current = data;
302
+
303
+ for (const k of keys) {
304
+ if (current === null || current === undefined || typeof current !== 'object') {
305
+ return null;
218
306
  }
219
-
220
- return current;
307
+ current = current[k];
221
308
  }
222
309
 
223
- return data[key];
310
+ return current;
224
311
  }
225
312
 
226
- pushToArray(fileName, value) {
227
- let data = this.readData(fileName);
313
+ pushItem(fileName, item) {
314
+ const data = this.readData(fileName) || [];
228
315
 
229
- if (!data) {
230
- data = [];
231
- } else if (!Array.isArray(data)) {
232
- console.error(`❌ ${fileName} ist kein Array!`);
316
+ if (!Array.isArray(data)) {
317
+ console.error(`❌ ${fileName} ist kein Array - kann nicht pushen`);
233
318
  return false;
234
319
  }
235
320
 
236
- data.push(value);
321
+ data.push(item);
237
322
  return this.writeData(fileName, data);
238
323
  }
239
324
 
240
- incrementKey(fileName, key, amount = 1) {
241
- const currentValue = this.getKey(fileName, key) || 0;
242
- const newValue = currentValue + amount;
243
- return this.setKey(fileName, key, newValue);
325
+ findItem(fileName, condition) {
326
+ const data = this.readData(fileName);
327
+ if (!Array.isArray(data)) return null;
328
+
329
+ return data.find(condition);
330
+ }
331
+
332
+ filterItems(fileName, condition) {
333
+ const data = this.readData(fileName);
334
+ if (!Array.isArray(data)) return [];
335
+
336
+ return data.filter(condition);
244
337
  }
245
338
 
246
339
  deleteFile(fileName) {
@@ -249,12 +342,10 @@ export class WAStorage {
249
342
 
250
343
  if (fs.existsSync(filePath)) {
251
344
  fs.unlinkSync(filePath);
252
- this.cache.delete(fileName);
253
- // Stille Löschung
254
- return true;
255
345
  }
256
346
 
257
- return false;
347
+ this.cache.delete(fileName);
348
+ return true;
258
349
  } catch (error) {
259
350
  console.error(`❌ Fehler beim Löschen von ${fileName}:`, error);
260
351
  return false;
@@ -262,141 +353,102 @@ export class WAStorage {
262
353
  }
263
354
 
264
355
  deleteKey(fileName, key) {
265
- let data = this.readData(fileName);
356
+ const data = this.readData(fileName);
266
357
  if (!data) return false;
267
358
 
268
- if (key.includes('.')) {
269
- // Nested key löschen
270
- const keys = key.split('.');
271
- let current = data;
272
-
273
- for (let i = 0; i < keys.length - 1; i++) {
274
- if (!current[keys[i]]) {
275
- return false;
276
- }
277
- current = current[keys[i]];
359
+ // Unterstütze nested keys
360
+ const keys = key.split('.');
361
+ let current = data;
362
+
363
+ for (let i = 0; i < keys.length - 1; i++) {
364
+ if (!current[keys[i]] || typeof current[keys[i]] !== 'object') {
365
+ return false; // Key existiert nicht
278
366
  }
279
-
280
- delete current[keys[keys.length - 1]];
281
- } else {
282
- delete data[key];
367
+ current = current[keys[i]];
283
368
  }
284
369
 
370
+ delete current[keys[keys.length - 1]];
285
371
  return this.writeData(fileName, data);
286
372
  }
287
373
 
288
374
  deleteWhere(fileName, condition) {
289
- let data = this.readData(fileName);
290
- if (!data) return false;
291
-
292
- if (Array.isArray(data)) {
293
- // Array filtern
294
- const originalLength = data.length;
295
- data = data.filter(item => !condition(item));
296
-
297
- if (data.length !== originalLength) {
298
- return this.writeData(fileName, data);
299
- }
300
- } else {
301
- // Object filtern
302
- let changed = false;
303
-
304
- for (const [key, value] of Object.entries(data)) {
305
- if (condition(value, key)) {
306
- delete data[key];
307
- changed = true;
308
- }
309
- }
310
-
311
- if (changed) {
312
- return this.writeData(fileName, data);
313
- }
314
- }
375
+ const data = this.readData(fileName);
376
+ if (!Array.isArray(data)) return false;
315
377
 
316
- return false;
317
- }
318
-
319
- fileExists(fileName) {
320
- const filePath = this.getFilePath(fileName);
321
- return fs.existsSync(filePath);
378
+ const filteredData = data.filter(item => !condition(item));
379
+ return this.writeData(fileName, filteredData);
322
380
  }
323
381
 
324
382
  // ===== UTILITY METHODS =====
325
383
 
326
- // Alle Dateien auflisten
327
- listFiles() {
384
+ exists(fileName) {
328
385
  try {
329
- const files = fs.readdirSync(this.baseDir);
330
- return files.filter(file => file.endsWith('.json')).map(file => file.replace('.json', ''));
386
+ const filePath = this.getFilePath(fileName);
387
+ return fs.existsSync(filePath);
331
388
  } catch (error) {
332
- console.error('❌ Fehler beim Auflisten der Dateien:', error);
333
- return [];
389
+ return false;
334
390
  }
335
391
  }
336
392
 
337
- // Cache leeren
338
- clearCache() {
339
- this.cache.clear();
340
- // Stille Cache-Bereinigung
393
+ size(fileName) {
394
+ try {
395
+ const filePath = this.getFilePath(fileName);
396
+ if (fs.existsSync(filePath)) {
397
+ return fs.statSync(filePath).size;
398
+ }
399
+ return 0;
400
+ } catch (error) {
401
+ return 0;
402
+ }
341
403
  }
342
404
 
343
- // Backup erstellen
344
- backup(backupDir = './waengine-backup') {
405
+ backup(fileName) {
345
406
  try {
346
- if (!fs.existsSync(backupDir)) {
347
- fs.mkdirSync(backupDir, { recursive: true });
348
- }
349
-
350
- const files = fs.readdirSync(this.baseDir);
351
- let backedUp = 0;
407
+ const filePath = this.getFilePath(fileName);
408
+ const backupPath = `${filePath}.backup`;
352
409
 
353
- for (const file of files) {
354
- const srcPath = path.join(this.baseDir, file);
355
- const destPath = path.join(backupDir, file);
356
- fs.copyFileSync(srcPath, destPath);
357
- backedUp++;
410
+ if (fs.existsSync(filePath)) {
411
+ fs.copyFileSync(filePath, backupPath);
412
+ return true;
358
413
  }
359
-
360
- // Nur bei explizitem Backup eine Meldung
361
- console.log(`💾 Backup erstellt: ${backedUp} Dateien in ${backupDir}`);
362
- return backupDir;
414
+ return false;
363
415
  } catch (error) {
364
- console.error('❌ Backup Fehler:', error);
416
+ console.error(`❌ Backup-Fehler für ${fileName}:`, error);
365
417
  return false;
366
418
  }
367
419
  }
368
420
 
369
- // Storage Statistics
421
+ // Bereinige Cache
422
+ clearCache() {
423
+ this.cache.clear();
424
+ }
425
+
426
+ // Storage-Statistiken
370
427
  getStats() {
371
- const files = this.listFiles();
372
- let totalSize = 0;
373
-
374
- for (const file of files) {
375
- try {
376
- const filePath = this.getFilePath(file);
377
- const stats = fs.statSync(filePath);
378
- totalSize += stats.size;
379
- } catch (error) {
380
- // Ignore
428
+ try {
429
+ const files = fs.readdirSync(this.baseDir);
430
+ const jsonFiles = files.filter(f => f.endsWith('.json'));
431
+
432
+ let totalSize = 0;
433
+ for (const file of jsonFiles) {
434
+ totalSize += this.size(file.replace('.json', ''));
381
435
  }
436
+
437
+ return {
438
+ totalFiles: jsonFiles.length,
439
+ totalSize: totalSize,
440
+ cacheSize: this.cache.size,
441
+ baseDir: this.baseDir
442
+ };
443
+ } catch (error) {
444
+ return {
445
+ totalFiles: 0,
446
+ totalSize: 0,
447
+ cacheSize: this.cache.size,
448
+ baseDir: this.baseDir,
449
+ error: error.message
450
+ };
382
451
  }
383
-
384
- return {
385
- totalFiles: files.length,
386
- totalSize: totalSize,
387
- totalSizeFormatted: this.formatBytes(totalSize),
388
- cacheSize: this.cache.size,
389
- baseDir: this.baseDir,
390
- files: files
391
- };
392
- }
393
-
394
- formatBytes(bytes) {
395
- if (bytes === 0) return '0 Bytes';
396
- const k = 1024;
397
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
398
- const i = Math.floor(Math.log(bytes) / Math.log(k));
399
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
400
452
  }
401
453
  }
402
454
 
@@ -404,19 +456,27 @@ export class WAStorage {
404
456
 
405
457
  let globalStorage = null;
406
458
 
407
- export function createStorage(baseDir) {
459
+ export function createStorage(baseDir = './waengine-data') {
408
460
  return new WAStorage(baseDir);
409
461
  }
410
462
 
411
- export function getStorage() {
463
+ export function getStorage(baseDir = './waengine-data') {
412
464
  if (!globalStorage) {
413
- globalStorage = new WAStorage();
465
+ globalStorage = new WAStorage(baseDir);
414
466
  }
415
467
  return globalStorage;
416
468
  }
417
469
 
418
- // ===== SHORTHAND FUNCTIONS =====
470
+ // ===== CONVENIENCE EXPORTS =====
471
+
472
+ export const write = {
473
+ in: (fileName) => getStorage().write.in(fileName)
474
+ };
475
+
476
+ export const read = {
477
+ from: (fileName) => getStorage().read.from(fileName)
478
+ };
419
479
 
420
- export const write = getStorage().write;
421
- export const read = getStorage().read;
422
- export const del = getStorage().delete;
480
+ export const del = {
481
+ from: (fileName) => getStorage().delete.from(fileName)
482
+ };