@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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
class ConnectionManager {
|
|
2
|
+
#connections;
|
|
3
|
+
#activeId;
|
|
4
|
+
#healthChecks;
|
|
5
|
+
#maxConnections;
|
|
6
|
+
|
|
7
|
+
constructor(maxConnections = 5) {
|
|
8
|
+
this.#connections = new Map();
|
|
9
|
+
this.#activeId = null;
|
|
10
|
+
this.#healthChecks = new Map();
|
|
11
|
+
this.#maxConnections = maxConnections;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
add(id, nekoDBInstance) {
|
|
15
|
+
if (this.#connections.size >= this.#maxConnections) {
|
|
16
|
+
throw new Error(`Max connections (${this.#maxConnections}) reached`);
|
|
17
|
+
}
|
|
18
|
+
this.#connections.set(id, {
|
|
19
|
+
db: nekoDBInstance,
|
|
20
|
+
addedAt: new Date(),
|
|
21
|
+
lastUsed: new Date(),
|
|
22
|
+
});
|
|
23
|
+
if (!this.#activeId) this.#activeId = id;
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
remove(id) {
|
|
28
|
+
const entry = this.#connections.get(id);
|
|
29
|
+
if (entry) {
|
|
30
|
+
try { entry.db.close(); } catch { }
|
|
31
|
+
this.#connections.delete(id);
|
|
32
|
+
if (this.#activeId === id) {
|
|
33
|
+
const keys = [...this.#connections.keys()];
|
|
34
|
+
this.#activeId = keys.length > 0 ? keys[0] : null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get(id) {
|
|
41
|
+
const entry = this.#connections.get(id);
|
|
42
|
+
if (entry) {
|
|
43
|
+
entry.lastUsed = new Date();
|
|
44
|
+
return entry.db;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
active() {
|
|
50
|
+
if (!this.#activeId) return null;
|
|
51
|
+
return this.get(this.#activeId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setActive(id) {
|
|
55
|
+
if (!this.#connections.has(id)) {
|
|
56
|
+
throw new Error(`Connection "${id}" not found`);
|
|
57
|
+
}
|
|
58
|
+
this.#activeId = id;
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get activeId() { return this.#activeId; }
|
|
63
|
+
|
|
64
|
+
has(id) {
|
|
65
|
+
return this.#connections.has(id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
list() {
|
|
69
|
+
const result = [];
|
|
70
|
+
for (const [id, entry] of this.#connections) {
|
|
71
|
+
result.push({
|
|
72
|
+
id,
|
|
73
|
+
connected: entry.db.connected,
|
|
74
|
+
addedAt: entry.addedAt,
|
|
75
|
+
lastUsed: entry.lastUsed,
|
|
76
|
+
isActive: id === this.#activeId,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get size() {
|
|
83
|
+
return this.#connections.size;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getHealthy() {
|
|
87
|
+
const healthy = [];
|
|
88
|
+
for (const [id, entry] of this.#connections) {
|
|
89
|
+
if (entry.db.connected) healthy.push(id);
|
|
90
|
+
}
|
|
91
|
+
return healthy;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getUnhealthy() {
|
|
95
|
+
const unhealthy = [];
|
|
96
|
+
for (const [id, entry] of this.#connections) {
|
|
97
|
+
if (!entry.db.connected) unhealthy.push(id);
|
|
98
|
+
}
|
|
99
|
+
return unhealthy;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
switchToHealthy() {
|
|
103
|
+
const healthy = this.getHealthy();
|
|
104
|
+
if (healthy.length === 0) return false;
|
|
105
|
+
if (healthy.includes(this.#activeId)) return true;
|
|
106
|
+
this.#activeId = healthy[0];
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async closeAll() {
|
|
111
|
+
for (const [id, entry] of this.#connections) {
|
|
112
|
+
try { entry.db.close(); } catch { }
|
|
113
|
+
}
|
|
114
|
+
this.#connections.clear();
|
|
115
|
+
this.#activeId = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getStats() {
|
|
119
|
+
let connected = 0;
|
|
120
|
+
let disconnected = 0;
|
|
121
|
+
for (const [, entry] of this.#connections) {
|
|
122
|
+
if (entry.db.connected) connected++;
|
|
123
|
+
else disconnected++;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
total: this.#connections.size,
|
|
127
|
+
connected,
|
|
128
|
+
disconnected,
|
|
129
|
+
activeId: this.#activeId,
|
|
130
|
+
maxConnections: this.#maxConnections,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = { ConnectionManager };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const ConnectionStatus = {
|
|
2
|
+
DISCONNECTED: 'disconnected',
|
|
3
|
+
CONNECTING: 'connecting',
|
|
4
|
+
CONNECTED: 'connected',
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
class ConnectionHealth {
|
|
8
|
+
static check(instance) {
|
|
9
|
+
if (!instance) return ConnectionStatus.DISCONNECTED;
|
|
10
|
+
return instance.connected ? ConnectionStatus.CONNECTED : ConnectionStatus.DISCONNECTED;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
ConnectionStatus,
|
|
16
|
+
ConnectionHealth,
|
|
17
|
+
};
|
package/errors/base.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class NekoError extends Error {
|
|
2
|
+
constructor(message, code) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'NekoError';
|
|
5
|
+
this.code = code || 'UNKNOWN';
|
|
6
|
+
this.timestamp = new Date();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
toJSON() {
|
|
10
|
+
return {
|
|
11
|
+
name: this.name,
|
|
12
|
+
code: this.code,
|
|
13
|
+
message: this.message,
|
|
14
|
+
timestamp: this.timestamp,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { NekoError };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const { NekoError } = require('./base');
|
|
2
|
+
const { AuthError, NotFoundError } = require('./database');
|
|
3
|
+
|
|
4
|
+
function classify(raw) {
|
|
5
|
+
if (typeof raw === 'string') {
|
|
6
|
+
const msg = raw.trim();
|
|
7
|
+
if (msg === 'invalid-credentials') return new AuthError();
|
|
8
|
+
if (msg === 'not-found') return new NotFoundError();
|
|
9
|
+
if (msg.startsWith('blocked:')) return new NekoError(msg, 'BLOCKED');
|
|
10
|
+
if (msg === 'invalid-json') return new NekoError('Invalid JSON payload', 'INVALID_PAYLOAD');
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { classify };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const { NekoError } = require('./base');
|
|
2
|
+
|
|
3
|
+
class AuthError extends NekoError {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message || 'Authentication failed', 'AUTH_ERROR');
|
|
6
|
+
this.name = 'AuthError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class NotFoundError extends NekoError {
|
|
11
|
+
constructor(collection, documentId) {
|
|
12
|
+
super(`Document not found: ${collection}/${documentId}`, 'NOT_FOUND');
|
|
13
|
+
this.name = 'NotFoundError';
|
|
14
|
+
this.collection = collection;
|
|
15
|
+
this.documentId = documentId;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class ValidationError extends NekoError {
|
|
20
|
+
#errors;
|
|
21
|
+
|
|
22
|
+
constructor(message, errors) {
|
|
23
|
+
super(message || 'Validation failed', 'VALIDATION_ERROR');
|
|
24
|
+
this.name = 'ValidationError';
|
|
25
|
+
this.#errors = errors || [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get errors() { return [...this.#errors]; }
|
|
29
|
+
|
|
30
|
+
toJSON() {
|
|
31
|
+
return { ...super.toJSON(), errors: this.#errors };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class BulkOperationError extends NekoError {
|
|
36
|
+
#results;
|
|
37
|
+
|
|
38
|
+
constructor(message, results) {
|
|
39
|
+
super(message || 'Bulk operation partially failed', 'BULK_ERROR');
|
|
40
|
+
this.name = 'BulkOperationError';
|
|
41
|
+
this.#results = results || [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get results() { return [...this.#results]; }
|
|
45
|
+
get successCount() { return this.#results.filter(r => r.success).length; }
|
|
46
|
+
get failCount() { return this.#results.filter(r => !r.success).length; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class CollectionError extends NekoError {
|
|
50
|
+
constructor(message, collection) {
|
|
51
|
+
super(message || 'Collection error', 'COLLECTION_ERROR');
|
|
52
|
+
this.name = 'CollectionError';
|
|
53
|
+
this.collection = collection;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
AuthError,
|
|
59
|
+
NotFoundError,
|
|
60
|
+
ValidationError,
|
|
61
|
+
BulkOperationError,
|
|
62
|
+
CollectionError,
|
|
63
|
+
};
|
package/errors/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { NekoError } = require('./base');
|
|
2
|
+
const { ConnectionError, TimeoutError } = require('./network');
|
|
3
|
+
const { AuthError, NotFoundError, ValidationError, BulkOperationError, CollectionError } = require('./database');
|
|
4
|
+
const { classify } = require('./classifier');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
NekoError,
|
|
8
|
+
ConnectionError,
|
|
9
|
+
TimeoutError,
|
|
10
|
+
AuthError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
BulkOperationError,
|
|
14
|
+
CollectionError,
|
|
15
|
+
classify,
|
|
16
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const { NekoError } = require('./base');
|
|
2
|
+
|
|
3
|
+
class ConnectionError extends NekoError {
|
|
4
|
+
constructor(message, host) {
|
|
5
|
+
super(message || 'Connection failed', 'CONNECTION_ERROR');
|
|
6
|
+
this.name = 'ConnectionError';
|
|
7
|
+
this.host = host || null;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class TimeoutError extends NekoError {
|
|
12
|
+
constructor(message, operation, durationMs) {
|
|
13
|
+
super(message || 'Operation timed out', 'TIMEOUT');
|
|
14
|
+
this.name = 'TimeoutError';
|
|
15
|
+
this.operation = operation || null;
|
|
16
|
+
this.durationMs = durationMs || 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
ConnectionError,
|
|
22
|
+
TimeoutError,
|
|
23
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const { Subscription } = require('./subscriber');
|
|
2
|
+
const { Events } = require('./constants');
|
|
3
|
+
|
|
4
|
+
class EventBus {
|
|
5
|
+
#listeners;
|
|
6
|
+
#onceFlags;
|
|
7
|
+
#maxListeners;
|
|
8
|
+
|
|
9
|
+
constructor(maxListeners = 50) {
|
|
10
|
+
this.#listeners = new Map();
|
|
11
|
+
this.#onceFlags = new WeakSet();
|
|
12
|
+
this.#maxListeners = maxListeners;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
on(event, handler) {
|
|
16
|
+
if (typeof handler !== 'function') throw new TypeError('Handler must be a function');
|
|
17
|
+
if (!this.#listeners.has(event)) this.#listeners.set(event, []);
|
|
18
|
+
const handlers = this.#listeners.get(event);
|
|
19
|
+
if (handlers.length >= this.#maxListeners) {
|
|
20
|
+
console.warn(`[EventBus] Max listeners (${this.#maxListeners}) reached for "${event}"`);
|
|
21
|
+
}
|
|
22
|
+
handlers.push(handler);
|
|
23
|
+
return new Subscription(this, event, handler);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
once(event, handler) {
|
|
27
|
+
if (typeof handler !== 'function') throw new TypeError('Handler must be a function');
|
|
28
|
+
const wrapper = (...args) => {
|
|
29
|
+
this.off(event, wrapper);
|
|
30
|
+
handler(...args);
|
|
31
|
+
};
|
|
32
|
+
this.#onceFlags.add(wrapper);
|
|
33
|
+
this.on(event, wrapper);
|
|
34
|
+
return new Subscription(this, event, wrapper);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
off(event, handler) {
|
|
38
|
+
if (!handler) {
|
|
39
|
+
this.#listeners.delete(event);
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
const handlers = this.#listeners.get(event);
|
|
43
|
+
if (handlers) {
|
|
44
|
+
const idx = handlers.indexOf(handler);
|
|
45
|
+
if (idx !== -1) handlers.splice(idx, 1);
|
|
46
|
+
if (handlers.length === 0) this.#listeners.delete(event);
|
|
47
|
+
}
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
emit(event, ...args) {
|
|
52
|
+
const handlers = this.#listeners.get(event);
|
|
53
|
+
if (!handlers || handlers.length === 0) return false;
|
|
54
|
+
for (const handler of [...handlers]) {
|
|
55
|
+
try { handler(...args); } catch (err) {
|
|
56
|
+
console.error(`[EventBus] Error in "${event}" handler:`, err);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (event !== Events.ALL) {
|
|
61
|
+
const wildcardHandlers = this.#listeners.get(Events.ALL);
|
|
62
|
+
if (wildcardHandlers) {
|
|
63
|
+
for (const handler of [...wildcardHandlers]) {
|
|
64
|
+
try { handler(event, ...args); } catch { }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
listenerCount(event) {
|
|
72
|
+
return this.#listeners.get(event)?.length || 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
eventNames() {
|
|
76
|
+
return [...this.#listeners.keys()];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
removeAllListeners(event) {
|
|
80
|
+
if (event) {
|
|
81
|
+
this.#listeners.delete(event);
|
|
82
|
+
} else {
|
|
83
|
+
this.#listeners.clear();
|
|
84
|
+
}
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
waitFor(event, timeoutMs = 30000) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
let timer;
|
|
91
|
+
const handler = (data) => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
resolve(data);
|
|
94
|
+
};
|
|
95
|
+
this.once(event, handler);
|
|
96
|
+
if (timeoutMs > 0) {
|
|
97
|
+
timer = setTimeout(() => {
|
|
98
|
+
this.off(event, handler);
|
|
99
|
+
reject(new Error(`Timeout waiting for "${event}"`));
|
|
100
|
+
}, timeoutMs);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { EventBus };
|
package/events/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class Subscription {
|
|
2
|
+
#bus;
|
|
3
|
+
#event;
|
|
4
|
+
#handler;
|
|
5
|
+
|
|
6
|
+
constructor(bus, event, handler) {
|
|
7
|
+
this.#bus = bus;
|
|
8
|
+
this.#event = event;
|
|
9
|
+
this.#handler = handler;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
unsubscribe() {
|
|
13
|
+
if (this.#bus) {
|
|
14
|
+
this.#bus.off(this.#event, this.#handler);
|
|
15
|
+
this.#bus = null;
|
|
16
|
+
this.#handler = null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { Subscription };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
class CollectionHelper {
|
|
2
|
+
#db;
|
|
3
|
+
#collection;
|
|
4
|
+
|
|
5
|
+
constructor(db, collectionName) {
|
|
6
|
+
this.#db = db;
|
|
7
|
+
this.#collection = collectionName;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
get name() { return this.#collection; }
|
|
11
|
+
|
|
12
|
+
async findOne(query) {
|
|
13
|
+
const ids = await this.#db.search(this.#collection, query);
|
|
14
|
+
if (!Array.isArray(ids) || ids.length === 0) return null;
|
|
15
|
+
return this.#db.get(this.#collection, ids[0]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async findById(id) {
|
|
19
|
+
const doc = await this.#db.get(this.#collection, id);
|
|
20
|
+
if (doc === 'not-found' || doc === null) return null;
|
|
21
|
+
return { _id: id, ...doc };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async exists(id) {
|
|
25
|
+
const doc = await this.#db.get(this.#collection, id);
|
|
26
|
+
return doc !== 'not-found' && doc !== null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async findMany(query) {
|
|
30
|
+
const ids = await this.#db.search(this.#collection, query);
|
|
31
|
+
if (!Array.isArray(ids)) return [];
|
|
32
|
+
const docs = [];
|
|
33
|
+
for (const id of ids) {
|
|
34
|
+
const doc = await this.#db.get(this.#collection, id);
|
|
35
|
+
if (doc && doc !== 'not-found') {
|
|
36
|
+
docs.push({ _id: id, ...doc });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return docs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async getAll() {
|
|
43
|
+
const ids = await this.#db.list(this.#collection);
|
|
44
|
+
if (!Array.isArray(ids)) return [];
|
|
45
|
+
const docs = [];
|
|
46
|
+
for (const id of ids) {
|
|
47
|
+
const doc = await this.#db.get(this.#collection, id);
|
|
48
|
+
if (doc && doc !== 'not-found') {
|
|
49
|
+
docs.push({ _id: id, ...doc });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return docs;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async upsert(query, data) {
|
|
56
|
+
const ids = await this.#db.search(this.#collection, query);
|
|
57
|
+
if (Array.isArray(ids) && ids.length > 0) {
|
|
58
|
+
await this.#db.update(this.#collection, ids[0], data);
|
|
59
|
+
return { action: 'updated', id: ids[0] };
|
|
60
|
+
}
|
|
61
|
+
const id = await this.#db.insert(this.#collection, data);
|
|
62
|
+
return { action: 'inserted', id };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async findOrInsert(query, defaultDoc) {
|
|
66
|
+
const existing = await this.findOne(query);
|
|
67
|
+
if (existing) return { doc: existing, created: false };
|
|
68
|
+
const id = await this.#db.insert(this.#collection, defaultDoc || query);
|
|
69
|
+
const doc = await this.#db.get(this.#collection, id);
|
|
70
|
+
return { doc: { _id: id, ...doc }, created: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async updateWhere(query, updates) {
|
|
74
|
+
const ids = await this.#db.search(this.#collection, query);
|
|
75
|
+
if (!Array.isArray(ids)) return { matched: 0, modified: 0 };
|
|
76
|
+
let modified = 0;
|
|
77
|
+
for (const id of ids) {
|
|
78
|
+
const ok = await this.#db.update(this.#collection, id, updates);
|
|
79
|
+
if (ok !== 'not-found') modified++;
|
|
80
|
+
}
|
|
81
|
+
return { matched: ids.length, modified };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async deleteWhere(query) {
|
|
85
|
+
const ids = await this.#db.search(this.#collection, query);
|
|
86
|
+
if (!Array.isArray(ids)) return { deleted: 0 };
|
|
87
|
+
let deleted = 0;
|
|
88
|
+
for (const id of ids) {
|
|
89
|
+
const ok = await this.#db.delete(this.#collection, id);
|
|
90
|
+
if (ok !== 'not-found') deleted++;
|
|
91
|
+
}
|
|
92
|
+
return { deleted };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async replaceOne(id, doc) {
|
|
96
|
+
const existing = await this.#db.get(this.#collection, id);
|
|
97
|
+
if (!existing || existing === 'not-found') return false;
|
|
98
|
+
await this.#db.delete(this.#collection, id);
|
|
99
|
+
await this.#db.insert(this.#collection, { _original_id: id, ...doc });
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async insertMany(docs) {
|
|
104
|
+
return this.#db.bulkInsert(this.#collection, docs);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async deleteMany(ids) {
|
|
108
|
+
return this.#db.bulkDelete(this.#collection, ids);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async *paginate(options = {}) {
|
|
112
|
+
const pageSize = options.limit || 20;
|
|
113
|
+
let page = 1;
|
|
114
|
+
let hasMore = true;
|
|
115
|
+
|
|
116
|
+
while (hasMore) {
|
|
117
|
+
const result = await this.#db.listPaginated(this.#collection, {
|
|
118
|
+
limit: pageSize,
|
|
119
|
+
page,
|
|
120
|
+
sort: options.sort || [],
|
|
121
|
+
filter: options.filter || null,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (result?.data) {
|
|
125
|
+
yield { page, data: result.data, pageInfo: result.page_info };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
hasMore = result?.page_info?.has_next || false;
|
|
129
|
+
page++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async forEach(query, callback) {
|
|
134
|
+
const docs = await this.findMany(query);
|
|
135
|
+
for (let i = 0; i < docs.length; i++) {
|
|
136
|
+
await callback(docs[i], i);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async map(query, transform) {
|
|
141
|
+
const docs = await this.findMany(query);
|
|
142
|
+
return docs.map(transform);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async filter(query, predicate) {
|
|
146
|
+
const docs = await this.findMany(query);
|
|
147
|
+
return docs.filter(predicate);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { CollectionHelper };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
function generateId() {
|
|
4
|
+
return crypto.randomBytes(16).toString('hex');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function getNestedValue(obj, path) {
|
|
8
|
+
if (!path) return obj;
|
|
9
|
+
return path.split('.').reduce((acc, part) => {
|
|
10
|
+
if (acc && typeof acc === 'object') return acc[part];
|
|
11
|
+
return undefined;
|
|
12
|
+
}, obj);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function setNestedValue(obj, path, value) {
|
|
16
|
+
if (!path) return obj;
|
|
17
|
+
const parts = path.split('.');
|
|
18
|
+
let current = obj;
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
21
|
+
const part = parts[i];
|
|
22
|
+
if (current[part] === undefined || typeof current[part] !== 'object' || current[part] === null) {
|
|
23
|
+
current[part] = {};
|
|
24
|
+
}
|
|
25
|
+
current = current[part];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
current[parts[parts.length - 1]] = value;
|
|
29
|
+
return obj;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function deepMerge(target, source) {
|
|
33
|
+
const result = { ...target };
|
|
34
|
+
for (const [key, value] of Object.entries(source)) {
|
|
35
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
36
|
+
result[key] = deepMerge(result[key] || {}, value);
|
|
37
|
+
} else {
|
|
38
|
+
result[key] = value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
generateId,
|
|
46
|
+
getNestedValue,
|
|
47
|
+
setNestedValue,
|
|
48
|
+
deepMerge,
|
|
49
|
+
};
|
package/helpers/index.js
ADDED