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 +22 -0
- package/core/Collection.js +140 -0
- package/core/Database.js +48 -0
- package/core/Query.js +117 -0
- package/index.js +3 -0
- package/package.json +13 -0
- package/utils/FileHandler.js +28 -0
- package/utils/ObjectId.js +38 -0
- package/utils/ObjectUtils.js +31 -0
- package/validators/SchemaValidator.js +140 -0
- package/validators/TypeChecker.js +38 -0
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
|
+
}
|
package/core/Database.js
ADDED
|
@@ -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
package/package.json
ADDED
|
@@ -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
|
+
}
|