@mostajs/orm-cli 0.4.7 → 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 +667 -2
- package/package.json +1 -1
package/bin/mostajs.sh
CHANGED
|
@@ -27,7 +27,7 @@ CLI_NAME="mostajs"
|
|
|
27
27
|
# PATHS — relative to the CALLER's CWD, not the script
|
|
28
28
|
# ============================================================
|
|
29
29
|
|
|
30
|
-
PROJECT_ROOT="$(pwd)"
|
|
30
|
+
PROJECT_ROOT="${PROJECT_ROOT:-$(pwd)}"
|
|
31
31
|
CONFIG_DIR="$PROJECT_ROOT/.mostajs"
|
|
32
32
|
CONFIG_FILE="$CONFIG_DIR/config.env"
|
|
33
33
|
LOG_DIR="$CONFIG_DIR/logs"
|
|
@@ -214,7 +214,16 @@ header() {
|
|
|
214
214
|
# ============================================================
|
|
215
215
|
|
|
216
216
|
load_env() {
|
|
217
|
-
|
|
217
|
+
# Source .mostajs/config.env WITHOUT overriding env vars that are already
|
|
218
|
+
# set (CLI invocation with DB_DIALECT=... mostajs ... takes precedence).
|
|
219
|
+
[[ -f "$CONFIG_FILE" ]] || return
|
|
220
|
+
while IFS='=' read -r key value; do
|
|
221
|
+
[[ -z "$key" || "$key" =~ ^# ]] && continue
|
|
222
|
+
# Trim whitespace from key and strip surrounding quotes from value
|
|
223
|
+
key="${key// /}"
|
|
224
|
+
value="${value%\"}"; value="${value#\"}"
|
|
225
|
+
[[ -z "${!key+x}" ]] && export "$key=$value"
|
|
226
|
+
done < "$CONFIG_FILE"
|
|
218
227
|
}
|
|
219
228
|
|
|
220
229
|
save_var() {
|
|
@@ -458,6 +467,7 @@ menu_main() {
|
|
|
458
467
|
echo -e " ${CYAN}9${RESET}) Generate boilerplate (src/db.ts with bridge)"
|
|
459
468
|
echo -e " ${CYAN}s${RESET}) ${BOLD}Seeding${RESET} (upload / validate / apply seed data)"
|
|
460
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"
|
|
461
471
|
echo -e " ${GREEN}b${RESET}) ${BOLD}Bootstrap${RESET} — one-shot migration of a Prisma project"
|
|
462
472
|
echo -e " ${GREEN}i${RESET}) ${BOLD}Install bridge${RESET} — codemod PrismaClient → bridge (dry-run / apply / restore)"
|
|
463
473
|
echo -e " ${CYAN}0${RESET}) About / Help"
|
|
@@ -478,6 +488,7 @@ menu_main() {
|
|
|
478
488
|
9) action_generate_boilerplate ;;
|
|
479
489
|
s|S) menu_seeding ;;
|
|
480
490
|
e|E) action_export_entities ;;
|
|
491
|
+
r|R) menu_replicator ;;
|
|
481
492
|
b|B) menu_bootstrap ;;
|
|
482
493
|
i|I) menu_install_bridge ;;
|
|
483
494
|
0) action_about ;;
|
|
@@ -1702,6 +1713,282 @@ menu_seeding() {
|
|
|
1702
1713
|
menu_seeding
|
|
1703
1714
|
}
|
|
1704
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
|
+
|
|
1705
1992
|
# ------------------------------------------------------------
|
|
1706
1993
|
# Export entities → Prisma / JSON Schema / OpenAPI / Native TS
|
|
1707
1994
|
# ------------------------------------------------------------
|
|
@@ -2636,6 +2923,364 @@ EOF
|
|
|
2636
2923
|
pause
|
|
2637
2924
|
}
|
|
2638
2925
|
|
|
2926
|
+
# ============================================================
|
|
2927
|
+
# `mostajs init` — scaffold a new project
|
|
2928
|
+
# ============================================================
|
|
2929
|
+
#
|
|
2930
|
+
# Creates every file a fresh project needs to run on the bridge :
|
|
2931
|
+
# - .env with PORT / DB_DIALECT / SGBD_URI / AUTH_SECRET
|
|
2932
|
+
# - prisma/schema.prisma (minimal User model — starting point)
|
|
2933
|
+
# - src/lib/db.ts (createPrismaLikeDb)
|
|
2934
|
+
# - .mostajs/config.env (mirrors .env for the seed-runner)
|
|
2935
|
+
# - .mostajs/generated/entities.json (empty array, filled by menu 1)
|
|
2936
|
+
#
|
|
2937
|
+
# Dialect defaults to sqlite ./data.sqlite. Pass --dialect=postgres etc.
|
|
2938
|
+
# Refuses to overwrite existing files unless --force.
|
|
2939
|
+
|
|
2940
|
+
action_cli_init() {
|
|
2941
|
+
local dialect="sqlite"
|
|
2942
|
+
local uri=""
|
|
2943
|
+
local force=0
|
|
2944
|
+
while [[ $# -gt 0 ]]; do
|
|
2945
|
+
case "$1" in
|
|
2946
|
+
--dialect) dialect="$2"; shift 2 ;;
|
|
2947
|
+
--dialect=*) dialect="${1#*=}"; shift ;;
|
|
2948
|
+
--uri) uri="$2"; shift 2 ;;
|
|
2949
|
+
--uri=*) uri="${1#*=}"; shift ;;
|
|
2950
|
+
--force|-f) force=1; shift ;;
|
|
2951
|
+
*) warn "Unknown flag: $1"; shift ;;
|
|
2952
|
+
esac
|
|
2953
|
+
done
|
|
2954
|
+
|
|
2955
|
+
# Default URIs per dialect
|
|
2956
|
+
if [[ -z "$uri" ]]; then
|
|
2957
|
+
case "$dialect" in
|
|
2958
|
+
sqlite) uri="./data.sqlite" ;;
|
|
2959
|
+
postgres) uri="postgres://user:pass@localhost:5432/mydb" ;;
|
|
2960
|
+
mysql) uri="mysql://user:pass@localhost:3306/mydb" ;;
|
|
2961
|
+
mariadb) uri="mariadb://user:pass@localhost:3306/mydb" ;;
|
|
2962
|
+
mongodb) uri="mongodb://user:pass@localhost:27017/mydb" ;;
|
|
2963
|
+
oracle) uri="oracle://user:pass@localhost:1521/XE" ;;
|
|
2964
|
+
mssql) uri="mssql://user:pass@localhost:1433/mydb" ;;
|
|
2965
|
+
cockroachdb) uri="postgresql://user:pass@localhost:26257/mydb?sslmode=disable" ;;
|
|
2966
|
+
*) uri="./data.sqlite"; dialect="sqlite" ;;
|
|
2967
|
+
esac
|
|
2968
|
+
fi
|
|
2969
|
+
|
|
2970
|
+
header
|
|
2971
|
+
echo -e "${BOLD}${MAGENTA}▶ mostajs init — scaffold a bridge-ready project${RESET}"
|
|
2972
|
+
echo
|
|
2973
|
+
echo -e " Dialect : ${CYAN}${dialect}${RESET}"
|
|
2974
|
+
echo -e " URI : ${DIM}${uri}${RESET}"
|
|
2975
|
+
echo -e " Root : ${DIM}${PROJECT_ROOT}${RESET}"
|
|
2976
|
+
echo
|
|
2977
|
+
|
|
2978
|
+
local created=0 skipped=0
|
|
2979
|
+
|
|
2980
|
+
write_if_missing() {
|
|
2981
|
+
local path="$1"; local content="$2"
|
|
2982
|
+
if [[ -f "$PROJECT_ROOT/$path" && $force -eq 0 ]]; then
|
|
2983
|
+
dim " - skip $path (exists — use --force to overwrite)"
|
|
2984
|
+
((skipped++))
|
|
2985
|
+
return
|
|
2986
|
+
fi
|
|
2987
|
+
mkdir -p "$(dirname "$PROJECT_ROOT/$path")"
|
|
2988
|
+
printf '%s' "$content" > "$PROJECT_ROOT/$path"
|
|
2989
|
+
ok "created $path"
|
|
2990
|
+
((created++))
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
# --- .env ---
|
|
2994
|
+
local secret
|
|
2995
|
+
secret=$(node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" 2>/dev/null || echo 'CHANGE-ME-IN-PROD')
|
|
2996
|
+
write_if_missing ".env" "\
|
|
2997
|
+
# Port — used by next dev / start (reads PORT from here)
|
|
2998
|
+
PORT=3000
|
|
2999
|
+
|
|
3000
|
+
# Database — consumed by @mostajs/orm-bridge (createPrismaLikeDb)
|
|
3001
|
+
DB_DIALECT=${dialect}
|
|
3002
|
+
SGBD_URI=${uri}
|
|
3003
|
+
DB_SCHEMA_STRATEGY=update
|
|
3004
|
+
|
|
3005
|
+
# NextAuth (if you use it)
|
|
3006
|
+
NEXTAUTH_URL=http://localhost:3000
|
|
3007
|
+
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
3008
|
+
AUTH_SECRET=${secret}
|
|
3009
|
+
"
|
|
3010
|
+
|
|
3011
|
+
# --- .mostajs/config.env (mirror for the seed-runner) ---
|
|
3012
|
+
write_if_missing ".mostajs/config.env" "\
|
|
3013
|
+
DB_DIALECT=${dialect}
|
|
3014
|
+
SGBD_URI=${uri}
|
|
3015
|
+
DB_SCHEMA_STRATEGY=update
|
|
3016
|
+
APP_PORT=3000
|
|
3017
|
+
"
|
|
3018
|
+
|
|
3019
|
+
# --- .mostajs/generated/entities.json (empty — filled by menu 1) ---
|
|
3020
|
+
write_if_missing ".mostajs/generated/entities.json" "[]
|
|
3021
|
+
"
|
|
3022
|
+
|
|
3023
|
+
# --- prisma/schema.prisma (minimal starter) ---
|
|
3024
|
+
# Prisma's valid providers : sqlite, postgresql, mysql, mongodb, sqlserver, cockroachdb
|
|
3025
|
+
local provider="$dialect"
|
|
3026
|
+
case "$dialect" in
|
|
3027
|
+
postgres|postgresql) provider="postgresql" ;;
|
|
3028
|
+
mssql) provider="sqlserver" ;;
|
|
3029
|
+
mariadb) provider="mysql" ;;
|
|
3030
|
+
oracle|db2|hana|hsqldb|spanner|sybase) provider="sqlite" ;; # Prisma has no native provider — keep sqlite placeholder
|
|
3031
|
+
esac
|
|
3032
|
+
write_if_missing "prisma/schema.prisma" "\
|
|
3033
|
+
// Minimal starter — edit freely. Run 'mostajs' menu 1 to convert to EntitySchema.
|
|
3034
|
+
generator client {
|
|
3035
|
+
provider = \"prisma-client-js\"
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
datasource db {
|
|
3039
|
+
provider = \"${provider}\"
|
|
3040
|
+
url = env(\"DATABASE_URL\")
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
model User {
|
|
3044
|
+
id String @id @default(uuid())
|
|
3045
|
+
email String @unique
|
|
3046
|
+
password String
|
|
3047
|
+
name String?
|
|
3048
|
+
createdAt DateTime @default(now())
|
|
3049
|
+
updatedAt DateTime @updatedAt
|
|
3050
|
+
}
|
|
3051
|
+
"
|
|
3052
|
+
|
|
3053
|
+
# --- src/lib/db.ts (createPrismaLikeDb) ---
|
|
3054
|
+
write_if_missing "src/lib/db.ts" "\
|
|
3055
|
+
// Generated by 'mostajs init' — @mostajs/orm-bridge entry point.
|
|
3056
|
+
// Every Prisma-style db.User.findUnique(...) call below is routed to
|
|
3057
|
+
// @mostajs/orm (13 dialects). Edit DB_DIALECT / SGBD_URI in .env to switch.
|
|
3058
|
+
import { createPrismaLikeDb } from '@mostajs/orm-bridge/prisma-client'
|
|
3059
|
+
|
|
3060
|
+
export const db = createPrismaLikeDb()
|
|
3061
|
+
"
|
|
3062
|
+
|
|
3063
|
+
echo
|
|
3064
|
+
echo -e " ${BOLD}${created}${RESET} file(s) created, ${DIM}${skipped}${RESET} skipped"
|
|
3065
|
+
echo
|
|
3066
|
+
echo -e " ${BOLD}Next steps${RESET} :"
|
|
3067
|
+
echo -e " ${CYAN}1.${RESET} npm install @mostajs/orm @mostajs/orm-bridge @mostajs/orm-cli --legacy-peer-deps"
|
|
3068
|
+
echo -e " ${CYAN}2.${RESET} Edit ${DIM}prisma/schema.prisma${RESET} — add your models"
|
|
3069
|
+
echo -e " ${CYAN}3.${RESET} ${CYAN}mostajs${RESET} → menu 1 (Convert) → menu 3 (init DDL)"
|
|
3070
|
+
echo -e " ${CYAN}4.${RESET} ${CYAN}mostajs${RESET} → menu S (Seeds) — populate, hash, apply"
|
|
3071
|
+
echo -e " ${CYAN}5.${RESET} ${CYAN}npm run dev${RESET}"
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
# ============================================================
|
|
3075
|
+
# `mostajs migrate` — incremental DDL diff / apply / status
|
|
3076
|
+
# ============================================================
|
|
3077
|
+
#
|
|
3078
|
+
# Subcommands :
|
|
3079
|
+
# diff — list ALTERs needed to make the live DB match entities.json
|
|
3080
|
+
# apply — execute those ALTERs (prompts for confirmation, --yes to skip)
|
|
3081
|
+
# status — show entities.json count + live tables count + missing columns
|
|
3082
|
+
|
|
3083
|
+
action_cli_migrate() {
|
|
3084
|
+
local sub="${1:-}"
|
|
3085
|
+
[[ -z "$sub" ]] && { action_migrate_help; return; }
|
|
3086
|
+
shift
|
|
3087
|
+
case "$sub" in
|
|
3088
|
+
diff|d) action_migrate_diff "$@" ;;
|
|
3089
|
+
apply|a) action_migrate_apply "$@" ;;
|
|
3090
|
+
status|s) action_migrate_status "$@" ;;
|
|
3091
|
+
help|h|--help) action_migrate_help ;;
|
|
3092
|
+
*) err "Unknown migrate subcommand: $sub"; action_migrate_help; return 1 ;;
|
|
3093
|
+
esac
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
action_migrate_help() {
|
|
3097
|
+
cat <<EOF
|
|
3098
|
+
|
|
3099
|
+
${BOLD}mostajs migrate${RESET} — incremental schema migration
|
|
3100
|
+
|
|
3101
|
+
${CYAN}diff${RESET} show ALTER statements the DB needs to match entities.json
|
|
3102
|
+
${CYAN}apply${RESET} execute those ALTERs (prompts for confirmation)
|
|
3103
|
+
flags : --yes (skip confirmation)
|
|
3104
|
+
${CYAN}status${RESET} show live-vs-schema summary per entity
|
|
3105
|
+
|
|
3106
|
+
Every subcommand honors DB_DIALECT + SGBD_URI from ${DIM}.mostajs/config.env${RESET}.
|
|
3107
|
+
|
|
3108
|
+
EOF
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
# Node helper : compare live columns vs schema.fields and emit ALTER plan as JSON.
|
|
3112
|
+
# Outputs to stdout : { changes: [{ table, column, sql }], ok: bool }
|
|
3113
|
+
_migrate_compute_plan() {
|
|
3114
|
+
load_env
|
|
3115
|
+
local entities_json="$GENERATED_DIR/entities.json"
|
|
3116
|
+
if [[ ! -f "$entities_json" ]]; then
|
|
3117
|
+
err "No entities.json — run menu 1 (Convert) first."
|
|
3118
|
+
return 1
|
|
3119
|
+
fi
|
|
3120
|
+
ENT_PATH="$entities_json" DIALECT="$DB_DIALECT" URI="$SGBD_URI" \
|
|
3121
|
+
node --input-type=module -e "
|
|
3122
|
+
import { readFileSync } from 'node:fs';
|
|
3123
|
+
import { getDialect } from '${PROJECT_ROOT}/node_modules/@mostajs/orm/dist/index.js';
|
|
3124
|
+
const entities = JSON.parse(readFileSync(process.env.ENT_PATH, 'utf8'));
|
|
3125
|
+
const d = await getDialect({ dialect: process.env.DIALECT, uri: process.env.URI, schemaStrategy: 'none' });
|
|
3126
|
+
|
|
3127
|
+
// Use the dialect's own introspection — protected method, exposed via cast
|
|
3128
|
+
const changes = [];
|
|
3129
|
+
for (const e of entities) {
|
|
3130
|
+
let live;
|
|
3131
|
+
try {
|
|
3132
|
+
live = await (d).getExistingColumns(e.collection);
|
|
3133
|
+
} catch {
|
|
3134
|
+
changes.push({ table: e.collection, column: '*', sql: '-- (cannot introspect — run menu 3 first)' });
|
|
3135
|
+
continue;
|
|
3136
|
+
}
|
|
3137
|
+
const hasCol = (name) => {
|
|
3138
|
+
const lc = name.toLowerCase();
|
|
3139
|
+
for (const c of live) if (c.toLowerCase() === lc) return true;
|
|
3140
|
+
return false;
|
|
3141
|
+
};
|
|
3142
|
+
// Field columns
|
|
3143
|
+
for (const [name, f] of Object.entries(e.fields || {})) {
|
|
3144
|
+
if (name === '_id') continue;
|
|
3145
|
+
if (hasCol(name)) continue;
|
|
3146
|
+
// Reconstruct the ALTER — d has fieldToSqlType + getIdColumnType + quoteIdentifier
|
|
3147
|
+
const q = (n) => (d).quoteIdentifier(n);
|
|
3148
|
+
let sql;
|
|
3149
|
+
if (name === 'id') {
|
|
3150
|
+
sql = 'ALTER TABLE ' + q(e.collection) + ' ADD ' + q('id') + ' ' + (d).getIdColumnType();
|
|
3151
|
+
} else {
|
|
3152
|
+
sql = 'ALTER TABLE ' + q(e.collection) + ' ADD ' + q(name) + ' ' + (d).fieldToSqlType(f);
|
|
3153
|
+
}
|
|
3154
|
+
changes.push({ table: e.collection, column: name, sql });
|
|
3155
|
+
}
|
|
3156
|
+
// Relation FK columns
|
|
3157
|
+
for (const [rname, rel] of Object.entries(e.relations || {})) {
|
|
3158
|
+
if (rel.type !== 'many-to-one' && rel.type !== 'one-to-one') continue;
|
|
3159
|
+
const colName = rel.joinColumn || (rname + 'Id');
|
|
3160
|
+
if (hasCol(colName)) continue;
|
|
3161
|
+
const q = (n) => (d).quoteIdentifier(n);
|
|
3162
|
+
changes.push({
|
|
3163
|
+
table: e.collection, column: colName,
|
|
3164
|
+
sql: 'ALTER TABLE ' + q(e.collection) + ' ADD ' + q(colName) + ' ' + (d).getIdColumnType(),
|
|
3165
|
+
});
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
await d.disconnect();
|
|
3169
|
+
console.log(JSON.stringify({ ok: true, changes }));
|
|
3170
|
+
"
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
action_migrate_diff() {
|
|
3174
|
+
header
|
|
3175
|
+
echo -e "${BOLD}${MAGENTA}▶ mostajs migrate diff${RESET}"
|
|
3176
|
+
echo
|
|
3177
|
+
local plan_json
|
|
3178
|
+
plan_json=$(_migrate_compute_plan) || { pause; return 1; }
|
|
3179
|
+
local count
|
|
3180
|
+
count=$(echo "$plan_json" | node -e "process.stdin.on('data',d=>{console.log(JSON.parse(d).changes.length)})" 2>/dev/null || echo '?')
|
|
3181
|
+
if [[ "$count" == "0" ]]; then
|
|
3182
|
+
ok "Schema is up to date — nothing to ALTER."
|
|
3183
|
+
return 0
|
|
3184
|
+
fi
|
|
3185
|
+
echo -e " ${BOLD}${count}${RESET} pending change(s) :"
|
|
3186
|
+
echo
|
|
3187
|
+
echo "$plan_json" | node -e "
|
|
3188
|
+
let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{
|
|
3189
|
+
const p = JSON.parse(d);
|
|
3190
|
+
for (const ch of p.changes) console.log(' ' + ch.sql + ';');
|
|
3191
|
+
});
|
|
3192
|
+
"
|
|
3193
|
+
echo
|
|
3194
|
+
echo -e " Run ${CYAN}mostajs migrate apply${RESET} to execute these statements."
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
action_migrate_apply() {
|
|
3198
|
+
local auto_yes=0
|
|
3199
|
+
[[ "${1:-}" == "--yes" || "${1:-}" == "-y" ]] && auto_yes=1
|
|
3200
|
+
header
|
|
3201
|
+
echo -e "${BOLD}${MAGENTA}▶ mostajs migrate apply${RESET}"
|
|
3202
|
+
echo
|
|
3203
|
+
local plan_json
|
|
3204
|
+
plan_json=$(_migrate_compute_plan) || return 1
|
|
3205
|
+
local count
|
|
3206
|
+
count=$(echo "$plan_json" | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d).changes.length))" 2>/dev/null || echo 0)
|
|
3207
|
+
if [[ "$count" == "0" ]]; then
|
|
3208
|
+
ok "Schema is up to date — nothing to ALTER."
|
|
3209
|
+
return 0
|
|
3210
|
+
fi
|
|
3211
|
+
echo " Pending : ${BOLD}${count}${RESET} statement(s)"
|
|
3212
|
+
echo
|
|
3213
|
+
echo "$plan_json" | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{for(const ch of JSON.parse(d).changes) console.log(' ' + ch.sql + ';')})"
|
|
3214
|
+
echo
|
|
3215
|
+
if [[ $auto_yes -eq 0 ]]; then
|
|
3216
|
+
if ! confirm "Execute these ALTER statements?"; then
|
|
3217
|
+
dim " Aborted."
|
|
3218
|
+
return
|
|
3219
|
+
fi
|
|
3220
|
+
fi
|
|
3221
|
+
|
|
3222
|
+
# Execute
|
|
3223
|
+
load_env
|
|
3224
|
+
PLAN="$plan_json" DIALECT="$DB_DIALECT" URI="$SGBD_URI" \
|
|
3225
|
+
node --input-type=module -e "
|
|
3226
|
+
import { getDialect } from '${PROJECT_ROOT}/node_modules/@mostajs/orm/dist/index.js';
|
|
3227
|
+
const plan = JSON.parse(process.env.PLAN);
|
|
3228
|
+
const d = await getDialect({ dialect: process.env.DIALECT, uri: process.env.URI, schemaStrategy: 'none' });
|
|
3229
|
+
let ok = 0, fail = 0;
|
|
3230
|
+
for (const ch of plan.changes) {
|
|
3231
|
+
try {
|
|
3232
|
+
await d.executeRun(ch.sql, []);
|
|
3233
|
+
console.log(' ✓ ' + ch.table + '.' + ch.column);
|
|
3234
|
+
ok++;
|
|
3235
|
+
} catch (e) {
|
|
3236
|
+
console.error(' ✗ ' + ch.table + '.' + ch.column + ' : ' + e.message);
|
|
3237
|
+
fail++;
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
await d.disconnect();
|
|
3241
|
+
console.log('\nApplied : ' + ok + ' ok, ' + fail + ' failed');
|
|
3242
|
+
process.exit(fail > 0 ? 1 : 0);
|
|
3243
|
+
"
|
|
3244
|
+
}
|
|
3245
|
+
|
|
3246
|
+
action_migrate_status() {
|
|
3247
|
+
header
|
|
3248
|
+
echo -e "${BOLD}${MAGENTA}▶ mostajs migrate status${RESET}"
|
|
3249
|
+
echo
|
|
3250
|
+
load_env
|
|
3251
|
+
local ent_json="$GENERATED_DIR/entities.json"
|
|
3252
|
+
if [[ ! -f "$ent_json" ]]; then
|
|
3253
|
+
err "No entities.json — run menu 1 (Convert) first."
|
|
3254
|
+
return 1
|
|
3255
|
+
fi
|
|
3256
|
+
ENT_PATH="$ent_json" DIALECT="$DB_DIALECT" URI="$SGBD_URI" \
|
|
3257
|
+
node --input-type=module -e "
|
|
3258
|
+
import { readFileSync } from 'node:fs';
|
|
3259
|
+
import { getDialect } from '${PROJECT_ROOT}/node_modules/@mostajs/orm/dist/index.js';
|
|
3260
|
+
const entities = JSON.parse(readFileSync(process.env.ENT_PATH, 'utf8'));
|
|
3261
|
+
const d = await getDialect({ dialect: process.env.DIALECT, uri: process.env.URI, schemaStrategy: 'none' });
|
|
3262
|
+
let existing = 0, missing = 0, lagging = 0;
|
|
3263
|
+
for (const e of entities) {
|
|
3264
|
+
let live;
|
|
3265
|
+
try { live = await (d).getExistingColumns(e.collection); }
|
|
3266
|
+
catch { live = new Set(); }
|
|
3267
|
+
if (!live || live.size === 0) { console.log(' ✗ ' + e.collection + ' — table not found'); missing++; continue; }
|
|
3268
|
+
const hasCol = (n) => { const lc = n.toLowerCase(); for (const c of live) if (c.toLowerCase() === lc) return true; return false; };
|
|
3269
|
+
const schemaCols = Object.keys(e.fields || {});
|
|
3270
|
+
const need = schemaCols.filter(c => !hasCol(c));
|
|
3271
|
+
if (need.length) {
|
|
3272
|
+
console.log(' ⚠ ' + e.collection + ' — missing ' + need.length + ' column(s) : ' + need.join(', '));
|
|
3273
|
+
lagging++;
|
|
3274
|
+
} else {
|
|
3275
|
+
console.log(' ✓ ' + e.collection + ' (' + live.size + ' cols live, ' + schemaCols.length + ' in schema)');
|
|
3276
|
+
existing++;
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
await d.disconnect();
|
|
3280
|
+
console.log('\n ' + existing + ' up-to-date · ' + lagging + ' need migrate · ' + missing + ' missing');
|
|
3281
|
+
"
|
|
3282
|
+
}
|
|
3283
|
+
|
|
2639
3284
|
# ============================================================
|
|
2640
3285
|
# CLI SUBCOMMANDS (non-interactive)
|
|
2641
3286
|
# ============================================================
|
|
@@ -2721,6 +3366,26 @@ run_subcommand() {
|
|
|
2721
3366
|
health|h)
|
|
2722
3367
|
action_healthcheck
|
|
2723
3368
|
;;
|
|
3369
|
+
init)
|
|
3370
|
+
# mostajs init [--dialect sqlite|postgres|mongodb|...] [--force]
|
|
3371
|
+
# Scaffold a fresh project with bridge-ready layout :
|
|
3372
|
+
# .env (PORT, DB_DIALECT, SGBD_URI, AUTH_SECRET)
|
|
3373
|
+
# prisma/schema.prisma (minimal — User model only)
|
|
3374
|
+
# src/lib/db.ts (createPrismaLikeDb)
|
|
3375
|
+
# .mostajs/config.env (mirrors .env for the runner)
|
|
3376
|
+
# .mostajs/generated/entities.json (empty array)
|
|
3377
|
+
shift
|
|
3378
|
+
action_cli_init "$@"
|
|
3379
|
+
;;
|
|
3380
|
+
migrate|mig|m)
|
|
3381
|
+
# mostajs migrate <subcommand> [options]
|
|
3382
|
+
# Subcommands :
|
|
3383
|
+
# diff — show ALTER statements the target DB needs to match entities.json
|
|
3384
|
+
# apply — execute those ALTERs (with confirmation)
|
|
3385
|
+
# status — show what's in entities.json vs what's live in the DB
|
|
3386
|
+
shift
|
|
3387
|
+
action_cli_migrate "$@"
|
|
3388
|
+
;;
|
|
2724
3389
|
install-bridge|ib)
|
|
2725
3390
|
# mostajs install-bridge [--apply] [--file X] [--project P] [--restore]
|
|
2726
3391
|
# Codemod : scans the project for `new PrismaClient(...)` sites and rewrites
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/orm-cli",
|
|
3
|
-
"version": "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",
|