@mostajs/orm-cli 0.5.4 → 0.5.6

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 +136 -59
  2. package/package.json +1 -1
package/bin/mostajs.sh CHANGED
@@ -1938,7 +1938,8 @@ action_rep_add_replica() {
1938
1938
  dim " Use a SHORT IDENTIFIER here (NOT a database URI — the URI comes later)."
1939
1939
  echo
1940
1940
  local project name role dialect uri lag
1941
- project=$(ask "Project name (short id, e.g. 'fitzone')" "fitzone")
1941
+ # Use the smart project picker (lists known projects, suggests first)
1942
+ project=$(_pick_project)
1942
1943
  # Basic validation : reject URIs or paths
1943
1944
  if [[ "$project" == *"://"* || "$project" == *"/"* ]]; then
1944
1945
  err " Project name looks like a URI or path. Use a short identifier (e.g. 'fitzone')."
@@ -1961,48 +1962,113 @@ action_rep_add_replica() {
1961
1962
  else
1962
1963
  lag="0"
1963
1964
  fi
1964
- _replicator_run "
1965
- // Auto-register the project in ProjectManager if it's not already there —
1966
- // addReplica() requires the project to exist. We use the replica's own
1967
- // dialect/uri as the project config (it's typically the master).
1968
- const existing = typeof pm.listProjects === 'function' ? pm.listProjects() : [];
1969
- const known = Array.isArray(existing) && existing.some(p => (p.name ?? p) === '$project');
1970
- if (!known) {
1971
- const method = ['addProject','createProject','registerProject'].find(m => typeof pm[m] === 'function');
1972
- if (method) {
1973
- await pm[method]({ name: '$project', dialect: '$dialect', uri: '$uri', schemas: [] });
1974
- console.log(' project \\'$project\\' registered in ProjectManager');
1975
- }
1976
- }
1977
- await rm.addReplica('$project', {
1978
- name: '$name', role: '$role', dialect: '$dialect', uri: '$uri',
1979
- lagTolerance: $lag,
1980
- });
1981
- await save();
1965
+ # Direct tree-JSON patch — preserves URI verbatim (no masking), no DB
1966
+ # connection needed. Validation of the URI happens later when replicator
1967
+ # service or sync action actually touches the DB.
1968
+ _tree_patch "
1969
+ tree.replicas['$project'] = tree.replicas['$project'] || {};
1970
+ tree.replicas['$project']['$name'] = {
1971
+ role: '$role',
1972
+ dialect: '$dialect',
1973
+ uri: '$uri',
1974
+ pool: { min: 2, max: 20 },
1975
+ ...( '$role' === 'slave' ? { lagTolerance: $lag } : {} ),
1976
+ };
1982
1977
  console.log(' ✓ replica added : $name (' + '$role' + ') on project $project');
1983
1978
  "
1984
1979
  pause
1985
1980
  }
1986
1981
 
1982
+ # ----------------------------------------------------------------
1983
+ # Tree-file direct manipulation helpers
1984
+ # ----------------------------------------------------------------
1985
+ # The replicator's saveToFile() masks credentials (oracle://u:***@host)
1986
+ # which makes loadFromFile() unable to reconnect. For add/list/remove we
1987
+ # bypass the lib and patch the tree JSON directly, preserving the
1988
+ # original URI verbatim. The lib is still used for sync() / promote()
1989
+ # where it has logic beyond state tracking.
1990
+
1991
+ _tree_file() { _replicator_tree_file; }
1992
+
1993
+ _tree_read_json() {
1994
+ # Dump the tree JSON on stdout, empty-tree fallback if file missing.
1995
+ local tree_file
1996
+ tree_file=$(_tree_file)
1997
+ node -e "
1998
+ const fs = require('fs');
1999
+ const def = { replicas: {}, rules: {}, routing: {} };
2000
+ try { console.log(JSON.stringify(Object.assign(def, JSON.parse(fs.readFileSync('$tree_file','utf8'))))); }
2001
+ catch { console.log(JSON.stringify(def)); }
2002
+ "
2003
+ }
2004
+
2005
+ # List known projects from the replicator-tree.json — if any — and propose
2006
+ # the first one as default. Echoes the chosen project name to stdout.
2007
+ _pick_project() {
2008
+ local projects=()
2009
+ while IFS= read -r p; do projects+=("$p"); done < <(
2010
+ _tree_read_json | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{for(const k of Object.keys(JSON.parse(d).replicas||{}))console.log(k)})"
2011
+ )
2012
+ local default_project="fitzone"
2013
+ if [[ ${#projects[@]} -gt 0 ]]; then
2014
+ default_project="${projects[0]}"
2015
+ if [[ ${#projects[@]} -gt 1 ]]; then
2016
+ echo >&2
2017
+ dim " Known projects : ${projects[*]}" >&2
2018
+ fi
2019
+ fi
2020
+ local picked
2021
+ picked=$(ask "Project name" "$default_project")
2022
+ echo "$picked"
2023
+ }
2024
+
2025
+ # Patch the tree JSON : exec a Node snippet with `tree` in scope.
2026
+ # After the snippet runs, the mutated tree is written back.
2027
+ _tree_patch() {
2028
+ local snippet="$1"
2029
+ local tree_file
2030
+ tree_file=$(_tree_file)
2031
+ mkdir -p "$(dirname "$tree_file")"
2032
+ TREE_FILE="$tree_file" node --input-type=module -e "
2033
+ const { readFileSync, writeFileSync, existsSync } = await import('fs');
2034
+ const { mkdirSync } = await import('fs');
2035
+ const def = { replicas: {}, rules: {}, routing: {} };
2036
+ let tree = def;
2037
+ if (existsSync(process.env.TREE_FILE)) {
2038
+ try { tree = Object.assign(def, JSON.parse(readFileSync(process.env.TREE_FILE, 'utf8'))); }
2039
+ catch { /* start fresh */ }
2040
+ }
2041
+ try { $snippet }
2042
+ catch (e) { console.error(' ✗ ' + e.message); process.exit(1); }
2043
+ writeFileSync(process.env.TREE_FILE, JSON.stringify(tree, null, 2) + '\n');
2044
+ "
2045
+ }
2046
+
1987
2047
  action_rep_list_replicas() {
1988
2048
  local project
1989
- project=$(ask "Project name" "default")
1990
- _replicator_run "
1991
- const status = rm.getReplicaStatus('$project');
1992
- if (!status || status.length === 0) {
1993
- console.log(' (no replicas registered for project $project)');
1994
- } else {
1995
- for (const r of status) {
1996
- console.log(' ' + (r.role === 'master' ? '★' : '•') + ' ' + r.name + ' [' + r.role + '] lag=' + (r.lag ?? 'n/a') + 'ms');
2049
+ project=$(_pick_project)
2050
+ echo
2051
+ _tree_read_json | PROJECT="$project" node -e "
2052
+ let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{
2053
+ const tree = JSON.parse(d);
2054
+ const reps = tree.replicas[process.env.PROJECT] || {};
2055
+ const entries = Object.entries(reps);
2056
+ if (entries.length === 0) {
2057
+ console.log(' (no replicas registered for project ' + process.env.PROJECT + ')');
2058
+ return;
1997
2059
  }
1998
- }
2060
+ for (const [name, cfg] of entries) {
2061
+ const star = cfg.role === 'master' ? '\x1b[36m★\x1b[0m' : '•';
2062
+ console.log(' ' + star + ' ' + name + ' [' + cfg.role + '] ' + cfg.dialect + ' ' + (cfg.uri ?? '').replace(/:[^:@]+@/, ':***@'));
2063
+ }
2064
+ });
1999
2065
  "
2000
2066
  pause
2001
2067
  }
2002
2068
 
2003
2069
  action_rep_promote() {
2004
2070
  local project name
2005
- project=$(ask "Project name" "default")
2071
+ project=$(_pick_project)
2006
2072
  name=$(ask "Slave to promote" "slave-1")
2007
2073
  if ! confirm "Promote '$name' to master on project '$project'?"; then return; fi
2008
2074
  _replicator_run "
@@ -2015,24 +2081,27 @@ action_rep_promote() {
2015
2081
 
2016
2082
  action_rep_remove_replica() {
2017
2083
  local project name
2018
- project=$(ask "Project name" "default")
2084
+ project=$(_pick_project)
2019
2085
  name=$(ask "Replica name" "slave-1")
2020
2086
  if ! confirm "Remove replica '$name' from project '$project'?"; then return; fi
2021
- _replicator_run "
2022
- await rm.removeReplica('$project', '$name');
2023
- await save();
2024
- console.log(' ✓ removed : $name');
2087
+ _tree_patch "
2088
+ if (!tree.replicas['$project'] || !tree.replicas['$project']['$name']) {
2089
+ console.log(' • not found : $project/$name');
2090
+ } else {
2091
+ delete tree.replicas['$project']['$name'];
2092
+ if (Object.keys(tree.replicas['$project']).length === 0) delete tree.replicas['$project'];
2093
+ console.log(' ✓ removed : $project/$name');
2094
+ }
2025
2095
  "
2026
2096
  pause
2027
2097
  }
2028
2098
 
2029
2099
  action_rep_set_routing() {
2030
2100
  local project strategy
2031
- project=$(ask "Project name" "default")
2101
+ project=$(_pick_project)
2032
2102
  strategy=$(ask "Strategy (round-robin | least-lag | random)" "least-lag")
2033
- _replicator_run "
2034
- rm.setReadRouting('$project', '$strategy');
2035
- await save();
2103
+ _tree_patch "
2104
+ tree.routing['$project'] = '$strategy';
2036
2105
  console.log(' ✓ read routing on $project = $strategy');
2037
2106
  "
2038
2107
  pause
@@ -2040,36 +2109,41 @@ action_rep_set_routing() {
2040
2109
 
2041
2110
  action_rep_add_rule() {
2042
2111
  local name source target mode colls conflict
2043
- name=$(ask "Rule name" "pg-to-mongo")
2044
- source=$(ask "Source project" "secuaccess")
2045
- target=$(ask "Target project" "analytics")
2112
+ # Pick source/target from known projects
2113
+ source=$(_pick_project)
2114
+ local src_default="$source"
2115
+ name=$(ask "Rule name (short, e.g. 'pg-to-mongo')" "cdc-${src_default}")
2116
+ target=$(ask "Target project" "$src_default")
2046
2117
  mode=$(ask "Mode (snapshot | cdc | bidirectional)" "cdc")
2047
2118
  colls=$(ask "Collections (comma-separated)" "users,clients")
2048
2119
  conflict=$(ask "Conflict resolution (source-wins | target-wins | timestamp)" "source-wins")
2049
- _replicator_run "
2050
- rm.addReplicationRule({
2051
- name: '$name', source: '$source', target: '$target', mode: '$mode',
2120
+ _tree_patch "
2121
+ tree.rules['$name'] = {
2122
+ source: '$source', target: '$target', mode: '$mode',
2052
2123
  collections: '$colls'.split(',').map(s => s.trim()).filter(Boolean),
2053
2124
  conflictResolution: '$conflict',
2054
2125
  enabled: true,
2055
- });
2056
- await save();
2126
+ };
2057
2127
  console.log(' ✓ rule added : $name ($source → $target, mode=$mode)');
2058
2128
  "
2059
2129
  pause
2060
2130
  }
2061
2131
 
2062
2132
  action_rep_list_rules() {
2063
- _replicator_run "
2064
- const rules = rm.listRules();
2065
- if (!rules || rules.length === 0) {
2066
- console.log(' (no CDC rules registered)');
2067
- } else {
2068
- for (const r of rules) {
2069
- const flag = r.enabled ? '✓' : '✗';
2070
- console.log(' ' + flag + ' ' + r.name + ' ' + r.source + ' → ' + r.target + ' [' + r.mode + '] ' + r.collections.join(','));
2133
+ echo
2134
+ _tree_read_json | node -e "
2135
+ let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{
2136
+ const tree = JSON.parse(d);
2137
+ const rules = Object.entries(tree.rules || {});
2138
+ if (rules.length === 0) {
2139
+ console.log(' (no CDC rules registered)');
2140
+ return;
2071
2141
  }
2072
- }
2142
+ for (const [name, r] of rules) {
2143
+ const flag = r.enabled ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
2144
+ console.log(' ' + flag + ' ' + name + ' ' + r.source + ' → ' + r.target + ' [' + r.mode + '] ' + (r.collections||[]).join(','));
2145
+ }
2146
+ });
2073
2147
  "
2074
2148
  pause
2075
2149
  }
@@ -2093,10 +2167,13 @@ action_rep_remove_rule() {
2093
2167
  local name
2094
2168
  name=$(ask "Rule name" "pg-to-mongo")
2095
2169
  if ! confirm "Remove CDC rule '$name'?"; then return; fi
2096
- _replicator_run "
2097
- rm.removeReplicationRule('$name');
2098
- await save();
2099
- console.log(' ✓ removed : $name');
2170
+ _tree_patch "
2171
+ if (!tree.rules['$name']) {
2172
+ console.log(' • not found : $name');
2173
+ } else {
2174
+ delete tree.rules['$name'];
2175
+ console.log(' ✓ removed : $name');
2176
+ }
2100
2177
  "
2101
2178
  pause
2102
2179
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/orm-cli",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
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",