dimond-db 1.0.0
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/LICENSE +21 -0
- package/README.md +362 -0
- package/package.json +46 -0
- package/src/database/Collection.js +401 -0
- package/src/database/Database.js +121 -0
- package/src/engine/QueryEngine.js +166 -0
- package/src/engine/StorageEngine.js +177 -0
- package/src/errors/DatabaseError.js +68 -0
- package/src/index.js +18 -0
- package/src/query/operators.js +187 -0
- package/src/storage/FileStorage.js +131 -0
- package/src/utils/deepClone.js +31 -0
- package/src/utils/idGenerator.js +35 -0
- package/src/utils/validator.js +133 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { FileStorage } from '../storage/FileStorage.js';
|
|
3
|
+
import { StorageError } from '../errors/DatabaseError.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Storage Engine - Manages collection file operations
|
|
7
|
+
*/
|
|
8
|
+
export class StorageEngine {
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} databasePath - Root path for the database
|
|
11
|
+
*/
|
|
12
|
+
constructor(databasePath) {
|
|
13
|
+
this.databasePath = databasePath;
|
|
14
|
+
this.collectionsPath = join(databasePath, 'collections');
|
|
15
|
+
this.metadataPath = join(databasePath, 'metadata.json');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initializes the database storage structure
|
|
20
|
+
*/
|
|
21
|
+
async initialize() {
|
|
22
|
+
await FileStorage.ensureDir(this.databasePath);
|
|
23
|
+
await FileStorage.ensureDir(this.collectionsPath);
|
|
24
|
+
|
|
25
|
+
// Create metadata if it doesn't exist
|
|
26
|
+
if (!(await FileStorage.exists(this.metadataPath))) {
|
|
27
|
+
await this.writeMetadata({
|
|
28
|
+
version: '1.0.0',
|
|
29
|
+
collections: [],
|
|
30
|
+
createdAt: new Date().toISOString()
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Gets the file path for a collection
|
|
37
|
+
* @param {string} collectionName - Name of the collection
|
|
38
|
+
* @returns {string} Full path to the collection file
|
|
39
|
+
*/
|
|
40
|
+
getCollectionPath(collectionName) {
|
|
41
|
+
return join(this.collectionsPath, `${collectionName}.collection`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Checks if a collection exists
|
|
46
|
+
* @param {string} collectionName - Name of the collection
|
|
47
|
+
* @returns {Promise<boolean>} True if collection exists
|
|
48
|
+
*/
|
|
49
|
+
async collectionExists(collectionName) {
|
|
50
|
+
const collectionPath = this.getCollectionPath(collectionName);
|
|
51
|
+
return await FileStorage.exists(collectionPath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Reads a collection file
|
|
56
|
+
* @param {string} collectionName - Name of the collection
|
|
57
|
+
* @returns {Promise<Array>} Array of documents
|
|
58
|
+
*/
|
|
59
|
+
async readCollection(collectionName) {
|
|
60
|
+
const collectionPath = this.getCollectionPath(collectionName);
|
|
61
|
+
|
|
62
|
+
if (!(await FileStorage.exists(collectionPath))) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
return await FileStorage.readJSON(collectionPath);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
throw new StorageError(
|
|
70
|
+
`Failed to read collection "${collectionName}"`,
|
|
71
|
+
error
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Writes documents to a collection file
|
|
78
|
+
* @param {string} collectionName - Name of the collection
|
|
79
|
+
* @param {Array} documents - Array of documents to write
|
|
80
|
+
*/
|
|
81
|
+
async writeCollection(collectionName, documents) {
|
|
82
|
+
// Ensure collection exists before writing
|
|
83
|
+
if (!(await this.collectionExists(collectionName))) {
|
|
84
|
+
await this.createCollection(collectionName);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const collectionPath = this.getCollectionPath(collectionName);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await FileStorage.writeJSON(collectionPath, documents);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new StorageError(
|
|
93
|
+
`Failed to write collection "${collectionName}"`,
|
|
94
|
+
error
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Creates a new collection
|
|
101
|
+
* @param {string} collectionName - Name of the collection
|
|
102
|
+
*/
|
|
103
|
+
async createCollection(collectionName) {
|
|
104
|
+
const collectionPath = this.getCollectionPath(collectionName);
|
|
105
|
+
|
|
106
|
+
// Create empty collection file
|
|
107
|
+
await FileStorage.writeJSON(collectionPath, []);
|
|
108
|
+
|
|
109
|
+
// Update metadata
|
|
110
|
+
const metadata = await this.readMetadata();
|
|
111
|
+
if (!metadata.collections.includes(collectionName)) {
|
|
112
|
+
metadata.collections.push(collectionName);
|
|
113
|
+
await this.writeMetadata(metadata);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Deletes a collection
|
|
119
|
+
* @param {string} collectionName - Name of the collection
|
|
120
|
+
*/
|
|
121
|
+
async deleteCollection(collectionName) {
|
|
122
|
+
const collectionPath = this.getCollectionPath(collectionName);
|
|
123
|
+
|
|
124
|
+
// Delete collection file
|
|
125
|
+
await FileStorage.deleteFile(collectionPath);
|
|
126
|
+
|
|
127
|
+
// Update metadata
|
|
128
|
+
const metadata = await this.readMetadata();
|
|
129
|
+
metadata.collections = metadata.collections.filter(
|
|
130
|
+
name => name !== collectionName
|
|
131
|
+
);
|
|
132
|
+
await this.writeMetadata(metadata);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Lists all collections
|
|
137
|
+
* @returns {Promise<Array<string>>} Array of collection names
|
|
138
|
+
*/
|
|
139
|
+
async listCollections() {
|
|
140
|
+
const files = await FileStorage.listFiles(this.collectionsPath);
|
|
141
|
+
return files
|
|
142
|
+
.filter(file => file.endsWith('.collection'))
|
|
143
|
+
.map(file => file.replace('.collection', ''));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Reads metadata
|
|
148
|
+
* @returns {Promise<Object>} Metadata object
|
|
149
|
+
*/
|
|
150
|
+
async readMetadata() {
|
|
151
|
+
try {
|
|
152
|
+
return await FileStorage.readJSON(this.metadataPath);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
// Return default metadata if file doesn't exist or is corrupted
|
|
155
|
+
return {
|
|
156
|
+
version: '1.0.0',
|
|
157
|
+
collections: [],
|
|
158
|
+
createdAt: new Date().toISOString()
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Writes metadata
|
|
165
|
+
* @param {Object} metadata - Metadata object to write
|
|
166
|
+
*/
|
|
167
|
+
async writeMetadata(metadata) {
|
|
168
|
+
await FileStorage.writeJSON(this.metadataPath, metadata);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Deletes the entire database
|
|
173
|
+
*/
|
|
174
|
+
async dropDatabase() {
|
|
175
|
+
await FileStorage.deleteDir(this.databasePath);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for all LocalDB errors
|
|
3
|
+
*/
|
|
4
|
+
export class DatabaseError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = this.constructor.name;
|
|
8
|
+
Error.captureStackTrace(this, this.constructor);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Thrown when a collection is not found
|
|
14
|
+
*/
|
|
15
|
+
export class CollectionNotFoundError extends DatabaseError {
|
|
16
|
+
constructor(collectionName) {
|
|
17
|
+
super(`Collection "${collectionName}" not found`);
|
|
18
|
+
this.collectionName = collectionName;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Thrown when attempting to insert a document with a duplicate _id
|
|
24
|
+
*/
|
|
25
|
+
export class DuplicateKeyError extends DatabaseError {
|
|
26
|
+
constructor(id) {
|
|
27
|
+
super(`Duplicate key error: document with _id "${id}" already exists`);
|
|
28
|
+
this.id = id;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Thrown when document validation fails
|
|
34
|
+
*/
|
|
35
|
+
export class ValidationError extends DatabaseError {
|
|
36
|
+
constructor(message) {
|
|
37
|
+
super(`Validation error: ${message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Thrown when a query is malformed or invalid
|
|
43
|
+
*/
|
|
44
|
+
export class QueryError extends DatabaseError {
|
|
45
|
+
constructor(message) {
|
|
46
|
+
super(`Query error: ${message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Thrown when storage operations fail
|
|
52
|
+
*/
|
|
53
|
+
export class StorageError extends DatabaseError {
|
|
54
|
+
constructor(message, cause) {
|
|
55
|
+
super(`Storage error: ${message}`);
|
|
56
|
+
this.cause = cause;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Thrown when database operations fail
|
|
62
|
+
*/
|
|
63
|
+
export class DatabaseOperationError extends DatabaseError {
|
|
64
|
+
constructor(operation, message) {
|
|
65
|
+
super(`Database operation "${operation}" failed: ${message}`);
|
|
66
|
+
this.operation = operation;
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Database } from './database/Database.js';
|
|
2
|
+
|
|
3
|
+
// Export the main Database class as LocalDB
|
|
4
|
+
export { Database as LocalDB };
|
|
5
|
+
|
|
6
|
+
// Export error classes for advanced error handling
|
|
7
|
+
export {
|
|
8
|
+
DatabaseError,
|
|
9
|
+
CollectionNotFoundError,
|
|
10
|
+
DuplicateKeyError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
QueryError,
|
|
13
|
+
StorageError,
|
|
14
|
+
DatabaseOperationError
|
|
15
|
+
} from './errors/DatabaseError.js';
|
|
16
|
+
|
|
17
|
+
// Default export
|
|
18
|
+
export default Database;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { QueryError } from '../errors/DatabaseError.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Comparison operators for querying documents
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Equality comparison ($eq)
|
|
9
|
+
* @param {*} fieldValue - The field value from the document
|
|
10
|
+
* @param {*} queryValue - The value to compare against
|
|
11
|
+
* @returns {boolean} True if values are equal
|
|
12
|
+
*/
|
|
13
|
+
export function $eq(fieldValue, queryValue) {
|
|
14
|
+
return fieldValue === queryValue;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Inequality comparison ($ne)
|
|
19
|
+
* @param {*} fieldValue - The field value from the document
|
|
20
|
+
* @param {*} queryValue - The value to compare against
|
|
21
|
+
* @returns {boolean} True if values are not equal
|
|
22
|
+
*/
|
|
23
|
+
export function $ne(fieldValue, queryValue) {
|
|
24
|
+
return fieldValue !== queryValue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Greater than comparison ($gt)
|
|
29
|
+
* @param {*} fieldValue - The field value from the document
|
|
30
|
+
* @param {*} queryValue - The value to compare against
|
|
31
|
+
* @returns {boolean} True if field value is greater than query value
|
|
32
|
+
*/
|
|
33
|
+
export function $gt(fieldValue, queryValue) {
|
|
34
|
+
return fieldValue > queryValue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Greater than or equal comparison ($gte)
|
|
39
|
+
* @param {*} fieldValue - The field value from the document
|
|
40
|
+
* @param {*} queryValue - The value to compare against
|
|
41
|
+
* @returns {boolean} True if field value is greater than or equal to query value
|
|
42
|
+
*/
|
|
43
|
+
export function $gte(fieldValue, queryValue) {
|
|
44
|
+
return fieldValue >= queryValue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Less than comparison ($lt)
|
|
49
|
+
* @param {*} fieldValue - The field value from the document
|
|
50
|
+
* @param {*} queryValue - The value to compare against
|
|
51
|
+
* @returns {boolean} True if field value is less than query value
|
|
52
|
+
*/
|
|
53
|
+
export function $lt(fieldValue, queryValue) {
|
|
54
|
+
return fieldValue < queryValue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Less than or equal comparison ($lte)
|
|
59
|
+
* @param {*} fieldValue - The field value from the document
|
|
60
|
+
* @param {*} queryValue - The value to compare against
|
|
61
|
+
* @returns {boolean} True if field value is less than or equal to query value
|
|
62
|
+
*/
|
|
63
|
+
export function $lte(fieldValue, queryValue) {
|
|
64
|
+
return fieldValue <= queryValue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* In array comparison ($in)
|
|
69
|
+
* @param {*} fieldValue - The field value from the document
|
|
70
|
+
* @param {Array} queryValue - The array of values to check against
|
|
71
|
+
* @returns {boolean} True if field value is in the array
|
|
72
|
+
*/
|
|
73
|
+
export function $in(fieldValue, queryValue) {
|
|
74
|
+
if (!Array.isArray(queryValue)) {
|
|
75
|
+
throw new QueryError('$in operator requires an array');
|
|
76
|
+
}
|
|
77
|
+
return queryValue.includes(fieldValue);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Not in array comparison ($nin)
|
|
82
|
+
* @param {*} fieldValue - The field value from the document
|
|
83
|
+
* @param {Array} queryValue - The array of values to check against
|
|
84
|
+
* @returns {boolean} True if field value is not in the array
|
|
85
|
+
*/
|
|
86
|
+
export function $nin(fieldValue, queryValue) {
|
|
87
|
+
if (!Array.isArray(queryValue)) {
|
|
88
|
+
throw new QueryError('$nin operator requires an array');
|
|
89
|
+
}
|
|
90
|
+
return !queryValue.includes(fieldValue);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Field exists check ($exists)
|
|
95
|
+
* @param {*} fieldValue - The field value from the document
|
|
96
|
+
* @param {boolean} queryValue - Whether the field should exist
|
|
97
|
+
* @returns {boolean} True if existence matches expectation
|
|
98
|
+
*/
|
|
99
|
+
export function $exists(fieldValue, queryValue) {
|
|
100
|
+
const exists = fieldValue !== undefined;
|
|
101
|
+
return queryValue ? exists : !exists;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Map of operator names to their functions
|
|
106
|
+
*/
|
|
107
|
+
export const OPERATORS = {
|
|
108
|
+
$eq,
|
|
109
|
+
$ne,
|
|
110
|
+
$gt,
|
|
111
|
+
$gte,
|
|
112
|
+
$lt,
|
|
113
|
+
$lte,
|
|
114
|
+
$in,
|
|
115
|
+
$nin,
|
|
116
|
+
$exists
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Checks if a key is a query operator
|
|
121
|
+
* @param {string} key - The key to check
|
|
122
|
+
* @returns {boolean} True if the key is an operator
|
|
123
|
+
*/
|
|
124
|
+
export function isOperator(key) {
|
|
125
|
+
return key.startsWith('$') && key in OPERATORS;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Gets the nested value from an object using dot notation
|
|
130
|
+
* @param {Object} obj - The object to get the value from
|
|
131
|
+
* @param {string} path - The dot-notation path (e.g., 'user.address.city')
|
|
132
|
+
* @returns {*} The value at the path, or undefined if not found
|
|
133
|
+
*/
|
|
134
|
+
export function getNestedValue(obj, path) {
|
|
135
|
+
const keys = path.split('.');
|
|
136
|
+
let value = obj;
|
|
137
|
+
|
|
138
|
+
for (const key of keys) {
|
|
139
|
+
if (value === null || value === undefined) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
value = value[key];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Sets a nested value in an object using dot notation
|
|
150
|
+
* @param {Object} obj - The object to set the value in
|
|
151
|
+
* @param {string} path - The dot-notation path
|
|
152
|
+
* @param {*} value - The value to set
|
|
153
|
+
*/
|
|
154
|
+
export function setNestedValue(obj, path, value) {
|
|
155
|
+
const keys = path.split('.');
|
|
156
|
+
const lastKey = keys.pop();
|
|
157
|
+
|
|
158
|
+
let current = obj;
|
|
159
|
+
for (const key of keys) {
|
|
160
|
+
if (!(key in current) || typeof current[key] !== 'object') {
|
|
161
|
+
current[key] = {};
|
|
162
|
+
}
|
|
163
|
+
current = current[key];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
current[lastKey] = value;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Deletes a nested value from an object using dot notation
|
|
171
|
+
* @param {Object} obj - The object to delete the value from
|
|
172
|
+
* @param {string} path - The dot-notation path
|
|
173
|
+
*/
|
|
174
|
+
export function deleteNestedValue(obj, path) {
|
|
175
|
+
const keys = path.split('.');
|
|
176
|
+
const lastKey = keys.pop();
|
|
177
|
+
|
|
178
|
+
let current = obj;
|
|
179
|
+
for (const key of keys) {
|
|
180
|
+
if (!(key in current)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
current = current[key];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
delete current[lastKey];
|
|
187
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { StorageError } from '../errors/DatabaseError.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* File Storage - Handles all file system operations
|
|
7
|
+
*/
|
|
8
|
+
export class FileStorage {
|
|
9
|
+
/**
|
|
10
|
+
* Reads a JSON file and returns parsed content
|
|
11
|
+
* @param {string} filePath - Path to the file
|
|
12
|
+
* @returns {Promise<*>} Parsed JSON content
|
|
13
|
+
*/
|
|
14
|
+
static async readJSON(filePath) {
|
|
15
|
+
try {
|
|
16
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
17
|
+
return JSON.parse(content);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error.code === 'ENOENT') {
|
|
20
|
+
throw new StorageError(`File not found: ${filePath}`, error);
|
|
21
|
+
}
|
|
22
|
+
if (error instanceof SyntaxError) {
|
|
23
|
+
throw new StorageError(`Invalid JSON in file: ${filePath}`, error);
|
|
24
|
+
}
|
|
25
|
+
throw new StorageError(`Failed to read file: ${filePath}`, error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Writes data to a JSON file
|
|
31
|
+
* @param {string} filePath - Path to the file
|
|
32
|
+
* @param {*} data - Data to write
|
|
33
|
+
*/
|
|
34
|
+
static async writeJSON(filePath, data) {
|
|
35
|
+
try {
|
|
36
|
+
// Ensure parent directory exists
|
|
37
|
+
const parentDir = dirname(filePath);
|
|
38
|
+
await this.ensureDir(parentDir);
|
|
39
|
+
|
|
40
|
+
const content = JSON.stringify(data, null, 2);
|
|
41
|
+
|
|
42
|
+
// Atomic write: write to temp file then rename
|
|
43
|
+
const tempPath = `${filePath}.tmp`;
|
|
44
|
+
await fs.writeFile(tempPath, content, 'utf-8');
|
|
45
|
+
await fs.rename(tempPath, filePath);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new StorageError(`Failed to write file: ${filePath}`, error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Checks if a file exists
|
|
53
|
+
* @param {string} filePath - Path to check
|
|
54
|
+
* @returns {Promise<boolean>} True if file exists
|
|
55
|
+
*/
|
|
56
|
+
static async exists(filePath) {
|
|
57
|
+
try {
|
|
58
|
+
await fs.access(filePath);
|
|
59
|
+
return true;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates a directory and all parent directories if they don't exist
|
|
67
|
+
* @param {string} dirPath - Directory path to create
|
|
68
|
+
*/
|
|
69
|
+
static async ensureDir(dirPath) {
|
|
70
|
+
try {
|
|
71
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new StorageError(`Failed to create directory: ${dirPath}`, error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Deletes a file
|
|
79
|
+
* @param {string} filePath - Path to the file
|
|
80
|
+
*/
|
|
81
|
+
static async deleteFile(filePath) {
|
|
82
|
+
try {
|
|
83
|
+
await fs.unlink(filePath);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error.code !== 'ENOENT') {
|
|
86
|
+
throw new StorageError(`Failed to delete file: ${filePath}`, error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Lists all files in a directory
|
|
93
|
+
* @param {string} dirPath - Directory path
|
|
94
|
+
* @returns {Promise<string[]>} Array of file names
|
|
95
|
+
*/
|
|
96
|
+
static async listFiles(dirPath) {
|
|
97
|
+
try {
|
|
98
|
+
return await fs.readdir(dirPath);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error.code === 'ENOENT') {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
throw new StorageError(`Failed to list directory: ${dirPath}`, error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Deletes a directory and all its contents
|
|
109
|
+
* @param {string} dirPath - Directory path
|
|
110
|
+
*/
|
|
111
|
+
static async deleteDir(dirPath) {
|
|
112
|
+
try {
|
|
113
|
+
await fs.rm(dirPath, { recursive: true, force: true });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
throw new StorageError(`Failed to delete directory: ${dirPath}`, error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Gets file stats
|
|
121
|
+
* @param {string} filePath - Path to the file
|
|
122
|
+
* @returns {Promise<Object>} File stats
|
|
123
|
+
*/
|
|
124
|
+
static async getStats(filePath) {
|
|
125
|
+
try {
|
|
126
|
+
return await fs.stat(filePath);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw new StorageError(`Failed to get file stats: ${filePath}`, error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a deep clone of an object
|
|
3
|
+
* @param {*} obj - The object to clone
|
|
4
|
+
* @returns {*} A deep clone of the object
|
|
5
|
+
*/
|
|
6
|
+
export function deepClone(obj) {
|
|
7
|
+
if (obj === null || typeof obj !== 'object') {
|
|
8
|
+
return obj;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (obj instanceof Date) {
|
|
12
|
+
return new Date(obj.getTime());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (obj instanceof RegExp) {
|
|
16
|
+
return new RegExp(obj.source, obj.flags);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (Array.isArray(obj)) {
|
|
20
|
+
return obj.map(item => deepClone(item));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const cloned = {};
|
|
24
|
+
for (const key in obj) {
|
|
25
|
+
if (obj.hasOwnProperty(key)) {
|
|
26
|
+
cloned[key] = deepClone(obj[key]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return cloned;
|
|
31
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generates a UUID v4 compliant unique identifier
|
|
5
|
+
* @returns {string} A UUID string
|
|
6
|
+
*/
|
|
7
|
+
export function generateId() {
|
|
8
|
+
const bytes = randomBytes(16);
|
|
9
|
+
|
|
10
|
+
// Set version (4) and variant bits according to RFC 4122
|
|
11
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4
|
|
12
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant 10
|
|
13
|
+
|
|
14
|
+
const hex = bytes.toString('hex');
|
|
15
|
+
|
|
16
|
+
return [
|
|
17
|
+
hex.substring(0, 8),
|
|
18
|
+
hex.substring(8, 12),
|
|
19
|
+
hex.substring(12, 16),
|
|
20
|
+
hex.substring(16, 20),
|
|
21
|
+
hex.substring(20, 32)
|
|
22
|
+
].join('-');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validates if a string is a valid UUID format
|
|
27
|
+
* @param {string} id - The ID to validate
|
|
28
|
+
* @returns {boolean} True if valid UUID format
|
|
29
|
+
*/
|
|
30
|
+
export function isValidId(id) {
|
|
31
|
+
if (typeof id !== 'string') return false;
|
|
32
|
+
|
|
33
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
34
|
+
return uuidRegex.test(id);
|
|
35
|
+
}
|