@mostajs/orm-cli 0.5.0 → 0.5.2

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.
Files changed (2) hide show
  1. package/bin/mostajs.sh +339 -0
  2. package/package.json +1 -1
package/bin/mostajs.sh CHANGED
@@ -467,6 +467,7 @@ menu_main() {
467
467
  echo -e " ${CYAN}9${RESET}) Generate boilerplate (src/db.ts with bridge)"
468
468
  echo -e " ${CYAN}s${RESET}) ${BOLD}Seeding${RESET} (upload / validate / apply seed data)"
469
469
  echo -e " ${CYAN}e${RESET}) ${BOLD}Export entities${RESET} → Prisma / JSON Schema / OpenAPI / Native"
470
+ echo -e " ${CYAN}r${RESET}) ${BOLD}Replicator${RESET} — CQRS master/slave, CDC rules, failover"
470
471
  echo -e " ${GREEN}b${RESET}) ${BOLD}Bootstrap${RESET} — one-shot migration of a Prisma project"
471
472
  echo -e " ${GREEN}i${RESET}) ${BOLD}Install bridge${RESET} — codemod PrismaClient → bridge (dry-run / apply / restore)"
472
473
  echo -e " ${CYAN}0${RESET}) About / Help"
@@ -487,6 +488,7 @@ menu_main() {
487
488
  9) action_generate_boilerplate ;;
488
489
  s|S) menu_seeding ;;
489
490
  e|E) action_export_entities ;;
491
+ r|R) menu_replicator ;;
490
492
  b|B) menu_bootstrap ;;
491
493
  i|I) menu_install_bridge ;;
492
494
  0) action_about ;;
@@ -1711,6 +1713,343 @@ menu_seeding() {
1711
1713
  menu_seeding
1712
1714
  }
1713
1715
 
1716
+ # ------------------------------------------------------------
1717
+ # Replicator menu — CQRS master/slave + cross-dialect CDC
1718
+ # ------------------------------------------------------------
1719
+ #
1720
+ # Thin wrapper around @mostajs/replicator. Every action is routed to
1721
+ # ReplicationManager methods via a Node --input-type=module shell.
1722
+ # State is persisted to $PROJECT_ROOT/.mostajs/replicator-tree.json
1723
+ # (same file format as saveToFile / loadFromFile of the lib).
1724
+
1725
+ _replicator_tree_file() {
1726
+ echo "$PROJECT_ROOT/.mostajs/replicator-tree.json"
1727
+ }
1728
+
1729
+ _replicator_has_lib() {
1730
+ [[ -d "$PROJECT_ROOT/node_modules/@mostajs/replicator" ]]
1731
+ }
1732
+
1733
+ _replicator_run() {
1734
+ # Execute a Node snippet with the replicator loaded. The snippet reads
1735
+ # from stdin, gets `rm` (ReplicationManager), `pm` (ProjectManager) in
1736
+ # scope and is expected to mutate them then call `await save()`.
1737
+ # $1 = inline snippet string.
1738
+ local tree_file
1739
+ tree_file=$(_replicator_tree_file)
1740
+ mkdir -p "$(dirname "$tree_file")"
1741
+ TREE_FILE="$tree_file" RUNTIME_ROOT="$PROJECT_ROOT" \
1742
+ node --input-type=module -e "
1743
+ const { existsSync } = await import('fs');
1744
+ const { ReplicationManager } = await import(process.env.RUNTIME_ROOT + '/node_modules/@mostajs/replicator/dist/index.js');
1745
+ const { ProjectManager } = await import(process.env.RUNTIME_ROOT + '/node_modules/@mostajs/mproject/dist/index.js');
1746
+ const pm = new ProjectManager();
1747
+ const rm = new ReplicationManager(pm);
1748
+ const save = async () => { await rm.saveToFile(process.env.TREE_FILE); };
1749
+ if (existsSync(process.env.TREE_FILE)) {
1750
+ try { await rm.loadFromFile(process.env.TREE_FILE); } catch (e) { console.error(' ⚠ load failed : ' + e.message); }
1751
+ }
1752
+ try {
1753
+ $1
1754
+ } catch (e) {
1755
+ console.error(' ✗ ' + e.message);
1756
+ process.exit(1);
1757
+ } finally {
1758
+ try { await rm.disconnectAll(); } catch {}
1759
+ }
1760
+ "
1761
+ }
1762
+
1763
+ menu_replicator() {
1764
+ header
1765
+ echo -e "${BOLD}${MAGENTA}▶ Replicator — CQRS, CDC, failover${RESET}"
1766
+ echo
1767
+ if ! _replicator_has_lib; then
1768
+ warn "@mostajs/replicator not installed in this project."
1769
+ echo
1770
+ if confirm "Install @mostajs/replicator + @mostajs/mproject now?"; then
1771
+ ensure_pkg "@mostajs/replicator" "@mostajs/mproject" || { pause; return; }
1772
+ else
1773
+ dim " Cannot proceed without the replicator lib."
1774
+ pause; return
1775
+ fi
1776
+ fi
1777
+
1778
+ local tree_file
1779
+ tree_file=$(_replicator_tree_file)
1780
+ echo -e " Tree file : ${DIM}${tree_file}${RESET}"
1781
+ if [[ -f "$tree_file" ]]; then
1782
+ local projects
1783
+ projects=$(node -e "try{const t=JSON.parse(require('fs').readFileSync('$tree_file','utf8'));console.log(Object.keys(t.replicas||{}).length+' projects, '+Object.keys(t.rules||{}).length+' CDC rules')}catch{console.log('?')}" 2>/dev/null)
1784
+ echo -e " State : ${DIM}${projects}${RESET}"
1785
+ else
1786
+ echo -e " State : ${DIM}(empty — will be created on first save)${RESET}"
1787
+ fi
1788
+
1789
+ echo
1790
+ echo -e "${BOLD}━━━ REPLICATOR MENU ━━━${RESET}"
1791
+ echo
1792
+ echo -e " ${CYAN}1${RESET}) Add replica (master / slave) to a project"
1793
+ echo -e " ${CYAN}2${RESET}) List replicas + status / lag"
1794
+ echo -e " ${CYAN}3${RESET}) Promote a slave to master (failover)"
1795
+ echo -e " ${CYAN}4${RESET}) Remove a replica"
1796
+ echo -e " ${CYAN}5${RESET}) Set read-routing strategy (round-robin / least-lag / random)"
1797
+ echo -e " ${CYAN}6${RESET}) Add a CDC rule (pg → mongo, mysql → analytics, …)"
1798
+ echo -e " ${CYAN}7${RESET}) List CDC rules"
1799
+ echo -e " ${CYAN}8${RESET}) Run a CDC sync + show stats"
1800
+ echo -e " ${CYAN}9${RESET}) Remove a CDC rule"
1801
+ echo -e " ${CYAN}m${RESET}) ${BOLD}Open monitor${RESET} (live dashboard — localhost:14499)"
1802
+ echo -e " ${CYAN}v${RESET}) View the raw tree file"
1803
+ echo -e " ${CYAN}c${RESET}) Clear (delete the tree file — DESTRUCTIVE)"
1804
+ echo
1805
+ echo -e " ${CYAN}b${RESET}) Back"
1806
+ echo
1807
+ local choice
1808
+ choice=$(ask "Choice" "2")
1809
+ case "$choice" in
1810
+ 1) action_rep_add_replica ;;
1811
+ 2) action_rep_list_replicas ;;
1812
+ 3) action_rep_promote ;;
1813
+ 4) action_rep_remove_replica ;;
1814
+ 5) action_rep_set_routing ;;
1815
+ 6) action_rep_add_rule ;;
1816
+ 7) action_rep_list_rules ;;
1817
+ 8) action_rep_sync ;;
1818
+ 9) action_rep_remove_rule ;;
1819
+ m|M) action_rep_open_monitor ;;
1820
+ v|V) action_rep_view_tree ;;
1821
+ c|C) action_rep_clear ;;
1822
+ b|B) return ;;
1823
+ *) warn "Unknown"; pause ;;
1824
+ esac
1825
+ menu_replicator
1826
+ }
1827
+
1828
+ action_rep_add_replica() {
1829
+ local project name role dialect uri lag
1830
+ project=$(ask "Project name" "default")
1831
+ name=$(ask "Replica name" "master")
1832
+ role=$(ask "Role (master|slave)" "master")
1833
+ dialect=$(ask "Dialect" "${DB_DIALECT:-postgres}")
1834
+ uri=$(ask "URI" "${SGBD_URI:-postgres://user:pass@localhost:5432/db}")
1835
+ if [[ "$role" == "slave" ]]; then
1836
+ lag=$(ask "Lag tolerance (ms)" "5000")
1837
+ else
1838
+ lag="0"
1839
+ fi
1840
+ _replicator_run "
1841
+ await rm.addReplica('$project', {
1842
+ name: '$name', role: '$role', dialect: '$dialect', uri: '$uri',
1843
+ lagTolerance: $lag,
1844
+ });
1845
+ await save();
1846
+ console.log(' ✓ replica added : $name (' + '$role' + ') on project $project');
1847
+ "
1848
+ pause
1849
+ }
1850
+
1851
+ action_rep_list_replicas() {
1852
+ local project
1853
+ project=$(ask "Project name" "default")
1854
+ _replicator_run "
1855
+ const status = rm.getReplicaStatus('$project');
1856
+ if (!status || status.length === 0) {
1857
+ console.log(' (no replicas registered for project $project)');
1858
+ } else {
1859
+ for (const r of status) {
1860
+ console.log(' ' + (r.role === 'master' ? '★' : '•') + ' ' + r.name + ' [' + r.role + '] lag=' + (r.lag ?? 'n/a') + 'ms');
1861
+ }
1862
+ }
1863
+ "
1864
+ pause
1865
+ }
1866
+
1867
+ action_rep_promote() {
1868
+ local project name
1869
+ project=$(ask "Project name" "default")
1870
+ name=$(ask "Slave to promote" "slave-1")
1871
+ if ! confirm "Promote '$name' to master on project '$project'?"; then return; fi
1872
+ _replicator_run "
1873
+ await rm.promoteToMaster('$project', '$name');
1874
+ await save();
1875
+ console.log(' ✓ $name is now the master of $project');
1876
+ "
1877
+ pause
1878
+ }
1879
+
1880
+ action_rep_remove_replica() {
1881
+ local project name
1882
+ project=$(ask "Project name" "default")
1883
+ name=$(ask "Replica name" "slave-1")
1884
+ if ! confirm "Remove replica '$name' from project '$project'?"; then return; fi
1885
+ _replicator_run "
1886
+ await rm.removeReplica('$project', '$name');
1887
+ await save();
1888
+ console.log(' ✓ removed : $name');
1889
+ "
1890
+ pause
1891
+ }
1892
+
1893
+ action_rep_set_routing() {
1894
+ local project strategy
1895
+ project=$(ask "Project name" "default")
1896
+ strategy=$(ask "Strategy (round-robin | least-lag | random)" "least-lag")
1897
+ _replicator_run "
1898
+ rm.setReadRouting('$project', '$strategy');
1899
+ await save();
1900
+ console.log(' ✓ read routing on $project = $strategy');
1901
+ "
1902
+ pause
1903
+ }
1904
+
1905
+ action_rep_add_rule() {
1906
+ local name source target mode colls conflict
1907
+ name=$(ask "Rule name" "pg-to-mongo")
1908
+ source=$(ask "Source project" "secuaccess")
1909
+ target=$(ask "Target project" "analytics")
1910
+ mode=$(ask "Mode (snapshot | cdc | bidirectional)" "cdc")
1911
+ colls=$(ask "Collections (comma-separated)" "users,clients")
1912
+ conflict=$(ask "Conflict resolution (source-wins | target-wins | timestamp)" "source-wins")
1913
+ _replicator_run "
1914
+ rm.addReplicationRule({
1915
+ name: '$name', source: '$source', target: '$target', mode: '$mode',
1916
+ collections: '$colls'.split(',').map(s => s.trim()).filter(Boolean),
1917
+ conflictResolution: '$conflict',
1918
+ enabled: true,
1919
+ });
1920
+ await save();
1921
+ console.log(' ✓ rule added : $name ($source → $target, mode=$mode)');
1922
+ "
1923
+ pause
1924
+ }
1925
+
1926
+ action_rep_list_rules() {
1927
+ _replicator_run "
1928
+ const rules = rm.listRules();
1929
+ if (!rules || rules.length === 0) {
1930
+ console.log(' (no CDC rules registered)');
1931
+ } else {
1932
+ for (const r of rules) {
1933
+ const flag = r.enabled ? '✓' : '✗';
1934
+ console.log(' ' + flag + ' ' + r.name + ' ' + r.source + ' → ' + r.target + ' [' + r.mode + '] ' + r.collections.join(','));
1935
+ }
1936
+ }
1937
+ "
1938
+ pause
1939
+ }
1940
+
1941
+ action_rep_sync() {
1942
+ local rule
1943
+ rule=$(ask "Rule name" "pg-to-mongo")
1944
+ _replicator_run "
1945
+ const stats = await rm.sync('$rule');
1946
+ console.log(' ✓ sync complete');
1947
+ console.log(' inserted: ' + (stats.inserted ?? 0));
1948
+ console.log(' updated : ' + (stats.updated ?? 0));
1949
+ console.log(' deleted : ' + (stats.deleted ?? 0));
1950
+ console.log(' failed : ' + (stats.failed ?? 0));
1951
+ await save();
1952
+ "
1953
+ pause
1954
+ }
1955
+
1956
+ action_rep_remove_rule() {
1957
+ local name
1958
+ name=$(ask "Rule name" "pg-to-mongo")
1959
+ if ! confirm "Remove CDC rule '$name'?"; then return; fi
1960
+ _replicator_run "
1961
+ rm.removeReplicationRule('$name');
1962
+ await save();
1963
+ console.log(' ✓ removed : $name');
1964
+ "
1965
+ pause
1966
+ }
1967
+
1968
+ action_rep_open_monitor() {
1969
+ header
1970
+ echo -e "${BOLD}${MAGENTA}▶ Open replica-monitor dashboard${RESET}"
1971
+ echo
1972
+ # Ensure the monitor package is installed
1973
+ if [[ ! -d "$PROJECT_ROOT/node_modules/@mostajs/replica-monitor" ]]; then
1974
+ warn "@mostajs/replica-monitor not installed in this project."
1975
+ if confirm "Install it now?"; then
1976
+ ensure_pkg "@mostajs/replica-monitor" || { pause; return; }
1977
+ else
1978
+ pause; return
1979
+ fi
1980
+ fi
1981
+ local tree_file
1982
+ tree_file=$(_replicator_tree_file)
1983
+ local port
1984
+ port=$(ask "Port" "14499")
1985
+ local token
1986
+ token=$(ask "Auth token (empty = no auth, local-only)" "")
1987
+ local url_suffix=""
1988
+ [[ -n "$token" ]] && url_suffix="?token=${token}"
1989
+
1990
+ local log_file="$PROJECT_ROOT/.mostajs/monitor.log"
1991
+ local pid_file="$PROJECT_ROOT/.mostajs/monitor.pid"
1992
+
1993
+ # If already running (pid file exists + process alive) : just open the URL.
1994
+ if [[ -f "$pid_file" ]] && kill -0 "$(cat "$pid_file" 2>/dev/null)" 2>/dev/null; then
1995
+ ok "Monitor already running at http://127.0.0.1:${port}"
1996
+ else
1997
+ # Spawn in background
1998
+ echo -e " ${DIM}spawning mostajs-monitor …${RESET}"
1999
+ MONITOR_TREE="$tree_file" MONITOR_PORT="$port" MONITOR_TOKEN="$token" \
2000
+ nohup node "$PROJECT_ROOT/node_modules/@mostajs/replica-monitor/dist/cli.js" \
2001
+ --tree "$tree_file" --port "$port" --runtime "$PROJECT_ROOT" \
2002
+ ${token:+--token "$token"} \
2003
+ > "$log_file" 2>&1 &
2004
+ local pid=$!
2005
+ echo "$pid" > "$pid_file"
2006
+ sleep 1
2007
+ if kill -0 "$pid" 2>/dev/null; then
2008
+ ok "Monitor started (pid=$pid) → http://127.0.0.1:${port}${url_suffix}"
2009
+ dim " logs : $log_file"
2010
+ dim " stop : kill \$(cat $pid_file) or menu r → m again then Ctrl+C"
2011
+ else
2012
+ err "Monitor failed to start — check $log_file"
2013
+ cat "$log_file" | tail -10
2014
+ pause; return
2015
+ fi
2016
+ fi
2017
+
2018
+ # Try to open in default browser
2019
+ if command -v xdg-open >/dev/null 2>&1; then
2020
+ xdg-open "http://127.0.0.1:${port}${url_suffix}" >/dev/null 2>&1 &
2021
+ elif command -v open >/dev/null 2>&1; then
2022
+ open "http://127.0.0.1:${port}${url_suffix}" >/dev/null 2>&1 &
2023
+ fi
2024
+ pause
2025
+ }
2026
+
2027
+ action_rep_view_tree() {
2028
+ local tree_file
2029
+ tree_file=$(_replicator_tree_file)
2030
+ if [[ -f "$tree_file" ]]; then
2031
+ echo -e "${DIM}${tree_file}${RESET}"
2032
+ echo
2033
+ if command -v jq >/dev/null 2>&1; then
2034
+ jq . "$tree_file"
2035
+ else
2036
+ cat "$tree_file"
2037
+ fi
2038
+ else
2039
+ warn "No tree file yet at $tree_file"
2040
+ fi
2041
+ pause
2042
+ }
2043
+
2044
+ action_rep_clear() {
2045
+ local tree_file
2046
+ tree_file=$(_replicator_tree_file)
2047
+ if ! confirm "DELETE the replicator tree file ($tree_file)?"; then return; fi
2048
+ rm -f "$tree_file"
2049
+ ok " tree file deleted"
2050
+ pause
2051
+ }
2052
+
1714
2053
  # ------------------------------------------------------------
1715
2054
  # Export entities → Prisma / JSON Schema / OpenAPI / Native TS
1716
2055
  # ------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/orm-cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Universal CLI to integrate @mostajs/orm into any project — one-shot `mostajs bootstrap` migrates a Prisma project (codemod + deps + schema convert + DDL) to 13 databases with zero code change.",
5
5
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
6
  "license": "AGPL-3.0-or-later",