@wowoengine/sawitdb 2.4.0 → 2.6.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/README.md +86 -25
- package/bin/sawit-server.js +12 -1
- package/cli/benchmark.js +318 -0
- package/cli/local.js +98 -20
- package/cli/remote.js +65 -11
- package/cli/test.js +203 -0
- package/cli/test_security.js +140 -0
- package/docs/DB Event.md +32 -0
- package/docs/index.html +699 -336
- package/package.json +10 -7
- package/src/SawitClient.js +122 -98
- package/src/SawitServer.js +78 -450
- package/src/SawitWorker.js +55 -0
- package/src/WowoEngine.js +360 -449
- package/src/modules/BTreeIndex.js +114 -43
- package/src/modules/ClusterManager.js +78 -0
- package/src/modules/Env.js +33 -0
- package/src/modules/Pager.js +215 -6
- package/src/modules/QueryParser.js +310 -82
- package/src/modules/ThreadManager.js +84 -0
- package/src/modules/ThreadPool.js +154 -0
- package/src/modules/WAL.js +340 -0
- package/src/server/DatabaseRegistry.js +92 -0
- package/src/server/auth/AuthManager.js +92 -0
- package/src/server/router/RequestRouter.js +278 -0
- package/src/server/session/ClientSession.js +19 -0
- package/src/services/IndexManager.js +183 -0
- package/src/services/QueryExecutor.js +11 -0
- package/src/services/TableManager.js +162 -0
- package/src/services/event/DBEvent.js +61 -0
- package/src/services/event/DBEventHandler.js +39 -0
- package/src/services/executors/AggregateExecutor.js +153 -0
- package/src/services/executors/DeleteExecutor.js +134 -0
- package/src/services/executors/InsertExecutor.js +113 -0
- package/src/services/executors/SelectExecutor.js +130 -0
- package/src/services/executors/UpdateExecutor.js +156 -0
- package/src/services/logic/ConditionEvaluator.js +75 -0
- package/src/services/logic/JoinProcessor.js +230 -0
package/src/SawitServer.js
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
const net = require('net');
|
|
2
|
-
const SawitDB = require('./WowoEngine');
|
|
3
2
|
const path = require('path');
|
|
4
3
|
const fs = require('fs');
|
|
5
4
|
|
|
5
|
+
// Modular Components
|
|
6
|
+
const AuthManager = require('./server/auth/AuthManager');
|
|
7
|
+
const DatabaseRegistry = require('./server/DatabaseRegistry');
|
|
8
|
+
const RequestRouter = require('./server/router/RequestRouter');
|
|
9
|
+
const ClientSession = require('./server/session/ClientSession');
|
|
10
|
+
|
|
6
11
|
/**
|
|
7
12
|
* SawitDB Server - Network Database Server
|
|
8
13
|
* Supports sawitdb:// protocol connections
|
|
14
|
+
* Refactored to use modular components.
|
|
9
15
|
*/
|
|
10
16
|
class SawitServer {
|
|
11
17
|
constructor(config = {}) {
|
|
12
|
-
//
|
|
13
|
-
this.port = this.
|
|
18
|
+
// Configuration
|
|
19
|
+
this.port = this.validatePort(config.port || 7878);
|
|
14
20
|
this.host = config.host || '0.0.0.0';
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
21
|
+
this.maxConnections = config.maxConnections || 100;
|
|
22
|
+
this.queryTimeout = config.queryTimeout || 30000;
|
|
23
|
+
this.logLevel = config.logLevel || 'info';
|
|
24
|
+
|
|
25
|
+
// State
|
|
26
|
+
this.config = config;
|
|
17
27
|
this.clients = new Set();
|
|
18
28
|
this.server = null;
|
|
19
|
-
this.auth = config.auth || null; // { username: 'password' }
|
|
20
|
-
this.maxConnections = config.maxConnections || 100;
|
|
21
|
-
this.queryTimeout = config.queryTimeout || 30000; // 30 seconds
|
|
22
|
-
this.logLevel = config.logLevel || 'info'; // 'debug', 'info', 'warn', 'error'
|
|
23
29
|
this.stats = {
|
|
24
30
|
totalConnections: 0,
|
|
25
31
|
activeConnections: 0,
|
|
@@ -28,24 +34,27 @@ class SawitServer {
|
|
|
28
34
|
startTime: Date.now()
|
|
29
35
|
};
|
|
30
36
|
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
// Initialize Managers
|
|
38
|
+
this.dbRegistry = new DatabaseRegistry(
|
|
39
|
+
config.dataDir || path.join(__dirname, '../data'),
|
|
40
|
+
config
|
|
41
|
+
);
|
|
42
|
+
this.authManager = new AuthManager(this);
|
|
43
|
+
this.router = new RequestRouter(this);
|
|
35
44
|
|
|
36
|
-
this.
|
|
37
|
-
this.
|
|
45
|
+
this.log('info', `Data directory: ${this.dbRegistry.dataDir}`);
|
|
46
|
+
this.log('info', `Max connections: ${this.maxConnections}`);
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
|
|
41
|
-
const p = parseInt(port);
|
|
49
|
+
validatePort(port) {
|
|
50
|
+
const p = parseInt(port, 10);
|
|
42
51
|
if (isNaN(p) || p < 1 || p > 65535) {
|
|
43
52
|
throw new Error(`Invalid port: ${port}. Must be between 1-65535`);
|
|
44
53
|
}
|
|
45
54
|
return p;
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
|
|
57
|
+
log(level, message, data = null) {
|
|
49
58
|
const levels = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
50
59
|
const currentLevel = levels[this.logLevel] || 1;
|
|
51
60
|
const msgLevel = levels[level] || 1;
|
|
@@ -61,15 +70,22 @@ class SawitServer {
|
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
start() {
|
|
64
|
-
this.server = net.createServer((socket) => this.
|
|
73
|
+
this.server = net.createServer((socket) => this.handleConnection(socket));
|
|
65
74
|
|
|
66
75
|
this.server.listen(this.port, this.host, () => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
const cluster = require('cluster');
|
|
77
|
+
const prefix = cluster.isWorker ? `[Worker ${cluster.worker.id}]` : '[Server]';
|
|
78
|
+
|
|
79
|
+
if (!cluster.isWorker) {
|
|
80
|
+
console.log(`╔══════════════════════════════════════════════════╗`);
|
|
81
|
+
console.log(`║ 🌴 SawitDB Server - Version 2.6.0 ║`);
|
|
82
|
+
console.log(`╚══════════════════════════════════════════════════╝`);
|
|
83
|
+
}
|
|
84
|
+
console.log(`${prefix} Listening on ${this.host}:${this.port}`);
|
|
85
|
+
console.log(
|
|
86
|
+
`${prefix} Protocol: sawitdb://${this.host}:${this.port}/[database]`
|
|
87
|
+
);
|
|
88
|
+
console.log(`${prefix} Ready to accept connections...`);
|
|
73
89
|
});
|
|
74
90
|
|
|
75
91
|
this.server.on('error', (err) => {
|
|
@@ -82,9 +98,12 @@ class SawitServer {
|
|
|
82
98
|
|
|
83
99
|
// Close all client connections
|
|
84
100
|
for (const client of this.clients) {
|
|
85
|
-
client.
|
|
101
|
+
client.end();
|
|
86
102
|
}
|
|
87
103
|
|
|
104
|
+
// Close all open databases
|
|
105
|
+
this.dbRegistry.closeAll();
|
|
106
|
+
|
|
88
107
|
// Close server
|
|
89
108
|
if (this.server) {
|
|
90
109
|
this.server.close(() => {
|
|
@@ -93,12 +112,11 @@ class SawitServer {
|
|
|
93
112
|
}
|
|
94
113
|
}
|
|
95
114
|
|
|
96
|
-
|
|
115
|
+
handleConnection(socket) {
|
|
97
116
|
const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
98
117
|
|
|
99
|
-
// Check connection limit
|
|
100
118
|
if (this.clients.size >= this.maxConnections) {
|
|
101
|
-
this.
|
|
119
|
+
this.log('warn', `Connection limit reached. Rejecting ${clientId}`);
|
|
102
120
|
socket.write(JSON.stringify({
|
|
103
121
|
type: 'error',
|
|
104
122
|
error: 'Server connection limit reached. Please try again later.'
|
|
@@ -107,30 +125,27 @@ class SawitServer {
|
|
|
107
125
|
return;
|
|
108
126
|
}
|
|
109
127
|
|
|
110
|
-
this.
|
|
128
|
+
this.log('info', `Client connected: ${clientId}`);
|
|
111
129
|
this.stats.totalConnections++;
|
|
112
130
|
this.stats.activeConnections++;
|
|
113
|
-
|
|
114
131
|
this.clients.add(socket);
|
|
115
|
-
socket.clientId = clientId;
|
|
116
|
-
socket.connectedAt = Date.now();
|
|
117
132
|
|
|
118
|
-
|
|
119
|
-
|
|
133
|
+
// Attach session to socket for cleanup if needed, but best to keep separate
|
|
134
|
+
// Just use local var for session, pass to router
|
|
135
|
+
const session = new ClientSession(socket, clientId);
|
|
136
|
+
|
|
120
137
|
let buffer = '';
|
|
121
138
|
|
|
122
139
|
socket.on('data', (data) => {
|
|
123
140
|
buffer += data.toString();
|
|
124
141
|
|
|
125
|
-
// Prevent buffer overflow attacks
|
|
126
142
|
if (buffer.length > 1048576) { // 1MB limit
|
|
127
|
-
this.
|
|
128
|
-
this.
|
|
143
|
+
this.log('warn', `Buffer overflow attempt from ${clientId}`);
|
|
144
|
+
this.sendError(socket, 'Request too large');
|
|
129
145
|
socket.destroy();
|
|
130
146
|
return;
|
|
131
147
|
}
|
|
132
148
|
|
|
133
|
-
// Process complete messages (delimited by newline)
|
|
134
149
|
let newlineIndex;
|
|
135
150
|
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
|
|
136
151
|
const message = buffer.substring(0, newlineIndex);
|
|
@@ -138,113 +153,62 @@ class SawitServer {
|
|
|
138
153
|
|
|
139
154
|
try {
|
|
140
155
|
const request = JSON.parse(message);
|
|
141
|
-
this.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
this._handleRequest(socket, request, {
|
|
145
|
-
authenticated,
|
|
146
|
-
currentDatabase,
|
|
147
|
-
setAuth: (val) => { authenticated = val; },
|
|
148
|
-
setDatabase: (db) => { currentDatabase = db; }
|
|
149
|
-
});
|
|
156
|
+
this.log('debug', `Request from ${clientId}`, request);
|
|
157
|
+
|
|
158
|
+
this.router.handle(socket, request, session);
|
|
150
159
|
} catch (err) {
|
|
151
|
-
this.
|
|
160
|
+
this.log('error', `Invalid request from ${clientId}: ${err.message}`);
|
|
152
161
|
this.stats.errors++;
|
|
153
|
-
this.
|
|
162
|
+
this.sendError(socket, `Invalid request format: ${err.message}`);
|
|
154
163
|
}
|
|
155
164
|
}
|
|
156
165
|
});
|
|
157
166
|
|
|
158
167
|
socket.on('end', () => {
|
|
159
|
-
const duration = Date.now() -
|
|
160
|
-
this.
|
|
168
|
+
const duration = Date.now() - session.connectedAt;
|
|
169
|
+
this.log('info', `Client disconnected: ${clientId} (duration: ${duration}ms)`);
|
|
161
170
|
this.clients.delete(socket);
|
|
162
171
|
this.stats.activeConnections--;
|
|
163
172
|
});
|
|
164
173
|
|
|
165
174
|
socket.on('error', (err) => {
|
|
166
|
-
this.
|
|
175
|
+
this.log('error', `Client error: ${clientId} - ${err.message}`);
|
|
167
176
|
this.clients.delete(socket);
|
|
168
177
|
this.stats.activeConnections--;
|
|
169
178
|
this.stats.errors++;
|
|
170
179
|
});
|
|
171
180
|
|
|
172
181
|
socket.on('timeout', () => {
|
|
173
|
-
this.
|
|
182
|
+
this.log('warn', `Client timeout: ${clientId}`);
|
|
174
183
|
socket.destroy();
|
|
175
184
|
});
|
|
176
185
|
|
|
177
|
-
// Set socket timeout
|
|
178
186
|
socket.setTimeout(this.queryTimeout);
|
|
179
187
|
|
|
180
|
-
|
|
181
|
-
this._sendResponse(socket, {
|
|
188
|
+
this.sendResponse(socket, {
|
|
182
189
|
type: 'welcome',
|
|
183
190
|
message: 'SawitDB Server',
|
|
184
|
-
version: '2.
|
|
191
|
+
version: '2.6.0',
|
|
185
192
|
protocol: 'sawitdb'
|
|
186
193
|
});
|
|
187
194
|
}
|
|
188
195
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
switch (type) {
|
|
198
|
-
case 'auth':
|
|
199
|
-
this._handleAuth(socket, payload, context);
|
|
200
|
-
break;
|
|
201
|
-
|
|
202
|
-
case 'use':
|
|
203
|
-
this._handleUseDatabase(socket, payload, context);
|
|
204
|
-
break;
|
|
205
|
-
|
|
206
|
-
case 'query':
|
|
207
|
-
this._handleQuery(socket, payload, context);
|
|
208
|
-
break;
|
|
209
|
-
|
|
210
|
-
case 'ping':
|
|
211
|
-
this._sendResponse(socket, { type: 'pong', timestamp: Date.now() });
|
|
212
|
-
break;
|
|
213
|
-
|
|
214
|
-
case 'list_databases':
|
|
215
|
-
this._handleListDatabases(socket);
|
|
216
|
-
break;
|
|
217
|
-
|
|
218
|
-
case 'drop_database':
|
|
219
|
-
this._handleDropDatabase(socket, payload, context);
|
|
220
|
-
break;
|
|
221
|
-
|
|
222
|
-
case 'stats':
|
|
223
|
-
this._handleStats(socket);
|
|
224
|
-
break;
|
|
225
|
-
|
|
226
|
-
default:
|
|
227
|
-
this._sendError(socket, `Unknown request type: ${type}`);
|
|
196
|
+
sendResponse(socket, data) {
|
|
197
|
+
try {
|
|
198
|
+
if (socket.writable) {
|
|
199
|
+
socket.write(JSON.stringify(data) + '\n');
|
|
200
|
+
}
|
|
201
|
+
} catch (err) {
|
|
202
|
+
console.error('[Server] Failed to send response:', err.message);
|
|
228
203
|
}
|
|
229
204
|
}
|
|
230
205
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const stats = {
|
|
234
|
-
...this.stats,
|
|
235
|
-
uptime,
|
|
236
|
-
uptimeFormatted: this._formatUptime(uptime),
|
|
237
|
-
databases: this.databases.size,
|
|
238
|
-
memoryUsage: process.memoryUsage()
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
this._sendResponse(socket, {
|
|
242
|
-
type: 'stats',
|
|
243
|
-
stats
|
|
244
|
-
});
|
|
206
|
+
sendError(socket, message) {
|
|
207
|
+
this.sendResponse(socket, { type: 'error', error: message });
|
|
245
208
|
}
|
|
246
209
|
|
|
247
|
-
|
|
210
|
+
// Utiliy for Stats
|
|
211
|
+
formatUptime(ms) {
|
|
248
212
|
const seconds = Math.floor(ms / 1000);
|
|
249
213
|
const minutes = Math.floor(seconds / 60);
|
|
250
214
|
const hours = Math.floor(minutes / 60);
|
|
@@ -255,348 +219,12 @@ class SawitServer {
|
|
|
255
219
|
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
|
256
220
|
return `${seconds}s`;
|
|
257
221
|
}
|
|
258
|
-
|
|
259
|
-
_handleAuth(socket, payload, context) {
|
|
260
|
-
const { username, password } = payload;
|
|
261
|
-
|
|
262
|
-
if (!this.auth) {
|
|
263
|
-
context.setAuth(true);
|
|
264
|
-
return this._sendResponse(socket, { type: 'auth_success', message: 'No authentication required' });
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (this.auth[username] === password) {
|
|
268
|
-
context.setAuth(true);
|
|
269
|
-
this._sendResponse(socket, { type: 'auth_success', message: 'Authentication successful' });
|
|
270
|
-
} else {
|
|
271
|
-
this._sendError(socket, 'Invalid credentials');
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
_handleUseDatabase(socket, payload, context) {
|
|
276
|
-
const { database } = payload;
|
|
277
|
-
|
|
278
|
-
if (!database || typeof database !== 'string') {
|
|
279
|
-
return this._sendError(socket, 'Invalid database name');
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Validate database name (alphanumeric, underscore, dash)
|
|
283
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(database)) {
|
|
284
|
-
return this._sendError(socket, 'Database name can only contain letters, numbers, underscore, and dash');
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
try {
|
|
288
|
-
const db = this._getOrCreateDatabase(database);
|
|
289
|
-
context.setDatabase(database);
|
|
290
|
-
this._sendResponse(socket, {
|
|
291
|
-
type: 'use_success',
|
|
292
|
-
database,
|
|
293
|
-
message: `Switched to database '${database}'`
|
|
294
|
-
});
|
|
295
|
-
} catch (err) {
|
|
296
|
-
this._sendError(socket, `Failed to use database: ${err.message}`);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
_handleQuery(socket, payload, context) {
|
|
301
|
-
const { query, params } = payload;
|
|
302
|
-
const startTime = Date.now();
|
|
303
|
-
|
|
304
|
-
// --- Intercept Server-Level Commands (Wilayah Management) ---
|
|
305
|
-
|
|
306
|
-
const qUpper = query.trim().toUpperCase();
|
|
307
|
-
|
|
308
|
-
// 1. LIHAT WILAYAH
|
|
309
|
-
if (qUpper === 'LIHAT WILAYAH') {
|
|
310
|
-
try {
|
|
311
|
-
const databases = fs.readdirSync(this.dataDir)
|
|
312
|
-
.filter(file => file.endsWith('.sawit'));
|
|
313
|
-
|
|
314
|
-
// Format nicely as a table-like string or JSON
|
|
315
|
-
const list = databases.map(f => `- ${f.replace('.sawit', '')}`).join('\n');
|
|
316
|
-
const result = `Daftar Wilayah:\n${list}`;
|
|
317
|
-
|
|
318
|
-
return this._sendResponse(socket, {
|
|
319
|
-
type: 'query_result',
|
|
320
|
-
result,
|
|
321
|
-
query,
|
|
322
|
-
executionTime: Date.now() - startTime
|
|
323
|
-
});
|
|
324
|
-
} catch (err) {
|
|
325
|
-
return this._sendError(socket, `Gagal melihat wilayah: ${err.message}`);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// 2. BUKA WILAYAH [nama]
|
|
330
|
-
if (qUpper.startsWith('BUKA WILAYAH')) {
|
|
331
|
-
const parts = query.trim().split(/\s+/);
|
|
332
|
-
if (parts.length < 3) {
|
|
333
|
-
return this._sendError(socket, 'Syntax: BUKA WILAYAH [nama_wilayah]');
|
|
334
|
-
}
|
|
335
|
-
const dbName = parts[2];
|
|
336
|
-
// Reuse logic from internal handler if possible, otherwise implement here
|
|
337
|
-
// Implementing directly to match the "create empty sawit file" logic
|
|
338
|
-
try {
|
|
339
|
-
// Validation
|
|
340
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(dbName)) {
|
|
341
|
-
return this._sendError(socket, 'Nama wilayah hanya boleh huruf, angka, _ dan -');
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const dbPath = path.join(this.dataDir, `${dbName}.sawit`);
|
|
345
|
-
if (fs.existsSync(dbPath)) {
|
|
346
|
-
// It's technically fine if it exists, just say it's opened/available
|
|
347
|
-
// But strict "Create" usually expects new. Let's follow "Open/Create" semantics of key-value stores or similar
|
|
348
|
-
// Design doc says: "Membuat file ... kosong".
|
|
349
|
-
// If exists, let's just say it exists.
|
|
350
|
-
return this._sendResponse(socket, {
|
|
351
|
-
type: 'query_result',
|
|
352
|
-
result: `Wilayah '${dbName}' sudah ada.`,
|
|
353
|
-
query,
|
|
354
|
-
executionTime: Date.now() - startTime
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Create empty file (via SawitDB constructor which initializes it)
|
|
359
|
-
new SawitDB(dbPath);
|
|
360
|
-
|
|
361
|
-
return this._sendResponse(socket, {
|
|
362
|
-
type: 'query_result',
|
|
363
|
-
result: `Wilayah '${dbName}' berhasil dibuka.`,
|
|
364
|
-
query,
|
|
365
|
-
executionTime: Date.now() - startTime
|
|
366
|
-
});
|
|
367
|
-
} catch (err) {
|
|
368
|
-
return this._sendError(socket, `Gagal membuka wilayah: ${err.message}`);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// 3. MASUK WILAYAH [nama]
|
|
373
|
-
if (qUpper.startsWith('MASUK WILAYAH')) {
|
|
374
|
-
const parts = query.trim().split(/\s+/);
|
|
375
|
-
if (parts.length < 3) {
|
|
376
|
-
return this._sendError(socket, 'Syntax: MASUK WILAYAH [nama_wilayah]');
|
|
377
|
-
}
|
|
378
|
-
const dbName = parts[2];
|
|
379
|
-
|
|
380
|
-
// Allow "MASUK WILAYAH DEFAULT" case-insensitive match for file? No, usually case sensitive filesystems.
|
|
381
|
-
// But let's assume case-sensitive for ID.
|
|
382
|
-
|
|
383
|
-
const dbPath = path.join(this.dataDir, `${dbName}.sawit`);
|
|
384
|
-
if (!fs.existsSync(dbPath)) {
|
|
385
|
-
return this._sendError(socket, `Wilayah '${dbName}' tidak ditemukan.`);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
context.setDatabase(dbName);
|
|
389
|
-
return this._sendResponse(socket, {
|
|
390
|
-
type: 'query_result', // Return as query result so CLI prints it normally
|
|
391
|
-
result: `Selamat datang di wilayah '${dbName}'.`,
|
|
392
|
-
query,
|
|
393
|
-
executionTime: Date.now() - startTime
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// 4. BAKAR WILAYAH [nama]
|
|
398
|
-
if (qUpper.startsWith('BAKAR WILAYAH')) {
|
|
399
|
-
const parts = query.trim().split(/\s+/);
|
|
400
|
-
if (parts.length < 3) {
|
|
401
|
-
return this._sendError(socket, 'Syntax: BAKAR WILAYAH [nama_wilayah]');
|
|
402
|
-
}
|
|
403
|
-
const dbName = parts[2];
|
|
404
|
-
|
|
405
|
-
// Reuse existing logic
|
|
406
|
-
const payload = { database: dbName };
|
|
407
|
-
// We need to adapt the existing _handleDropDatabase to generic usage or call it.
|
|
408
|
-
// _handleDropDatabase sends its own response. We want query_result format preferably for consistency in CLI?
|
|
409
|
-
// The existing _handleDropDatabase sends 'drop_success'. CLI might not print that as a query result string.
|
|
410
|
-
// Let's reimplement logic here for 'query' flow to ensure 'query_result' type.
|
|
411
|
-
|
|
412
|
-
try {
|
|
413
|
-
const dbPath = path.join(this.dataDir, `${dbName}.sawit`);
|
|
414
|
-
if (!fs.existsSync(dbPath)) {
|
|
415
|
-
return this._sendError(socket, `Wilayah '${dbName}' tidak ditemukan.`);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (this.databases.has(dbName)) {
|
|
419
|
-
this.databases.delete(dbName);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
try { fs.unlinkSync(dbPath); } catch (e) { }
|
|
423
|
-
|
|
424
|
-
if (context.currentDatabase === dbName) {
|
|
425
|
-
context.setDatabase(null);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
return this._sendResponse(socket, {
|
|
429
|
-
type: 'query_result',
|
|
430
|
-
result: `Wilayah '${dbName}' telah hangus terbakar.`,
|
|
431
|
-
query,
|
|
432
|
-
executionTime: Date.now() - startTime
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
} catch (err) {
|
|
436
|
-
return this._sendError(socket, `Gagal membakar wilayah: ${err.message}`);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// --- Generic Syntax Aliases (Server Level) ---
|
|
441
|
-
|
|
442
|
-
// 5. CREATE DATABASE [name] -> BUKA WILAYAH
|
|
443
|
-
if (qUpper.startsWith('CREATE DATABASE')) {
|
|
444
|
-
const parts = query.trim().split(/\s+/);
|
|
445
|
-
if (parts.length < 3) return this._sendError(socket, 'Syntax: CREATE DATABASE [name]');
|
|
446
|
-
// Recruit BUKA WILAYAH logic
|
|
447
|
-
const newQuery = `BUKA WILAYAH ${parts[2]}`;
|
|
448
|
-
return this._handleQuery(socket, { ...payload, query: newQuery }, context);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// 6. USE [name] -> MASUK WILAYAH
|
|
452
|
-
if (qUpper.startsWith('USE ')) {
|
|
453
|
-
const parts = query.trim().split(/\s+/);
|
|
454
|
-
if (parts.length < 2) return this._sendError(socket, 'Syntax: USE [name]');
|
|
455
|
-
// Recruit MASUK WILAYAH logic
|
|
456
|
-
const newQuery = `MASUK WILAYAH ${parts[1]}`;
|
|
457
|
-
return this._handleQuery(socket, { ...payload, query: newQuery }, context);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// 7. SHOW DATABASES -> LIHAT WILAYAH
|
|
461
|
-
if (qUpper === 'SHOW DATABASES') {
|
|
462
|
-
const newQuery = `LIHAT WILAYAH`;
|
|
463
|
-
return this._handleQuery(socket, { ...payload, query: newQuery }, context);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// 8. DROP DATABASE [name] -> BAKAR WILAYAH
|
|
467
|
-
if (qUpper.startsWith('DROP DATABASE')) {
|
|
468
|
-
const parts = query.trim().split(/\s+/);
|
|
469
|
-
if (parts.length < 3) return this._sendError(socket, 'Syntax: DROP DATABASE [name]');
|
|
470
|
-
// Recruit BAKAR WILAYAH logic
|
|
471
|
-
const newQuery = `BAKAR WILAYAH ${parts[2]}`;
|
|
472
|
-
return this._handleQuery(socket, { ...payload, query: newQuery }, context);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// --- End Intercept ---
|
|
476
|
-
|
|
477
|
-
if (!context.currentDatabase) {
|
|
478
|
-
return this._sendError(socket, 'Anda belum masuk wilayah manapun. Gunakan: MASUK WILAYAH [nama]');
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
try {
|
|
482
|
-
const db = this._getOrCreateDatabase(context.currentDatabase);
|
|
483
|
-
const result = db.query(query, params);
|
|
484
|
-
const duration = Date.now() - startTime;
|
|
485
|
-
|
|
486
|
-
this.stats.totalQueries++;
|
|
487
|
-
this._log('debug', `Query executed in ${duration}ms: ${query.substring(0, 50)}...`);
|
|
488
|
-
|
|
489
|
-
this._sendResponse(socket, {
|
|
490
|
-
type: 'query_result',
|
|
491
|
-
result,
|
|
492
|
-
query,
|
|
493
|
-
executionTime: duration
|
|
494
|
-
});
|
|
495
|
-
} catch (err) {
|
|
496
|
-
this._log('error', `Query failed: ${err.message} - Query: ${query}`);
|
|
497
|
-
this.stats.errors++;
|
|
498
|
-
this._sendError(socket, `Query error: ${err.message}`);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
_handleListDatabases(socket) {
|
|
503
|
-
try {
|
|
504
|
-
const databases = fs.readdirSync(this.dataDir)
|
|
505
|
-
.filter(file => file.endsWith('.sawit'))
|
|
506
|
-
.map(file => file.replace('.sawit', ''));
|
|
507
|
-
|
|
508
|
-
this._sendResponse(socket, {
|
|
509
|
-
type: 'database_list',
|
|
510
|
-
databases,
|
|
511
|
-
count: databases.length
|
|
512
|
-
});
|
|
513
|
-
} catch (err) {
|
|
514
|
-
this._sendError(socket, `Failed to list databases: ${err.message}`);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
_handleDropDatabase(socket, payload, context) {
|
|
519
|
-
const { database } = payload;
|
|
520
|
-
|
|
521
|
-
if (!database) {
|
|
522
|
-
return this._sendError(socket, 'Database name required');
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
try {
|
|
526
|
-
const dbPath = path.join(this.dataDir, `${database}.sawit`);
|
|
527
|
-
|
|
528
|
-
if (!fs.existsSync(dbPath)) {
|
|
529
|
-
return this._sendError(socket, `Database '${database}' does not exist`);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Close database if open
|
|
533
|
-
if (this.databases.has(database)) {
|
|
534
|
-
this.databases.delete(database);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// Delete file
|
|
538
|
-
fs.unlinkSync(dbPath);
|
|
539
|
-
|
|
540
|
-
// Clear current database if it was dropped
|
|
541
|
-
if (context.currentDatabase === database) {
|
|
542
|
-
context.setDatabase(null);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
this._sendResponse(socket, {
|
|
546
|
-
type: 'drop_success',
|
|
547
|
-
database,
|
|
548
|
-
message: `Database '${database}' has been burned (dropped)`
|
|
549
|
-
});
|
|
550
|
-
} catch (err) {
|
|
551
|
-
this._sendError(socket, `Failed to drop database: ${err.message}`);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
_getOrCreateDatabase(name) {
|
|
556
|
-
if (!this.databases.has(name)) {
|
|
557
|
-
const dbPath = path.join(this.dataDir, `${name}.sawit`);
|
|
558
|
-
const db = new SawitDB(dbPath);
|
|
559
|
-
this.databases.set(name, db);
|
|
560
|
-
}
|
|
561
|
-
return this.databases.get(name);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
_sendResponse(socket, data) {
|
|
565
|
-
try {
|
|
566
|
-
socket.write(JSON.stringify(data) + '\n');
|
|
567
|
-
} catch (err) {
|
|
568
|
-
console.error('[Server] Failed to send response:', err.message);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
_sendError(socket, message) {
|
|
573
|
-
this._sendResponse(socket, { type: 'error', error: message });
|
|
574
|
-
}
|
|
575
222
|
}
|
|
576
223
|
|
|
577
224
|
module.exports = SawitServer;
|
|
578
225
|
|
|
579
226
|
// Allow running as standalone server
|
|
580
227
|
if (require.main === module) {
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
host: process.env.SAWIT_HOST || '0.0.0.0',
|
|
584
|
-
dataDir: process.env.SAWIT_DATA_DIR || path.join(__dirname, '../data'),
|
|
585
|
-
// Optional auth: { username: 'petani', password: 'sawit123' }
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
server.start();
|
|
589
|
-
|
|
590
|
-
// Graceful shutdown
|
|
591
|
-
process.on('SIGINT', () => {
|
|
592
|
-
console.log('\n[Server] Received SIGINT, shutting down gracefully...');
|
|
593
|
-
server.stop();
|
|
594
|
-
process.exit(0);
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
process.on('SIGTERM', () => {
|
|
598
|
-
console.log('\n[Server] Received SIGTERM, shutting down gracefully...');
|
|
599
|
-
server.stop();
|
|
600
|
-
process.exit(0);
|
|
601
|
-
});
|
|
228
|
+
const ClusterManager = require('./modules/ClusterManager');
|
|
229
|
+
ClusterManager.start(SawitServer);
|
|
602
230
|
}
|