database-local 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/constants.js ADDED
@@ -0,0 +1,22 @@
1
+ export const DEFAULT_DB_PATH = './databases';
2
+ export const DEFAULT_ID_CONFIG = {
3
+ type: String,
4
+ required: true
5
+ };
6
+
7
+ export const FILTER_OPERATORS = {
8
+ $gt: (value, target) => value > target,
9
+ $lt: (value, target) => value < target,
10
+ $gte: (value, target) => value >= target,
11
+ $lte: (value, target) => value <= target,
12
+ $eq: (value, target) => value === target,
13
+ $neq: (value, target) => value !== target,
14
+ $in: (value, target) => value.includes(target),
15
+ $nin: (value, target) => !value.includes(target),
16
+ $regex: (value, target) => value.match(target),
17
+ $exists: (value) => value !== undefined
18
+ };
19
+
20
+ export const RESERVED_KEYS = ['$id', '$ref'];
21
+ export const QUERY_SPECIAL_KEYS = ['$limit'];
22
+ export const OPERATOR_PREFIX = '$';
@@ -0,0 +1,140 @@
1
+ import SchemaValidator from '../validators/SchemaValidator.js';
2
+ import Query from './Query.js';
3
+ import FileHandler from '../utils/FileHandler.js';
4
+ import ObjectUtils from '../utils/ObjectUtils.js';
5
+ import { ObjectId } from '../utils/ObjectId.js';
6
+
7
+ export default class Collection {
8
+ constructor(name, schema, dbPath) {
9
+ this.name = name;
10
+ this.schema = schema;
11
+ this.filePath = FileHandler.resolvePath(dbPath, `${name}.json`);
12
+ this.validator = new SchemaValidator(schema);
13
+ this.data = this.#loadData();
14
+ this.idGenerator = this.#createIdGenerator();
15
+ }
16
+
17
+ #loadData() {
18
+ return FileHandler.readJSON(this.filePath);
19
+ }
20
+
21
+ #save() {
22
+ FileHandler.writeJSON(this.filePath, this.data);
23
+ }
24
+
25
+ #createIdGenerator() {
26
+ const idConfig = this.schema._id || {};
27
+ if (typeof idConfig.default === 'function') {
28
+ return idConfig.default;
29
+ }
30
+ return () => new ObjectId().toString();
31
+ }
32
+
33
+ #enhanceDocument(doc) {
34
+ return {
35
+ ...doc,
36
+ update: (values) => this.updateOne(doc._id, values),
37
+ remove: () => this.deleteOne(doc._id),
38
+ save: () => this.saveOne(doc)
39
+ };
40
+ }
41
+
42
+ #enhanceResults(results) {
43
+ if (Array.isArray(results)) {
44
+ return results.map(doc => this.#enhanceDocument(doc));
45
+ }
46
+ return results ? this.#enhanceDocument(results) : null;
47
+ }
48
+
49
+ find(query = {}) {
50
+ const queryEngine = new Query(this.data);
51
+ const results = queryEngine.filter(query);
52
+ return this.#enhanceResults(results);
53
+ }
54
+
55
+ findOne(query = {}) {
56
+ const results = this.find(query);
57
+ return Array.isArray(results) ? results[0] : results;
58
+ }
59
+
60
+ create(data) {
61
+ const validatedData = this.validator.validate({
62
+ ...data,
63
+ _id: data._id || this.idGenerator()
64
+ });
65
+
66
+ // Check for duplicate IDs
67
+ if (this.data.some(doc => doc._id === validatedData._id)) {
68
+ throw new Error('Duplicate ID detected');
69
+ }
70
+
71
+ return {
72
+ data: validatedData,
73
+ save: () => {
74
+ this.data.unshift(validatedData);
75
+ this.#save();
76
+ return this.#enhanceDocument(validatedData);
77
+ }
78
+ };
79
+ }
80
+
81
+ updateOne(id, updates) {
82
+ const doc = this.data.find(item => item._id === id);
83
+
84
+ if (!doc) {
85
+ throw new Error(`Document with id ${id} not found`);
86
+ }
87
+
88
+ const updated = this.validator.mergeObjects(doc, updates, this.schema);
89
+
90
+ return {
91
+ data: updated,
92
+ save: () => {
93
+ const index = this.data.findIndex(item => item._id === id);
94
+ this.data[index] = updated;
95
+ this.#save();
96
+ return this.#enhanceDocument(updated);
97
+ }
98
+ };
99
+ }
100
+
101
+ deleteOne(id) {
102
+ const initialLength = this.data.length;
103
+ this.data = this.data.filter(item => item._id !== id);
104
+
105
+ if (this.data.length === initialLength) {
106
+ throw new Error(`Document with id ${id} not found`);
107
+ }
108
+
109
+ this.#save();
110
+ }
111
+
112
+ deleteMany(query) {
113
+ const queryEngine = new Query(this.data);
114
+ const toDelete = queryEngine.filter(query);
115
+ const deleteIds = new Set(toDelete.map(doc => doc._id));
116
+
117
+ const initialLength = this.data.length;
118
+ this.data = this.data.filter(doc => !deleteIds.has(doc._id));
119
+
120
+ this.#save();
121
+ return initialLength - this.data.length;
122
+ }
123
+
124
+ saveOne(data) {
125
+ const index = this.data.findIndex(item => item._id === data._id);
126
+
127
+ if (index === -1) {
128
+ throw new Error(`Document with id ${data._id} not found`);
129
+ }
130
+
131
+ this.data[index] = data;
132
+ this.#save();
133
+ return this.#enhanceDocument(data);
134
+ }
135
+
136
+ clear() {
137
+ this.data = [];
138
+ this.#save();
139
+ }
140
+ }
@@ -0,0 +1,48 @@
1
+ import Collection from './Collection.js';
2
+ import FileHandler from '../utils/FileHandler.js';
3
+ import { ObjectId } from '../utils/ObjectId.js';
4
+ export { DEFAULT_DB_PATH, DEFAULT_ID_CONFIG } from '../constants.js';
5
+
6
+ export class Database {
7
+ constructor(options = {}) {
8
+ this.path = options.path || DEFAULT_DB_PATH;
9
+ this.readOnFind = options.readOnFind || false;
10
+ this.collections = new Map();
11
+
12
+ FileHandler.ensureDirectory(this.path);
13
+ this.Schema = this.createCollection.bind(this);
14
+ }
15
+
16
+ createCollection(name, schema = {}) {
17
+ // Ensure _id configuration
18
+ if (!schema._id) {
19
+ schema._id = { ...DEFAULT_ID_CONFIG, default: () => new ObjectId().toString() };
20
+ }
21
+
22
+ const collection = new Collection(name, schema, this.path);
23
+ this.collections.set(name, collection);
24
+
25
+ return collection;
26
+ }
27
+
28
+ getCollection(name) {
29
+ return this.collections.get(name);
30
+ }
31
+
32
+ removeCollection(name) {
33
+ this.collections.delete(name);
34
+ }
35
+
36
+ get Types() {
37
+ return {
38
+ ObjectId: () => new ObjectId().toString(),
39
+ String,
40
+ Number,
41
+ Boolean,
42
+ Date,
43
+ Array,
44
+ Object
45
+ };
46
+ }
47
+ }
48
+ export default Database;
package/core/Query.js ADDED
@@ -0,0 +1,117 @@
1
+ import ObjectUtils from '../utils/ObjectUtils.js';
2
+ import { FILTER_OPERATORS, RESERVED_KEYS, QUERY_SPECIAL_KEYS, OPERATOR_PREFIX } from '../constants.js';
3
+
4
+ export default class Query {
5
+ constructor(data) {
6
+ this.data = data || [];
7
+ }
8
+
9
+ filter(query) {
10
+ if (!query || (typeof query === 'object' && Object.keys(query).length === 0)) {
11
+ return [...this.data];
12
+ }
13
+
14
+ const queryType = ObjectUtils.getType(query);
15
+
16
+ const handlers = {
17
+ function: () => this.data.filter(query),
18
+ object: () => this.#filterByObject(query),
19
+ string: () => this.#findById(query),
20
+ number: () => this.#findById(query)
21
+ };
22
+
23
+ return handlers[queryType]?.() || [];
24
+ }
25
+
26
+ findOne(query) {
27
+ const results = this.filter(query);
28
+ return Array.isArray(results) ? results[0] : results;
29
+ }
30
+
31
+ #findById(id) {
32
+ return this.data.filter(item => item._id === id);
33
+ }
34
+
35
+ #filterByObject(query) {
36
+ const { operators, conditions } = this.#separateQueryParts(query);
37
+
38
+ let results = this.data;
39
+
40
+ // Apply simple conditions first
41
+ if (Object.keys(conditions).length > 0) {
42
+ results = results.filter(item =>
43
+ Object.entries(conditions).every(([key, value]) => {
44
+ if (typeof value === 'object' && value !== null) return true;
45
+ return item[key] === value;
46
+ })
47
+ );
48
+ }
49
+
50
+ // Apply operators
51
+ if (Object.keys(operators).length > 0) {
52
+ results = this.#applyOperators(results, operators);
53
+ }
54
+
55
+ return results;
56
+ }
57
+
58
+ #separateQueryParts(query) {
59
+ const operators = {};
60
+ const conditions = {};
61
+
62
+ for (const [key, value] of Object.entries(query)) {
63
+ if (QUERY_SPECIAL_KEYS.includes(key)) {
64
+ operators[key] = value;
65
+ } else if (this.#hasOperators(value)) {
66
+ operators[key] = value;
67
+ } else {
68
+ conditions[key] = value;
69
+ }
70
+ }
71
+
72
+ return { operators, conditions };
73
+ }
74
+
75
+ #hasOperators(value) {
76
+ if (typeof value !== 'object' || value === null) return false;
77
+ return Object.keys(value).some(key => key.startsWith(OPERATOR_PREFIX));
78
+ }
79
+
80
+ #applyOperators(data, operators) {
81
+ let results = data;
82
+ const limit = operators.$limit;
83
+
84
+ delete operators.$limit;
85
+
86
+ for (const [field, operations] of Object.entries(operators)) {
87
+ if (RESERVED_KEYS.includes(field)) continue;
88
+
89
+ if (typeof operations === 'object') {
90
+ results = results.filter(item =>
91
+ this.#evaluateOperators(item, field, operations)
92
+ );
93
+ }
94
+ }
95
+
96
+ return this.#applyLimit(results, limit);
97
+ }
98
+
99
+ #evaluateOperators(item, field, operations) {
100
+ const value = ObjectUtils.getNestedProperty(item, field);
101
+
102
+ return Object.entries(operations).every(([operator, target]) => {
103
+ const filterFn = FILTER_OPERATORS[operator];
104
+ return filterFn ? filterFn(value, target) : true;
105
+ });
106
+ }
107
+
108
+ #applyLimit(data, limit) {
109
+ if (typeof limit !== 'number' || limit < 1) return data;
110
+
111
+ if (limit === 1) {
112
+ return data.length > 0 ? data[0] : null;
113
+ }
114
+
115
+ return data.slice(0, limit);
116
+ }
117
+ }
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import Database from './core/Database.js';
2
+ export { ObjectId } from './utils/ObjectId.js';
3
+ export default Database;
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "database-local",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [],
10
+ "author": "",
11
+ "license": "ISC",
12
+ "type": "module"
13
+ }
@@ -0,0 +1,28 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export default class FileHandler {
5
+ static ensureDirectory(dirPath) {
6
+ if (!fs.existsSync(dirPath)) {
7
+ fs.mkdirSync(dirPath, { recursive: true });
8
+ }
9
+ }
10
+
11
+ static readJSON(filePath) {
12
+ try {
13
+ const data = fs.readFileSync(filePath, 'utf8');
14
+ return JSON.parse(data);
15
+ } catch (error) {
16
+ return [];
17
+ }
18
+ }
19
+
20
+ static writeJSON(filePath, data) {
21
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
22
+ return data;
23
+ }
24
+
25
+ static resolvePath(basePath, filename) {
26
+ return path.resolve(basePath, filename);
27
+ }
28
+ }
@@ -0,0 +1,38 @@
1
+ export class ObjectId {
2
+ constructor(id = null) {
3
+ if (id instanceof ObjectId) return id;
4
+
5
+ this.timestamp = Math.floor(Date.now() / 1000);
6
+ this.machineId = Math.floor(Math.random() * 0xFFFFFF);
7
+ this.processId = Math.floor(Math.random() * 0xFFFF);
8
+ this.counter = ObjectId.increment();
9
+
10
+ this.value = this.generateId();
11
+ }
12
+
13
+ static increment() {
14
+ ObjectId.counter = ((ObjectId.counter || Math.floor(Math.random() * 0xFFFFFF)) + 1) % 0xFFFFFF;
15
+ return ObjectId.counter;
16
+ }
17
+
18
+ generateId() {
19
+ return [
20
+ this.timestamp.toString(16).padStart(8, '0'),
21
+ this.machineId.toString(16).padStart(6, '0'),
22
+ this.processId.toString(16).padStart(4, '0'),
23
+ this.counter.toString(16).padStart(6, '0')
24
+ ].join('');
25
+ }
26
+
27
+ toString() {
28
+ return this.value;
29
+ }
30
+
31
+ toJSON() {
32
+ return this.toString();
33
+ }
34
+
35
+ static isValid(id) {
36
+ return /^[0-9a-fA-F]{24}$/.test(id);
37
+ }
38
+ }
@@ -0,0 +1,31 @@
1
+ export default class ObjectUtils {
2
+ static getType(value) {
3
+ if (value === null) return 'null';
4
+ if (value === undefined) return 'undefined';
5
+
6
+ const type = typeof value;
7
+ return type === 'object' || type === 'function'
8
+ ? Object.prototype.toString.call(value).slice(8, -1).toLowerCase()
9
+ : type;
10
+ }
11
+
12
+ static getNestedProperty(obj, path) {
13
+ return path.split('.').reduce((current, key) => current?.[key], obj);
14
+ }
15
+
16
+ static isOperator(value) {
17
+ return typeof value === 'string' && value.startsWith('$');
18
+ }
19
+
20
+ static deepClone(obj) {
21
+ return JSON.parse(JSON.stringify(obj));
22
+ }
23
+
24
+ static defineProperty(obj, key, value) {
25
+ Object.defineProperty(obj, key, {
26
+ value,
27
+ enumerable: false,
28
+ configurable: true
29
+ });
30
+ }
31
+ }
@@ -0,0 +1,140 @@
1
+ import TypeChecker from './TypeChecker.js';
2
+ import ObjectUtils from '../utils/ObjectUtils.js';
3
+ import { RESERVED_KEYS } from '../constants.js';
4
+
5
+ export default class SchemaValidator {
6
+ constructor(schema) {
7
+ this.schema = schema;
8
+ }
9
+
10
+ validate(data) {
11
+ return this.#validateObject(data, this.schema);
12
+ }
13
+
14
+ #validateObject(data, schema) {
15
+ const validated = {};
16
+
17
+ for (const [key, config] of Object.entries(schema)) {
18
+ const value = data?.[key];
19
+ const typeConfig = this.#normalizeConfig(config, key);
20
+
21
+ validated[key] = this.#validateField(key, value, typeConfig);
22
+ }
23
+
24
+ return validated;
25
+ }
26
+
27
+ #normalizeConfig(config, key) {
28
+ if (typeof config === 'function') {
29
+ return { type: config };
30
+ }
31
+
32
+ // Si el config es un objeto plano sin type, asumimos que es un Object
33
+ if (typeof config === 'object' && config !== null && !Array.isArray(config) && !config.type) {
34
+ // Verificar si tiene propiedades (es un objeto anidado)
35
+ if (Object.keys(config).length > 0) {
36
+ return { type: Object, nested: config };
37
+ }
38
+ }
39
+
40
+ const { type, ...options } = config;
41
+
42
+ if (!type) {
43
+ throw new Error(`Type definition is required for field "${key}"`);
44
+ }
45
+
46
+ return { type, ...options };
47
+ }
48
+
49
+ #validateField(key, value, config) {
50
+ // Handle default values
51
+ if (value === undefined) {
52
+ if (config.default !== undefined) {
53
+ return TypeChecker.getDefaultValue(config);
54
+ }
55
+ if (config.required) {
56
+ throw new Error(`Field "${key}" is required`);
57
+ }
58
+ return undefined;
59
+ }
60
+
61
+ // Handle null values
62
+ if (value === null) {
63
+ TypeChecker.checkNullable(value, config);
64
+ return null;
65
+ }
66
+
67
+ // Validate type
68
+ const validatedValue = this.#validateType(key, value, config);
69
+
70
+ // Validate enum
71
+ if (config.enum) {
72
+ if (!TypeChecker.validateEnum(validatedValue, config.enum)) {
73
+ throw new Error(
74
+ `Field "${key}" must be one of: ${config.enum.join(', ')}`
75
+ );
76
+ }
77
+ }
78
+
79
+ return validatedValue;
80
+ }
81
+
82
+ #validateType(key, value, config) {
83
+ const { type } = config;
84
+
85
+ // Si tiene un esquema anidado, validar como objeto
86
+ if (config.nested) {
87
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
88
+ throw new Error(`Field "${key}" must be an object`);
89
+ }
90
+ return this.#validateObject(value, config.nested);
91
+ }
92
+
93
+ if (Array.isArray(type)) {
94
+ if (!Array.isArray(value)) {
95
+ throw new Error(`Field "${key}" must be an array`);
96
+ }
97
+ return value.map((item, index) => this.#validateArrayItem(key, item, type[0], index));
98
+ }
99
+
100
+ if (typeof type === 'object' && !Array.isArray(type)) {
101
+ return this.#validateObject(value, type);
102
+ }
103
+
104
+ if (!TypeChecker.isValidType(value, type)) {
105
+ const expectedType = typeof type === 'function' ? type.name : type;
106
+ const actualType = ObjectUtils.getType(value);
107
+ throw new Error(
108
+ `Field "${key}" expected ${expectedType} but got ${actualType}`
109
+ );
110
+ }
111
+
112
+ return value;
113
+ }
114
+
115
+ #validateArrayItem(key, item, type, index) {
116
+ if (typeof type === 'object' && !Array.isArray(type)) {
117
+ return this.#validateObject(item, type);
118
+ }
119
+
120
+ if (!TypeChecker.isValidType(item, type)) {
121
+ throw new Error(
122
+ `Field "${key}[${index}]" has invalid type`
123
+ );
124
+ }
125
+
126
+ return item;
127
+ }
128
+
129
+ mergeObjects(base, updates, schema) {
130
+ const merged = { ...base };
131
+
132
+ for (const [key, value] of Object.entries(updates)) {
133
+ if (value !== undefined) {
134
+ merged[key] = value;
135
+ }
136
+ }
137
+
138
+ return this.validate(merged);
139
+ }
140
+ }
@@ -0,0 +1,38 @@
1
+ import ObjectUtils from '../utils/ObjectUtils.js';
2
+
3
+ export default class TypeChecker {
4
+ static isValidType(value, type) {
5
+ const valueType = ObjectUtils.getType(value);
6
+
7
+ if (typeof type === 'function') {
8
+ return value instanceof type || valueType === type.name.toLowerCase();
9
+ }
10
+
11
+ return valueType === type;
12
+ }
13
+
14
+ static validateEnum(value, allowedValues) {
15
+ return allowedValues.includes(value);
16
+ }
17
+
18
+ static checkRequired(value, config) {
19
+ if (value === undefined && config.required) {
20
+ throw new Error(`Required value is missing`);
21
+ }
22
+ }
23
+
24
+ static checkNullable(value, config) {
25
+ if (value === null && !config.nullable) {
26
+ throw new Error('Null value is not allowed');
27
+ }
28
+ }
29
+
30
+ static getDefaultValue(config) {
31
+ if (config.default !== undefined) {
32
+ return typeof config.default === 'function'
33
+ ? config.default()
34
+ : config.default;
35
+ }
36
+ return undefined;
37
+ }
38
+ }