@mostajs/orm-cli 0.1.0 → 0.2.0

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 +586 -100
  2. package/package.json +1 -1
package/bin/mostajs.sh CHANGED
@@ -53,6 +53,110 @@ warn() { echo -e " ${YELLOW}⚠${RESET} $*"; }
53
53
  err() { echo -e " ${RED}✗${RESET} $*"; }
54
54
  dim() { echo -e " ${DIM}$*${RESET}"; }
55
55
 
56
+ # ============================================================
57
+ # ERROR HANDLING + AUTO-INSTALL
58
+ # ============================================================
59
+
60
+ # Run a node command with clean error reporting.
61
+ # Usage: run_node <script_path> [env=val ...]
62
+ run_node() {
63
+ local script="$1"; shift
64
+ local envvars=("$@")
65
+ local rc=0
66
+ if [[ ${#envvars[@]} -gt 0 ]]; then
67
+ env "${envvars[@]}" node "$script" 2>&1
68
+ else
69
+ node "$script" 2>&1
70
+ fi
71
+ rc=${PIPESTATUS[0]}
72
+ if [[ $rc -ne 0 ]]; then
73
+ err "Script failed (exit $rc)"
74
+ return $rc
75
+ fi
76
+ return 0
77
+ }
78
+
79
+ # Check if a package is installed locally; if not, offer to install it.
80
+ # Usage: ensure_pkg <package-name> [<additional-pkg> ...]
81
+ ensure_pkg() {
82
+ local pkgs=("$@")
83
+ local missing=()
84
+ for pkg in "${pkgs[@]}"; do
85
+ # Try to resolve via node's require resolution (works for any layout)
86
+ if ! node -e "require.resolve('$pkg')" >/dev/null 2>&1; then
87
+ # Also check if there's a node_modules with it
88
+ if [[ ! -d "$PROJECT_ROOT/node_modules/$pkg" ]]; then
89
+ missing+=("$pkg")
90
+ fi
91
+ fi
92
+ done
93
+
94
+ if [[ ${#missing[@]} -eq 0 ]]; then
95
+ return 0
96
+ fi
97
+
98
+ warn "Missing package(s): ${missing[*]}"
99
+ if confirm "Install now with $PKG_MANAGER?"; then
100
+ cd "$PROJECT_ROOT" || return 1
101
+ case "$PKG_MANAGER" in
102
+ pnpm) pnpm add "${missing[@]}" 2>&1 | tail -5 ;;
103
+ yarn) yarn add "${missing[@]}" 2>&1 | tail -5 ;;
104
+ bun) bun add "${missing[@]}" 2>&1 | tail -5 ;;
105
+ *) npm install --save "${missing[@]}" --legacy-peer-deps 2>&1 | tail -5 ;;
106
+ esac
107
+ local rc=${PIPESTATUS[0]}
108
+ if [[ $rc -ne 0 ]]; then
109
+ err "Install failed"
110
+ return $rc
111
+ fi
112
+ ok "Installed"
113
+ return 0
114
+ else
115
+ err "Cannot proceed without: ${missing[*]}"
116
+ return 1
117
+ fi
118
+ }
119
+
120
+ # Resolve path to a specific installed package module file
121
+ # Usage: resolve_pkg <package>/dist/index.js
122
+ # Writes path to stdout; returns 0 on success, 1 on failure
123
+ resolve_pkg_path() {
124
+ local pkg="$1"
125
+ local result
126
+ result=$(node -e "
127
+ try {
128
+ const p = require.resolve('$pkg', { paths: [process.cwd(), '$PROJECT_ROOT'] });
129
+ console.log(p);
130
+ } catch (e) {
131
+ process.exit(1);
132
+ }
133
+ " 2>/dev/null) || return 1
134
+ echo "$result"
135
+ }
136
+
137
+ # Check if a driver is needed for the given dialect, and install it if missing.
138
+ # Usage: ensure_dialect_driver <dialect>
139
+ ensure_dialect_driver() {
140
+ local dialect="$1"
141
+ local driver=""
142
+ case "$dialect" in
143
+ sqlite) driver="better-sqlite3" ;;
144
+ postgres|cockroachdb) driver="pg" ;;
145
+ mysql) driver="mysql2" ;;
146
+ mariadb) driver="mariadb" ;;
147
+ mssql) driver="mssql" ;;
148
+ oracle) driver="oracledb" ;;
149
+ db2) driver="ibm_db" ;;
150
+ hana) driver="@sap/hana-client" ;;
151
+ spanner) driver="@google-cloud/spanner" ;;
152
+ sybase) driver="sybase" ;;
153
+ mongodb) driver="mongoose" ;;
154
+ *) return 0 ;;
155
+ esac
156
+ [[ -z "$driver" ]] && return 0
157
+ ensure_pkg "$driver"
158
+ }
159
+
56
160
  pause() {
57
161
  echo
58
162
  read -n 1 -r -s -p "$(echo -e "${DIM}Press any key...${RESET}")" || true
@@ -154,29 +258,34 @@ run_adapter_convert() {
154
258
  local input_file="$2"
155
259
  local output_file="$3"
156
260
 
157
- # Check if @mostajs/orm-adapter is installed locally
261
+ # Ensure @mostajs/orm-adapter is available — auto-install if missing
262
+ info "Checking @mostajs/orm-adapter..."
158
263
  local adapter_path=""
264
+
265
+ # 1. Try local project install
159
266
  if [[ -f "$PROJECT_ROOT/node_modules/@mostajs/orm-adapter/dist/index.js" ]]; then
160
267
  adapter_path="$PROJECT_ROOT/node_modules/@mostajs/orm-adapter/dist/index.js"
161
- info "Using local @mostajs/orm-adapter"
268
+ ok "Using local install"
162
269
  else
163
- # Try a neighbor path (dev setup — mosta-orm-adapter may be next to this CLI)
270
+ # 2. Try sibling (dev setup)
164
271
  local cli_dir
165
272
  cli_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
166
273
  if [[ -f "$cli_dir/../mosta-orm-adapter/dist/index.js" ]]; then
167
274
  adapter_path="$cli_dir/../mosta-orm-adapter/dist/index.js"
168
- info "Using sibling @mostajs/orm-adapter from $adapter_path"
275
+ info "Using sibling dev install"
169
276
  else
170
- warn "@mostajs/orm-adapter not found locally."
171
- if confirm "Install it now ($PKG_MANAGER install --save-dev @mostajs/orm-adapter @mostajs/orm)?"; then
172
- cd "$PROJECT_ROOT"
173
- case "$PKG_MANAGER" in
174
- pnpm) pnpm add -D @mostajs/orm-adapter @mostajs/orm ;;
175
- yarn) yarn add -D @mostajs/orm-adapter @mostajs/orm ;;
176
- bun) bun add -D @mostajs/orm-adapter @mostajs/orm ;;
177
- *) npm install --save-dev @mostajs/orm-adapter @mostajs/orm --legacy-peer-deps ;;
178
- esac
277
+ # 3. Offer auto-install
278
+ warn "Not installed locally"
279
+ if ensure_pkg "@mostajs/orm-adapter" "@mostajs/orm"; then
179
280
  adapter_path="$PROJECT_ROOT/node_modules/@mostajs/orm-adapter/dist/index.js"
281
+ if [[ ! -f "$adapter_path" ]]; then
282
+ # Try via require.resolve
283
+ adapter_path=$(resolve_pkg_path "@mostajs/orm-adapter") || {
284
+ err "Install reported success but module cannot be resolved."
285
+ return 1
286
+ }
287
+ fi
288
+ ok "Installed"
180
289
  else
181
290
  err "Cannot proceed without the adapter."
182
291
  return 1
@@ -194,13 +303,42 @@ run_adapter_convert() {
194
303
 
195
304
  cat > "$CONFIG_DIR/convert.mjs" << EOF
196
305
  import { readFileSync, writeFileSync } from 'fs';
197
- import { $adapter_class } from '$adapter_path';
198
306
 
199
- const source = readFileSync('$input_file', 'utf8');
307
+ let adapterModule;
308
+ try {
309
+ adapterModule = await import('$adapter_path');
310
+ } catch (e) {
311
+ console.error('Failed to import adapter from $adapter_path');
312
+ console.error('Reason :', e.message);
313
+ process.exit(2);
314
+ }
315
+ const { $adapter_class } = adapterModule;
316
+ if (!$adapter_class) {
317
+ console.error('$adapter_class not exported from the adapter module');
318
+ process.exit(3);
319
+ }
320
+
321
+ let source;
322
+ try {
323
+ source = readFileSync('$input_file', 'utf8');
324
+ } catch (e) {
325
+ console.error('Cannot read input file : $input_file');
326
+ console.error('Reason :', e.message);
327
+ process.exit(4);
328
+ }
329
+
200
330
  const adapter = new $adapter_class();
201
331
  const warnings = [];
202
332
  const input = '$input_type' === 'jsonschema' ? JSON.parse(source) : source;
203
- const entities = await adapter.toEntitySchema(input, { onWarning: w => warnings.push(w) });
333
+
334
+ let entities;
335
+ try {
336
+ entities = await adapter.toEntitySchema(input, { onWarning: w => warnings.push(w) });
337
+ } catch (e) {
338
+ console.error('Conversion failed :', e.message);
339
+ if (e.details) console.error('Details :', JSON.stringify(e.details, null, 2).slice(0, 500));
340
+ process.exit(5);
341
+ }
204
342
 
205
343
  console.log('entities : ' + entities.length);
206
344
  console.log('warnings : ' + warnings.length);
@@ -217,12 +355,28 @@ const code = header +
217
355
  ' entities.map(e => [e.name, e])\n' +
218
356
  ');\n';
219
357
 
220
- writeFileSync('$output_file', code);
221
- console.log('\u2713 Saved : $output_file');
358
+ try {
359
+ writeFileSync('$output_file', code);
360
+ // Also write .json (easier to load from ESM without TS support)
361
+ const jsonFile = '$output_file'.replace(/\.ts$/, '.json');
362
+ writeFileSync(jsonFile, JSON.stringify(entities, null, 2));
363
+ console.log('\u2713 Saved : $output_file');
364
+ console.log('\u2713 Saved : ' + jsonFile);
365
+ } catch (e) {
366
+ console.error('Cannot write output : ' + e.message);
367
+ process.exit(6);
368
+ }
222
369
  EOF
223
370
 
371
+ local rc=0
224
372
  node "$CONFIG_DIR/convert.mjs" 2>&1 | tee "$LOG_DIR/convert.log"
225
- return "${PIPESTATUS[0]}"
373
+ rc=${PIPESTATUS[0]}
374
+ if [[ $rc -ne 0 ]]; then
375
+ err "Conversion exited with code $rc"
376
+ info "See $LOG_DIR/convert.log for details"
377
+ return $rc
378
+ fi
379
+ return 0
226
380
  }
227
381
 
228
382
  # ============================================================
@@ -356,37 +510,43 @@ menu_databases() {
356
510
  header
357
511
  echo -e "${BOLD}${MAGENTA}▶ Database configuration${RESET}"
358
512
  echo
359
- echo -e " ${CYAN}1${RESET}) MongoDB : ${DIM}${MONGODB_URI:-<not set>}${RESET}"
360
- echo -e " ${CYAN}2${RESET}) PostgreSQL : ${DIM}${POSTGRES_URI:-<not set>}${RESET}"
361
- echo -e " ${CYAN}3${RESET}) MySQL / MariaDB : ${DIM}${MYSQL_URI:-<not set>}${RESET}"
362
- echo -e " ${CYAN}4${RESET}) SQLite : ${DIM}${SQLITE_URI:-<not set>}${RESET}"
363
- echo -e " ${CYAN}5${RESET}) Oracle : ${DIM}${ORACLE_URI:-<not set>}${RESET}"
364
- echo -e " ${CYAN}6${RESET}) MSSQL : ${DIM}${MSSQL_URI:-<not set>}${RESET}"
365
- echo -e " ${CYAN}7${RESET}) DB2 : ${DIM}${DB2_URI:-<not set>}${RESET}"
366
- echo -e " ${CYAN}8${RESET}) HANA : ${DIM}${HANA_URI:-<not set>}${RESET}"
367
- echo -e " ${CYAN}9${RESET}) CockroachDB : ${DIM}${COCKROACH_URI:-<not set>}${RESET}"
513
+ echo -e "${DIM}Compatible with @mostajs/orm .env convention (see SecuAccessPro/.env.local)${RESET}"
514
+ echo
515
+ echo -e "${BOLD}Primary DB (single backend 90% of apps) :${RESET}"
516
+ echo -e " ${CYAN}1${RESET}) DB_DIALECT : ${DIM}${DB_DIALECT:-<not set>}${RESET}"
517
+ echo -e " ${CYAN}2${RESET}) SGBD_URI : ${DIM}${SGBD_URI:-<not set>}${RESET}"
518
+ echo -e " ${CYAN}3${RESET}) DB_SCHEMA_STRATEGY : ${DIM}${DB_SCHEMA_STRATEGY:-update}${RESET}"
519
+ echo -e " ${CYAN}4${RESET}) DB_POOL_SIZE : ${DIM}${DB_POOL_SIZE:-20}${RESET}"
520
+ echo -e " ${CYAN}5${RESET}) DB_SHOW_SQL : ${DIM}${DB_SHOW_SQL:-false}${RESET}"
368
521
  echo
369
- echo -e " ${CYAN}p${RESET}) App port (Next.js/Express) : ${DIM}${APP_PORT:-3000}${RESET}"
370
- echo -e " ${CYAN}n${RESET}) mosta-net port : ${DIM}${MOSTA_NET_PORT:-4447}${RESET}"
522
+ echo -e "${BOLD}Extra DBs (hybrid apps with Prisma Bridge) :${RESET}"
523
+ echo -e " ${CYAN}a${RESET}) Add extra binding ${DIM}(e.g. MongoDB for audit while PG is primary)${RESET}"
524
+ echo -e " ${CYAN}l${RESET}) List extra bindings : ${DIM}${EXTRA_BINDINGS:-<none>}${RESET}"
525
+ echo
526
+ echo -e "${BOLD}mosta-net + app :${RESET}"
527
+ echo -e " ${CYAN}u${RESET}) MOSTA_NET_URL : ${DIM}${MOSTA_NET_URL:-http://localhost:14488}${RESET}"
528
+ echo -e " ${CYAN}n${RESET}) MOSTA_NET_TRANSPORT : ${DIM}${MOSTA_NET_TRANSPORT:-rest}${RESET}"
529
+ echo -e " ${CYAN}p${RESET}) APP_PORT : ${DIM}${APP_PORT:-3000}${RESET}"
371
530
  echo
372
531
  echo -e " ${CYAN}t${RESET}) Test all connections"
532
+ echo -e " ${CYAN}e${RESET}) Export to .env.local in project"
373
533
  echo -e " ${CYAN}r${RESET}) Reset config"
374
534
  echo -e " ${CYAN}b${RESET}) Back"
375
535
  echo
376
536
  local choice; choice=$(ask "Choice" "1")
377
537
  case "$choice" in
378
- 1) save_var MONGODB_URI "$(ask 'MongoDB URI' "${MONGODB_URI:-mongodb://localhost:27017/app}")";;
379
- 2) save_var POSTGRES_URI "$(ask 'PostgreSQL URI' "${POSTGRES_URI:-postgres://user:pw@localhost:5432/app}")";;
380
- 3) save_var MYSQL_URI "$(ask 'MySQL/MariaDB URI' "${MYSQL_URI:-mysql://user:pw@localhost:3306/app}")";;
381
- 4) save_var SQLITE_URI "$(ask 'SQLite path or :memory:' "${SQLITE_URI:-./data.sqlite}")";;
382
- 5) save_var ORACLE_URI "$(ask 'Oracle URI' "${ORACLE_URI:-oracle://user:pw@localhost:1521/ORCLPDB}")";;
383
- 6) save_var MSSQL_URI "$(ask 'MSSQL URI' "${MSSQL_URI:-mssql://user:pw@localhost:1433/app}")";;
384
- 7) save_var DB2_URI "$(ask 'DB2 URI' "${DB2_URI:-db2://user:pw@localhost:50000/app}")";;
385
- 8) save_var HANA_URI "$(ask 'HANA URI' "${HANA_URI:-hana://user:pw@localhost:39041}")";;
386
- 9) save_var COCKROACH_URI "$(ask 'CockroachDB URI' "${COCKROACH_URI:-postgres://user@localhost:26257/app}")";;
387
- p|P) save_var APP_PORT "$(ask 'App port' "${APP_PORT:-3000}")";;
388
- n|N) save_var MOSTA_NET_PORT "$(ask 'mosta-net port' "${MOSTA_NET_PORT:-4447}")";;
538
+ 1) prompt_dialect ;;
539
+ 2) save_var SGBD_URI "$(ask 'SGBD_URI (path or connection string)' "${SGBD_URI:-./data.sqlite}")";;
540
+ 3) save_var DB_SCHEMA_STRATEGY "$(ask 'DB_SCHEMA_STRATEGY (update|create|validate|none|create-drop)' "${DB_SCHEMA_STRATEGY:-update}")";;
541
+ 4) save_var DB_POOL_SIZE "$(ask 'DB_POOL_SIZE' "${DB_POOL_SIZE:-20}")";;
542
+ 5) save_var DB_SHOW_SQL "$(ask 'DB_SHOW_SQL (true|false)' "${DB_SHOW_SQL:-false}")";;
543
+ a|A) add_extra_binding ;;
544
+ l|L) list_extra_bindings; pause ;;
545
+ u|U) save_var MOSTA_NET_URL "$(ask 'MOSTA_NET_URL' "${MOSTA_NET_URL:-http://localhost:14488}")";;
546
+ n|N) save_var MOSTA_NET_TRANSPORT "$(ask 'MOSTA_NET_TRANSPORT (rest|sse|graphql|mcp|websocket|jsonrpc|grpc|odata)' "${MOSTA_NET_TRANSPORT:-rest}")";;
547
+ p|P) save_var APP_PORT "$(ask 'APP_PORT' "${APP_PORT:-3000}")";;
389
548
  t|T) action_test_connections; return;;
549
+ e|E) export_env_local ;;
390
550
  r|R) confirm "Really reset config?" && rm -f "$CONFIG_FILE" && ok "Reset";;
391
551
  b|B) return;;
392
552
  *) warn "Unknown";;
@@ -395,38 +555,235 @@ menu_databases() {
395
555
  menu_databases
396
556
  }
397
557
 
558
+ # Prompt user to choose a dialect from a numbered list
559
+ prompt_dialect() {
560
+ echo
561
+ echo "Pick a dialect:"
562
+ local i=1
563
+ local -a dialects=(sqlite postgres mysql mariadb mongodb mssql oracle db2 cockroachdb hana hsqldb spanner sybase)
564
+ for d in "${dialects[@]}"; do
565
+ echo -e " ${CYAN}$i${RESET}) $d"
566
+ i=$((i+1))
567
+ done
568
+ local num; num=$(ask "Number" 1)
569
+ local idx=$((num-1))
570
+ if [[ $idx -ge 0 && $idx -lt ${#dialects[@]} ]]; then
571
+ local d="${dialects[$idx]}"
572
+ save_var DB_DIALECT "$d"
573
+ # Suggest default URI for that dialect if SGBD_URI is empty
574
+ if [[ -z "${SGBD_URI:-}" ]]; then
575
+ local suggest
576
+ case "$d" in
577
+ sqlite) suggest="./data.sqlite" ;;
578
+ postgres) suggest="postgres://user:pw@localhost:5432/app" ;;
579
+ mysql|mariadb) suggest="mysql://user:pw@localhost:3306/app" ;;
580
+ mongodb) suggest="mongodb://localhost:27017/app" ;;
581
+ mssql) suggest="mssql://user:pw@localhost:1433/app" ;;
582
+ oracle) suggest="oracle://user:pw@localhost:1521/ORCLPDB" ;;
583
+ db2) suggest="db2://user:pw@localhost:50000/app" ;;
584
+ cockroachdb) suggest="postgres://user@localhost:26257/app" ;;
585
+ hana) suggest="hana://user:pw@localhost:39041" ;;
586
+ *) suggest="" ;;
587
+ esac
588
+ [[ -n "$suggest" ]] && save_var SGBD_URI "$(ask 'SGBD_URI' "$suggest")"
589
+ fi
590
+ else
591
+ warn "Invalid number"
592
+ fi
593
+ }
594
+
595
+ # Add an extra dialect binding for hybrid apps
596
+ add_extra_binding() {
597
+ local name; name=$(ask "Binding name (e.g. AuditLog, Reports) — used by the Prisma Bridge")
598
+ [[ -z "$name" ]] && return
599
+ local dialect; dialect=$(ask "Dialect for $name (sqlite|postgres|mongodb|oracle|...)")
600
+ [[ -z "$dialect" ]] && return
601
+ local uri; uri=$(ask "URI for $name")
602
+ [[ -z "$uri" ]] && return
603
+ local current="${EXTRA_BINDINGS:-}"
604
+ local new="${name}:${dialect}:${uri}"
605
+ if [[ -z "$current" ]]; then
606
+ save_var EXTRA_BINDINGS "$new"
607
+ else
608
+ save_var EXTRA_BINDINGS "${current};${new}"
609
+ fi
610
+ ok "Added: $name ($dialect @ $uri)"
611
+ }
612
+
613
+ list_extra_bindings() {
614
+ echo
615
+ if [[ -z "${EXTRA_BINDINGS:-}" ]]; then
616
+ dim " (none)"
617
+ return
618
+ fi
619
+ echo -e "${BOLD}Extra bindings:${RESET}"
620
+ local IFS=';'
621
+ for b in $EXTRA_BINDINGS; do
622
+ local name="${b%%:*}"
623
+ local rest="${b#*:}"
624
+ local dialect="${rest%%:*}"
625
+ local uri="${rest#*:}"
626
+ echo -e " ${CYAN}$name${RESET} → ${MAGENTA}$dialect${RESET} @ ${DIM}$uri${RESET}"
627
+ done
628
+ }
629
+
630
+ # Export a .env.local file compatible with @mostajs/orm convention
631
+ export_env_local() {
632
+ local target="$PROJECT_ROOT/.env.mostajs"
633
+ cat > "$target" <<EOF
634
+ # Generated by @mostajs/orm-cli on $(date -u +%Y-%m-%dT%H:%M:%SZ)
635
+ # Primary database
636
+ DB_DIALECT=${DB_DIALECT:-sqlite}
637
+ SGBD_URI=${SGBD_URI:-./data.sqlite}
638
+ DB_SCHEMA_STRATEGY=${DB_SCHEMA_STRATEGY:-update}
639
+ DB_POOL_SIZE=${DB_POOL_SIZE:-20}
640
+ DB_SHOW_SQL=${DB_SHOW_SQL:-false}
641
+
642
+ # mosta-net server
643
+ MOSTA_NET_URL=${MOSTA_NET_URL:-http://localhost:14488}
644
+ MOSTA_NET_TRANSPORT=${MOSTA_NET_TRANSPORT:-rest}
645
+
646
+ # App
647
+ APP_PORT=${APP_PORT:-3000}
648
+
649
+ # Extra bindings for Prisma Bridge (hybrid apps)
650
+ # Format: EXTRA_BINDINGS="ModelName:dialect:uri;OtherModel:dialect:uri"
651
+ EXTRA_BINDINGS=${EXTRA_BINDINGS:-}
652
+ EOF
653
+ ok "Exported : $target"
654
+ info "Review, rename to .env.local, and commit to your .env.example (without secrets)"
655
+ }
656
+
657
+ # Auto-detect mosta-orm dialect from URI scheme
658
+ detect_dialect_from_uri() {
659
+ local uri="$1"
660
+ case "$uri" in
661
+ mongodb://*|mongodb+srv://*) echo "mongodb" ;;
662
+ postgres://*|postgresql://*) echo "postgres" ;;
663
+ mysql://*) echo "mysql" ;;
664
+ mariadb://*) echo "mariadb" ;;
665
+ mssql://*|sqlserver://*) echo "mssql" ;;
666
+ oracle://*) echo "oracle" ;;
667
+ db2://*) echo "db2" ;;
668
+ hana://*) echo "hana" ;;
669
+ cockroachdb://*) echo "cockroachdb" ;;
670
+ spanner://*) echo "spanner" ;;
671
+ sybase://*) echo "sybase" ;;
672
+ sqlite://*|sqlite:*|*.sqlite|*.db|:memory:) echo "sqlite" ;;
673
+ *) echo "unknown" ;;
674
+ esac
675
+ }
676
+
677
+ # Strip scheme prefix from URI (used for SQLite path)
678
+ strip_uri_scheme() {
679
+ local uri="$1"
680
+ case "$uri" in
681
+ sqlite://*) echo "${uri#sqlite://}" ;;
682
+ sqlite:*) echo "${uri#sqlite:}" ;;
683
+ *) echo "$uri" ;;
684
+ esac
685
+ }
686
+
398
687
  action_test_connections() {
399
688
  header
400
- echo -e "${BOLD}${MAGENTA}▶ Testing connections${RESET}"
689
+ echo -e "${BOLD}${MAGENTA}▶ Testing connections (via @mostajs/orm)${RESET}"
401
690
  echo
402
691
  load_env
403
- local tested=0
404
-
405
- if [[ -n "${MONGODB_URI:-}" ]]; then
406
- info "MongoDB : $MONGODB_URI"
407
- if command -v mongosh >/dev/null 2>&1; then
408
- echo "db.stats()" | mongosh "$MONGODB_URI" --quiet >/dev/null 2>&1 && ok "reachable" || err "failed"
409
- else warn "mongosh not installed"; fi
410
- tested=$((tested+1))
411
- fi
412
-
413
- for pair in "POSTGRES_URI:psql" "MYSQL_URI:mysql" "MSSQL_URI:sqlcmd" "ORACLE_URI:sqlplus"; do
414
- local var="${pair%%:*}"
415
- local tool="${pair##*:}"
416
- local uri="${!var:-}"
417
- [[ -z "$uri" ]] && continue
418
- info "$var : $uri"
419
- if command -v "$tool" >/dev/null 2>&1; then
420
- case "$tool" in
421
- psql) psql "$uri" -c "SELECT 1" >/dev/null 2>&1 && ok reachable || err failed;;
422
- mysql) mysql --defaults-file="/dev/null" -e "SELECT 1" >/dev/null 2>&1 && ok reachable || warn "manual test needed";;
423
- *) warn "tested via tool $tool — manual check";;
424
- esac
425
- else warn "$tool not installed"; fi
426
- tested=$((tested+1))
692
+
693
+ # Collect all configured URIs as (dialect, uri) pairs
694
+ local -a pairs=()
695
+
696
+ # Primary DB
697
+ if [[ -n "${DB_DIALECT:-}" && -n "${SGBD_URI:-}" ]]; then
698
+ pairs+=("${DB_DIALECT}|${SGBD_URI}")
699
+ fi
700
+
701
+ # Extra bindings (format: name:dialect:uri;name:dialect:uri)
702
+ if [[ -n "${EXTRA_BINDINGS:-}" ]]; then
703
+ local IFS=';'
704
+ for b in $EXTRA_BINDINGS; do
705
+ local rest="${b#*:}" # strip name
706
+ local dialect="${rest%%:*}"
707
+ local uri="${rest#*:}"
708
+ pairs+=("$dialect|$uri")
709
+ done
710
+ IFS=$' \t\n'
711
+ fi
712
+
713
+ if [[ ${#pairs[@]} -eq 0 ]]; then
714
+ warn "No URIs configured. Go to menu 2 first."
715
+ pause; return
716
+ fi
717
+
718
+ # Ensure @mostajs/orm is installed (test uses its native testConnection)
719
+ info "Checking @mostajs/orm installation..."
720
+ if ! ensure_pkg "@mostajs/orm"; then
721
+ err "Cannot test connections without @mostajs/orm"
722
+ pause; return
723
+ fi
724
+
725
+ # Ensure drivers for each dialect being tested
726
+ info "Checking drivers..."
727
+ for p in "${pairs[@]}"; do
728
+ local d="${p%%|*}"
729
+ ensure_dialect_driver "$d" || warn "Driver for $d may be missing"
730
+ done
731
+
732
+ local orm_path
733
+ orm_path=$(resolve_pkg_path "@mostajs/orm") || {
734
+ err "Cannot resolve @mostajs/orm"
735
+ pause; return
736
+ }
737
+
738
+ # Build a small node script that tests each connection
739
+ local -a args=()
740
+ for p in "${pairs[@]}"; do
741
+ args+=("$p")
427
742
  done
428
743
 
429
- [[ $tested -eq 0 ]] && warn "No URIs configured"
744
+ cat > "$CONFIG_DIR/test-connections.mjs" <<EOF
745
+ import { getDialect } from '$orm_path';
746
+
747
+ const pairs = process.argv.slice(2).map(s => {
748
+ const i = s.indexOf('|');
749
+ return [s.slice(0, i), s.slice(i + 1)];
750
+ });
751
+
752
+ function stripScheme(uri) {
753
+ if (uri.startsWith('sqlite://')) return uri.slice(9);
754
+ if (uri.startsWith('sqlite:')) return uri.slice(7);
755
+ return uri;
756
+ }
757
+
758
+ let ok = 0, fail = 0;
759
+ for (const [dialect, rawUri] of pairs) {
760
+ const uri = dialect === 'sqlite' ? stripScheme(rawUri) : rawUri;
761
+ process.stdout.write(dialect.padEnd(12) + ' ' + rawUri + '\n');
762
+ try {
763
+ const d = await getDialect({ dialect, uri });
764
+ const alive = await d.testConnection();
765
+ if (alive) {
766
+ console.log(' \u2713 reachable');
767
+ ok++;
768
+ } else {
769
+ console.log(' \u2717 testConnection returned false');
770
+ fail++;
771
+ }
772
+ await d.disconnect().catch(() => {});
773
+ } catch (e) {
774
+ console.error(' \u2717 ' + (e.message ?? e));
775
+ if (e.code) console.error(' code : ' + e.code);
776
+ fail++;
777
+ }
778
+ }
779
+ console.log();
780
+ console.log('Results : ' + ok + ' reachable, ' + fail + ' failed');
781
+ process.exit(fail > 0 ? 1 : 0);
782
+ EOF
783
+
784
+ cd "$PROJECT_ROOT"
785
+ node "$CONFIG_DIR/test-connections.mjs" "${args[@]}" 2>&1 | tee "$LOG_DIR/test-connections.log"
786
+ echo
430
787
  pause
431
788
  }
432
789
 
@@ -445,46 +802,139 @@ action_init_dialects() {
445
802
  pause; return
446
803
  fi
447
804
 
448
- info "Will attempt to initialize dialects for these URIs:"
449
- local any=0
450
- for var in MONGODB_URI POSTGRES_URI MYSQL_URI SQLITE_URI ORACLE_URI MSSQL_URI DB2_URI; do
451
- local val="${!var:-}"
452
- [[ -n "$val" ]] && { dim " $var = $val"; any=1; }
805
+ # Collect configured dialects from DB_DIALECT/SGBD_URI + EXTRA_BINDINGS
806
+ local -a configured=()
807
+ local -a dialect_names=()
808
+
809
+ if [[ -n "${DB_DIALECT:-}" && -n "${SGBD_URI:-}" ]]; then
810
+ configured+=("${DB_DIALECT}:${SGBD_URI}")
811
+ dialect_names+=("$DB_DIALECT")
812
+ fi
813
+
814
+ if [[ -n "${EXTRA_BINDINGS:-}" ]]; then
815
+ local IFS=';'
816
+ for b in $EXTRA_BINDINGS; do
817
+ local rest="${b#*:}"
818
+ local dialect="${rest%%:*}"
819
+ local uri="${rest#*:}"
820
+ configured+=("${dialect}:${uri}")
821
+ dialect_names+=("$dialect")
822
+ done
823
+ IFS=$' \t\n'
824
+ fi
825
+
826
+ if [[ ${#configured[@]} -eq 0 ]]; then
827
+ err "No URIs set. Menu 2 first."
828
+ pause; return
829
+ fi
830
+
831
+ info "Will attempt to initialize:"
832
+ for item in "${configured[@]}"; do
833
+ dim " ${item%%:*} → ${item#*:}"
453
834
  done
454
- [[ $any -eq 0 ]] && { err "No URIs set. Menu 2 first."; pause; return; }
835
+ echo
455
836
 
456
837
  confirm "Proceed?" || return
457
838
 
458
- cat > "$CONFIG_DIR/init-all.mjs" << 'EOF'
839
+ # ---- Step 1 : ensure @mostajs/orm is installed ----
840
+ info "Step 1/3 : checking @mostajs/orm installation..."
841
+ if ! ensure_pkg "@mostajs/orm"; then
842
+ err "Cannot initialize dialects without @mostajs/orm"
843
+ pause; return
844
+ fi
845
+ ok "@mostajs/orm available"
846
+
847
+ # ---- Step 2 : ensure drivers for each dialect are installed ----
848
+ info "Step 2/3 : checking dialect drivers..."
849
+ for dialect in "${dialect_names[@]}"; do
850
+ ensure_dialect_driver "$dialect" || warn "Driver for $dialect may be missing"
851
+ done
852
+ ok "Drivers checked"
853
+
854
+ # ---- Step 3 : resolve absolute path to @mostajs/orm (avoids import resolution issues) ----
855
+ local orm_path
856
+ orm_path=$(resolve_pkg_path "@mostajs/orm") || {
857
+ err "Could not resolve @mostajs/orm path even after install"
858
+ pause; return
859
+ }
860
+ dim "Using @mostajs/orm at : $orm_path"
861
+ echo
862
+
863
+ # Pass each dialect:uri pair as argv to the node script
864
+ cat > "$CONFIG_DIR/init-all.mjs" <<EOF
865
+ // Auto-generated by mostajs-cli — runs from project root
459
866
  import { readFileSync } from 'fs';
460
- import { getDialect } from '@mostajs/orm';
461
- import { entities } from './generated/entities.js';
462
-
463
- const dialects = [
464
- ['mongodb', process.env.MONGODB_URI],
465
- ['postgres', process.env.POSTGRES_URI],
466
- ['mysql', process.env.MYSQL_URI],
467
- ['sqlite', process.env.SQLITE_URI],
468
- ['oracle', process.env.ORACLE_URI],
469
- ['mssql', process.env.MSSQL_URI],
470
- ['db2', process.env.DB2_URI],
471
- ];
472
-
473
- for (const [dialect, uri] of dialects) {
474
- if (!uri) continue;
475
- console.log(`\n\u2192 ${dialect} : ${uri}`);
867
+ import { getDialect } from '$orm_path';
868
+
869
+ let entities;
870
+ try {
871
+ entities = JSON.parse(readFileSync('$GENERATED_DIR/entities.json', 'utf8'));
872
+ } catch (e) {
873
+ console.error('Cannot load entities.json — run menu 1 (Convert) first.');
874
+ console.error('Reason : ' + e.message);
875
+ process.exit(1);
876
+ }
877
+
878
+ function stripScheme(uri) {
879
+ if (uri.startsWith('sqlite://')) return uri.slice(9);
880
+ if (uri.startsWith('sqlite:')) return uri.slice(7);
881
+ return uri;
882
+ }
883
+
884
+ const pairs = process.argv.slice(2).map(s => {
885
+ const i = s.indexOf('|');
886
+ return [s.slice(0, i), s.slice(i + 1)];
887
+ });
888
+
889
+ let ok = 0, fail = 0;
890
+ const schemaStrategy = process.env.DB_SCHEMA_STRATEGY ?? 'update';
891
+ const poolSize = parseInt(process.env.DB_POOL_SIZE ?? '20', 10);
892
+ const showSql = process.env.DB_SHOW_SQL === 'true';
893
+
894
+ for (const [dialect, rawUri] of pairs) {
895
+ const uri = dialect === 'sqlite' ? stripScheme(rawUri) : rawUri;
896
+ process.stdout.write('→ ' + dialect.padEnd(12) + ' : ' + rawUri + '\n');
476
897
  try {
477
- const d = await getDialect({ dialect, uri, schemaStrategy: 'update' });
898
+ const d = await getDialect({ dialect, uri, schemaStrategy, poolSize, showSql });
478
899
  await d.initSchema(entities);
479
- console.log(` \u2713 ${dialect} ready (${entities.length} entities)`);
480
- await d.disconnect();
900
+ console.log(' ' + dialect + ' ready (' + entities.length + ' entities)');
901
+ await d.disconnect().catch(() => {});
902
+ ok++;
481
903
  } catch (e) {
482
- console.error(` \u2717 ${dialect} failed : ${e.message}`);
904
+ console.error(' ' + dialect + ' failed : ' + (e.message ?? e));
905
+ if (e.code) console.error(' code : ' + e.code);
906
+ fail++;
483
907
  }
484
908
  }
909
+ console.log();
910
+ console.log('Summary : ' + ok + ' succeeded, ' + fail + ' failed');
911
+ process.exit(fail > 0 ? 1 : 0);
485
912
  EOF
486
- cd "$PROJECT_ROOT"
487
- node "$CONFIG_DIR/init-all.mjs" 2>&1 | tee "$LOG_DIR/init.log"
913
+
914
+ info "Step 3/3 : running initialization..."
915
+ cd "$PROJECT_ROOT" || return
916
+
917
+ # Pass each dialect:uri as an argv pair
918
+ local -a args=()
919
+ for item in "${configured[@]}"; do
920
+ local d="${item%%:*}"
921
+ local u="${item#*:}"
922
+ args+=("$d|$u")
923
+ done
924
+
925
+ if node "$CONFIG_DIR/init-all.mjs" "${args[@]}" 2>&1 | tee "$LOG_DIR/init.log"; then
926
+ echo
927
+ ok "Initialization complete"
928
+ else
929
+ echo
930
+ warn "One or more dialects failed — check the log above"
931
+ info "Log : $LOG_DIR/init.log"
932
+ echo
933
+ info "Common fixes :"
934
+ dim " - Verify the URI in menu 2 is reachable (menu 2 → T)"
935
+ dim " - Install missing driver : $PKG_MANAGER install <driver>"
936
+ dim " - Oracle/DB2/HANA need native libs installed on your system"
937
+ fi
488
938
  pause
489
939
  }
490
940
 
@@ -587,20 +1037,52 @@ action_curl_test() {
587
1037
  header
588
1038
  echo -e "${BOLD}${MAGENTA}▶ curl smoke test${RESET}"
589
1039
  echo
1040
+
1041
+ if ! command -v curl >/dev/null 2>&1; then
1042
+ err "curl is not installed"
1043
+ info "Install with : sudo apt install curl"
1044
+ return 1
1045
+ fi
1046
+
1047
+ local any_reachable=0
590
1048
  for url in \
591
1049
  "http://localhost:${APP_PORT:-3000}/" \
592
1050
  "http://localhost:${APP_PORT:-3000}/api/health" \
593
1051
  "http://localhost:${MOSTA_NET_PORT:-4447}/" \
594
1052
  "http://localhost:${MOSTA_NET_PORT:-4447}/mcp"; do
595
1053
  info "GET $url"
596
- curl -s -o /dev/null -w " status=%{http_code} time=%{time_total}s\n" --max-time 5 "$url" 2>/dev/null || err "failed"
1054
+ local output
1055
+ output=$(curl -s -o /dev/null -w " status=%{http_code} time=%{time_total}s" --max-time 5 "$url" 2>&1)
1056
+ local rc=$?
1057
+ if [[ $rc -eq 0 ]]; then
1058
+ echo "$output"
1059
+ any_reachable=1
1060
+ else
1061
+ err " unreachable (curl exit $rc — check service is running)"
1062
+ fi
597
1063
  done
1064
+ [[ $any_reachable -eq 0 ]] && {
1065
+ echo
1066
+ warn "No endpoints reachable. Start services first (menu 5)."
1067
+ }
598
1068
  }
599
1069
 
600
1070
  run_in_project() {
601
- cd "$PROJECT_ROOT"
602
- info "Running: $1"
603
- eval "$1"
1071
+ local cmd="$1"
1072
+ cd "$PROJECT_ROOT" || { err "Cannot cd to project root"; return 1; }
1073
+ info "Running: $cmd"
1074
+ set +e
1075
+ eval "$cmd"
1076
+ local rc=$?
1077
+ set -e
1078
+ if [[ $rc -ne 0 ]]; then
1079
+ err "Command exited with code $rc"
1080
+ info "Check the output above, or common issues :"
1081
+ dim " - Tests failing → fix them, or skip with a flag"
1082
+ dim " - 'command not found' → install missing dev tools"
1083
+ dim " - Port conflict → stop other services first (menu 5 → 3)"
1084
+ fi
1085
+ return $rc
604
1086
  }
605
1087
 
606
1088
  # ============================================================
@@ -982,6 +1464,10 @@ EOF
982
1464
  # MAIN
983
1465
  # ============================================================
984
1466
 
1467
+ # If this script is being *sourced*, stop here — don't start the menu.
1468
+ # This lets other scripts (e.g. tests) reuse helper functions.
1469
+ [[ "${BASH_SOURCE[0]}" != "${0}" ]] && return 0 2>/dev/null
1470
+
985
1471
  # Non-interactive mode if args provided
986
1472
  if [[ $# -gt 0 ]]; then
987
1473
  run_subcommand "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/orm-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Universal CLI to integrate @mostajs/orm into any project — auto-detects Prisma, OpenAPI, JSON Schema. Interactive menu + subcommands. 13 databases.",
5
5
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
6
  "license": "AGPL-3.0-or-later",