jsql-neo 4.3.0 → 4.4.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/README.md +234 -110
- package/bin/jsql +138 -0
- package/index.d.ts +351 -0
- package/index.js +18 -0
- package/lib/migrate.js +242 -0
- package/lib/mysql_server.js +51 -1
- package/lib/redis_server.js +448 -0
- package/lib/sql.js +125 -51
- package/lib/web_ui.js +226 -0
- package/package.json +66 -56
- package/test/smoke.js +58 -0
- package/wasm/browser.d.ts +88 -0
package/lib/mysql_server.js
CHANGED
|
@@ -31,6 +31,7 @@ const MYSQL_TYPE_TINY = 1, MYSQL_TYPE_LONG = 3, MYSQL_TYPE_LONGLONG = 8,
|
|
|
31
31
|
MYSQL_TYPE_JSON = 245, MYSQL_TYPE_NULL = 6;
|
|
32
32
|
|
|
33
33
|
function encodeLenenc(value) {
|
|
34
|
+
if (typeof value === 'bigint') value = Number(value);
|
|
34
35
|
if (value === null) return Buffer.from([0xfb]);
|
|
35
36
|
if (value < 0xfb) return Buffer.from([value]);
|
|
36
37
|
if (value <= 0xffff) {
|
|
@@ -135,7 +136,38 @@ function toMysqlErrno(e) {
|
|
|
135
136
|
if (e && typeof e.code === 'number') return e.code;
|
|
136
137
|
const m = e && e.message ? String(e.message).match(/^(ER_[A-Z_]+)/) : null;
|
|
137
138
|
if (m) {
|
|
138
|
-
const known = {
|
|
139
|
+
const known = {
|
|
140
|
+
ER_DUP_ENTRY: 1062,
|
|
141
|
+
ER_NO_SUCH_TABLE: 1146,
|
|
142
|
+
ER_TABLE_EXISTS: 1050,
|
|
143
|
+
ER_TABLE_EXISTS_ERROR: 1050,
|
|
144
|
+
ER_PARSE_ERROR: 1064,
|
|
145
|
+
ER_BAD_FIELD_ERROR: 1054,
|
|
146
|
+
ER_BAD_NULL_ERROR: 1048,
|
|
147
|
+
ER_ACCESS_DENIED_ERROR: 1045,
|
|
148
|
+
ER_DBACCESS_DENIED_ERROR: 1044,
|
|
149
|
+
ER_BAD_DB_ERROR: 1049,
|
|
150
|
+
ER_WRONG_DB_NAME: 1102,
|
|
151
|
+
ER_WRONG_TABLE_NAME: 1103,
|
|
152
|
+
ER_WRONG_COLUMN_NAME: 1166,
|
|
153
|
+
ER_DATA_TOO_LONG: 1406,
|
|
154
|
+
ER_OUT_OF_RANGE: 1264,
|
|
155
|
+
ER_CHECK_CONSTRAINT: 3819,
|
|
156
|
+
ER_NO_DEFAULT_FOR_FIELD: 1364,
|
|
157
|
+
ER_CANT_DROP_FIELD_OR_KEY: 1091,
|
|
158
|
+
ER_CANT_DROP_DATABASE: 1008,
|
|
159
|
+
ER_EMPTY_QUERY: 1065,
|
|
160
|
+
ER_UNKNOWN_TABLE: 1109,
|
|
161
|
+
ER_NON_UNIQ_ERROR: 1052,
|
|
162
|
+
ER_WRONG_FIELD_WITH_GROUP: 1055,
|
|
163
|
+
ER_WRONG_VALUE_COUNT_ON_ROW: 1136,
|
|
164
|
+
ER_MISSING_TABLE: 1052,
|
|
165
|
+
ER_SP_DOES_NOT_EXIST: 1305,
|
|
166
|
+
ER_NOT_SUPPORTED_YET: 1235,
|
|
167
|
+
ER_LOCK_DEADLOCK: 1213,
|
|
168
|
+
ER_LOCK_WAIT_TIMEOUT: 1205,
|
|
169
|
+
ER_UNKNOWN_ERROR: 1105,
|
|
170
|
+
};
|
|
139
171
|
if (known[m[1]]) return known[m[1]];
|
|
140
172
|
}
|
|
141
173
|
return 1105;
|
|
@@ -273,11 +305,13 @@ function binaryResultSetPacket(result, tableSchema, baseSeq) {
|
|
|
273
305
|
if (type === MYSQL_TYPE_LONG || type === MYSQL_TYPE_LONGLONG || type === MYSQL_TYPE_DOUBLE) {
|
|
274
306
|
const num = Number(v);
|
|
275
307
|
if (type === MYSQL_TYPE_DOUBLE) {
|
|
308
|
+
if (!Number.isFinite(num)) return encodeLenencString(String(v));
|
|
276
309
|
const b = Buffer.alloc(8); b.writeDoubleLE(num, 0); return b;
|
|
277
310
|
}
|
|
278
311
|
if (Number.isInteger(num) && num <= 2147483647 && num >= -2147483648) {
|
|
279
312
|
const b = Buffer.alloc(4); b.writeInt32LE(num, 0); return b;
|
|
280
313
|
}
|
|
314
|
+
if (!Number.isFinite(num)) return encodeLenencString(String(v));
|
|
281
315
|
const b = Buffer.alloc(8); b.writeBigInt64LE(BigInt(Math.trunc(num)), 0); return b;
|
|
282
316
|
}
|
|
283
317
|
if (type === MYSQL_TYPE_TINY) {
|
|
@@ -339,6 +373,14 @@ class MysqlConnection {
|
|
|
339
373
|
this.multiStatements = false;
|
|
340
374
|
this._stmts = new Map();
|
|
341
375
|
this._stmtSeq = 0;
|
|
376
|
+
this.session = {
|
|
377
|
+
lastInsertId: 0,
|
|
378
|
+
rowCount: 0,
|
|
379
|
+
foundRows: 0,
|
|
380
|
+
connectionId: server._connectionCounter,
|
|
381
|
+
currentDb: null,
|
|
382
|
+
sysvars: {},
|
|
383
|
+
};
|
|
342
384
|
if (server.handshakeTimeout > 0) {
|
|
343
385
|
this._authTimer = setTimeout(() => {
|
|
344
386
|
if (!this.authenticated) {
|
|
@@ -804,10 +846,12 @@ class MysqlConnection {
|
|
|
804
846
|
allowComments: this.server.allowComments,
|
|
805
847
|
safety: this.server.safety,
|
|
806
848
|
maxStatements: 1,
|
|
849
|
+
session: this.session,
|
|
807
850
|
});
|
|
808
851
|
if (r.type === 'select' || r.type === 'showTables' || r.type === 'showDatabases' || r.type === 'describe'
|
|
809
852
|
|| r.type === 'showColumns' || r.type === 'showIndex' || r.type === 'showCreateTable'
|
|
810
853
|
|| r.type === 'showVariables' || r.type === 'showStatus' || r.type === 'showGrants' || r.type === 'showWarnings') {
|
|
854
|
+
this.session.foundRows = (r.raw && r.raw.length !== undefined) ? r.raw.length : (r.rows ? r.rows.length : 0);
|
|
811
855
|
let schema = null;
|
|
812
856
|
if (r.table) {
|
|
813
857
|
schema = engine.getTableSchema
|
|
@@ -820,6 +864,12 @@ class MysqlConnection {
|
|
|
820
864
|
this.sequence = sequence;
|
|
821
865
|
this.socket.write(Buffer.concat(packets));
|
|
822
866
|
} else {
|
|
867
|
+
if (r.type === 'insert') {
|
|
868
|
+
this.session.lastInsertId = r.insertId;
|
|
869
|
+
this.session.rowCount = r.affectedRows;
|
|
870
|
+
} else if (r.type === 'update' || r.type === 'delete' || r.type === 'truncate') {
|
|
871
|
+
this.session.rowCount = r.affectedRows;
|
|
872
|
+
}
|
|
823
873
|
this._send(okPacket(r.affectedRows || 0, r.insertId || 0));
|
|
824
874
|
}
|
|
825
875
|
}
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* JSQL-NEO Redis-compatible server (RESP2, zero dependencies).
|
|
3
|
+
*
|
|
4
|
+
* const { createRedisServer } = require('jsql-neo');
|
|
5
|
+
* const srv = createRedisServer({ port: 6379, dataDir: './data' });
|
|
6
|
+
* srv.listen();
|
|
7
|
+
*
|
|
8
|
+
* Supported commands:
|
|
9
|
+
* PING, ECHO, SET, GET, SETNX, DEL, EXISTS, KEYS, TYPE, EXPIRE, TTL, PERSIST,
|
|
10
|
+
* INCR, DECR, INCRBY, DECRBY, APPEND, STRLEN,
|
|
11
|
+
* HSET, HGET, HGETALL, HDEL, HEXISTS, HLEN, HKEYS, HVALS,
|
|
12
|
+
* LPUSH, RPUSH, LPOP, RPOP, LLEN, LRANGE, LINDEX, LREM,
|
|
13
|
+
* SADD, SREM, SMEMBERS, SISMEMBER, SCARD,
|
|
14
|
+
* DBSIZE, FLUSHALL, FLUSHDB, SELECT, INFO, AUTH, QUIT
|
|
15
|
+
*
|
|
16
|
+
* Persistence: keys are held in memory and snapshotted to data.rdb.json
|
|
17
|
+
* (debounced 500ms) plus a final snapshot on shutdown.
|
|
18
|
+
*/
|
|
19
|
+
const net = require('net');
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
const TYPE_SIGNATURES = {
|
|
24
|
+
PING: 0, ECHO: 1, SET: 2, SETNX: 2, GET: 1, DEL: -1, EXISTS: -1, KEYS: 1,
|
|
25
|
+
TYPE: 1, EXPIRE: 2, TTL: 1, PERSIST: 1,
|
|
26
|
+
INCR: 1, DECR: 1, INCRBY: 2, DECRBY: 2, APPEND: 2, STRLEN: 1,
|
|
27
|
+
HSET: -3, HGET: 2, HGETALL: 1, HDEL: -2, HEXISTS: 2, HLEN: 1, HKEYS: 1, HVALS: 1,
|
|
28
|
+
LPUSH: -2, RPUSH: -2, LPOP: 1, RPOP: 1, LLEN: 1, LRANGE: 3, LINDEX: 2, LREM: 3,
|
|
29
|
+
SADD: -2, SREM: -2, SMEMBERS: 1, SISMEMBER: 2, SCARD: 1,
|
|
30
|
+
DBSIZE: 0, FLUSHALL: 0, FLUSHDB: 0, SELECT: 1, INFO: 0, AUTH: 1, QUIT: 0,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
class RedisServer {
|
|
34
|
+
constructor(opts = {}) {
|
|
35
|
+
this.port = opts.port || 6379;
|
|
36
|
+
this.host = opts.host || '127.0.0.1';
|
|
37
|
+
this.password = opts.password || null;
|
|
38
|
+
this.dataDir = opts.dataDir || null;
|
|
39
|
+
this.onQuery = opts.onQuery || null;
|
|
40
|
+
this.db = new Map();
|
|
41
|
+
this.snapshotTimer = null;
|
|
42
|
+
this._load();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
snapshotPath() {
|
|
46
|
+
return this.dataDir ? path.join(this.dataDir, 'data.rdb.json') : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_load() {
|
|
50
|
+
const p = this.snapshotPath();
|
|
51
|
+
if (!p || !fs.existsSync(p)) return;
|
|
52
|
+
try {
|
|
53
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
54
|
+
for (const [k, v] of Object.entries(raw)) this.db.set(k, v);
|
|
55
|
+
} catch (_) {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_scheduleSnapshot() {
|
|
59
|
+
if (!this.dataDir) return;
|
|
60
|
+
if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
|
|
61
|
+
this.snapshotTimer = setTimeout(() => this._snapshot(), 500);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_snapshot() {
|
|
65
|
+
if (this.snapshotTimer) { clearTimeout(this.snapshotTimer); this.snapshotTimer = null; }
|
|
66
|
+
const p = this.snapshotPath();
|
|
67
|
+
if (!p) return;
|
|
68
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
69
|
+
const out = {};
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
for (const [k, v] of this.db) {
|
|
72
|
+
if (v.ttl && v.ttl <= now) continue;
|
|
73
|
+
out[k] = v;
|
|
74
|
+
}
|
|
75
|
+
const tmp = p + '.tmp';
|
|
76
|
+
fs.writeFileSync(tmp, JSON.stringify(out));
|
|
77
|
+
fs.renameSync(tmp, p);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
stop() {
|
|
81
|
+
if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
|
|
82
|
+
this._snapshot();
|
|
83
|
+
if (this.server) this.server.close();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* ---------- value helpers ---------- */
|
|
87
|
+
_get(key) {
|
|
88
|
+
const v = this.db.get(key);
|
|
89
|
+
if (!v) return null;
|
|
90
|
+
if (v.ttl && v.ttl <= Date.now()) { this.db.delete(key); return null; }
|
|
91
|
+
return v;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* ---------- command execution ---------- */
|
|
95
|
+
execute(cmd, args) {
|
|
96
|
+
const sig = TYPE_SIGNATURES[cmd];
|
|
97
|
+
if (sig === undefined) throw new Error(`ERR unknown command '${cmd}'`);
|
|
98
|
+
if (sig > 0 && args.length < sig) throw new Error(`ERR wrong number of arguments for '${cmd}' command`);
|
|
99
|
+
if (sig === -1 && args.length < 1) throw new Error(`ERR wrong number of arguments for '${cmd}' command`);
|
|
100
|
+
switch (cmd) {
|
|
101
|
+
case 'PING': return args.length ? args[0] : 'PONG';
|
|
102
|
+
case 'ECHO': return args[0];
|
|
103
|
+
case 'AUTH':
|
|
104
|
+
if (!this.password) throw new Error('ERR Client sent AUTH, but no password is set');
|
|
105
|
+
if (args[0] !== this.password) throw new Error('ERR invalid password');
|
|
106
|
+
return 'OK';
|
|
107
|
+
case 'SELECT':
|
|
108
|
+
if (!/^\d+$/.test(args[0])) throw new Error(`ERR invalid DB index`);
|
|
109
|
+
this.selected = parseInt(args[0], 10);
|
|
110
|
+
return 'OK';
|
|
111
|
+
case 'SET': {
|
|
112
|
+
const prev = this._get(args[0]);
|
|
113
|
+
this.db.set(args[0], { type: 'string', val: args[1], ttl: null });
|
|
114
|
+
this._scheduleSnapshot();
|
|
115
|
+
return 'OK';
|
|
116
|
+
}
|
|
117
|
+
case 'SETNX': {
|
|
118
|
+
const prev = this._get(args[0]);
|
|
119
|
+
if (prev) return 0;
|
|
120
|
+
this.db.set(args[0], { type: 'string', val: args[1], ttl: null });
|
|
121
|
+
this._scheduleSnapshot();
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
case 'GET': {
|
|
125
|
+
const v = this._get(args[0]);
|
|
126
|
+
if (!v) return null;
|
|
127
|
+
if (v.type !== 'string') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
128
|
+
return v.val;
|
|
129
|
+
}
|
|
130
|
+
case 'APPEND': {
|
|
131
|
+
const v = this._get(args[0]);
|
|
132
|
+
const prev = v && v.type === 'string' ? v.val : '';
|
|
133
|
+
this.db.set(args[0], { type: 'string', val: prev + args[1], ttl: v ? v.ttl : null });
|
|
134
|
+
this._scheduleSnapshot();
|
|
135
|
+
return this.db.get(args[0]).val.length;
|
|
136
|
+
}
|
|
137
|
+
case 'STRLEN': {
|
|
138
|
+
const v = this._get(args[0]);
|
|
139
|
+
if (!v) return 0;
|
|
140
|
+
if (v.type !== 'string') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
141
|
+
return v.val.length;
|
|
142
|
+
}
|
|
143
|
+
case 'INCR': case 'DECR': case 'INCRBY': case 'DECRBY': {
|
|
144
|
+
const by = cmd === 'INCRBY' || cmd === 'DECRBY' ? parseInt(args[1], 10) : 1;
|
|
145
|
+
if (isNaN(by)) throw new Error('ERR value is not an integer or out of range');
|
|
146
|
+
const dir = cmd === 'DECR' || cmd === 'DECRBY' ? -1 : 1;
|
|
147
|
+
const v = this._get(args[0]);
|
|
148
|
+
let n;
|
|
149
|
+
if (!v) { n = 0; }
|
|
150
|
+
else if (v.type === 'string' && /^-?\d+$/.test(v.val)) { n = parseInt(v.val, 10); }
|
|
151
|
+
else throw new Error('ERR value is not an integer or out of range');
|
|
152
|
+
n += dir * by;
|
|
153
|
+
this.db.set(args[0], { type: 'string', val: String(n), ttl: v ? v.ttl : null });
|
|
154
|
+
this._scheduleSnapshot();
|
|
155
|
+
return n;
|
|
156
|
+
}
|
|
157
|
+
case 'DEL': {
|
|
158
|
+
let n = 0;
|
|
159
|
+
for (const k of args) if (this.db.delete(k)) n++;
|
|
160
|
+
this._scheduleSnapshot();
|
|
161
|
+
return n;
|
|
162
|
+
}
|
|
163
|
+
case 'EXISTS': {
|
|
164
|
+
let n = 0;
|
|
165
|
+
for (const k of args) if (this._get(k)) n++;
|
|
166
|
+
return n;
|
|
167
|
+
}
|
|
168
|
+
case 'KEYS': {
|
|
169
|
+
const pat = args[0];
|
|
170
|
+
const re = new RegExp('^' + pat.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
|
|
171
|
+
const keys = [];
|
|
172
|
+
for (const k of this.db.keys()) {
|
|
173
|
+
if (re.test(k) && this._get(k)) keys.push(k);
|
|
174
|
+
}
|
|
175
|
+
return keys;
|
|
176
|
+
}
|
|
177
|
+
case 'TYPE': {
|
|
178
|
+
const v = this._get(args[0]);
|
|
179
|
+
return v ? v.type : 'none';
|
|
180
|
+
}
|
|
181
|
+
case 'EXPIRE': {
|
|
182
|
+
const v = this._get(args[0]);
|
|
183
|
+
if (!v) return 0;
|
|
184
|
+
v.ttl = Date.now() + parseInt(args[1], 10) * 1000;
|
|
185
|
+
this._scheduleSnapshot();
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
case 'TTL': {
|
|
189
|
+
const v = this._get(args[0]);
|
|
190
|
+
if (!v) return -2;
|
|
191
|
+
if (!v.ttl) return -1;
|
|
192
|
+
return Math.max(0, Math.round((v.ttl - Date.now()) / 1000));
|
|
193
|
+
}
|
|
194
|
+
case 'PERSIST': {
|
|
195
|
+
const v = this._get(args[0]);
|
|
196
|
+
if (!v || !v.ttl) return 0;
|
|
197
|
+
v.ttl = null;
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
200
|
+
case 'HSET': {
|
|
201
|
+
if (args.length % 2 !== 1) throw new Error(`ERR wrong number of arguments for 'hset' command`);
|
|
202
|
+
const v = this._get(args[0]);
|
|
203
|
+
const h = (v && v.type === 'hash') ? { ...v.val } : {};
|
|
204
|
+
let n = 0;
|
|
205
|
+
for (let i = 1; i < args.length; i += 2) {
|
|
206
|
+
if (!(args[i] in h)) n++;
|
|
207
|
+
h[args[i]] = args[i + 1];
|
|
208
|
+
}
|
|
209
|
+
this.db.set(args[0], { type: 'hash', val: h, ttl: v ? v.ttl : null });
|
|
210
|
+
this._scheduleSnapshot();
|
|
211
|
+
return n;
|
|
212
|
+
}
|
|
213
|
+
case 'HGET': {
|
|
214
|
+
const v = this._get(args[0]);
|
|
215
|
+
if (!v) return null;
|
|
216
|
+
if (v.type !== 'hash') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
217
|
+
return v.val[args[1]] === undefined ? null : v.val[args[1]];
|
|
218
|
+
}
|
|
219
|
+
case 'HGETALL': {
|
|
220
|
+
const v = this._get(args[0]);
|
|
221
|
+
if (!v) return [];
|
|
222
|
+
if (v.type !== 'hash') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
223
|
+
return Object.entries(v.val).flat();
|
|
224
|
+
}
|
|
225
|
+
case 'HDEL': {
|
|
226
|
+
const v = this._get(args[0]);
|
|
227
|
+
if (!v) return 0;
|
|
228
|
+
if (v.type !== 'hash') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
229
|
+
let n = 0;
|
|
230
|
+
for (const f of args.slice(1)) if (v.val[f] !== undefined) { delete v.val[f]; n++; }
|
|
231
|
+
this._scheduleSnapshot();
|
|
232
|
+
return n;
|
|
233
|
+
}
|
|
234
|
+
case 'HEXISTS': {
|
|
235
|
+
const v = this._get(args[0]);
|
|
236
|
+
return (v && v.type === 'hash' && v.val[args[1]] !== undefined) ? 1 : 0;
|
|
237
|
+
}
|
|
238
|
+
case 'HLEN': {
|
|
239
|
+
const v = this._get(args[0]);
|
|
240
|
+
return v && v.type === 'hash' ? Object.keys(v.val).length : 0;
|
|
241
|
+
}
|
|
242
|
+
case 'HKEYS': {
|
|
243
|
+
const v = this._get(args[0]);
|
|
244
|
+
return v && v.type === 'hash' ? Object.keys(v.val) : [];
|
|
245
|
+
}
|
|
246
|
+
case 'HVALS': {
|
|
247
|
+
const v = this._get(args[0]);
|
|
248
|
+
return v && v.type === 'hash' ? Object.values(v.val) : [];
|
|
249
|
+
}
|
|
250
|
+
case 'LPUSH': case 'RPUSH': {
|
|
251
|
+
const v = this._get(args[0]);
|
|
252
|
+
const list = (v && v.type === 'list') ? [...v.val] : [];
|
|
253
|
+
if (cmd === 'LPUSH') list.unshift(...args.slice(1));
|
|
254
|
+
else list.push(...args.slice(1));
|
|
255
|
+
this.db.set(args[0], { type: 'list', val: list, ttl: v ? v.ttl : null });
|
|
256
|
+
this._scheduleSnapshot();
|
|
257
|
+
return list.length;
|
|
258
|
+
}
|
|
259
|
+
case 'LPOP': case 'RPOP': {
|
|
260
|
+
const v = this._get(args[0]);
|
|
261
|
+
if (!v) return null;
|
|
262
|
+
if (v.type !== 'list') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
263
|
+
const el = cmd === 'LPOP' ? v.val.shift() : v.val.pop();
|
|
264
|
+
this._scheduleSnapshot();
|
|
265
|
+
return el === undefined ? null : el;
|
|
266
|
+
}
|
|
267
|
+
case 'LLEN': {
|
|
268
|
+
const v = this._get(args[0]);
|
|
269
|
+
return v && v.type === 'list' ? v.val.length : 0;
|
|
270
|
+
}
|
|
271
|
+
case 'LRANGE': {
|
|
272
|
+
const v = this._get(args[0]);
|
|
273
|
+
if (!v) return [];
|
|
274
|
+
if (v.type !== 'list') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
275
|
+
let start = parseInt(args[1], 10), stop = parseInt(args[2], 10);
|
|
276
|
+
const len = v.val.length;
|
|
277
|
+
if (start < 0) start = Math.max(0, len + start);
|
|
278
|
+
if (stop < 0) stop = len + stop;
|
|
279
|
+
if (start > stop || start >= len) return [];
|
|
280
|
+
return v.val.slice(start, Math.min(stop + 1, len));
|
|
281
|
+
}
|
|
282
|
+
case 'LINDEX': {
|
|
283
|
+
const v = this._get(args[0]);
|
|
284
|
+
if (!v) return null;
|
|
285
|
+
if (v.type !== 'list') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
286
|
+
let i = parseInt(args[1], 10);
|
|
287
|
+
if (i < 0) i = v.val.length + i;
|
|
288
|
+
return v.val[i] === undefined ? null : v.val[i];
|
|
289
|
+
}
|
|
290
|
+
case 'LREM': {
|
|
291
|
+
const v = this._get(args[0]);
|
|
292
|
+
if (!v) return 0;
|
|
293
|
+
if (v.type !== 'list') throw new Error('WRONGTYPE Operation against a key holding the wrong kind of value');
|
|
294
|
+
const count = parseInt(args[1], 10);
|
|
295
|
+
const target = args[2];
|
|
296
|
+
const out = [];
|
|
297
|
+
let removed = 0;
|
|
298
|
+
const remaining = Math.abs(count);
|
|
299
|
+
for (const el of v.val) {
|
|
300
|
+
if (el === target && (count === 0 || removed < remaining)) { removed++; continue; }
|
|
301
|
+
out.push(el);
|
|
302
|
+
}
|
|
303
|
+
v.val = out;
|
|
304
|
+
this._scheduleSnapshot();
|
|
305
|
+
return removed;
|
|
306
|
+
}
|
|
307
|
+
case 'SADD': case 'SREM': {
|
|
308
|
+
const v = this._get(args[0]);
|
|
309
|
+
const set = (v && v.type === 'set') ? new Set(v.val) : new Set();
|
|
310
|
+
let n = 0;
|
|
311
|
+
for (const m of args.slice(1)) {
|
|
312
|
+
if (cmd === 'SADD') { if (!set.has(m)) { set.add(m); n++; } }
|
|
313
|
+
else { if (set.delete(m)) n++; }
|
|
314
|
+
}
|
|
315
|
+
if (set.size === 0) this.db.delete(args[0]);
|
|
316
|
+
else this.db.set(args[0], { type: 'set', val: [...set], ttl: v ? v.ttl : null });
|
|
317
|
+
this._scheduleSnapshot();
|
|
318
|
+
return n;
|
|
319
|
+
}
|
|
320
|
+
case 'SMEMBERS': {
|
|
321
|
+
const v = this._get(args[0]);
|
|
322
|
+
return v && v.type === 'set' ? [...v.val] : [];
|
|
323
|
+
}
|
|
324
|
+
case 'SISMEMBER': {
|
|
325
|
+
const v = this._get(args[0]);
|
|
326
|
+
return (v && v.type === 'set' && v.val.includes(args[1])) ? 1 : 0;
|
|
327
|
+
}
|
|
328
|
+
case 'SCARD': {
|
|
329
|
+
const v = this._get(args[0]);
|
|
330
|
+
return v && v.type === 'set' ? v.val.length : 0;
|
|
331
|
+
}
|
|
332
|
+
case 'DBSIZE': return this.db.size;
|
|
333
|
+
case 'FLUSHALL': case 'FLUSHDB':
|
|
334
|
+
this.db.clear();
|
|
335
|
+
this._scheduleSnapshot();
|
|
336
|
+
return 'OK';
|
|
337
|
+
case 'INFO': {
|
|
338
|
+
const lines = [
|
|
339
|
+
'# Server',
|
|
340
|
+
'redis_version:7.0.0',
|
|
341
|
+
'jsql_neo_version:' + require('../package.json').version,
|
|
342
|
+
'# Memory',
|
|
343
|
+
'used_memory:' + JSON.stringify([...this.db.values()]).length,
|
|
344
|
+
'# Stats',
|
|
345
|
+
'db_size:' + this.db.size,
|
|
346
|
+
'connected_clients:1',
|
|
347
|
+
];
|
|
348
|
+
return lines.join('\r\n') + '\r\n';
|
|
349
|
+
}
|
|
350
|
+
case 'QUIT': return 'OK';
|
|
351
|
+
default: throw new Error(`ERR unknown command '${cmd}'`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* ---------- RESP protocol ---------- */
|
|
356
|
+
listen() {
|
|
357
|
+
this.server = net.createServer((socket) => {
|
|
358
|
+
let buf = Buffer.alloc(0);
|
|
359
|
+
socket.setEncoding('utf8');
|
|
360
|
+
socket.on('data', (chunk) => {
|
|
361
|
+
buf += chunk;
|
|
362
|
+
for (;;) {
|
|
363
|
+
const msg = this._parse(buf);
|
|
364
|
+
if (!msg) break;
|
|
365
|
+
buf = buf.slice(msg.consumed);
|
|
366
|
+
this._handle(socket, msg.cmd, msg.args);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
socket.on('error', () => {});
|
|
370
|
+
});
|
|
371
|
+
this.server.listen(this.port, this.host);
|
|
372
|
+
return this;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
_parse(buf) {
|
|
376
|
+
if (buf[0] !== '*') {
|
|
377
|
+
const i = buf.indexOf('\r\n');
|
|
378
|
+
if (i < 0) return null;
|
|
379
|
+
const cmd = buf.slice(0, i).trim().split(/\s+/);
|
|
380
|
+
if (!cmd.length) return null;
|
|
381
|
+
return { consumed: i + 2, cmd: cmd.map(c => c.toUpperCase()), args: cmd.slice(1) };
|
|
382
|
+
}
|
|
383
|
+
const i = buf.indexOf('\r\n');
|
|
384
|
+
if (i < 0) return null;
|
|
385
|
+
const n = parseInt(buf.slice(1, i), 10);
|
|
386
|
+
if (isNaN(n)) throw new Error('ERR Protocol error');
|
|
387
|
+
let off = i + 2;
|
|
388
|
+
const parts = [];
|
|
389
|
+
for (let k = 0; k < n; k++) {
|
|
390
|
+
if (buf[off] !== '$') throw new Error('ERR Protocol error: expected bulk string');
|
|
391
|
+
const j = buf.indexOf('\r\n', off);
|
|
392
|
+
if (j < 0) return null;
|
|
393
|
+
const len = parseInt(buf.slice(off + 1, j), 10);
|
|
394
|
+
if (isNaN(len)) throw new Error('ERR Protocol error');
|
|
395
|
+
if (buf.length < j + 2 + len + 2) return null;
|
|
396
|
+
parts.push(buf.slice(j + 2, j + 2 + len));
|
|
397
|
+
off = j + 2 + len + 2;
|
|
398
|
+
}
|
|
399
|
+
const cmd = parts[0].toUpperCase();
|
|
400
|
+
return { consumed: off, cmd, args: parts.slice(1) };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
_handle(socket, cmd, args) {
|
|
404
|
+
try {
|
|
405
|
+
if (this.onQuery) this.onQuery(cmd, args);
|
|
406
|
+
if (cmd === 'AUTH') {
|
|
407
|
+
if (!this.password) return this._send(socket, -new Error('ERR Client sent AUTH, but no password is set'));
|
|
408
|
+
}
|
|
409
|
+
if (this.password && !this._authed) {
|
|
410
|
+
if (cmd === 'AUTH') { const ok = this.execute('AUTH', args); this._authed = ok === 'OK'; return this._send(socket, ok); }
|
|
411
|
+
return this._send(socket, -new Error('NOAUTH Authentication required.'));
|
|
412
|
+
}
|
|
413
|
+
if (cmd === 'QUIT') { this._send(socket, 'OK'); socket.end(); return; }
|
|
414
|
+
const r = this.execute(cmd, args);
|
|
415
|
+
this._send(socket, r);
|
|
416
|
+
} catch (e) {
|
|
417
|
+
this._send(socket, -e);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
_send(socket, val) {
|
|
422
|
+
let out;
|
|
423
|
+
if (val instanceof Error) {
|
|
424
|
+
out = '-' + val.message + '\r\n';
|
|
425
|
+
} else if (val === null) {
|
|
426
|
+
out = '$-1\r\n';
|
|
427
|
+
} else if (typeof val === 'number') {
|
|
428
|
+
out = ':' + val + '\r\n';
|
|
429
|
+
} else if (Array.isArray(val)) {
|
|
430
|
+
out = '*' + val.length + '\r\n' + val.map(v => '$' + String(v).length + '\r\n' + v + '\r\n').join('');
|
|
431
|
+
} else if (typeof val === 'string') {
|
|
432
|
+
if (val === 'OK' || val === 'PONG') {
|
|
433
|
+
out = '+' + val + '\r\n';
|
|
434
|
+
} else {
|
|
435
|
+
out = '$' + Buffer.byteLength(val, 'utf8') + '\r\n' + val + '\r\n';
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
out = '$-1\r\n';
|
|
439
|
+
}
|
|
440
|
+
socket.write(out);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function createRedisServer(opts = {}) {
|
|
445
|
+
return new RedisServer(opts);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
module.exports = { RedisServer, createRedisServer };
|