@mostajs/orm-cli 0.5.0 → 0.5.1

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 +278 -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,282 @@ 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}v${RESET}) View the raw tree file"
1802
+ echo -e " ${CYAN}c${RESET}) Clear (delete the tree file — DESTRUCTIVE)"
1803
+ echo
1804
+ echo -e " ${CYAN}b${RESET}) Back"
1805
+ echo
1806
+ local choice
1807
+ choice=$(ask "Choice" "2")
1808
+ case "$choice" in
1809
+ 1) action_rep_add_replica ;;
1810
+ 2) action_rep_list_replicas ;;
1811
+ 3) action_rep_promote ;;
1812
+ 4) action_rep_remove_replica ;;
1813
+ 5) action_rep_set_routing ;;
1814
+ 6) action_rep_add_rule ;;
1815
+ 7) action_rep_list_rules ;;
1816
+ 8) action_rep_sync ;;
1817
+ 9) action_rep_remove_rule ;;
1818
+ v|V) action_rep_view_tree ;;
1819
+ c|C) action_rep_clear ;;
1820
+ b|B) return ;;
1821
+ *) warn "Unknown"; pause ;;
1822
+ esac
1823
+ menu_replicator
1824
+ }
1825
+
1826
+ action_rep_add_replica() {
1827
+ local project name role dialect uri lag
1828
+ project=$(ask "Project name" "default")
1829
+ name=$(ask "Replica name" "master")
1830
+ role=$(ask "Role (master|slave)" "master")
1831
+ dialect=$(ask "Dialect" "${DB_DIALECT:-postgres}")
1832
+ uri=$(ask "URI" "${SGBD_URI:-postgres://user:pass@localhost:5432/db}")
1833
+ if [[ "$role" == "slave" ]]; then
1834
+ lag=$(ask "Lag tolerance (ms)" "5000")
1835
+ else
1836
+ lag="0"
1837
+ fi
1838
+ _replicator_run "
1839
+ await rm.addReplica('$project', {
1840
+ name: '$name', role: '$role', dialect: '$dialect', uri: '$uri',
1841
+ lagTolerance: $lag,
1842
+ });
1843
+ await save();
1844
+ console.log(' ✓ replica added : $name (' + '$role' + ') on project $project');
1845
+ "
1846
+ pause
1847
+ }
1848
+
1849
+ action_rep_list_replicas() {
1850
+ local project
1851
+ project=$(ask "Project name" "default")
1852
+ _replicator_run "
1853
+ const status = rm.getReplicaStatus('$project');
1854
+ if (!status || status.length === 0) {
1855
+ console.log(' (no replicas registered for project $project)');
1856
+ } else {
1857
+ for (const r of status) {
1858
+ console.log(' ' + (r.role === 'master' ? '★' : '•') + ' ' + r.name + ' [' + r.role + '] lag=' + (r.lag ?? 'n/a') + 'ms');
1859
+ }
1860
+ }
1861
+ "
1862
+ pause
1863
+ }
1864
+
1865
+ action_rep_promote() {
1866
+ local project name
1867
+ project=$(ask "Project name" "default")
1868
+ name=$(ask "Slave to promote" "slave-1")
1869
+ if ! confirm "Promote '$name' to master on project '$project'?"; then return; fi
1870
+ _replicator_run "
1871
+ await rm.promoteToMaster('$project', '$name');
1872
+ await save();
1873
+ console.log(' ✓ $name is now the master of $project');
1874
+ "
1875
+ pause
1876
+ }
1877
+
1878
+ action_rep_remove_replica() {
1879
+ local project name
1880
+ project=$(ask "Project name" "default")
1881
+ name=$(ask "Replica name" "slave-1")
1882
+ if ! confirm "Remove replica '$name' from project '$project'?"; then return; fi
1883
+ _replicator_run "
1884
+ await rm.removeReplica('$project', '$name');
1885
+ await save();
1886
+ console.log(' ✓ removed : $name');
1887
+ "
1888
+ pause
1889
+ }
1890
+
1891
+ action_rep_set_routing() {
1892
+ local project strategy
1893
+ project=$(ask "Project name" "default")
1894
+ strategy=$(ask "Strategy (round-robin | least-lag | random)" "least-lag")
1895
+ _replicator_run "
1896
+ rm.setReadRouting('$project', '$strategy');
1897
+ await save();
1898
+ console.log(' ✓ read routing on $project = $strategy');
1899
+ "
1900
+ pause
1901
+ }
1902
+
1903
+ action_rep_add_rule() {
1904
+ local name source target mode colls conflict
1905
+ name=$(ask "Rule name" "pg-to-mongo")
1906
+ source=$(ask "Source project" "secuaccess")
1907
+ target=$(ask "Target project" "analytics")
1908
+ mode=$(ask "Mode (snapshot | cdc | bidirectional)" "cdc")
1909
+ colls=$(ask "Collections (comma-separated)" "users,clients")
1910
+ conflict=$(ask "Conflict resolution (source-wins | target-wins | timestamp)" "source-wins")
1911
+ _replicator_run "
1912
+ rm.addReplicationRule({
1913
+ name: '$name', source: '$source', target: '$target', mode: '$mode',
1914
+ collections: '$colls'.split(',').map(s => s.trim()).filter(Boolean),
1915
+ conflictResolution: '$conflict',
1916
+ enabled: true,
1917
+ });
1918
+ await save();
1919
+ console.log(' ✓ rule added : $name ($source → $target, mode=$mode)');
1920
+ "
1921
+ pause
1922
+ }
1923
+
1924
+ action_rep_list_rules() {
1925
+ _replicator_run "
1926
+ const rules = rm.listRules();
1927
+ if (!rules || rules.length === 0) {
1928
+ console.log(' (no CDC rules registered)');
1929
+ } else {
1930
+ for (const r of rules) {
1931
+ const flag = r.enabled ? '✓' : '✗';
1932
+ console.log(' ' + flag + ' ' + r.name + ' ' + r.source + ' → ' + r.target + ' [' + r.mode + '] ' + r.collections.join(','));
1933
+ }
1934
+ }
1935
+ "
1936
+ pause
1937
+ }
1938
+
1939
+ action_rep_sync() {
1940
+ local rule
1941
+ rule=$(ask "Rule name" "pg-to-mongo")
1942
+ _replicator_run "
1943
+ const stats = await rm.sync('$rule');
1944
+ console.log(' ✓ sync complete');
1945
+ console.log(' inserted: ' + (stats.inserted ?? 0));
1946
+ console.log(' updated : ' + (stats.updated ?? 0));
1947
+ console.log(' deleted : ' + (stats.deleted ?? 0));
1948
+ console.log(' failed : ' + (stats.failed ?? 0));
1949
+ await save();
1950
+ "
1951
+ pause
1952
+ }
1953
+
1954
+ action_rep_remove_rule() {
1955
+ local name
1956
+ name=$(ask "Rule name" "pg-to-mongo")
1957
+ if ! confirm "Remove CDC rule '$name'?"; then return; fi
1958
+ _replicator_run "
1959
+ rm.removeReplicationRule('$name');
1960
+ await save();
1961
+ console.log(' ✓ removed : $name');
1962
+ "
1963
+ pause
1964
+ }
1965
+
1966
+ action_rep_view_tree() {
1967
+ local tree_file
1968
+ tree_file=$(_replicator_tree_file)
1969
+ if [[ -f "$tree_file" ]]; then
1970
+ echo -e "${DIM}${tree_file}${RESET}"
1971
+ echo
1972
+ if command -v jq >/dev/null 2>&1; then
1973
+ jq . "$tree_file"
1974
+ else
1975
+ cat "$tree_file"
1976
+ fi
1977
+ else
1978
+ warn "No tree file yet at $tree_file"
1979
+ fi
1980
+ pause
1981
+ }
1982
+
1983
+ action_rep_clear() {
1984
+ local tree_file
1985
+ tree_file=$(_replicator_tree_file)
1986
+ if ! confirm "DELETE the replicator tree file ($tree_file)?"; then return; fi
1987
+ rm -f "$tree_file"
1988
+ ok " tree file deleted"
1989
+ pause
1990
+ }
1991
+
1714
1992
  # ------------------------------------------------------------
1715
1993
  # Export entities → Prisma / JSON Schema / OpenAPI / Native TS
1716
1994
  # ------------------------------------------------------------
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.1",
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",