@wowoengine/sawitdb 2.4.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 +245 -0
- package/bin/sawit-server.js +68 -0
- package/cli/local.js +49 -0
- package/cli/remote.js +165 -0
- package/docs/index.html +727 -0
- package/docs/sawitdb.jpg +0 -0
- package/package.json +63 -0
- package/src/SawitClient.js +265 -0
- package/src/SawitServer.js +602 -0
- package/src/WowoEngine.js +539 -0
- package/src/modules/BTreeIndex.js +282 -0
- package/src/modules/Pager.js +70 -0
- package/src/modules/QueryParser.js +569 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
const net = require('net');
|
|
2
|
+
const SawitDB = require('./WowoEngine');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SawitDB Server - Network Database Server
|
|
8
|
+
* Supports sawitdb:// protocol connections
|
|
9
|
+
*/
|
|
10
|
+
class SawitServer {
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
// Validate and set configuration
|
|
13
|
+
this.port = this._validatePort(config.port || 7878);
|
|
14
|
+
this.host = config.host || '0.0.0.0';
|
|
15
|
+
this.dataDir = config.dataDir || path.join(__dirname, '../data');
|
|
16
|
+
this.databases = new Map(); // Map of database name -> SawitDB instance
|
|
17
|
+
this.clients = new Set();
|
|
18
|
+
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
|
+
this.stats = {
|
|
24
|
+
totalConnections: 0,
|
|
25
|
+
activeConnections: 0,
|
|
26
|
+
totalQueries: 0,
|
|
27
|
+
errors: 0,
|
|
28
|
+
startTime: Date.now()
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Ensure data directory exists
|
|
32
|
+
if (!fs.existsSync(this.dataDir)) {
|
|
33
|
+
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this._log('info', `Data directory: ${this.dataDir}`);
|
|
37
|
+
this._log('info', `Max connections: ${this.maxConnections}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_validatePort(port) {
|
|
41
|
+
const p = parseInt(port);
|
|
42
|
+
if (isNaN(p) || p < 1 || p > 65535) {
|
|
43
|
+
throw new Error(`Invalid port: ${port}. Must be between 1-65535`);
|
|
44
|
+
}
|
|
45
|
+
return p;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_log(level, message, data = null) {
|
|
49
|
+
const levels = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
50
|
+
const currentLevel = levels[this.logLevel] || 1;
|
|
51
|
+
const msgLevel = levels[level] || 1;
|
|
52
|
+
|
|
53
|
+
if (msgLevel >= currentLevel) {
|
|
54
|
+
const timestamp = new Date().toISOString();
|
|
55
|
+
const prefix = level.toUpperCase().padEnd(5);
|
|
56
|
+
console.log(`[${timestamp}] [${prefix}] ${message}`);
|
|
57
|
+
if (data && this.logLevel === 'debug') {
|
|
58
|
+
console.log(JSON.stringify(data, null, 2));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
start() {
|
|
64
|
+
this.server = net.createServer((socket) => this._handleConnection(socket));
|
|
65
|
+
|
|
66
|
+
this.server.listen(this.port, this.host, () => {
|
|
67
|
+
console.log(`╔══════════════════════════════════════════════════╗`);
|
|
68
|
+
console.log(`║ 🌴 SawitDB Server - Version 2.4 ║`);
|
|
69
|
+
console.log(`╚══════════════════════════════════════════════════╝`);
|
|
70
|
+
console.log(`[Server] Listening on ${this.host}:${this.port}`);
|
|
71
|
+
console.log(`[Server] Protocol: sawitdb://${this.host}:${this.port}/[database]`);
|
|
72
|
+
console.log(`[Server] Ready to accept connections...`);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
this.server.on('error', (err) => {
|
|
76
|
+
console.error('[Server] Error:', err.message);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
stop() {
|
|
81
|
+
console.log('[Server] Shutting down...');
|
|
82
|
+
|
|
83
|
+
// Close all client connections
|
|
84
|
+
for (const client of this.clients) {
|
|
85
|
+
client.destroy();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Close server
|
|
89
|
+
if (this.server) {
|
|
90
|
+
this.server.close(() => {
|
|
91
|
+
console.log('[Server] Server stopped.');
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_handleConnection(socket) {
|
|
97
|
+
const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
98
|
+
|
|
99
|
+
// Check connection limit
|
|
100
|
+
if (this.clients.size >= this.maxConnections) {
|
|
101
|
+
this._log('warn', `Connection limit reached. Rejecting ${clientId}`);
|
|
102
|
+
socket.write(JSON.stringify({
|
|
103
|
+
type: 'error',
|
|
104
|
+
error: 'Server connection limit reached. Please try again later.'
|
|
105
|
+
}) + '\n');
|
|
106
|
+
socket.end();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this._log('info', `Client connected: ${clientId}`);
|
|
111
|
+
this.stats.totalConnections++;
|
|
112
|
+
this.stats.activeConnections++;
|
|
113
|
+
|
|
114
|
+
this.clients.add(socket);
|
|
115
|
+
socket.clientId = clientId;
|
|
116
|
+
socket.connectedAt = Date.now();
|
|
117
|
+
|
|
118
|
+
let authenticated = !this.auth; // If no auth required, auto-authenticate
|
|
119
|
+
let currentDatabase = null;
|
|
120
|
+
let buffer = '';
|
|
121
|
+
|
|
122
|
+
socket.on('data', (data) => {
|
|
123
|
+
buffer += data.toString();
|
|
124
|
+
|
|
125
|
+
// Prevent buffer overflow attacks
|
|
126
|
+
if (buffer.length > 1048576) { // 1MB limit
|
|
127
|
+
this._log('warn', `Buffer overflow attempt from ${clientId}`);
|
|
128
|
+
this._sendError(socket, 'Request too large');
|
|
129
|
+
socket.destroy();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Process complete messages (delimited by newline)
|
|
134
|
+
let newlineIndex;
|
|
135
|
+
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
|
|
136
|
+
const message = buffer.substring(0, newlineIndex);
|
|
137
|
+
buffer = buffer.substring(newlineIndex + 1);
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const request = JSON.parse(message);
|
|
141
|
+
this._log('debug', `Request from ${clientId}`, request);
|
|
142
|
+
|
|
143
|
+
// Handle with timeout
|
|
144
|
+
this._handleRequest(socket, request, {
|
|
145
|
+
authenticated,
|
|
146
|
+
currentDatabase,
|
|
147
|
+
setAuth: (val) => { authenticated = val; },
|
|
148
|
+
setDatabase: (db) => { currentDatabase = db; }
|
|
149
|
+
});
|
|
150
|
+
} catch (err) {
|
|
151
|
+
this._log('error', `Invalid request from ${clientId}: ${err.message}`);
|
|
152
|
+
this.stats.errors++;
|
|
153
|
+
this._sendError(socket, `Invalid request format: ${err.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
socket.on('end', () => {
|
|
159
|
+
const duration = Date.now() - (socket.connectedAt || Date.now());
|
|
160
|
+
this._log('info', `Client disconnected: ${clientId} (duration: ${duration}ms)`);
|
|
161
|
+
this.clients.delete(socket);
|
|
162
|
+
this.stats.activeConnections--;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
socket.on('error', (err) => {
|
|
166
|
+
this._log('error', `Client error: ${clientId} - ${err.message}`);
|
|
167
|
+
this.clients.delete(socket);
|
|
168
|
+
this.stats.activeConnections--;
|
|
169
|
+
this.stats.errors++;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
socket.on('timeout', () => {
|
|
173
|
+
this._log('warn', `Client timeout: ${clientId}`);
|
|
174
|
+
socket.destroy();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Set socket timeout
|
|
178
|
+
socket.setTimeout(this.queryTimeout);
|
|
179
|
+
|
|
180
|
+
// Send welcome message
|
|
181
|
+
this._sendResponse(socket, {
|
|
182
|
+
type: 'welcome',
|
|
183
|
+
message: 'SawitDB Server',
|
|
184
|
+
version: '2.4',
|
|
185
|
+
protocol: 'sawitdb'
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_handleRequest(socket, request, context) {
|
|
190
|
+
const { type, payload } = request;
|
|
191
|
+
|
|
192
|
+
// Authentication check
|
|
193
|
+
if (this.auth && !context.authenticated && type !== 'auth') {
|
|
194
|
+
return this._sendError(socket, 'Authentication required');
|
|
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}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_handleStats(socket) {
|
|
232
|
+
const uptime = Date.now() - this.stats.startTime;
|
|
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
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
_formatUptime(ms) {
|
|
248
|
+
const seconds = Math.floor(ms / 1000);
|
|
249
|
+
const minutes = Math.floor(seconds / 60);
|
|
250
|
+
const hours = Math.floor(minutes / 60);
|
|
251
|
+
const days = Math.floor(hours / 24);
|
|
252
|
+
|
|
253
|
+
if (days > 0) return `${days}d ${hours % 24}h`;
|
|
254
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
255
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
|
256
|
+
return `${seconds}s`;
|
|
257
|
+
}
|
|
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
|
+
}
|
|
576
|
+
|
|
577
|
+
module.exports = SawitServer;
|
|
578
|
+
|
|
579
|
+
// Allow running as standalone server
|
|
580
|
+
if (require.main === module) {
|
|
581
|
+
const server = new SawitServer({
|
|
582
|
+
port: process.env.SAWIT_PORT || 7878,
|
|
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
|
+
});
|
|
602
|
+
}
|