@nekodb/client 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 +557 -0
- package/auth/index.js +7 -0
- package/auth/key-derivation.js +7 -0
- package/auth/manager.js +45 -0
- package/connection/index.js +8 -0
- package/connection/manager.js +135 -0
- package/connection/status.js +17 -0
- package/errors/base.js +19 -0
- package/errors/classifier.js +15 -0
- package/errors/database.js +63 -0
- package/errors/index.js +16 -0
- package/errors/network.js +23 -0
- package/events/constants.js +10 -0
- package/events/emitter.js +106 -0
- package/events/index.js +9 -0
- package/events/subscriber.js +21 -0
- package/helpers/collection.js +151 -0
- package/helpers/document.js +49 -0
- package/helpers/index.js +9 -0
- package/helpers/pagination.js +59 -0
- package/index.js +392 -0
- package/middleware/batch.js +49 -0
- package/middleware/buffer.js +33 -0
- package/middleware/cache.js +55 -0
- package/middleware/index.js +15 -0
- package/middleware/logger.js +35 -0
- package/middleware/ping.js +29 -0
- package/middleware/reconnect.js +36 -0
- package/package.json +46 -0
- package/query/builder.js +181 -0
- package/query/field.js +21 -0
- package/query/index.js +9 -0
- package/query/operators.js +19 -0
- package/schema/index.js +7 -0
- package/schema/schema.js +70 -0
- package/schema/types.js +31 -0
- package/schema/validator.js +60 -0
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nekodb/client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "NekoDB is a lightweight encrypted document database for Node.js, built with real-time WebSocket communication, simple JSON-based storage, and a developer-friendly API. Designed for speed, security, and ease of integration without external database dependencies.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"database",
|
|
11
|
+
"nosql",
|
|
12
|
+
"document-database",
|
|
13
|
+
"encrypted-database",
|
|
14
|
+
"realtime-database",
|
|
15
|
+
"websocket",
|
|
16
|
+
"json-database",
|
|
17
|
+
"lightweight",
|
|
18
|
+
"nodejs",
|
|
19
|
+
"nekodb"
|
|
20
|
+
],
|
|
21
|
+
"author": "Galang Purnama",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/Galang-Purnama/NekoDB.git"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/Galang-Purnama/NekoDB/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/Galang-Purnama/NekoDB#readme",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"ws": "^8.16.0"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"index.js",
|
|
36
|
+
"auth/",
|
|
37
|
+
"middleware/",
|
|
38
|
+
"query/",
|
|
39
|
+
"schema/",
|
|
40
|
+
"events/",
|
|
41
|
+
"helpers/",
|
|
42
|
+
"connection/",
|
|
43
|
+
"errors/",
|
|
44
|
+
"README.md"
|
|
45
|
+
]
|
|
46
|
+
}
|
package/query/builder.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
const { FieldQuery } = require('./field');
|
|
2
|
+
|
|
3
|
+
class QueryBuilder {
|
|
4
|
+
#db;
|
|
5
|
+
#collection;
|
|
6
|
+
#conditions;
|
|
7
|
+
#projection;
|
|
8
|
+
#sortFields;
|
|
9
|
+
#limitVal;
|
|
10
|
+
#offsetVal;
|
|
11
|
+
#pageVal;
|
|
12
|
+
#cursorVal;
|
|
13
|
+
|
|
14
|
+
constructor(db, collection) {
|
|
15
|
+
this.#db = db;
|
|
16
|
+
this.#collection = collection;
|
|
17
|
+
this.#conditions = {};
|
|
18
|
+
this.#projection = null;
|
|
19
|
+
this.#sortFields = [];
|
|
20
|
+
this.#limitVal = 0;
|
|
21
|
+
this.#offsetVal = 0;
|
|
22
|
+
this.#pageVal = 0;
|
|
23
|
+
this.#cursorVal = '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
where(field) {
|
|
27
|
+
return new FieldQuery(this, field);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
eq(field, value) {
|
|
31
|
+
this.#conditions[field] = value;
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
gt(field, value) {
|
|
36
|
+
this._addOp(field, '$gt', value);
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
gte(field, value) {
|
|
41
|
+
this._addOp(field, '$gte', value);
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
lt(field, value) {
|
|
46
|
+
this._addOp(field, '$lt', value);
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
lte(field, value) {
|
|
51
|
+
this._addOp(field, '$lte', value);
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ne(field, value) {
|
|
56
|
+
this._addOp(field, '$ne', value);
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
in(field, values) {
|
|
61
|
+
this._addOp(field, '$in', values);
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
nin(field, values) {
|
|
66
|
+
this._addOp(field, '$nin', values);
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
between(field, min, max) {
|
|
71
|
+
this._addOp(field, '$gte', min);
|
|
72
|
+
this._addOp(field, '$lte', max);
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_addOp(field, op, value) {
|
|
77
|
+
if (typeof this.#conditions[field] !== 'object' || this.#conditions[field] === null) {
|
|
78
|
+
this.#conditions[field] = {};
|
|
79
|
+
}
|
|
80
|
+
this.#conditions[field][op] = value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
select(...fields) {
|
|
84
|
+
this.#projection = {};
|
|
85
|
+
for (const f of fields) {
|
|
86
|
+
this.#projection[f] = 1;
|
|
87
|
+
}
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
exclude(...fields) {
|
|
92
|
+
this.#projection = {};
|
|
93
|
+
for (const f of fields) {
|
|
94
|
+
this.#projection[f] = 0;
|
|
95
|
+
}
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
sort(field, order = 'asc') {
|
|
100
|
+
this.#sortFields.push({ field, order });
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
sortAsc(field) { return this.sort(field, 'asc'); }
|
|
105
|
+
sortDesc(field) { return this.sort(field, 'desc'); }
|
|
106
|
+
|
|
107
|
+
limit(n) {
|
|
108
|
+
this.#limitVal = n;
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
offset(n) {
|
|
113
|
+
this.#offsetVal = n;
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
page(n) {
|
|
118
|
+
this.#pageVal = n;
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
cursor(c) {
|
|
123
|
+
this.#cursorVal = c;
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getQuery() {
|
|
128
|
+
return { ...this.#conditions };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getProjection() {
|
|
132
|
+
return this.#projection ? { ...this.#projection } : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getPaginationOptions() {
|
|
136
|
+
const opts = {};
|
|
137
|
+
if (this.#limitVal > 0) opts.limit = this.#limitVal;
|
|
138
|
+
if (this.#offsetVal > 0) opts.offset = this.#offsetVal;
|
|
139
|
+
if (this.#pageVal > 0) opts.page = this.#pageVal;
|
|
140
|
+
if (this.#cursorVal) opts.cursor = this.#cursorVal;
|
|
141
|
+
if (this.#sortFields.length > 0) opts.sort = [...this.#sortFields];
|
|
142
|
+
return opts;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async exec() {
|
|
146
|
+
const query = this.getQuery();
|
|
147
|
+
const hasQuery = Object.keys(query).length > 0;
|
|
148
|
+
const hasPagination = this.#limitVal > 0 || this.#pageVal > 0 || this.#cursorVal;
|
|
149
|
+
const hasProjection = this.#projection !== null;
|
|
150
|
+
|
|
151
|
+
if (hasPagination && hasQuery) {
|
|
152
|
+
return this.#db.searchPaginated(this.#collection, query, this.getPaginationOptions());
|
|
153
|
+
}
|
|
154
|
+
if (hasPagination) {
|
|
155
|
+
return this.#db.listPaginated(this.#collection, {
|
|
156
|
+
...this.getPaginationOptions(),
|
|
157
|
+
filter: hasQuery ? query : null,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (hasProjection && hasQuery) {
|
|
161
|
+
return this.#db.searchProjected(this.#collection, query, this.#projection);
|
|
162
|
+
}
|
|
163
|
+
if (hasQuery) {
|
|
164
|
+
return this.#db.search(this.#collection, query);
|
|
165
|
+
}
|
|
166
|
+
return this.#db.list(this.#collection);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async count() {
|
|
170
|
+
return this.#db.count(this.#collection);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async first() {
|
|
174
|
+
const results = await this.limit(1).exec();
|
|
175
|
+
if (Array.isArray(results)) return results[0] || null;
|
|
176
|
+
if (results?.data) return results.data[0] || null;
|
|
177
|
+
return results;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = { QueryBuilder };
|
package/query/field.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class FieldQuery {
|
|
2
|
+
#builder;
|
|
3
|
+
#field;
|
|
4
|
+
|
|
5
|
+
constructor(builder, field) {
|
|
6
|
+
this.#builder = builder;
|
|
7
|
+
this.#field = field;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
eq(value) { return this.#builder.eq(this.#field, value); }
|
|
11
|
+
gt(value) { return this.#builder.gt(this.#field, value); }
|
|
12
|
+
gte(value) { return this.#builder.gte(this.#field, value); }
|
|
13
|
+
lt(value) { return this.#builder.lt(this.#field, value); }
|
|
14
|
+
lte(value) { return this.#builder.lte(this.#field, value); }
|
|
15
|
+
ne(value) { return this.#builder.ne(this.#field, value); }
|
|
16
|
+
in(values) { return this.#builder.in(this.#field, values); }
|
|
17
|
+
nin(values) { return this.#builder.nin(this.#field, values); }
|
|
18
|
+
between(min, max) { return this.#builder.between(this.#field, min, max); }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { FieldQuery };
|
package/query/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function eq(val) { return val; }
|
|
2
|
+
function gt(val) { return { $gt: val }; }
|
|
3
|
+
function gte(val) { return { $gte: val }; }
|
|
4
|
+
function lt(val) { return { $lt: val }; }
|
|
5
|
+
function lte(val) { return { $lte: val }; }
|
|
6
|
+
function ne(val) { return { $ne: val }; }
|
|
7
|
+
function matches(val) { return { $in: val }; }
|
|
8
|
+
function nin(val) { return { $nin: val }; }
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
eq,
|
|
12
|
+
gt,
|
|
13
|
+
gte,
|
|
14
|
+
lt,
|
|
15
|
+
lte,
|
|
16
|
+
ne,
|
|
17
|
+
matches,
|
|
18
|
+
nin,
|
|
19
|
+
};
|
package/schema/index.js
ADDED
package/schema/schema.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const { validateDocument } = require('./validator');
|
|
2
|
+
|
|
3
|
+
class Schema {
|
|
4
|
+
#fields;
|
|
5
|
+
#strict;
|
|
6
|
+
|
|
7
|
+
constructor(definition = {}, options = {}) {
|
|
8
|
+
this.#fields = {};
|
|
9
|
+
this.#strict = options.strict || false;
|
|
10
|
+
|
|
11
|
+
for (const [name, rule] of Object.entries(definition)) {
|
|
12
|
+
if (typeof rule === 'string') {
|
|
13
|
+
this.#fields[name] = { type: rule, required: false };
|
|
14
|
+
} else if (typeof rule === 'object' && rule !== null) {
|
|
15
|
+
this.#fields[name] = {
|
|
16
|
+
type: rule.type || 'any',
|
|
17
|
+
required: rule.required || false,
|
|
18
|
+
default: rule.default,
|
|
19
|
+
min: rule.min,
|
|
20
|
+
max: rule.max,
|
|
21
|
+
minLength: rule.minLength,
|
|
22
|
+
maxLength: rule.maxLength,
|
|
23
|
+
enum: rule.enum,
|
|
24
|
+
match: rule.match,
|
|
25
|
+
message: rule.message,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
validate(doc) {
|
|
32
|
+
return validateDocument(doc, this.#fields, this.#strict);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
applyDefaults(doc) {
|
|
36
|
+
const result = { ...doc };
|
|
37
|
+
for (const [name, rule] of Object.entries(this.#fields)) {
|
|
38
|
+
if (result[name] === undefined && rule.default !== undefined) {
|
|
39
|
+
result[name] = typeof rule.default === 'function' ? rule.default() : rule.default;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pick(doc, ...fields) {
|
|
46
|
+
const result = {};
|
|
47
|
+
for (const f of fields) {
|
|
48
|
+
if (doc[f] !== undefined) result[f] = doc[f];
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
omit(doc, ...fields) {
|
|
54
|
+
const result = { ...doc };
|
|
55
|
+
for (const f of fields) delete result[f];
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getFieldNames() {
|
|
60
|
+
return Object.keys(this.#fields);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getRequiredFields() {
|
|
64
|
+
return Object.entries(this.#fields)
|
|
65
|
+
.filter(([, rule]) => rule.required)
|
|
66
|
+
.map(([name]) => name);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { Schema };
|
package/schema/types.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
2
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3
|
+
|
|
4
|
+
const Types = {
|
|
5
|
+
STRING: 'string',
|
|
6
|
+
NUMBER: 'number',
|
|
7
|
+
BOOLEAN: 'boolean',
|
|
8
|
+
OBJECT: 'object',
|
|
9
|
+
ARRAY: 'array',
|
|
10
|
+
ANY: 'any',
|
|
11
|
+
EMAIL: 'email',
|
|
12
|
+
UUID: 'uuid',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function isValidType(type) {
|
|
16
|
+
return Object.values(Types).includes(type);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function checkType(value, type) {
|
|
20
|
+
if (type === Types.ANY) return true;
|
|
21
|
+
if (type === Types.ARRAY) return Array.isArray(value);
|
|
22
|
+
if (type === Types.EMAIL) return typeof value === 'string' && EMAIL_REGEX.test(value);
|
|
23
|
+
if (type === Types.UUID) return typeof value === 'string' && UUID_REGEX.test(value);
|
|
24
|
+
return typeof value === type;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
Types,
|
|
29
|
+
isValidType,
|
|
30
|
+
checkType,
|
|
31
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const { checkType } = require('./types');
|
|
2
|
+
|
|
3
|
+
function validateDocument(doc, fields, strict) {
|
|
4
|
+
const errors = [];
|
|
5
|
+
|
|
6
|
+
for (const [name, rule] of Object.entries(fields)) {
|
|
7
|
+
const value = doc[name];
|
|
8
|
+
|
|
9
|
+
if (value === undefined || value === null) {
|
|
10
|
+
if (rule.required) {
|
|
11
|
+
errors.push({ field: name, rule: 'required', message: rule.message || `"${name}" is required` });
|
|
12
|
+
}
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (rule.type !== 'any') {
|
|
17
|
+
if (!checkType(value, rule.type)) {
|
|
18
|
+
errors.push({ field: name, rule: 'type', message: `"${name}" expected ${rule.type}, got ${Array.isArray(value) ? 'array' : typeof value}` });
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (rule.type === 'number') {
|
|
24
|
+
if (rule.min !== undefined && value < rule.min) {
|
|
25
|
+
errors.push({ field: name, rule: 'min', message: `"${name}" must be >= ${rule.min}` });
|
|
26
|
+
}
|
|
27
|
+
if (rule.max !== undefined && value > rule.max) {
|
|
28
|
+
errors.push({ field: name, rule: 'max', message: `"${name}" must be <= ${rule.max}` });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (rule.type === 'string') {
|
|
33
|
+
if (rule.minLength !== undefined && value.length < rule.minLength) {
|
|
34
|
+
errors.push({ field: name, rule: 'minLength', message: `"${name}" min length is ${rule.minLength}` });
|
|
35
|
+
}
|
|
36
|
+
if (rule.maxLength !== undefined && value.length > rule.maxLength) {
|
|
37
|
+
errors.push({ field: name, rule: 'maxLength', message: `"${name}" max length is ${rule.maxLength}` });
|
|
38
|
+
}
|
|
39
|
+
if (rule.match && !rule.match.test(value)) {
|
|
40
|
+
errors.push({ field: name, rule: 'match', message: `"${name}" format is invalid` });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (rule.enum && !rule.enum.includes(value)) {
|
|
45
|
+
errors.push({ field: name, rule: 'enum', message: `"${name}" must be one of: ${rule.enum.join(', ')}` });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (strict) {
|
|
50
|
+
for (const key of Object.keys(doc)) {
|
|
51
|
+
if (!(key in fields)) {
|
|
52
|
+
errors.push({ field: key, rule: 'strict', message: `Unknown field "${key}"` });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { valid: errors.length === 0, errors };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { validateDocument };
|