titanpl-sdk 3.0.1 → 6.0.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 CHANGED
@@ -24,6 +24,8 @@ It provides the necessary **Type Definitions** to make your IDE understand the g
24
24
 
25
25
  > **Note:** The actual implementation of `t.log`, `t.fetch`, and other APIs are embedded directly into the [Titan Planet Binary](https://github.com/ezet-galaxy/titanpl). This SDK simply provides the "blueprints" (types) and a "sandbox" (test runner).
26
26
 
27
+ **Important Note:** Currently, Titan Planet and its entire package ecosystem are only for Windows. The Linux version is in development (dev only) for the new architecture and will be launched later.
28
+
27
29
  ---
28
30
 
29
31
  ## ✨ Features
package/package.json CHANGED
@@ -1,37 +1,40 @@
1
1
  {
2
- "name": "titanpl-sdk",
3
- "version": "3.0.1",
4
- "description": "Development SDK for Titan Planet. Provides TypeScript type definitions for the global 't' runtime object and a 'lite' test-harness runtime for building and verifying extensions.",
5
- "main": "index.js",
6
- "type": "module",
7
- "types": "index.d.ts",
8
- "bin": {
9
- "titanpl-sdk": "./bin/run.js"
10
- },
11
- "files": [
12
- "bin/",
13
- "templates/",
14
- "titan",
15
- "assets",
16
- "index.js",
17
- "index.d.ts",
18
- "README.md"
19
- ],
20
- "keywords": [
21
- "titan",
22
- "titan-planet",
23
- "titanpl-sdk",
24
- "ezetgalaxy",
25
- "types",
26
- "typescript",
27
- "intellisense",
28
- "sdk",
29
- "backend-sdk",
30
- "extension-development"
31
- ],
32
- "license": "ISC",
33
- "dependencies": {
34
- "@titanpl/core": "latest",
35
- "@titanpl/node": "latest"
36
- }
2
+ "name": "titanpl-sdk",
3
+ "version": "6.0.0",
4
+ "description": "Development SDK for Titan Planet. Provides TypeScript type definitions for the global 't' runtime object and a 'lite' test-harness runtime for building and verifying extensions.",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "types": "index.d.ts",
8
+ "bin": {
9
+ "titanpl-sdk": "./bin/run.js"
10
+ },
11
+ "files": [
12
+ "bin/",
13
+ "templates/",
14
+ "titan",
15
+ "assets",
16
+ "index.js",
17
+ "index.d.ts",
18
+ "README.md"
19
+ ],
20
+ "keywords": [
21
+ "titan",
22
+ "titan-planet",
23
+ "titanpl-sdk",
24
+ "ezetgalaxy",
25
+ "types",
26
+ "typescript",
27
+ "intellisense",
28
+ "sdk",
29
+ "backend-sdk",
30
+ "extension-development"
31
+ ],
32
+ "license": "ISC",
33
+ "dependencies": {
34
+ "@titanpl/core": "latest",
35
+ "@titanpl/node": "latest"
36
+ },
37
+ "devDependencies": {
38
+ "@tgrv/microgravity": "latest"
39
+ }
37
40
  }
@@ -1,4 +1,4 @@
1
- import t from "@titan/route";
1
+ import t from "@titanpl/route";
2
2
 
3
3
  t.post("/hello").action("hello") // pass a json payload { "name": "titan" }
4
4
 
@@ -29,6 +29,8 @@ dashmap = "6.1.0"
29
29
  bytes = "1.11.0"
30
30
  smallvec = "1.15.1"
31
31
  num_cpus = "1.17.0"
32
+ deadpool-postgres = "0.12"
33
+ tokio-postgres = "0.7"
32
34
 
33
35
  # Performance: Global Allocator
34
36
  mimalloc = { version = "0.1", default-features = false }
@@ -17,9 +17,10 @@ use std::time::{SystemTime, UNIX_EPOCH};
17
17
  use serde_json::Value;
18
18
  use jsonwebtoken::{encode, decode, Header, EncodingKey, DecodingKey, Validation};
19
19
  use bcrypt::{hash, verify, DEFAULT_COST};
20
- use postgres::{Client as PgClient, NoTls};
21
- use std::sync::{Mutex, OnceLock};
22
- use std::collections::{HashMap, BTreeMap};
20
+ use std::sync::OnceLock;
21
+ use deadpool_postgres::{Manager, Pool};
22
+ use tokio_postgres::{NoTls, Config};
23
+
23
24
 
24
25
  use crate::utils::{blue, gray, red, parse_expires_in};
25
26
  use super::{TitanRuntime, v8_str, v8_to_string, throw, ShareContextStore};
@@ -27,7 +28,7 @@ use super::{TitanRuntime, v8_str, v8_to_string, throw, ShareContextStore};
27
28
  const TITAN_CORE_JS: &str = include_str!("titan_core.js");
28
29
 
29
30
  // Database connection pool
30
- static DB_POOL: Mutex<Option<HashMap<String, PgClient>>> = Mutex::new(None);
31
+ static DB_POOL: OnceLock<Pool> = OnceLock::new();
31
32
  static HTTP_CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
32
33
 
33
34
  fn get_http_client() -> &'static reqwest::Client {
@@ -359,7 +360,9 @@ fn native_log(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments,
359
360
 
360
361
  fn native_jwt_sign(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
361
362
  let payload_val = args.get(0);
362
- let json_str = v8::json::stringify(scope, payload_val).unwrap().to_rust_string_lossy(scope);
363
+ let json_str = v8::json::stringify(scope, payload_val)
364
+ .map(|s| s.to_rust_string_lossy(scope))
365
+ .unwrap_or_else(|| "{}".to_string());
363
366
  let mut payload: serde_json::Map<String, Value> = serde_json::from_str(&json_str).unwrap_or_default();
364
367
  let secret = v8_to_string(scope, args.get(1));
365
368
 
@@ -450,74 +453,121 @@ fn native_define_action(_scope: &mut v8::HandleScope, args: v8::FunctionCallback
450
453
  }
451
454
 
452
455
  fn native_db_connect(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
456
+
453
457
  let conn_string = v8_to_string(scope, args.get(0));
454
-
458
+
455
459
  if conn_string.is_empty() {
456
- throw(scope, "t.db.connect(): connection string is required");
460
+ throw(scope, "t.db.connect(): connection string required");
457
461
  return;
458
462
  }
459
463
 
460
- match PgClient::connect(&conn_string, NoTls) {
461
- Ok(mut client) => {
462
- let mut pool = DB_POOL.lock().unwrap();
463
- let map = pool.get_or_insert_with(HashMap::new);
464
- map.insert(conn_string.clone(), client);
465
- },
466
- Err(e) => {
467
- throw(scope, &format!("Database connection failed: {}", e));
468
- return;
464
+ let mut max_size = 16;
465
+
466
+ if args.length() > 1 && args.get(1).is_object() {
467
+ let opts = args.get(1).to_object(scope).unwrap();
468
+ let max_key = v8_str(scope, "max");
469
+ if let Some(v) = opts.get(scope, max_key.into()) {
470
+ if let Some(n) = v.number_value(scope) {
471
+ max_size = n as usize;
472
+ }
469
473
  }
470
474
  }
475
+
476
+ if DB_POOL.get().is_none() {
477
+ let cfg: Config = match conn_string.parse() {
478
+ Ok(c) => c,
479
+ Err(e) => {
480
+ let msg = format!("t.db.connect(): Invalid connection string: {}", e);
481
+ let message = v8::String::new(scope, &msg).unwrap_or_else(|| v8::String::new(scope, "Error").unwrap());
482
+ let exception = v8::Exception::error(scope, message);
483
+ scope.throw_exception(exception);
484
+ return;
485
+ }
486
+ };
487
+ let mgr = Manager::new(cfg, NoTls);
471
488
 
472
- let db_conn_obj = v8::Object::new(scope);
473
-
474
- let conn_key = v8_str(scope, "__conn_string");
475
- let conn_val = v8_str(scope, &conn_string);
476
- db_conn_obj.set(scope, conn_key.into(), conn_val.into());
489
+ let pool = match Pool::builder(mgr)
490
+ .max_size(max_size)
491
+ .build() {
492
+ Ok(p) => p,
493
+ Err(e) => {
494
+ throw(scope, &format!("t.db.connect(): Failed to build connection pool: {}", e));
495
+ return;
496
+ }
497
+ };
477
498
 
478
- let query_fn = v8::Function::new(scope, native_db_query).unwrap();
499
+ DB_POOL.set(pool).ok();
500
+ }
501
+
502
+ let db_conn_obj = v8::Object::new(scope);
503
+
504
+ let query_fn = match v8::Function::new(scope, native_db_query) {
505
+ Some(f) => f,
506
+ None => {
507
+ throw(scope, "t.db.connect(): Failed to create query function");
508
+ return;
509
+ }
510
+ };
479
511
  let query_key = v8_str(scope, "query");
480
512
  db_conn_obj.set(scope, query_key.into(), query_fn.into());
481
-
513
+
482
514
  retval.set(db_conn_obj.into());
483
515
  }
484
516
 
485
- fn native_db_query(scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut retval: v8::ReturnValue) {
486
- let this = args.this();
487
- let this_obj = this.to_object(scope).unwrap();
488
-
489
- let conn_key = v8_str(scope, "__conn_string");
490
- let conn_val = this_obj.get(scope, conn_key.into()).unwrap();
491
- let conn_string = v8_to_string(scope, conn_val);
492
-
493
- let query = v8_to_string(scope, args.get(0));
494
-
495
- if query.is_empty() {
496
- throw(scope, "db.query(): SQL query is required");
497
- return;
517
+ fn native_db_query(
518
+ scope: &mut v8::HandleScope,
519
+ args: v8::FunctionCallbackArguments,
520
+ mut retval: v8::ReturnValue,
521
+ ) {
522
+ let sql = v8_to_string(scope, args.get(0));
523
+
524
+ // Collect params
525
+ let mut params = Vec::new();
526
+ if args.length() > 1 && args.get(1).is_array() {
527
+ let arr = v8::Local::<v8::Array>::try_from(args.get(1)).unwrap();
528
+ for i in 0..arr.length() {
529
+ if let Some(v) = arr.get_index(scope, i) {
530
+ params.push(v8_to_string(scope, v));
531
+ }
532
+ }
498
533
  }
499
534
 
535
+ // Main async wrapper object
500
536
  let obj = v8::Object::new(scope);
501
- let op_key = v8_str(scope, "__titanAsync");
502
- let op_val = v8::Boolean::new(scope, true);
503
- obj.set(scope, op_key.into(), op_val.into());
504
-
537
+
538
+ let async_key = v8_str(scope, "__titanAsync");
539
+ let async_val = v8::Boolean::new(scope, true);
540
+ obj.set(scope, async_key.into(), async_val.into());
541
+
505
542
  let type_key = v8_str(scope, "type");
506
543
  let type_val = v8_str(scope, "db_query");
507
544
  obj.set(scope, type_key.into(), type_val.into());
508
-
545
+
546
+ // Data object
509
547
  let data_obj = v8::Object::new(scope);
510
- let conn_k = v8_str(scope, "conn");
511
- let conn_v = v8_str(scope, &conn_string);
512
- data_obj.set(scope, conn_k.into(), conn_v.into());
513
-
514
- let q_k = v8_str(scope, "query");
515
- let q_v = v8_str(scope, &query);
516
- data_obj.set(scope, q_k.into(), q_v.into());
517
-
548
+
549
+ let conn_key = v8_str(scope, "conn");
550
+ let conn_val = v8_str(scope, "default");
551
+ data_obj.set(scope, conn_key.into(), conn_val.into());
552
+
553
+ let query_key = v8_str(scope, "query");
554
+ let query_val = v8_str(scope, &sql);
555
+ data_obj.set(scope, query_key.into(), query_val.into());
556
+
557
+ // Params array
558
+ let params_arr = v8::Array::new(scope, params.len() as i32);
559
+
560
+ for (i, p) in params.iter().enumerate() {
561
+ let param_val = v8_str(scope, p);
562
+ params_arr.set_index(scope, i as u32, param_val.into());
563
+ }
564
+
565
+ let params_key = v8_str(scope, "params");
566
+ data_obj.set(scope, params_key.into(), params_arr.into());
567
+
518
568
  let data_key = v8_str(scope, "data");
519
569
  obj.set(scope, data_key.into(), data_obj.into());
520
-
570
+
521
571
  retval.set(obj.into());
522
572
  }
523
573
 
@@ -604,15 +654,35 @@ fn parse_async_op(scope: &mut v8::HandleScope, op_val: v8::Local<v8::Value>) ->
604
654
  }
605
655
  Some(super::TitanAsyncOp::Fetch { url, method, body, headers })
606
656
  },
657
+
607
658
  "db_query" => {
608
- let conn_key = v8_str(scope, "conn");
609
- let conn_obj = data_obj.get(scope, conn_key.into())?;
610
- let conn = v8_to_string(scope, conn_obj);
611
- let query_key = v8_str(scope, "query");
612
- let query_obj = data_obj.get(scope, query_key.into())?;
613
- let query = v8_to_string(scope, query_obj);
614
- Some(super::TitanAsyncOp::DbQuery { conn, query })
615
- },
659
+
660
+ let conn_key = v8_str(scope, "conn");
661
+ let conn_val = data_obj.get(scope, conn_key.into())?;
662
+ let conn = v8_to_string(scope, conn_val);
663
+
664
+ let query_key = v8_str(scope, "query");
665
+ let query_val = data_obj.get(scope, query_key.into())?;
666
+ let query = v8_to_string(scope, query_val);
667
+
668
+ let params_key = v8_str(scope, "params");
669
+ let mut params = Vec::new();
670
+
671
+ if let Some(p_val) = data_obj.get(scope, params_key.into()) {
672
+ if p_val.is_array() {
673
+ let arr = v8::Local::<v8::Array>::try_from(p_val).unwrap();
674
+ for i in 0..arr.length() {
675
+ if let Some(v) = arr.get_index(scope, i) {
676
+ params.push(v8_to_string(scope, v));
677
+ }
678
+ }
679
+ }
680
+ }
681
+
682
+ Some(super::TitanAsyncOp::DbQuery { conn, query, params })
683
+ }
684
+
685
+
616
686
  "fs_read" => {
617
687
  let path_key = v8_str(scope, "path");
618
688
  let path_obj = data_obj.get(scope, path_key.into())?;
@@ -810,9 +880,15 @@ fn native_finish_request(scope: &mut v8::HandleScope, mut args: v8::FunctionCall
810
880
  }
811
881
  }
812
882
 
813
- pub fn run_async_operation(op: super::TitanAsyncOp) -> std::pin::Pin<Box<dyn std::future::Future<Output = serde_json::Value> + Send>> {
883
+ pub fn run_async_operation(
884
+ op: super::TitanAsyncOp,
885
+ ) -> std::pin::Pin<Box<dyn std::future::Future<Output = serde_json::Value> + Send>> {
814
886
  Box::pin(async move {
815
887
  match op {
888
+
889
+ // =========================
890
+ // FETCH
891
+ // =========================
816
892
  super::TitanAsyncOp::Fetch {
817
893
  url,
818
894
  method,
@@ -820,107 +896,163 @@ pub fn run_async_operation(op: super::TitanAsyncOp) -> std::pin::Pin<Box<dyn std
820
896
  headers,
821
897
  } => {
822
898
  let client = get_http_client();
823
- let m = reqwest::Method::from_bytes(method.as_bytes()).unwrap_or(reqwest::Method::GET);
824
-
825
- let mut req = client.request(m, &url);
826
-
899
+
900
+ let method = reqwest::Method::from_bytes(method.as_bytes())
901
+ .unwrap_or(reqwest::Method::GET);
902
+
903
+ let mut req = client.request(method, &url);
904
+
827
905
  for (k, v) in headers {
828
906
  req = req.header(k, v);
829
907
  }
830
-
908
+
831
909
  if let Some(b) = body {
832
- req = req.body(b);
910
+ req = req.body(b);
833
911
  }
834
-
912
+
835
913
  match req.send().await {
836
914
  Ok(resp) => {
837
- let status = resp.status().as_u16();
838
- let api_headers = resp.headers().clone();
839
- let text = resp.text().await.unwrap_or_default();
840
-
841
- let mut h_map = serde_json::Map::new();
842
- for (k, v) in api_headers.iter() {
843
- if let Ok(s) = v.to_str() {
844
- h_map.insert(k.as_str().to_string(), serde_json::Value::String(s.to_string()));
845
- }
846
- }
847
-
848
- serde_json::json!({
849
- "_isResponse": true,
850
- "status": status,
851
- "body": text,
852
- "headers": h_map
853
- })
854
- },
855
- Err(e) => serde_json::json!({ "error": e.to_string() })
915
+ let status = resp.status().as_u16();
916
+ let api_headers = resp.headers().clone();
917
+ let text = resp.text().await.unwrap_or_default();
918
+
919
+ let mut h_map = serde_json::Map::new();
920
+ for (k, v) in api_headers.iter() {
921
+ if let Ok(s) = v.to_str() {
922
+ h_map.insert(
923
+ k.as_str().to_string(),
924
+ serde_json::Value::String(s.to_string()),
925
+ );
926
+ }
927
+ }
928
+
929
+ serde_json::json!({
930
+ "_isResponse": true,
931
+ "status": status,
932
+ "body": text,
933
+ "headers": h_map
934
+ })
935
+ }
936
+ Err(e) => serde_json::json!({ "error": e.to_string() }),
856
937
  }
857
- },
858
- super::TitanAsyncOp::DbQuery { conn, query } => {
859
- let conn_str = conn;
860
- let query_str = query;
861
-
862
- let res = tokio::task::spawn_blocking(move || {
863
- let mut pool_guard = DB_POOL.lock().unwrap();
864
- if let Some(pool) = pool_guard.as_mut() {
865
- if let Some(client) = pool.get_mut(&conn_str) {
866
- match client.query(&query_str, &[]) {
867
- Ok(rows) => {
868
- let mut arr = Vec::new();
869
- for row in rows {
870
- let mut obj = serde_json::Map::new();
871
- for (i, col) in row.columns().iter().enumerate() {
872
- let name = col.name();
873
- let val = if let Ok(v) = row.try_get::<_, String>(i) {
874
- serde_json::Value::String(v)
875
- } else if let Ok(v) = row.try_get::<_, i32>(i) {
876
- serde_json::Value::Number(serde_json::Number::from(v))
877
- } else if let Ok(v) = row.try_get::<_, bool>(i) {
878
- serde_json::Value::Bool(v)
879
- } else {
880
- serde_json::Value::Null
881
- };
882
- obj.insert(name.to_string(), val);
883
- }
884
- arr.push(serde_json::Value::Object(obj));
885
- }
886
- Ok(serde_json::Value::Array(arr))
887
- },
888
- Err(e) => Err(e.to_string())
889
- }
890
- } else {
891
- Err("Connection not found".to_string())
938
+ }
939
+
940
+ // =========================
941
+ // DB QUERY
942
+ // =========================
943
+ super::TitanAsyncOp::DbQuery { conn: _, query, params } => {
944
+
945
+ let pool = match DB_POOL.get() {
946
+ Some(p) => p,
947
+ None => {
948
+ return serde_json::json!({
949
+ "error": "DB pool not initialized"
950
+ });
951
+ }
952
+ };
953
+
954
+ match pool.get().await {
955
+ Ok(client) => {
956
+
957
+ let stmt = match client.prepare(&query).await {
958
+ Ok(s) => s,
959
+ Err(e) => {
960
+ return serde_json::json!({
961
+ "error": e.to_string()
962
+ });
963
+ }
964
+ };
965
+
966
+ let param_refs: Vec<&(dyn tokio_postgres::types::ToSql + Sync)> =
967
+ params.iter()
968
+ .map(|p| p as &(dyn tokio_postgres::types::ToSql + Sync))
969
+ .collect();
970
+
971
+ match client.query(&stmt, &param_refs).await {
972
+ Ok(rows) => {
973
+
974
+ let mut result = Vec::new();
975
+
976
+ for row in rows {
977
+ let mut obj = serde_json::Map::new();
978
+
979
+ for (i, col) in row.columns().iter().enumerate() {
980
+
981
+ let val =
982
+ if let Ok(v) = row.try_get::<_, String>(i) {
983
+ serde_json::Value::String(v)
984
+ } else if let Ok(v) = row.try_get::<_, i64>(i) {
985
+ serde_json::Value::Number(v.into())
986
+ } else if let Ok(v) = row.try_get::<_, i32>(i) {
987
+ serde_json::Value::Number(v.into())
988
+ } else if let Ok(v) = row.try_get::<_, bool>(i) {
989
+ serde_json::Value::Bool(v)
990
+ } else {
991
+ serde_json::Value::Null
992
+ };
993
+
994
+ obj.insert(col.name().to_string(), val);
995
+ }
996
+
997
+ result.push(serde_json::Value::Object(obj));
998
+ }
999
+
1000
+ serde_json::Value::Array(result)
1001
+ }
1002
+ Err(e) => serde_json::json!({
1003
+ "error": e.to_string()
1004
+ }),
892
1005
  }
893
- } else {
894
- Err("Pool not initialized".to_string())
895
1006
  }
896
- }).await;
897
-
898
- match res {
899
- Ok(Ok(v)) => v,
900
- Ok(Err(s)) => serde_json::json!({ "error": s }),
901
- Err(e) => serde_json::json!({ "error": e.to_string() })
1007
+ Err(e) => serde_json::json!({
1008
+ "error": e.to_string()
1009
+ }),
902
1010
  }
903
- },
1011
+ }
1012
+
1013
+ // =========================
1014
+ // FS READ
1015
+ // =========================
904
1016
  super::TitanAsyncOp::FsRead { path } => {
905
- let root = super::PROJECT_ROOT.get().cloned().unwrap_or(std::path::PathBuf::from("."));
1017
+
1018
+ let root = super::PROJECT_ROOT
1019
+ .get()
1020
+ .cloned()
1021
+ .unwrap_or(std::path::PathBuf::from("."));
1022
+
906
1023
  let target = root.join(&path);
907
-
908
- let safe = target.canonicalize().map(|p| p.starts_with(root.canonicalize().unwrap_or(root.clone()))).unwrap_or(false);
909
-
1024
+
1025
+ let safe = target
1026
+ .canonicalize()
1027
+ .map(|p| {
1028
+ p.starts_with(
1029
+ root.canonicalize()
1030
+ .unwrap_or(root.clone())
1031
+ )
1032
+ })
1033
+ .unwrap_or(false);
1034
+
910
1035
  if safe {
911
- match tokio::fs::read_to_string(target).await {
912
- Ok(c) => serde_json::json!({ "data": c }),
913
- Err(e) => serde_json::json!({ "error": e.to_string() })
914
- }
1036
+ match tokio::fs::read_to_string(target).await {
1037
+ Ok(c) => serde_json::json!({ "data": c }),
1038
+ Err(e) => serde_json::json!({ "error": e.to_string() }),
1039
+ }
915
1040
  } else {
916
- serde_json::json!({ "error": "Access denied" })
1041
+ serde_json::json!({ "error": "Access denied" })
917
1042
  }
918
- },
1043
+ }
1044
+
1045
+ // =========================
1046
+ // BATCH
1047
+ // =========================
919
1048
  super::TitanAsyncOp::Batch(ops) => {
1049
+
920
1050
  let mut res = Vec::new();
1051
+
921
1052
  for op in ops {
922
1053
  res.push(run_async_operation(op).await);
923
1054
  }
1055
+
924
1056
  serde_json::Value::Array(res)
925
1057
  }
926
1058
  }