@joystick.js/db-canary 0.0.0-canary.2242 → 0.0.0-canary.2244
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 +13 -0
- package/dist/client/index.js +1 -1
- package/dist/server/cluster/worker.js +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/lib/development_mode.js +1 -1
- package/dist/server/lib/op_types.js +1 -1
- package/dist/server/lib/operation_dispatcher.js +1 -1
- package/dist/server/lib/operations/delete_many.js +1 -0
- package/package.json +2 -2
- package/src/client/index.js +33 -0
- package/src/server/cluster/worker.js +6 -5
- package/src/server/index.js +1 -0
- package/src/server/lib/development_mode.js +2 -2
- package/src/server/lib/op_types.js +1 -0
- package/src/server/lib/operation_dispatcher.js +5 -0
- package/src/server/lib/operations/delete_many.js +130 -0
- package/test_data_api_key_1758220165907_u8d8j2z37/data.mdb +0 -0
- package/test_data_api_key_1758220165907_u8d8j2z37/lock.mdb +0 -0
- package/test_data_api_key_1758220166138_q079fkt44/data.mdb +0 -0
- package/test_data_api_key_1758220166138_q079fkt44/lock.mdb +0 -0
- package/test_data_api_key_1758220166370_elaxmwvuj/data.mdb +0 -0
- package/test_data_api_key_1758220166370_elaxmwvuj/lock.mdb +0 -0
- package/test_data_api_key_1758220166487_689q46e05/data.mdb +0 -0
- package/test_data_api_key_1758220166487_689q46e05/lock.mdb +0 -0
- package/test_data_api_key_1758220174956_9lhw6u6la/data.mdb +0 -0
- package/test_data_api_key_1758220174956_9lhw6u6la/lock.mdb +0 -0
- package/test_data_api_key_1758220175070_1hruzftqf/data.mdb +0 -0
- package/test_data_api_key_1758220175070_1hruzftqf/lock.mdb +0 -0
- package/test_data_api_key_1758220175183_ean83s4od/data.mdb +0 -0
- package/test_data_api_key_1758220175183_ean83s4od/lock.mdb +0 -0
- package/test_data_api_key_1758220736169_8xv9swdwn/data.mdb +0 -0
- package/test_data_api_key_1758220736169_8xv9swdwn/lock.mdb +0 -0
- package/test_data_api_key_1758220736405_tzltnwwf5/data.mdb +0 -0
- package/test_data_api_key_1758220736405_tzltnwwf5/lock.mdb +0 -0
- package/test_data_api_key_1758220736640_avs9xxx7j/data.mdb +0 -0
- package/test_data_api_key_1758220736640_avs9xxx7j/lock.mdb +0 -0
- package/test_data_api_key_1758220736757_c4hhzan82/data.mdb +0 -0
- package/test_data_api_key_1758220736757_c4hhzan82/lock.mdb +0 -0
- package/test_data_api_key_1758220744324_89rzc4cak/data.mdb +0 -0
- package/test_data_api_key_1758220744324_89rzc4cak/lock.mdb +0 -0
- package/test_data_api_key_1758220744436_y2244449u/data.mdb +0 -0
- package/test_data_api_key_1758220744436_y2244449u/lock.mdb +0 -0
- package/test_data_api_key_1758220744548_968u1bkrk/data.mdb +0 -0
- package/test_data_api_key_1758220744548_968u1bkrk/lock.mdb +0 -0
- package/tests/client/index.test.js +393 -0
- package/tests/server/cluster/master_read_write_operations.test.js +7 -6
- package/tests/server/lib/operations/delete_many.test.js +263 -0
- package/types/client/index.d.ts +17 -0
package/README.md
CHANGED
|
@@ -518,6 +518,18 @@ await products.delete_one({
|
|
|
518
518
|
stock: { $lte: 0 },
|
|
519
519
|
discontinued: true
|
|
520
520
|
});
|
|
521
|
+
|
|
522
|
+
// Delete multiple documents at once
|
|
523
|
+
await users.delete_many({
|
|
524
|
+
status: 'inactive',
|
|
525
|
+
last_login: { $lt: new Date(Date.now() - 365*24*60*60*1000) }
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
await products.delete_many({
|
|
529
|
+
stock: { $lte: 0 }
|
|
530
|
+
}, {
|
|
531
|
+
limit: 100 // Optional: limit number of deletions
|
|
532
|
+
});
|
|
521
533
|
```
|
|
522
534
|
|
|
523
535
|
|
|
@@ -1649,6 +1661,7 @@ const monitor_performance = async () => {
|
|
|
1649
1661
|
- `collection.find(filter, options)` - Find multiple documents
|
|
1650
1662
|
- `collection.update_one(filter, update, options)` - Update single document
|
|
1651
1663
|
- `collection.delete_one(filter, options)` - Delete single document
|
|
1664
|
+
- `collection.delete_many(filter, options)` - Delete multiple documents
|
|
1652
1665
|
- `collection.bulk_write(operations, options)` - Bulk operations
|
|
1653
1666
|
|
|
1654
1667
|
#### Index Management (Collection Chaining API)
|
package/dist/client/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import u from"net";import{EventEmitter as h}from"events";import{encode as d,decode as l}from"msgpackr";import m from"./database.js";const p=n=>{const e=d(n,{useFloat32:!1,int64AsType:"number",mapsAsObjects:!0}),t=Buffer.allocUnsafe(4);return t.writeUInt32BE(e.length,0),Buffer.concat([t,e])},f=()=>{let n=Buffer.alloc(0),e=null;return{parse_messages:i=>{n=Buffer.concat([n,i]);const r=[];for(;n.length>0;){if(e===null){if(n.length<4)break;e=n.readUInt32BE(0),n=n.slice(4)}if(n.length<e)break;const a=n.slice(0,e);n=n.slice(e),e=null;try{const c=l(a,{useFloat32:!1,int64AsType:"number",mapsAsObjects:!0});r.push(c)}catch(c){throw new Error(`Invalid message format: ${c.message}`)}}return r},reset:()=>{n=Buffer.alloc(0),e=null}}};class _ extends h{constructor(e={}){super(),this.host=e.host||"localhost",this.port=e.port||1983,this.password=e.password||null,this.timeout=e.timeout||5e3,this.reconnect=e.reconnect!==!1,this.max_reconnect_attempts=e.max_reconnect_attempts||10,this.reconnect_delay=e.reconnect_delay||1e3,this.socket=null,this.message_parser=null,this.is_connected=!1,this.is_authenticated=!1,this.is_connecting=!1,this.reconnect_attempts=0,this.reconnect_timeout=null,this.pending_requests=new Map,this.request_id_counter=0,this.request_queue=[],e.auto_connect!==!1&&this.connect()}connect(){if(this.is_connecting||this.is_connected)return;this.is_connecting=!0,this.socket=new u.Socket,this.message_parser=f();const e=setTimeout(()=>{this.socket&&!this.is_connected&&(this.socket.destroy(),this.handle_connection_error(new Error("Connection timeout")))},this.timeout);this.socket.connect(this.port,this.host,()=>{clearTimeout(e),this.is_connected=!0,this.is_connecting=!1,this.reconnect_attempts=0,this.emit("connect"),this.password?this.authenticate():(this.is_authenticated=!0,this.emit("authenticated"),this.process_request_queue())}),this.socket.on("data",t=>{try{const s=this.message_parser.parse_messages(t);for(const i of s)this.handle_message(i)}catch(s){this.emit("error",new Error(`Message parsing failed: ${s.message}`))}}),this.socket.on("error",t=>{clearTimeout(e),this.handle_connection_error(t)}),this.socket.on("close",()=>{clearTimeout(e),this.handle_disconnect()})}async authenticate(){if(!this.password){this.emit("error",new Error('Password required for authentication. Provide password in client options: joystickdb.client({ password: "your_password" })')),this.disconnect();return}try{if((await this.send_request("authentication",{password:this.password})).ok===1)this.is_authenticated=!0,this.emit("authenticated"),this.process_request_queue();else throw new Error("Authentication failed")}catch(e){this.emit("error",new Error(`Authentication error: ${e.message}`)),this.disconnect()}}handle_message(e){if(this.pending_requests.size>0){const[t,{resolve:s,reject:i,timeout:r}]=this.pending_requests.entries().next().value;if(clearTimeout(r),this.pending_requests.delete(t),e.ok===1||e.ok===!0)s(e);else if(e.ok===0||e.ok===!1){const a=typeof e.error=="string"?e.error:JSON.stringify(e.error)||"Operation failed";i(new Error(a))}else s(e)}else this.emit("response",e)}handle_connection_error(e){this.is_connecting=!1,this.is_connected=!1,this.is_authenticated=!1,this.socket&&(this.socket.removeAllListeners(),this.socket.destroy(),this.socket=null),this.message_parser&&this.message_parser.reset();for(const[t,{reject:s,timeout:i}]of this.pending_requests)clearTimeout(i),s(new Error("Connection lost"));this.pending_requests.clear(),this.emit("error",e),this.reconnect&&this.reconnect_attempts<this.max_reconnect_attempts?this.schedule_reconnect():this.emit("disconnect")}handle_disconnect(){this.is_connected=!1,this.is_authenticated=!1,this.is_connecting=!1,this.socket&&(this.socket.removeAllListeners(),this.socket=null),this.message_parser&&this.message_parser.reset();for(const[e,{reject:t,timeout:s}]of this.pending_requests)clearTimeout(s),t(new Error("Connection closed"));this.pending_requests.clear(),this.reconnect&&this.reconnect_attempts<this.max_reconnect_attempts?this.schedule_reconnect():this.emit("disconnect")}schedule_reconnect(){this.reconnect_attempts++;const e=Math.min(this.reconnect_delay*Math.pow(2,this.reconnect_attempts-1),3e4);this.emit("reconnecting",{attempt:this.reconnect_attempts,delay:e}),this.reconnect_timeout=setTimeout(()=>{this.connect()},e)}send_request(e,t={},s=!0){return new Promise((i,r)=>{const a=++this.request_id_counter,o={message:{op:e,data:t},resolve:i,reject:r,request_id:a};if(!this.is_connected||e!=="authentication"&&e!=="setup"&&e!=="ping"&&!this.is_authenticated)if(s){this.request_queue.push(o);return}else{r(new Error("Not connected or authenticated"));return}this.send_request_now(o)})}send_request_now(e){const{message:t,resolve:s,reject:i,request_id:r}=e,a=setTimeout(()=>{this.pending_requests.delete(r),i(new Error("Request timeout"))},this.timeout);this.pending_requests.set(r,{resolve:s,reject:i,timeout:a});try{const c=p(t);this.socket.write(c)}catch(c){clearTimeout(a),this.pending_requests.delete(r),i(c)}}process_request_queue(){for(;this.request_queue.length>0&&this.is_connected&&this.is_authenticated;){const e=this.request_queue.shift();this.send_request_now(e)}}disconnect(){this.reconnect=!1,this.reconnect_timeout&&(clearTimeout(this.reconnect_timeout),this.reconnect_timeout=null),this.socket&&this.socket.end()}async backup_now(){return this.send_request("admin",{admin_action:"backup_now"})}async list_backups(){return this.send_request("admin",{admin_action:"list_backups"})}async restore_backup(e){return this.send_request("admin",{admin_action:"restore_backup",backup_name:e})}async get_replication_status(){return this.send_request("admin",{admin_action:"get_replication_status"})}async add_secondary(e){return this.send_request("admin",{admin_action:"add_secondary",...e})}async remove_secondary(e){return this.send_request("admin",{admin_action:"remove_secondary",secondary_id:e})}async sync_secondaries(){return this.send_request("admin",{admin_action:"sync_secondaries"})}async get_secondary_health(){return this.send_request("admin",{admin_action:"get_secondary_health"})}async get_forwarder_status(){return this.send_request("admin",{admin_action:"get_forwarder_status"})}async ping(){return this.send_request("ping",{},!1)}async reload(){return this.send_request("reload")}async get_auto_index_stats(){return this.send_request("admin",{admin_action:"get_auto_index_stats"})}async setup(){const e=await this.send_request("setup",{},!1);return e.data&&e.data.instructions&&console.log(e.data.instructions),e}db(e){return new m(this,e)}async list_databases(){return this.send_request("admin",{admin_action:"list_databases"})}}class
|
|
1
|
+
import u from"net";import{EventEmitter as h}from"events";import{encode as d,decode as l}from"msgpackr";import m from"./database.js";const p=n=>{const e=d(n,{useFloat32:!1,int64AsType:"number",mapsAsObjects:!0}),t=Buffer.allocUnsafe(4);return t.writeUInt32BE(e.length,0),Buffer.concat([t,e])},f=()=>{let n=Buffer.alloc(0),e=null;return{parse_messages:i=>{n=Buffer.concat([n,i]);const r=[];for(;n.length>0;){if(e===null){if(n.length<4)break;e=n.readUInt32BE(0),n=n.slice(4)}if(n.length<e)break;const a=n.slice(0,e);n=n.slice(e),e=null;try{const c=l(a,{useFloat32:!1,int64AsType:"number",mapsAsObjects:!0});r.push(c)}catch(c){throw new Error(`Invalid message format: ${c.message}`)}}return r},reset:()=>{n=Buffer.alloc(0),e=null}}};class _ extends h{constructor(e={}){super(),this.host=e.host||"localhost",this.port=e.port||1983,this.password=e.password||null,this.timeout=e.timeout||5e3,this.reconnect=e.reconnect!==!1,this.max_reconnect_attempts=e.max_reconnect_attempts||10,this.reconnect_delay=e.reconnect_delay||1e3,this.socket=null,this.message_parser=null,this.is_connected=!1,this.is_authenticated=!1,this.is_connecting=!1,this.reconnect_attempts=0,this.reconnect_timeout=null,this.pending_requests=new Map,this.request_id_counter=0,this.request_queue=[],e.auto_connect!==!1&&this.connect()}connect(){if(this.is_connecting||this.is_connected)return;this.is_connecting=!0,this.socket=new u.Socket,this.message_parser=f();const e=setTimeout(()=>{this.socket&&!this.is_connected&&(this.socket.destroy(),this.handle_connection_error(new Error("Connection timeout")))},this.timeout);this.socket.connect(this.port,this.host,()=>{clearTimeout(e),this.is_connected=!0,this.is_connecting=!1,this.reconnect_attempts=0,this.emit("connect"),this.password?this.authenticate():(this.is_authenticated=!0,this.emit("authenticated"),this.process_request_queue())}),this.socket.on("data",t=>{try{const s=this.message_parser.parse_messages(t);for(const i of s)this.handle_message(i)}catch(s){this.emit("error",new Error(`Message parsing failed: ${s.message}`))}}),this.socket.on("error",t=>{clearTimeout(e),this.handle_connection_error(t)}),this.socket.on("close",()=>{clearTimeout(e),this.handle_disconnect()})}async authenticate(){if(!this.password){this.emit("error",new Error('Password required for authentication. Provide password in client options: joystickdb.client({ password: "your_password" })')),this.disconnect();return}try{if((await this.send_request("authentication",{password:this.password})).ok===1)this.is_authenticated=!0,this.emit("authenticated"),this.process_request_queue();else throw new Error("Authentication failed")}catch(e){this.emit("error",new Error(`Authentication error: ${e.message}`)),this.disconnect()}}handle_message(e){if(this.pending_requests.size>0){const[t,{resolve:s,reject:i,timeout:r}]=this.pending_requests.entries().next().value;if(clearTimeout(r),this.pending_requests.delete(t),e.ok===1||e.ok===!0)s(e);else if(e.ok===0||e.ok===!1){const a=typeof e.error=="string"?e.error:JSON.stringify(e.error)||"Operation failed";i(new Error(a))}else s(e)}else this.emit("response",e)}handle_connection_error(e){this.is_connecting=!1,this.is_connected=!1,this.is_authenticated=!1,this.socket&&(this.socket.removeAllListeners(),this.socket.destroy(),this.socket=null),this.message_parser&&this.message_parser.reset();for(const[t,{reject:s,timeout:i}]of this.pending_requests)clearTimeout(i),s(new Error("Connection lost"));this.pending_requests.clear(),this.emit("error",e),this.reconnect&&this.reconnect_attempts<this.max_reconnect_attempts?this.schedule_reconnect():this.emit("disconnect")}handle_disconnect(){this.is_connected=!1,this.is_authenticated=!1,this.is_connecting=!1,this.socket&&(this.socket.removeAllListeners(),this.socket=null),this.message_parser&&this.message_parser.reset();for(const[e,{reject:t,timeout:s}]of this.pending_requests)clearTimeout(s),t(new Error("Connection closed"));this.pending_requests.clear(),this.reconnect&&this.reconnect_attempts<this.max_reconnect_attempts?this.schedule_reconnect():this.emit("disconnect")}schedule_reconnect(){this.reconnect_attempts++;const e=Math.min(this.reconnect_delay*Math.pow(2,this.reconnect_attempts-1),3e4);this.emit("reconnecting",{attempt:this.reconnect_attempts,delay:e}),this.reconnect_timeout=setTimeout(()=>{this.connect()},e)}send_request(e,t={},s=!0){return new Promise((i,r)=>{const a=++this.request_id_counter,o={message:{op:e,data:t},resolve:i,reject:r,request_id:a};if(!this.is_connected||e!=="authentication"&&e!=="setup"&&e!=="ping"&&!this.is_authenticated)if(s){this.request_queue.push(o);return}else{r(new Error("Not connected or authenticated"));return}this.send_request_now(o)})}send_request_now(e){const{message:t,resolve:s,reject:i,request_id:r}=e,a=setTimeout(()=>{this.pending_requests.delete(r),i(new Error("Request timeout"))},this.timeout);this.pending_requests.set(r,{resolve:s,reject:i,timeout:a});try{const c=p(t);this.socket.write(c)}catch(c){clearTimeout(a),this.pending_requests.delete(r),i(c)}}process_request_queue(){for(;this.request_queue.length>0&&this.is_connected&&this.is_authenticated;){const e=this.request_queue.shift();this.send_request_now(e)}}disconnect(){this.reconnect=!1,this.reconnect_timeout&&(clearTimeout(this.reconnect_timeout),this.reconnect_timeout=null),this.socket&&this.socket.end()}async backup_now(){return this.send_request("admin",{admin_action:"backup_now"})}async list_backups(){return this.send_request("admin",{admin_action:"list_backups"})}async restore_backup(e){return this.send_request("admin",{admin_action:"restore_backup",backup_name:e})}async get_replication_status(){return this.send_request("admin",{admin_action:"get_replication_status"})}async add_secondary(e){return this.send_request("admin",{admin_action:"add_secondary",...e})}async remove_secondary(e){return this.send_request("admin",{admin_action:"remove_secondary",secondary_id:e})}async sync_secondaries(){return this.send_request("admin",{admin_action:"sync_secondaries"})}async get_secondary_health(){return this.send_request("admin",{admin_action:"get_secondary_health"})}async get_forwarder_status(){return this.send_request("admin",{admin_action:"get_forwarder_status"})}async ping(){return this.send_request("ping",{},!1)}async reload(){return this.send_request("reload")}async get_auto_index_stats(){return this.send_request("admin",{admin_action:"get_auto_index_stats"})}async setup(){const e=await this.send_request("setup",{},!1);return e.data&&e.data.instructions&&console.log(e.data.instructions),e}async delete_many(e,t={},s={}){return this.send_request("delete_many",{collection:e,filter:t,options:s})}db(e){return new m(this,e)}async list_databases(){return this.send_request("admin",{admin_action:"list_databases"})}}class q{constructor(e,t,s){this.client=e,this.database_name=t,this.collection_name=s}async insert_one(e,t={}){return this.client.send_request("insert_one",{database:this.database_name,collection:this.collection_name,document:e,options:t})}async find_one(e={},t={}){return(await this.client.send_request("find_one",{database:this.database_name,collection:this.collection_name,filter:e,options:t})).document}async find(e={},t={}){return(await this.client.send_request("find",{database:this.database_name,collection:this.collection_name,filter:e,options:t})).documents||[]}async update_one(e,t,s={}){return this.client.send_request("update_one",{database:this.database_name,collection:this.collection_name,filter:e,update:t,options:s})}async delete_one(e,t={}){return this.client.send_request("delete_one",{database:this.database_name,collection:this.collection_name,filter:e,options:t})}async delete_many(e={},t={}){return this.client.send_request("delete_many",{database:this.database_name,collection:this.collection_name,filter:e,options:t})}async bulk_write(e,t={}){return this.client.send_request("bulk_write",{database:this.database_name,collection:this.collection_name,operations:e,options:t})}async create_index(e,t={}){return this.client.send_request("create_index",{database:this.database_name,collection:this.collection_name,field:e,options:t})}async upsert_index(e,t={}){return this.client.send_request("create_index",{database:this.database_name,collection:this.collection_name,field:e,options:{...t,upsert:!0}})}async drop_index(e){return this.client.send_request("drop_index",{database:this.database_name,collection:this.collection_name,field:e})}async get_indexes(){return this.client.send_request("get_indexes",{database:this.database_name,collection:this.collection_name})}}_.Collection=q;const g={client:n=>new _(n)};var x=g;export{x as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import p from"net";import u from"../lib/op_types.js";import{send_success as l,send_error as a,send_message as g}from"../lib/send_response.js";import{shutdown_write_queue as w}from"../lib/write_queue.js";import{create_message_parser as m,encode_message as h}from"../lib/tcp_protocol.js";import f from"../lib/logger.js";import{initialize_database as y,cleanup_database as b}from"../lib/query_engine.js";import{handle_admin_operation as v,handle_ping_operation as k}from"../lib/operation_dispatcher.js";import{get_settings as $}from"../lib/load_settings.js";import{is_development_mode as S}from"../lib/development_mode.js";class D{constructor(){this.server=null,this.connections=new Map,this.connection_count=0,this.settings=null,this.port=null,this.write_id_counter=0,this.pending_writes=new Map,this.authenticated_clients=new Set,this.heartbeat_interval=null;const{create_context_logger:e}=f("worker");this.log=e({worker_pid:process.pid}),this.setup_worker()}setup_worker(){process.on("message",e=>{this.handle_master_message(e)}),process.on("SIGTERM",()=>{this.shutdown()}),process.on("SIGINT",()=>{this.shutdown()}),this.send_heartbeat(),this.heartbeat_interval=setInterval(()=>{this.send_heartbeat()},5e3),process.connected&&process.send({type:"worker_ready"})}handle_master_message(e){switch(e.type){case"config":this.handle_config(e);break;case"write_response":this.handle_write_response(e);break;case"auth_response":this.handle_auth_response(e);break;case"setup_response":this.handle_setup_response(e);break;case"write_notification":this.handle_write_notification(e);break;case"shutdown":this.shutdown();break;default:this.log.warn("Unknown message type received from master",{message_type:e.type})}}handle_config(e){const t=e.data.master_id;if(this.master_id&&this.master_id!==t){this.log.info("Worker already configured by different master, ignoring config message",{current_master_id:this.master_id,incoming_master_id:t,current_port:this.port,new_port:e.data.port});return}if(this.port!==null&&this.master_id===t){this.log.info("Worker already configured by same master, ignoring duplicate config message",{master_id:t,current_port:this.port,new_port:e.data.port});return}this.log.info("Received config message",{port:e.data.port,master_id:t}),this.port=e.data.port,this.settings=e.data.settings,this.master_id=t;try{let s="./data";try{const r=$();r?.data_path&&(s=r.data_path)}catch{}y(s),this.log.info("Database initialized in worker process",{database_path:s})}catch(s){this.log.error("Failed to initialize database in worker process",{error:s.message})}this.log.info("Starting server",{port:this.port}),this.start_server()}start_server(){this.server=p.createServer(e=>{this.handle_connection(e)}),this.server.listen(this.port,()=>{this.log.info("Server listening",{port:this.port}),process.connected&&process.send({type:"server_ready"})}),this.server.on("error",e=>{this.log.error("Server error",{error:e.message})})}handle_connection(e){const t=`${process.pid}_${Date.now()}_${Math.random()}`;e.id=t,e.message_parser=m(),this.connections.set(t,e),this.connection_count++,this.update_connection_count(),e.on("data",s=>{this.handle_socket_data(e,s)}),e.on("end",()=>{this.handle_socket_end(e)}),e.on("error",s=>{this.log.error("Socket error",{socket_id:t,error_message:s.message}),this.handle_socket_end(e)})}handle_socket_data(e,t){try{const s=e.message_parser.parse_messages(t);for(const r of s){const o=r,i=o?.op||null;if(!i){a(e,{message:"Missing operation type"});continue}if(!this.check_op_type(i)){a(e,{message:"Invalid operation type"});continue}this.route_operation(e,i,o?.data||{})}}catch(s){this.log.error("Data parsing error",{socket_id:e.id,error_message:s.message}),a(e,{message:"Invalid data format"})}}handle_socket_end(e){e.id&&(this.connections.delete(e.id),this.authenticated_clients.delete(e.id),this.connection_count--,this.update_connection_count()),this.log.info("Client disconnected",{socket_id:e.id})}check_op_type(e=""){return e?u.includes(e):!1}route_operation(e,t,s){switch(t){case"authentication":this.handle_authentication(e,s);break;case"setup":this.handle_setup(e,s);break;case"find_one":case"find":case"get_indexes":this.handle_read_operation(e,t,s);break;case"create_index":case"drop_index":this.handle_write_operation(e,t,s);break;case"insert_one":case"update_one":case"delete_one":case"bulk_write":this.handle_write_operation(e,t,s);break;case"ping":k(e);break;case"admin":v(e,s,this.is_authenticated.bind(this));break;default:a(e,{message:`Unsupported operation: ${t}`})}}handle_authentication(e,t){if(this.is_authenticated(e))g(e,"Already authenticated");else{const s=`${e.id}_${Date.now()}`;process.send({type:"auth_request",data:{auth_id:s,socket_id:e.id,password:t.password}}),this.pending_writes.set(s,{socket:e,type:"auth"})}}handle_setup(e,t){const s=`${e.id}_${Date.now()}`;process.send({type:"setup_request",data:{setup_id:s,socket_id:e.id}}),this.pending_writes.set(s,{socket:e,type:"setup"})}handle_read_operation(e,t,s){if(!this.is_authenticated(e)){a(e,{message:"Authentication required"});return}const r=`${e.id}_${++this.write_id_counter}`;process.send({type:"write_request",data:{write_id:r,op_type:t,data:s,socket_id:e.id}}),this.pending_writes.set(r,{socket:e,type:"read",op_type:t})}handle_write_operation(e,t,s){if(!this.is_authenticated(e)){a(e,{message:"Authentication required"});return}const r=`${e.id}_${++this.write_id_counter}`;process.send({type:"write_request",data:{write_id:r,op_type:t,data:s,socket_id:e.id}}),this.pending_writes.set(r,{socket:e,type:"write",op_type:t})}handle_write_response(e){const{write_id:t,success:s,result:r,error:o}=e.data,i=this.pending_writes.get(t);if(!i){this.log.warn("No pending write found",{write_id:t});return}const{socket:n,op_type:d}=i;if(this.pending_writes.delete(t),n.destroyed||!n.writable){this.log.warn("Socket disconnected before response could be sent",{write_id:t});return}try{if(s){let _;d==="
|
|
1
|
+
import p from"net";import u from"../lib/op_types.js";import{send_success as l,send_error as a,send_message as g}from"../lib/send_response.js";import{shutdown_write_queue as w}from"../lib/write_queue.js";import{create_message_parser as m,encode_message as h}from"../lib/tcp_protocol.js";import f from"../lib/logger.js";import{initialize_database as y,cleanup_database as b}from"../lib/query_engine.js";import{handle_admin_operation as v,handle_ping_operation as k}from"../lib/operation_dispatcher.js";import{get_settings as $}from"../lib/load_settings.js";import{is_development_mode as S}from"../lib/development_mode.js";class D{constructor(){this.server=null,this.connections=new Map,this.connection_count=0,this.settings=null,this.port=null,this.write_id_counter=0,this.pending_writes=new Map,this.authenticated_clients=new Set,this.heartbeat_interval=null;const{create_context_logger:e}=f("worker");this.log=e({worker_pid:process.pid}),this.setup_worker()}setup_worker(){process.on("message",e=>{this.handle_master_message(e)}),process.on("SIGTERM",()=>{this.shutdown()}),process.on("SIGINT",()=>{this.shutdown()}),this.send_heartbeat(),this.heartbeat_interval=setInterval(()=>{this.send_heartbeat()},5e3),process.connected&&process.send({type:"worker_ready"})}handle_master_message(e){switch(e.type){case"config":this.handle_config(e);break;case"write_response":this.handle_write_response(e);break;case"auth_response":this.handle_auth_response(e);break;case"setup_response":this.handle_setup_response(e);break;case"write_notification":this.handle_write_notification(e);break;case"shutdown":this.shutdown();break;default:this.log.warn("Unknown message type received from master",{message_type:e.type})}}handle_config(e){const t=e.data.master_id;if(this.master_id&&this.master_id!==t){this.log.info("Worker already configured by different master, ignoring config message",{current_master_id:this.master_id,incoming_master_id:t,current_port:this.port,new_port:e.data.port});return}if(this.port!==null&&this.master_id===t){this.log.info("Worker already configured by same master, ignoring duplicate config message",{master_id:t,current_port:this.port,new_port:e.data.port});return}this.log.info("Received config message",{port:e.data.port,master_id:t}),this.port=e.data.port,this.settings=e.data.settings,this.master_id=t;try{let s="./data";try{const r=$();r?.data_path&&(s=r.data_path)}catch{}y(s),this.log.info("Database initialized in worker process",{database_path:s})}catch(s){this.log.error("Failed to initialize database in worker process",{error:s.message})}this.log.info("Starting server",{port:this.port}),this.start_server()}start_server(){this.server=p.createServer(e=>{this.handle_connection(e)}),this.server.listen(this.port,()=>{this.log.info("Server listening",{port:this.port}),process.connected&&process.send({type:"server_ready"})}),this.server.on("error",e=>{this.log.error("Server error",{error:e.message})})}handle_connection(e){const t=`${process.pid}_${Date.now()}_${Math.random()}`;e.id=t,e.message_parser=m(),this.connections.set(t,e),this.connection_count++,this.update_connection_count(),e.on("data",s=>{this.handle_socket_data(e,s)}),e.on("end",()=>{this.handle_socket_end(e)}),e.on("error",s=>{this.log.error("Socket error",{socket_id:t,error_message:s.message}),this.handle_socket_end(e)})}handle_socket_data(e,t){try{const s=e.message_parser.parse_messages(t);for(const r of s){const o=r,i=o?.op||null;if(!i){a(e,{message:"Missing operation type"});continue}if(!this.check_op_type(i)){a(e,{message:"Invalid operation type"});continue}this.route_operation(e,i,o?.data||{})}}catch(s){this.log.error("Data parsing error",{socket_id:e.id,error_message:s.message}),a(e,{message:"Invalid data format"})}}handle_socket_end(e){e.id&&(this.connections.delete(e.id),this.authenticated_clients.delete(e.id),this.connection_count--,this.update_connection_count()),this.log.info("Client disconnected",{socket_id:e.id})}check_op_type(e=""){return e?u.includes(e):!1}route_operation(e,t,s){switch(t){case"authentication":this.handle_authentication(e,s);break;case"setup":this.handle_setup(e,s);break;case"find_one":case"find":case"get_indexes":this.handle_read_operation(e,t,s);break;case"create_index":case"drop_index":this.handle_write_operation(e,t,s);break;case"insert_one":case"update_one":case"delete_one":case"bulk_write":this.handle_write_operation(e,t,s);break;case"ping":k(e);break;case"admin":v(e,s,this.is_authenticated.bind(this));break;default:a(e,{message:`Unsupported operation: ${t}`})}}handle_authentication(e,t){if(this.is_authenticated(e))g(e,"Already authenticated");else{const s=`${e.id}_${Date.now()}`;process.send({type:"auth_request",data:{auth_id:s,socket_id:e.id,password:t.password}}),this.pending_writes.set(s,{socket:e,type:"auth"})}}handle_setup(e,t){const s=`${e.id}_${Date.now()}`;process.send({type:"setup_request",data:{setup_id:s,socket_id:e.id}}),this.pending_writes.set(s,{socket:e,type:"setup"})}handle_read_operation(e,t,s){if(!this.is_authenticated(e)){a(e,{message:"Authentication required"});return}const r=`${e.id}_${++this.write_id_counter}`;process.send({type:"write_request",data:{write_id:r,op_type:t,data:s,socket_id:e.id}}),this.pending_writes.set(r,{socket:e,type:"read",op_type:t})}handle_write_operation(e,t,s){if(!this.is_authenticated(e)){a(e,{message:"Authentication required"});return}const r=`${e.id}_${++this.write_id_counter}`;process.send({type:"write_request",data:{write_id:r,op_type:t,data:s,socket_id:e.id}}),this.pending_writes.set(r,{socket:e,type:"write",op_type:t})}handle_write_response(e){const{write_id:t,success:s,result:r,error:o}=e.data,i=this.pending_writes.get(t);if(!i){this.log.warn("No pending write found",{write_id:t});return}const{socket:n,op_type:d}=i;if(this.pending_writes.delete(t),n.destroyed||!n.writable){this.log.warn("Socket disconnected before response could be sent",{write_id:t});return}try{if(s){let _;d==="find_one"?_={ok:1,document:r}:d==="find"?_={ok:1,documents:r}:_={ok:1,...r};const c=h(_);n.write(c)}else{const c=h({ok:0,error:o});n.write(c)}}catch(_){this.log.error("Error sending response to client",{write_id:t,error:_.message})}}handle_auth_response(e){const{auth_id:t,success:s,message:r}=e.data,o=this.pending_writes.get(t);if(!o){this.log.warn("No pending auth found",{auth_id:t});return}const{socket:i}=o;if(this.pending_writes.delete(t),i.destroyed||!i.writable){this.log.warn("Socket disconnected before auth response could be sent",{auth_id:t});return}try{if(s){this.authenticated_clients.add(i.id);const d=h({ok:1,version:"1.0.0",message:r});i.write(d)}else a(i,{message:r}),i.end()}catch(n){this.log.error("Error sending auth response to client",{auth_id:t,error:n.message})}}handle_setup_response(e){const{setup_id:t,success:s,password:r,message:o,error:i}=e.data,n=this.pending_writes.get(t);if(!n){this.log.warn("No pending setup found",{setup_id:t});return}const{socket:d}=n;this.pending_writes.delete(t),s?l(d,{password:r,message:o}):a(d,{message:i})}handle_write_notification(e){this.log.info("Received write notification",{op_type:e.data.op_type,timestamp:e.data.timestamp})}is_authenticated(e){return S()?!0:this.authenticated_clients.has(e.id)}update_connection_count(){process.connected&&process.send({type:"connection_count",data:{count:this.connection_count}})}send_heartbeat(){if(process.connected)try{process.send({type:"heartbeat",data:{timestamp:Date.now()}})}catch{clearInterval(this.heartbeat_interval)}}async shutdown(){const e=Date.now();this.log.info("Initiating graceful shutdown");try{await w(),this.log.info("Write queue shutdown complete")}catch(s){this.log.error("Error shutting down write queue",{error:s.message})}try{await b(),this.log.info("Database cleanup complete")}catch(s){this.log.error("Error cleaning up database",{error:s.message})}this.server&&this.server.close(()=>{this.log.info("Server closed")});for(const[s,r]of this.connections)r.end();const t=process.env.NODE_ENV==="test"?100:5e3;setTimeout(()=>{const s=Date.now()-e;this.log.info("Worker shutdown complete",{shutdown_duration_ms:s}),process.exit(0)},t)}}const C=new D;
|
package/dist/server/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import E from"net";import{decode as q}from"msgpackr";import N from"./lib/op_types.js";import x from"./lib/safe_json_parse.js";import{load_settings as v,get_settings as h,get_port_configuration as b}from"./lib/load_settings.js";import{send_error as f}from"./lib/send_response.js";import{start_cluster as A}from"./cluster/index.js";import T from"./lib/logger.js";import{initialize_database as B,cleanup_database as D}from"./lib/query_engine.js";import{create_message_parser as $,encode_message as c}from"./lib/tcp_protocol.js";import{create_connection_manager as F}from"./lib/connection_manager.js";import{shutdown_write_queue as J}from"./lib/write_queue.js";import{setup_authentication as K,verify_password as P,get_client_ip as j,is_rate_limited as G,initialize_auth_manager as M,reset_auth_state as W}from"./lib/auth_manager.js";import{initialize_api_key_manager as H}from"./lib/api_key_manager.js";import{is_development_mode as O,display_development_startup_message as U,warn_undefined_node_env as V}from"./lib/development_mode.js";import{restore_backup as Y,start_backup_schedule as L,stop_backup_schedule as Q}from"./lib/backup_manager.js";import{initialize_replication_manager as X,shutdown_replication_manager as Z}from"./lib/replication_manager.js";import{initialize_write_forwarder as ee,shutdown_write_forwarder as re}from"./lib/write_forwarder.js";import{handle_database_operation as te,handle_admin_operation as oe,handle_ping_operation as ae}from"./lib/operation_dispatcher.js";import{start_http_server as ne,stop_http_server as se}from"./lib/http_server.js";import{create_recovery_token as ie,initialize_recovery_manager as z,reset_recovery_state as ce}from"./lib/recovery_manager.js";const p=new Set;let i=null;const _e=async(t,r={})=>{if(!r?.password){const n=c({ok:0,error:"Authentication operation requires password to be set in data."});t.write(n),t.end();return}try{const o=j(t);if(G(o)){const a=c({ok:0,error:"Too many failed attempts. Please try again later."});t.write(a),t.end();return}if(!await P(r.password,o)){const a=c({ok:0,error:"Authentication failed"});t.write(a),t.end();return}p.add(t.id);const d=c({ok:1,version:"1.0.0",message:"Authentication successful"});t.write(d)}catch(o){const n={ok:0,error:`Authentication error: ${o.message}`},s=c(n);t.write(s),t.end()}},pe=async(t,r={})=>{try{const n={ok:1,password:K(),message:"Authentication setup completed successfully. Save this password - it will not be shown again."},s=c(n);t.write(s)}catch(o){const n={ok:0,error:`Setup error: ${o.message}`},s=c(n);t.write(s)}},de=(t="")=>{if(!t)throw new Error("Must pass an op type for operation.");return N.includes(t)},De=t=>{try{if(typeof t=="string")return x(t);if(Buffer.isBuffer(t)){const r=q(t);return typeof r=="string"?x(r):r}else return t}catch{return null}},S=t=>O()?!0:p.has(t.id),$e=async()=>{const{create_context_logger:t}=T("server"),r=t();let o=null;try{v(),o=h()}catch{}if(o?.restore_from)try{r.info("Startup restore requested",{backup_filename:o.restore_from});const e=await Y(o.restore_from);r.info("Startup restore completed",{backup_filename:o.restore_from,duration_ms:e.duration_ms});const a={...o};delete a.restore_from,process.env.JOYSTICK_DB_SETTINGS=JSON.stringify(a),v(),o=h(),r.info("Removed restore_from from settings after successful restore")}catch(e){r.error("Startup restore failed",{backup_filename:o.restore_from,error:e.message}),r.info("Continuing with fresh database after restore failure")}let n="./data";o?.data_path&&(n=o.data_path),B(n),M(),await H(),z();try{X(),r.info("Replication manager initialized")}catch(e){r.warn("Failed to initialize replication manager",{error:e.message})}try{ee(),r.info("Write forwarder initialized")}catch(e){r.warn("Failed to initialize write forwarder",{error:e.message})}if(o?.s3)try{L(),r.info("Backup scheduling started")}catch(e){r.warn("Failed to start backup scheduling",{error:e.message})}i=F({max_connections:1e3,idle_timeout:600*1e3,request_timeout:5*1e3});let s=null;try{const{http_port:e}=b();s=await ne(e),s&&r.info("HTTP server started",{http_port:e})}catch(e){r.warn("Failed to start HTTP server",{error:e.message})}if(O()){const{tcp_port:e,http_port:a}=b();U(e,a)}else V();const d=E.createServer((e={})=>{if(!i.add_connection(e))return;const a=$();e.on("data",async g=>{i.update_activity(e.id);try{const w=a.parse_messages(g);for(const I of w){const u=I,l=u?.op||null;if(!l){f(e,{message:"Missing operation type"});continue}if(!de(l)){f(e,{message:"Invalid operation type"});continue}const R=i.create_request_timeout(e.id,l);try{switch(l){case"authentication":await _e(e,u?.data||{});break;case"setup":await pe(e,u?.data||{});break;case"insert_one":case"update_one":case"delete_one":case"bulk_write":case"find_one":case"find":case"create_index":case"drop_index":case"get_indexes":await te(e,l,u?.data||{},S,g.length,i,p);break;case"ping":ae(e);break;case"admin":await oe(e,u?.data||{},S,i,p);break;case"reload":if(!S(e)){f(e,{message:"Authentication required"});break}try{let _=null;try{_=h()}catch{}let m=null;try{await v(),m=h()}catch{m={port:1983,authentication:{}}}const y={ok:1,status:"success",message:"Configuration reloaded successfully",changes:{port_changed:_?_.port!==m.port:!1,authentication_changed:_?_.authentication?.password_hash!==m.authentication?.password_hash:!1},timestamp:new Date().toISOString()},k=c(y);e.write(k)}catch(_){const m={ok:0,error:`Reload operation failed: ${_.message}`},y=c(m);e.write(y)}break;default:f(e,{message:`Operation ${l} not implemented`})}}finally{clearTimeout(R)}}}catch(w){r.error("Message parsing failed",{client_id:e.id,error:w.message}),f(e,{message:"Invalid message format"}),e.end()}}),e.on("end",()=>{r.info("Client disconnected",{socket_id:e.id}),p.delete(e.id),i.remove_connection(e.id)}),e.on("error",g=>{r.error("Socket error",{socket_id:e.id,error:g.message}),p.delete(e.id),i.remove_connection(e.id)})});return d.cleanup=async()=>{try{await se(),Q(),await Z(),await re(),i&&i.shutdown(),p.clear(),await J(),await new Promise(e=>setTimeout(e,100)),await D(),W(),ce()}catch{}},d};if(import.meta.url===`file://${process.argv[1]}`){const{create_context_logger:t}=T("main"),r=t();if(process.argv.includes("--generate-recovery-token"))try{z();const a=ie();console.log("Emergency Recovery Token Generated"),console.log(`Visit: ${a.url}`),console.log("Token expires in 10 minutes"),r.info("Recovery token generated via CLI",{expires_at:new Date(a.expires_at).toISOString()}),process.exit(0)}catch(a){console.error("Failed to generate recovery token:",a.message),r.error("Recovery token generation failed",{error:a.message}),process.exit(1)}const{tcp_port:o,http_port:n}=b(),s={worker_count:process.env.WORKER_COUNT?parseInt(process.env.WORKER_COUNT):void 0,port:o,environment:process.env.NODE_ENV||"development"},{has_settings:d}=await import("./lib/load_settings.js"),e=d();r.info("Starting JoystickDB server...",{workers:s.worker_count||"auto",tcp_port:o,http_port:n,environment:s.environment,has_settings:e,port_source:e?"JOYSTICK_DB_SETTINGS":"default"}),A(s)}export{_e as authentication,de as check_op_type,$e as create_server,De as parse_data,pe as setup};
|
|
1
|
+
import E from"net";import{decode as q}from"msgpackr";import N from"./lib/op_types.js";import x from"./lib/safe_json_parse.js";import{load_settings as v,get_settings as h,get_port_configuration as b}from"./lib/load_settings.js";import{send_error as f}from"./lib/send_response.js";import{start_cluster as A}from"./cluster/index.js";import T from"./lib/logger.js";import{initialize_database as B,cleanup_database as D}from"./lib/query_engine.js";import{create_message_parser as $,encode_message as c}from"./lib/tcp_protocol.js";import{create_connection_manager as F}from"./lib/connection_manager.js";import{shutdown_write_queue as J}from"./lib/write_queue.js";import{setup_authentication as K,verify_password as P,get_client_ip as j,is_rate_limited as G,initialize_auth_manager as M,reset_auth_state as W}from"./lib/auth_manager.js";import{initialize_api_key_manager as H}from"./lib/api_key_manager.js";import{is_development_mode as O,display_development_startup_message as U,warn_undefined_node_env as V}from"./lib/development_mode.js";import{restore_backup as Y,start_backup_schedule as L,stop_backup_schedule as Q}from"./lib/backup_manager.js";import{initialize_replication_manager as X,shutdown_replication_manager as Z}from"./lib/replication_manager.js";import{initialize_write_forwarder as ee,shutdown_write_forwarder as re}from"./lib/write_forwarder.js";import{handle_database_operation as te,handle_admin_operation as oe,handle_ping_operation as ae}from"./lib/operation_dispatcher.js";import{start_http_server as ne,stop_http_server as se}from"./lib/http_server.js";import{create_recovery_token as ie,initialize_recovery_manager as z,reset_recovery_state as ce}from"./lib/recovery_manager.js";const p=new Set;let i=null;const _e=async(t,r={})=>{if(!r?.password){const n=c({ok:0,error:"Authentication operation requires password to be set in data."});t.write(n),t.end();return}try{const o=j(t);if(G(o)){const a=c({ok:0,error:"Too many failed attempts. Please try again later."});t.write(a),t.end();return}if(!await P(r.password,o)){const a=c({ok:0,error:"Authentication failed"});t.write(a),t.end();return}p.add(t.id);const d=c({ok:1,version:"1.0.0",message:"Authentication successful"});t.write(d)}catch(o){const n={ok:0,error:`Authentication error: ${o.message}`},s=c(n);t.write(s),t.end()}},pe=async(t,r={})=>{try{const n={ok:1,password:K(),message:"Authentication setup completed successfully. Save this password - it will not be shown again."},s=c(n);t.write(s)}catch(o){const n={ok:0,error:`Setup error: ${o.message}`},s=c(n);t.write(s)}},de=(t="")=>{if(!t)throw new Error("Must pass an op type for operation.");return N.includes(t)},De=t=>{try{if(typeof t=="string")return x(t);if(Buffer.isBuffer(t)){const r=q(t);return typeof r=="string"?x(r):r}else return t}catch{return null}},S=t=>O()?!0:p.has(t.id),$e=async()=>{const{create_context_logger:t}=T("server"),r=t();let o=null;try{v(),o=h()}catch{}if(o?.restore_from)try{r.info("Startup restore requested",{backup_filename:o.restore_from});const e=await Y(o.restore_from);r.info("Startup restore completed",{backup_filename:o.restore_from,duration_ms:e.duration_ms});const a={...o};delete a.restore_from,process.env.JOYSTICK_DB_SETTINGS=JSON.stringify(a),v(),o=h(),r.info("Removed restore_from from settings after successful restore")}catch(e){r.error("Startup restore failed",{backup_filename:o.restore_from,error:e.message}),r.info("Continuing with fresh database after restore failure")}let n="./data";o?.data_path&&(n=o.data_path),B(n),M(),await H(),z();try{X(),r.info("Replication manager initialized")}catch(e){r.warn("Failed to initialize replication manager",{error:e.message})}try{ee(),r.info("Write forwarder initialized")}catch(e){r.warn("Failed to initialize write forwarder",{error:e.message})}if(o?.s3)try{L(),r.info("Backup scheduling started")}catch(e){r.warn("Failed to start backup scheduling",{error:e.message})}i=F({max_connections:1e3,idle_timeout:600*1e3,request_timeout:5*1e3});let s=null;try{const{http_port:e}=b();s=await ne(e),s&&r.info("HTTP server started",{http_port:e})}catch(e){r.warn("Failed to start HTTP server",{error:e.message})}if(O()){const{tcp_port:e,http_port:a}=b();U(e,a)}else V();const d=E.createServer((e={})=>{if(!i.add_connection(e))return;const a=$();e.on("data",async g=>{i.update_activity(e.id);try{const w=a.parse_messages(g);for(const I of w){const u=I,l=u?.op||null;if(!l){f(e,{message:"Missing operation type"});continue}if(!de(l)){f(e,{message:"Invalid operation type"});continue}const R=i.create_request_timeout(e.id,l);try{switch(l){case"authentication":await _e(e,u?.data||{});break;case"setup":await pe(e,u?.data||{});break;case"insert_one":case"update_one":case"delete_one":case"delete_many":case"bulk_write":case"find_one":case"find":case"create_index":case"drop_index":case"get_indexes":await te(e,l,u?.data||{},S,g.length,i,p);break;case"ping":ae(e);break;case"admin":await oe(e,u?.data||{},S,i,p);break;case"reload":if(!S(e)){f(e,{message:"Authentication required"});break}try{let _=null;try{_=h()}catch{}let m=null;try{await v(),m=h()}catch{m={port:1983,authentication:{}}}const y={ok:1,status:"success",message:"Configuration reloaded successfully",changes:{port_changed:_?_.port!==m.port:!1,authentication_changed:_?_.authentication?.password_hash!==m.authentication?.password_hash:!1},timestamp:new Date().toISOString()},k=c(y);e.write(k)}catch(_){const m={ok:0,error:`Reload operation failed: ${_.message}`},y=c(m);e.write(y)}break;default:f(e,{message:`Operation ${l} not implemented`})}}finally{clearTimeout(R)}}}catch(w){r.error("Message parsing failed",{client_id:e.id,error:w.message}),f(e,{message:"Invalid message format"}),e.end()}}),e.on("end",()=>{r.info("Client disconnected",{socket_id:e.id}),p.delete(e.id),i.remove_connection(e.id)}),e.on("error",g=>{r.error("Socket error",{socket_id:e.id,error:g.message}),p.delete(e.id),i.remove_connection(e.id)})});return d.cleanup=async()=>{try{await se(),Q(),await Z(),await re(),i&&i.shutdown(),p.clear(),await J(),await new Promise(e=>setTimeout(e,100)),await D(),W(),ce()}catch{}},d};if(import.meta.url===`file://${process.argv[1]}`){const{create_context_logger:t}=T("main"),r=t();if(process.argv.includes("--generate-recovery-token"))try{z();const a=ie();console.log("Emergency Recovery Token Generated"),console.log(`Visit: ${a.url}`),console.log("Token expires in 10 minutes"),r.info("Recovery token generated via CLI",{expires_at:new Date(a.expires_at).toISOString()}),process.exit(0)}catch(a){console.error("Failed to generate recovery token:",a.message),r.error("Recovery token generation failed",{error:a.message}),process.exit(1)}const{tcp_port:o,http_port:n}=b(),s={worker_count:process.env.WORKER_COUNT?parseInt(process.env.WORKER_COUNT):void 0,port:o,environment:process.env.NODE_ENV||"development"},{has_settings:d}=await import("./lib/load_settings.js"),e=d();r.info("Starting JoystickDB server...",{workers:s.worker_count||"auto",tcp_port:o,http_port:n,environment:s.environment,has_settings:e,port_source:e?"JOYSTICK_DB_SETTINGS":"default"}),A(s)}export{_e as authentication,de as check_op_type,$e as create_server,De as parse_data,pe as setup};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import s from"./logger.js";const{create_context_logger:t}=s("development_mode"),n=t(),l=()=>process.env.NODE_ENV==="development"
|
|
1
|
+
import s from"./logger.js";const{create_context_logger:t}=s("development_mode"),n=t(),l=()=>process.env.NODE_ENV==="development",r=(e,o)=>{console.log(`
|
|
2
2
|
JoystickDB Development Mode
|
|
3
3
|
`),console.log("Development environment detected (NODE_ENV=development)."),console.log(`Security features have been bypassed for local development.
|
|
4
4
|
`),console.log("Default admin user created:"),console.log(" Username: admin"),console.log(` Password: password
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const e=["authentication","setup","find_one","find","insert_one","update_one","delete_one","bulk_write","create_index","drop_index","get_indexes","admin","ping","reload"];var n=e;export{n as default};
|
|
1
|
+
const e=["authentication","setup","find_one","find","insert_one","update_one","delete_one","delete_many","bulk_write","create_index","drop_index","get_indexes","admin","ping","reload"];var n=e;export{n as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{encode_message as s}from"./tcp_protocol.js";import{get_write_forwarder as g}from"./write_forwarder.js";import{get_replication_manager as h}from"./replication_manager.js";import{check_and_grow_map_size as b}from"./query_engine.js";import{performance_monitor as f}from"./performance_monitor.js";import x from"./logger.js";import k from"./operations/insert_one.js";import v from"./operations/update_one.js";import q from"./operations/delete_one.js";import D from"./operations/
|
|
1
|
+
import{encode_message as s}from"./tcp_protocol.js";import{get_write_forwarder as g}from"./write_forwarder.js";import{get_replication_manager as h}from"./replication_manager.js";import{check_and_grow_map_size as b}from"./query_engine.js";import{performance_monitor as f}from"./performance_monitor.js";import x from"./logger.js";import k from"./operations/insert_one.js";import v from"./operations/update_one.js";import q from"./operations/delete_one.js";import D from"./operations/delete_many.js";import A from"./operations/bulk_write.js";import I from"./operations/find_one.js";import $ from"./operations/find.js";import C from"./operations/create_index.js";import E from"./operations/drop_index.js";import L from"./operations/get_indexes.js";import U from"./operations/admin.js";const{create_context_logger:Z}=x("operation_dispatcher"),j=o=>!o||typeof o!="string"||o.length>64||["admin","config","local"].includes(o.toLowerCase())?!1:/^[a-zA-Z0-9_-]+$/.test(o),ee=async(o,r,e,u,c=0,a=null,d=null)=>{const l=Z(),m=Date.now();if(!u(o)){const t=s({ok:0,error:"Authentication required"});o.write(t),f.log_structured_operation(o.id,r,null,0,"error","Authentication required",c,0);return}const i=e.database||"default";if(!j(i)){const t=s({ok:0,error:"Invalid database name. Database names must be alphanumeric with underscores/hyphens, max 64 characters, and cannot be reserved names (admin, config, local)."});o.write(t),f.log_structured_operation(o.id,r,e.collection,0,"error","Invalid database name",c,0);return}if(!await g().forward_operation(o,r,e))try{let n;switch(r){case"insert_one":n=await k(i,e.collection,e.document,e.options);break;case"update_one":n=await v(i,e.collection,e.filter,e.update,e.options);break;case"delete_one":n=await q(i,e.collection,e.filter,e.options);break;case"delete_many":n=await D(i,e.collection,e.filter,e.options);break;case"bulk_write":n=await A(i,e.collection,e.operations,e.options);break;case"find_one":n=await I(i,e.collection,e.filter,e.options);break;case"find":n=await $(i,e.collection,e.filter,e.options);break;case"create_index":n=await C(i,e.collection,e.field,e.options);break;case"drop_index":n=await E(i,e.collection,e.field);break;case"get_indexes":n=await L(i,e.collection);break;default:throw new Error(`Unsupported operation: ${r}`)}const t=Date.now()-m;let _;r==="find_one"?_={ok:1,document:n}:r==="find"?_={ok:1,documents:n}:_={ok:1,...n};const p=s(_),w=p.length;o.write(p),f.log_structured_operation(o.id,r,e.collection,t,"success",null,c,w),l.info("Database operation completed",{client_id:o.id,op:r,collection:e.collection,duration_ms:t,status:"success",request_size:c,response_size:w}),r!=="find"&&r!=="find_one"&&r!=="get_indexes"&&(h().queue_replication(r,e.collection,e),setImmediate(()=>b()))}catch(n){const t=Date.now()-m;f.log_structured_operation(o.id,r,e.collection,t,"error",n.message,c,0),l.error("Database operation failed",{client_id:o.id,op:r,collection:e.collection,duration_ms:t,status:"error",error:n.message,request_size:c});const _={ok:0,error:n.message},p=s(_);o.write(p)}},oe=async(o,r,e,u=null,c=null)=>{if(!e(o)){const d=s({ok:!1,error:"Authentication required"});o.write(d);return}try{const a=r?.admin_action,l=await U(a,r||{},u,c);if(a){const m={ok:1,...l},i=s(m);o.write(i)}else{const m={ok:!0,...l},i=s(m);o.write(i)}}catch(a){const d={ok:0,error:`Admin operation failed: ${a.message}`},l=s(d);o.write(l)}},re=o=>{const e=s({ok:1});o.write(e)};export{oe as handle_admin_operation,ee as handle_database_operation,re as handle_ping_operation};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{get_database as _}from"../query_engine.js";import{update_indexes_on_delete as w}from"../index_manager.js";import{get_write_queue as p}from"../write_queue.js";import g from"../logger.js";const{create_context_logger:y}=g("delete_many"),b=(t,e)=>{if(!e||Object.keys(e).length===0)return!0;for(const[r,o]of Object.entries(e))if(t[r]!==o)return!1;return!0},h=async(t,e,r,o={})=>{const s=y();if(!t)throw new Error("Database name is required");if(!e)throw new Error("Collection name is required");if(!r||typeof r!="object")throw new Error("Filter must be a valid object");const{limit:n}=o;if(n!==void 0&&(typeof n!="number"||n<0))throw new Error("Limit must be a non-negative number");const c=_();let i=0;const d=[];await c.transaction(()=>{const a=`${t}:${e}:`,l=c.getRange({start:a,end:a+"\xFF"});for(const{key:f,value:m}of l){if(n!==void 0&&i>=n)break;try{const u=JSON.parse(m);b(u,r)&&(c.remove(f),d.push(u),i++)}catch{continue}}});for(const a of d)await w(t,e,a);return s.info("Delete many operation completed",{database:t,collection:e,deleted_count:i,limit:n||"none"}),{acknowledged:!0,deleted_count:i,operation_time:new Date().toISOString()}},k=async(t,e,r,o={})=>await p().enqueue_write_operation(()=>h(t,e,r,o),{operation:"delete_many",database:t,collection:e,filter_keys:Object.keys(r||{}),limit:o.limit});var O=k;export{O as default};
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joystick.js/db-canary",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.0-canary.
|
|
5
|
-
"canary_version": "0.0.0-canary.
|
|
4
|
+
"version": "0.0.0-canary.2244",
|
|
5
|
+
"canary_version": "0.0.0-canary.2243",
|
|
6
6
|
"description": "JoystickDB - A minimalist database server for the Joystick framework",
|
|
7
7
|
"main": "./dist/server/index.js",
|
|
8
8
|
"scripts": {
|
package/src/client/index.js
CHANGED
|
@@ -551,6 +551,23 @@ class JoystickDBClient extends EventEmitter {
|
|
|
551
551
|
return result;
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
+
// NOTE: Database Operations.
|
|
555
|
+
/**
|
|
556
|
+
* Deletes multiple documents from a collection.
|
|
557
|
+
* @param {string} collection - Collection name
|
|
558
|
+
* @param {Object} [filter={}] - Query filter to match documents
|
|
559
|
+
* @param {Object} [options={}] - Delete options
|
|
560
|
+
* @param {number} [options.limit] - Maximum number of documents to delete
|
|
561
|
+
* @returns {Promise<Object>} Delete result with acknowledged, deleted_count, and operation_time
|
|
562
|
+
*/
|
|
563
|
+
async delete_many(collection, filter = {}, options = {}) {
|
|
564
|
+
return this.send_request('delete_many', {
|
|
565
|
+
collection,
|
|
566
|
+
filter,
|
|
567
|
+
options
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
554
571
|
// NOTE: Database Interface.
|
|
555
572
|
/**
|
|
556
573
|
* Returns a database interface for method chaining operations.
|
|
@@ -668,6 +685,22 @@ class Collection {
|
|
|
668
685
|
});
|
|
669
686
|
}
|
|
670
687
|
|
|
688
|
+
/**
|
|
689
|
+
* Deletes multiple documents from the collection.
|
|
690
|
+
* @param {Object} [filter={}] - Query filter to match documents
|
|
691
|
+
* @param {Object} [options={}] - Delete options
|
|
692
|
+
* @param {number} [options.limit] - Maximum number of documents to delete
|
|
693
|
+
* @returns {Promise<Object>} Delete result with acknowledged, deleted_count, and operation_time
|
|
694
|
+
*/
|
|
695
|
+
async delete_many(filter = {}, options = {}) {
|
|
696
|
+
return this.client.send_request('delete_many', {
|
|
697
|
+
database: this.database_name,
|
|
698
|
+
collection: this.collection_name,
|
|
699
|
+
filter,
|
|
700
|
+
options
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
671
704
|
/**
|
|
672
705
|
* Performs multiple write operations in a single request.
|
|
673
706
|
* @param {Array<Object>} operations - Array of write operations
|
|
@@ -385,14 +385,15 @@ class ClusterWorker {
|
|
|
385
385
|
|
|
386
386
|
try {
|
|
387
387
|
if (success) {
|
|
388
|
-
// NOTE:
|
|
388
|
+
// NOTE: Format response consistently with operation dispatcher.
|
|
389
389
|
let response;
|
|
390
|
-
if (op_type === '
|
|
391
|
-
response = { ok: 1, documents: result };
|
|
392
|
-
} else if (op_type === 'find_one') {
|
|
390
|
+
if (op_type === 'find_one') {
|
|
393
391
|
response = { ok: 1, document: result };
|
|
392
|
+
} else if (op_type === 'find') {
|
|
393
|
+
response = { ok: 1, documents: result };
|
|
394
394
|
} else {
|
|
395
|
-
|
|
395
|
+
// NOTE: For all other operations, spread the result properties directly.
|
|
396
|
+
response = { ok: 1, ...result };
|
|
396
397
|
}
|
|
397
398
|
const encoded_response = encode_message(response);
|
|
398
399
|
socket.write(encoded_response);
|
package/src/server/index.js
CHANGED
|
@@ -12,10 +12,10 @@ const log = create_context_logger();
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Checks if the server is running in development mode.
|
|
15
|
-
* @returns {boolean} True if NODE_ENV is set to 'development'
|
|
15
|
+
* @returns {boolean} True if NODE_ENV is set to 'development'
|
|
16
16
|
*/
|
|
17
17
|
const is_development_mode = () => {
|
|
18
|
-
return process.env.NODE_ENV === 'development'
|
|
18
|
+
return process.env.NODE_ENV === 'development';
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -15,6 +15,7 @@ import create_logger from './logger.js';
|
|
|
15
15
|
import insert_one from './operations/insert_one.js';
|
|
16
16
|
import update_one from './operations/update_one.js';
|
|
17
17
|
import delete_one from './operations/delete_one.js';
|
|
18
|
+
import delete_many from './operations/delete_many.js';
|
|
18
19
|
import bulk_write from './operations/bulk_write.js';
|
|
19
20
|
import find_one from './operations/find_one.js';
|
|
20
21
|
import find from './operations/find.js';
|
|
@@ -109,6 +110,10 @@ export const handle_database_operation = async (socket, op_type, data, check_aut
|
|
|
109
110
|
result = await delete_one(database_name, data.collection, data.filter, data.options);
|
|
110
111
|
break;
|
|
111
112
|
|
|
113
|
+
case 'delete_many':
|
|
114
|
+
result = await delete_many(database_name, data.collection, data.filter, data.options);
|
|
115
|
+
break;
|
|
116
|
+
|
|
112
117
|
case 'bulk_write':
|
|
113
118
|
result = await bulk_write(database_name, data.collection, data.operations, data.options);
|
|
114
119
|
break;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { get_database } from '../query_engine.js';
|
|
2
|
+
import { update_indexes_on_delete } from '../index_manager.js';
|
|
3
|
+
import { get_write_queue } from '../write_queue.js';
|
|
4
|
+
import create_logger from '../logger.js';
|
|
5
|
+
|
|
6
|
+
const { create_context_logger } = create_logger('delete_many');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Checks if a document matches the given filter criteria (simple equality check).
|
|
10
|
+
* @param {Object} document - Document to test
|
|
11
|
+
* @param {Object} filter - Filter criteria
|
|
12
|
+
* @returns {boolean} True if document matches filter
|
|
13
|
+
*/
|
|
14
|
+
const matches_filter = (document, filter) => {
|
|
15
|
+
if (!filter || Object.keys(filter).length === 0) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (const [field, value] of Object.entries(filter)) {
|
|
20
|
+
if (document[field] !== value) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return true;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Internal implementation of delete_many operation without write queue serialization.
|
|
30
|
+
* @param {string} database_name - Name of the database
|
|
31
|
+
* @param {string} collection_name - Name of the collection
|
|
32
|
+
* @param {Object} filter - Filter criteria to match documents for deletion
|
|
33
|
+
* @param {Object} [options={}] - Delete options
|
|
34
|
+
* @param {number} [options.limit] - Maximum number of documents to delete
|
|
35
|
+
* @returns {Promise<Object>} Delete result with acknowledged and deleted_count
|
|
36
|
+
* @throws {Error} When database name, collection name is missing or filter is invalid
|
|
37
|
+
*/
|
|
38
|
+
const delete_many_internal = async (database_name, collection_name, filter, options = {}) => {
|
|
39
|
+
const log = create_context_logger();
|
|
40
|
+
|
|
41
|
+
if (!database_name) {
|
|
42
|
+
throw new Error('Database name is required');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!collection_name) {
|
|
46
|
+
throw new Error('Collection name is required');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!filter || typeof filter !== 'object') {
|
|
50
|
+
throw new Error('Filter must be a valid object');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { limit } = options;
|
|
54
|
+
|
|
55
|
+
if (limit !== undefined && (typeof limit !== 'number' || limit < 0)) {
|
|
56
|
+
throw new Error('Limit must be a non-negative number');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const db = get_database();
|
|
60
|
+
let deleted_count = 0;
|
|
61
|
+
const deleted_documents = [];
|
|
62
|
+
|
|
63
|
+
await db.transaction(() => {
|
|
64
|
+
const collection_prefix = `${database_name}:${collection_name}:`;
|
|
65
|
+
|
|
66
|
+
const range = db.getRange({ start: collection_prefix, end: collection_prefix + '\xFF' });
|
|
67
|
+
for (const { key, value } of range) {
|
|
68
|
+
// NOTE: Check limit before processing more documents.
|
|
69
|
+
if (limit !== undefined && deleted_count >= limit) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const document = JSON.parse(value);
|
|
75
|
+
if (matches_filter(document, filter)) {
|
|
76
|
+
db.remove(key);
|
|
77
|
+
deleted_documents.push(document);
|
|
78
|
+
deleted_count++;
|
|
79
|
+
}
|
|
80
|
+
} catch (parse_error) {
|
|
81
|
+
// Skip documents that can't be parsed
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// NOTE: Update indexes for all deleted documents.
|
|
88
|
+
for (const deleted_document of deleted_documents) {
|
|
89
|
+
await update_indexes_on_delete(database_name, collection_name, deleted_document);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
log.info('Delete many operation completed', {
|
|
93
|
+
database: database_name,
|
|
94
|
+
collection: collection_name,
|
|
95
|
+
deleted_count,
|
|
96
|
+
limit: limit || 'none'
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
acknowledged: true,
|
|
101
|
+
deleted_count,
|
|
102
|
+
operation_time: new Date().toISOString()
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Deletes multiple documents from a collection with write queue serialization.
|
|
108
|
+
* @param {string} database_name - Name of the database
|
|
109
|
+
* @param {string} collection_name - Name of the collection
|
|
110
|
+
* @param {Object} filter - Filter criteria to match documents for deletion
|
|
111
|
+
* @param {Object} [options={}] - Delete options
|
|
112
|
+
* @param {number} [options.limit] - Maximum number of documents to delete
|
|
113
|
+
* @returns {Promise<Object>} Delete result with acknowledged, deleted_count, and operation_time
|
|
114
|
+
*/
|
|
115
|
+
const delete_many = async (database_name, collection_name, filter, options = {}) => {
|
|
116
|
+
const write_queue = get_write_queue();
|
|
117
|
+
|
|
118
|
+
return await write_queue.enqueue_write_operation(
|
|
119
|
+
() => delete_many_internal(database_name, collection_name, filter, options),
|
|
120
|
+
{
|
|
121
|
+
operation: 'delete_many',
|
|
122
|
+
database: database_name,
|
|
123
|
+
collection: collection_name,
|
|
124
|
+
filter_keys: Object.keys(filter || {}),
|
|
125
|
+
limit: options.limit
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export default delete_many;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|