@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.
- package/bin/mostajs.sh +278 -0
- 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.
|
|
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",
|