mongo-realtime 2.0.4 → 3.0.1
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/.env.example +5 -0
- package/README.md +386 -438
- package/package.json +17 -5
- package/src/env.js +87 -0
- package/src/index.js +15 -0
- package/src/query.js +308 -0
- package/src/server.js +971 -0
- package/index.js +0 -570
- package/logo.png +0 -0
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mongo-realtime",
|
|
3
|
-
"version": "
|
|
4
|
-
"main": "index.js",
|
|
3
|
+
"version": "3.0.1",
|
|
4
|
+
"main": "src/index.js",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./src/index.js"
|
|
7
|
+
},
|
|
5
8
|
"keywords": [
|
|
6
9
|
"mongo",
|
|
7
10
|
"mongoose",
|
|
@@ -10,18 +13,27 @@
|
|
|
10
13
|
"realtime",
|
|
11
14
|
"mongodb",
|
|
12
15
|
"socket",
|
|
16
|
+
"websocket",
|
|
17
|
+
"flutter",
|
|
18
|
+
"server",
|
|
13
19
|
"db"
|
|
14
20
|
],
|
|
21
|
+
"files": [
|
|
22
|
+
"src",
|
|
23
|
+
"README.md",
|
|
24
|
+
".env.example"
|
|
25
|
+
],
|
|
15
26
|
"author": "D3R50N",
|
|
16
27
|
"license": "MIT",
|
|
17
28
|
"repository": {
|
|
18
29
|
"url": "git+https://github.com/D3R50N/mongo-realtime.git"
|
|
19
30
|
},
|
|
20
|
-
"description": "A Node.js package that combines
|
|
31
|
+
"description": "A Node.js package that combines WebSockets and MongoDB Change Streams to deliver real-time database updates to your WebSocket clients.",
|
|
21
32
|
"dependencies": {
|
|
22
33
|
"chalk": "^4.1.2",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
34
|
+
"dotenv": "^17.4.2",
|
|
35
|
+
"mongodb": "^7.2.0",
|
|
36
|
+
"ws": "^8.20.0"
|
|
25
37
|
},
|
|
26
38
|
"scripts": {}
|
|
27
39
|
}
|
package/src/env.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const dotenv = require("dotenv");
|
|
4
|
+
|
|
5
|
+
let loaded = false;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Loads environment variables from `.env` once for the current process.
|
|
9
|
+
*/
|
|
10
|
+
function loadEnvironment() {
|
|
11
|
+
if (!loaded) {
|
|
12
|
+
dotenv.config({
|
|
13
|
+
quiet: true,
|
|
14
|
+
});
|
|
15
|
+
loaded = true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolves runtime options from explicit overrides first, then environment variables.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} [overrides={}] Explicit runtime overrides.
|
|
23
|
+
* @param {string} [overrides.host] Host used when this package owns the HTTP server.
|
|
24
|
+
* @param {number} [overrides.port] Port used when this package owns the HTTP server.
|
|
25
|
+
* @param {string} [overrides.path] WebSocket upgrade path.
|
|
26
|
+
* @param {string} [overrides.mongoUri] MongoDB connection URI.
|
|
27
|
+
* @param {string} [overrides.dbName] MongoDB database name.
|
|
28
|
+
* @param {number} [overrides.cacheTtlMs] Cache TTL in milliseconds.
|
|
29
|
+
* @returns {{host: string, port: number, path: string, mongoUri: string, dbName: string, cacheTtlMs: number}}
|
|
30
|
+
*/
|
|
31
|
+
function readEnvironmentOptions(overrides = {}) {
|
|
32
|
+
loadEnvironment();
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
host: overrides.host || process.env.HOST || "0.0.0.0",
|
|
36
|
+
port: normalizePort(overrides.port || process.env.PORT || 3000),
|
|
37
|
+
path: overrides.path || process.env.WS_PATH || "/",
|
|
38
|
+
mongoUri:
|
|
39
|
+
overrides.mongoUri ||
|
|
40
|
+
process.env.MONGODB_URI ||
|
|
41
|
+
process.env.MONGO_URI ||
|
|
42
|
+
"mongodb://127.0.0.1:27017",
|
|
43
|
+
dbName:
|
|
44
|
+
overrides.dbName ||
|
|
45
|
+
process.env.MONGODB_DB_NAME ||
|
|
46
|
+
process.env.MONGO_DB ||
|
|
47
|
+
"mongo_realtime_test",
|
|
48
|
+
cacheTtlMs:
|
|
49
|
+
normalizeCacheTtlMs(overrides.cacheTtlMs) ||
|
|
50
|
+
normalizeCacheTtlMs(process.env.CACHE_TTL_MS) ||
|
|
51
|
+
normalizeCacheTtlMs(process.env.CACHE_TTL_SECONDS, { seconds: true }) ||
|
|
52
|
+
5 * 60 * 1000,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {unknown} value Candidate TTL value.
|
|
58
|
+
* @param {{seconds?: boolean}} [options]
|
|
59
|
+
* @returns {number|undefined}
|
|
60
|
+
*/
|
|
61
|
+
function normalizeCacheTtlMs(value, options = {}) {
|
|
62
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
63
|
+
value = Number(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (typeof value !== "number" || Number.isNaN(value) || value < 0) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return options.seconds ? Math.round(value * 1000) : value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Normalizes a port value into a valid non-negative integer.
|
|
75
|
+
*
|
|
76
|
+
* @param {unknown} value Candidate port value.
|
|
77
|
+
* @returns {number}
|
|
78
|
+
*/
|
|
79
|
+
function normalizePort(value) {
|
|
80
|
+
const port = Number(value);
|
|
81
|
+
return Number.isInteger(port) && port >= 0 ? port : 3000;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
loadEnvironment,
|
|
86
|
+
readEnvironmentOptions,
|
|
87
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { loadEnvironment } = require('./env');
|
|
4
|
+
const { MongoRealTimeServer } = require('./server');
|
|
5
|
+
|
|
6
|
+
loadEnvironment();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Public package export.
|
|
10
|
+
*
|
|
11
|
+
* @type {{MongoRealTimeServer: typeof import('./server').MongoRealTimeServer}}
|
|
12
|
+
*/
|
|
13
|
+
module.exports = {
|
|
14
|
+
MongoRealTimeServer,
|
|
15
|
+
};
|
package/src/query.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isDeepStrictEqual } = require('node:util');
|
|
4
|
+
|
|
5
|
+
function deepCopy(value) {
|
|
6
|
+
if (value === undefined) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return JSON.parse(JSON.stringify(value));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isPlainObject(value) {
|
|
14
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readPath(document, path) {
|
|
18
|
+
if (!path) {
|
|
19
|
+
return document;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return path.split('.').reduce((current, segment) => {
|
|
23
|
+
if (current && typeof current === 'object') {
|
|
24
|
+
return current[segment];
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}, document);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writePath(document, path, value) {
|
|
31
|
+
const segments = path.split('.');
|
|
32
|
+
let current = document;
|
|
33
|
+
|
|
34
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
35
|
+
const segment = segments[index];
|
|
36
|
+
if (!isPlainObject(current[segment])) {
|
|
37
|
+
current[segment] = {};
|
|
38
|
+
}
|
|
39
|
+
current = current[segment];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
current[segments[segments.length - 1]] = value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function compareValues(left, right) {
|
|
46
|
+
if (left === right) {
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
if (left == null) {
|
|
50
|
+
return -1;
|
|
51
|
+
}
|
|
52
|
+
if (right == null) {
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
if (typeof left === 'number' && typeof right === 'number') {
|
|
56
|
+
return left - right;
|
|
57
|
+
}
|
|
58
|
+
if (typeof left === 'string' && typeof right === 'string') {
|
|
59
|
+
return left.localeCompare(right);
|
|
60
|
+
}
|
|
61
|
+
if (typeof left === 'boolean' && typeof right === 'boolean') {
|
|
62
|
+
return left === right ? 0 : left ? 1 : -1;
|
|
63
|
+
}
|
|
64
|
+
return JSON.stringify(left).localeCompare(JSON.stringify(right));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function matchesFilter(document, filter = {}) {
|
|
68
|
+
if (!isPlainObject(filter) || Object.keys(filter).length === 0) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return Object.entries(filter).every(([key, condition]) => {
|
|
73
|
+
if (key === '$and') {
|
|
74
|
+
return Array.isArray(condition) &&
|
|
75
|
+
condition.every((clause) => matchesFilter(document, clause));
|
|
76
|
+
}
|
|
77
|
+
if (key === '$or') {
|
|
78
|
+
return Array.isArray(condition) &&
|
|
79
|
+
condition.some((clause) => matchesFilter(document, clause));
|
|
80
|
+
}
|
|
81
|
+
if (key === '$nor') {
|
|
82
|
+
return Array.isArray(condition) &&
|
|
83
|
+
condition.every((clause) => !matchesFilter(document, clause));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const value = readPath(document, key);
|
|
87
|
+
return matchesCondition(value, condition);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function matchesCondition(value, condition) {
|
|
92
|
+
if (isPlainObject(condition) &&
|
|
93
|
+
Object.keys(condition).some((key) => key.startsWith('$'))) {
|
|
94
|
+
if ('$regex' in condition &&
|
|
95
|
+
!matchesRegex(value, condition.$regex, condition.$options)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return Object.entries(condition).every(([operator, operand]) => {
|
|
100
|
+
if (operator === '$regex' || operator === '$options') {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return applyOperator(value, operator, operand);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (Array.isArray(value)) {
|
|
108
|
+
return value.some((item) => isDeepStrictEqual(item, condition));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return isDeepStrictEqual(value, condition);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function applyOperator(value, operator, operand) {
|
|
115
|
+
switch (operator) {
|
|
116
|
+
case '$eq':
|
|
117
|
+
return isDeepStrictEqual(value, operand);
|
|
118
|
+
case '$ne':
|
|
119
|
+
return !isDeepStrictEqual(value, operand);
|
|
120
|
+
case '$gt':
|
|
121
|
+
return compareValues(value, operand) > 0;
|
|
122
|
+
case '$gte':
|
|
123
|
+
return compareValues(value, operand) >= 0;
|
|
124
|
+
case '$lt':
|
|
125
|
+
return compareValues(value, operand) < 0;
|
|
126
|
+
case '$lte':
|
|
127
|
+
return compareValues(value, operand) <= 0;
|
|
128
|
+
case '$in':
|
|
129
|
+
if (!Array.isArray(operand)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
return value.some((item) =>
|
|
134
|
+
operand.some((candidate) => isDeepStrictEqual(candidate, item)),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return operand.some((candidate) => isDeepStrictEqual(candidate, value));
|
|
138
|
+
case '$nin':
|
|
139
|
+
if (!Array.isArray(operand)) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
if (Array.isArray(value)) {
|
|
143
|
+
return value.every((item) =>
|
|
144
|
+
operand.every((candidate) => !isDeepStrictEqual(candidate, item)),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return operand.every((candidate) => !isDeepStrictEqual(candidate, value));
|
|
148
|
+
case '$exists':
|
|
149
|
+
return operand ? value !== undefined : value === undefined;
|
|
150
|
+
case '$regex':
|
|
151
|
+
return matchesRegex(value, operand, undefined);
|
|
152
|
+
case '$contains':
|
|
153
|
+
if (Array.isArray(value)) {
|
|
154
|
+
return value.some((item) => isDeepStrictEqual(item, operand));
|
|
155
|
+
}
|
|
156
|
+
if (typeof value === 'string' && typeof operand === 'string') {
|
|
157
|
+
return value.includes(operand);
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
default:
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function matchesRegex(value, pattern, options) {
|
|
166
|
+
if (typeof value !== 'string' || pattern == null) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const flags = typeof options === 'string' ? options : '';
|
|
171
|
+
return new RegExp(String(pattern), flags).test(value);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function normalizeSort(sort = {}) {
|
|
175
|
+
return Object.fromEntries(
|
|
176
|
+
Object.entries(sort).map(([field, direction]) => [field, direction < 0 ? -1 : 1]),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function sortDocuments(documents, sort = {}, limit = null) {
|
|
181
|
+
const normalizedSort = normalizeSort(sort);
|
|
182
|
+
const sorted = [...documents];
|
|
183
|
+
|
|
184
|
+
if (Object.keys(normalizedSort).length > 0) {
|
|
185
|
+
sorted.sort((left, right) => {
|
|
186
|
+
for (const [field, direction] of Object.entries(normalizedSort)) {
|
|
187
|
+
const comparison = compareValues(readPath(left, field), readPath(right, field));
|
|
188
|
+
if (comparison !== 0) {
|
|
189
|
+
return direction < 0 ? -comparison : comparison;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return compareValues(String(left._id ?? ''), String(right._id ?? ''));
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (typeof limit === 'number' && limit >= 0) {
|
|
197
|
+
return sorted.slice(0, limit);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return sorted;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isMongoOperatorUpdate(update) {
|
|
204
|
+
return isPlainObject(update) &&
|
|
205
|
+
Object.keys(update).some((key) => key.startsWith('$'));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function applyMongoUpdate(document, update) {
|
|
209
|
+
const working = deepCopy(document);
|
|
210
|
+
|
|
211
|
+
if (!isMongoOperatorUpdate(update)) {
|
|
212
|
+
for (const [path, value] of Object.entries(update)) {
|
|
213
|
+
writePath(working, path, deepCopy(value));
|
|
214
|
+
}
|
|
215
|
+
return working;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const [operator, payload] of Object.entries(update)) {
|
|
219
|
+
const entries = isPlainObject(payload) ? Object.entries(payload) : [];
|
|
220
|
+
|
|
221
|
+
switch (operator) {
|
|
222
|
+
case '$set':
|
|
223
|
+
for (const [path, value] of entries) {
|
|
224
|
+
writePath(working, path, deepCopy(value));
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
case '$inc':
|
|
228
|
+
for (const [path, value] of entries) {
|
|
229
|
+
const current = readPath(working, path);
|
|
230
|
+
const currentNumber = typeof current === 'number' ? current : 0;
|
|
231
|
+
const delta = typeof value === 'number' ? value : 0;
|
|
232
|
+
writePath(working, path, currentNumber + delta);
|
|
233
|
+
}
|
|
234
|
+
break;
|
|
235
|
+
case '$addToSet':
|
|
236
|
+
for (const [path, value] of entries) {
|
|
237
|
+
const list = readOrCreateList(working, path);
|
|
238
|
+
for (const candidate of expandUpdateValue(value)) {
|
|
239
|
+
if (!list.some((item) => isDeepStrictEqual(item, candidate))) {
|
|
240
|
+
list.push(deepCopy(candidate));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
245
|
+
case '$push':
|
|
246
|
+
for (const [path, value] of entries) {
|
|
247
|
+
const list = readOrCreateList(working, path);
|
|
248
|
+
for (const candidate of expandUpdateValue(value)) {
|
|
249
|
+
list.push(deepCopy(candidate));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
case '$pull':
|
|
254
|
+
for (const [path, value] of entries) {
|
|
255
|
+
const list = readOrCreateList(working, path);
|
|
256
|
+
for (let index = list.length - 1; index >= 0; index -= 1) {
|
|
257
|
+
if (shouldPull(list[index], value)) {
|
|
258
|
+
list.splice(index, 1);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
263
|
+
default:
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return working;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function readOrCreateList(document, path) {
|
|
272
|
+
const current = readPath(document, path);
|
|
273
|
+
if (Array.isArray(current)) {
|
|
274
|
+
return current;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const replacement = [];
|
|
278
|
+
writePath(document, path, replacement);
|
|
279
|
+
return replacement;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function expandUpdateValue(value) {
|
|
283
|
+
if (isPlainObject(value) && Array.isArray(value.$each)) {
|
|
284
|
+
return value.$each;
|
|
285
|
+
}
|
|
286
|
+
return [value];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function shouldPull(item, condition) {
|
|
290
|
+
if (isPlainObject(condition) && isPlainObject(item)) {
|
|
291
|
+
return matchesFilter(item, condition);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return isDeepStrictEqual(item, condition);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = {
|
|
298
|
+
applyMongoUpdate,
|
|
299
|
+
compareValues,
|
|
300
|
+
deepCopy,
|
|
301
|
+
isMongoOperatorUpdate,
|
|
302
|
+
isPlainObject,
|
|
303
|
+
matchesFilter,
|
|
304
|
+
normalizeSort,
|
|
305
|
+
readPath,
|
|
306
|
+
sortDocuments,
|
|
307
|
+
writePath,
|
|
308
|
+
};
|