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.
@@ -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
+ }