@mostajs/orm-cli 0.1.0 → 0.2.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 +612 -109
  2. package/package.json +6 -2
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,35 @@ 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
158
- local adapter_path=""
159
- if [[ -f "$PROJECT_ROOT/node_modules/@mostajs/orm-adapter/dist/index.js" ]]; then
160
- adapter_path="$PROJECT_ROOT/node_modules/@mostajs/orm-adapter/dist/index.js"
161
- info "Using local @mostajs/orm-adapter"
261
+ # Ensure @mostajs/orm-adapter is available — auto-install if missing
262
+ info "Checking @mostajs/orm-adapter..."
263
+ local adapter_base=""
264
+
265
+ # 1. Try local project install
266
+ if [[ -d "$PROJECT_ROOT/node_modules/@mostajs/orm-adapter/dist" ]]; then
267
+ adapter_base="$PROJECT_ROOT/node_modules/@mostajs/orm-adapter/dist"
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
- if [[ -f "$cli_dir/../mosta-orm-adapter/dist/index.js" ]]; then
167
- adapter_path="$cli_dir/../mosta-orm-adapter/dist/index.js"
168
- info "Using sibling @mostajs/orm-adapter from $adapter_path"
273
+ if [[ -d "$cli_dir/../mosta-orm-adapter/dist" ]]; then
274
+ adapter_base="$cli_dir/../mosta-orm-adapter/dist"
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
179
- adapter_path="$PROJECT_ROOT/node_modules/@mostajs/orm-adapter/dist/index.js"
277
+ # 3. Offer auto-install
278
+ warn "Not installed locally"
279
+ if ensure_pkg "@mostajs/orm-adapter" "@mostajs/orm"; then
280
+ adapter_base="$PROJECT_ROOT/node_modules/@mostajs/orm-adapter/dist"
281
+ if [[ ! -d "$adapter_base" ]]; then
282
+ local resolved
283
+ resolved=$(resolve_pkg_path "@mostajs/orm-adapter") || {
284
+ err "Install reported success but module cannot be resolved."
285
+ return 1
286
+ }
287
+ adapter_base="$(dirname "$resolved")"
288
+ fi
289
+ ok "Installed"
180
290
  else
181
291
  err "Cannot proceed without the adapter."
182
292
  return 1
@@ -184,23 +294,68 @@ run_adapter_convert() {
184
294
  fi
185
295
  fi
186
296
 
297
+ # Use subpath-specific import to avoid loading ALL adapters (and their
298
+ # transitive deps : ajv, ref-parser, openapi-parser...). For example,
299
+ # importing only prisma.adapter.js avoids pulling in ajv-draft-04 issues
300
+ # when the project only needs Prisma conversion.
301
+ local adapter_file
187
302
  local adapter_class
188
303
  case "$input_type" in
189
- prisma) adapter_class="PrismaAdapter" ;;
190
- openapi) adapter_class="OpenApiAdapter" ;;
191
- jsonschema) adapter_class="JsonSchemaAdapter" ;;
304
+ prisma) adapter_file="$adapter_base/adapters/prisma.adapter.js" ; adapter_class="PrismaAdapter" ;;
305
+ openapi) adapter_file="$adapter_base/adapters/openapi.adapter.js" ; adapter_class="OpenApiAdapter" ;;
306
+ jsonschema) adapter_file="$adapter_base/adapters/jsonschema.adapter.js" ; adapter_class="JsonSchemaAdapter" ;;
192
307
  *) err "Unknown input type: $input_type"; return 1 ;;
193
308
  esac
194
309
 
310
+ if [[ ! -f "$adapter_file" ]]; then
311
+ warn "Subpath import $adapter_file not found — falling back to root index.js"
312
+ adapter_file="$adapter_base/index.js"
313
+ fi
314
+
195
315
  cat > "$CONFIG_DIR/convert.mjs" << EOF
196
316
  import { readFileSync, writeFileSync } from 'fs';
197
- import { $adapter_class } from '$adapter_path';
198
317
 
199
- const source = readFileSync('$input_file', 'utf8');
318
+ let adapterModule;
319
+ try {
320
+ adapterModule = await import('$adapter_file');
321
+ } catch (e) {
322
+ console.error('Failed to import adapter from $adapter_file');
323
+ console.error('Reason :', e.message);
324
+ // Common issue : ajv/ref-parser/yaml resolution problems
325
+ if (e.message?.includes('ajv') || e.message?.includes('ref-parser') || e.message?.includes('yaml')) {
326
+ console.error();
327
+ console.error('Hint : this adapter requires peer deps that may be missing.');
328
+ console.error('Try : $PKG_MANAGER install ajv@^8 @apidevtools/json-schema-ref-parser@^11');
329
+ }
330
+ process.exit(2);
331
+ }
332
+ const { $adapter_class } = adapterModule;
333
+ if (!$adapter_class) {
334
+ console.error('$adapter_class not exported from the adapter module');
335
+ process.exit(3);
336
+ }
337
+
338
+ let source;
339
+ try {
340
+ source = readFileSync('$input_file', 'utf8');
341
+ } catch (e) {
342
+ console.error('Cannot read input file : $input_file');
343
+ console.error('Reason :', e.message);
344
+ process.exit(4);
345
+ }
346
+
200
347
  const adapter = new $adapter_class();
201
348
  const warnings = [];
202
349
  const input = '$input_type' === 'jsonschema' ? JSON.parse(source) : source;
203
- const entities = await adapter.toEntitySchema(input, { onWarning: w => warnings.push(w) });
350
+
351
+ let entities;
352
+ try {
353
+ entities = await adapter.toEntitySchema(input, { onWarning: w => warnings.push(w) });
354
+ } catch (e) {
355
+ console.error('Conversion failed :', e.message);
356
+ if (e.details) console.error('Details :', JSON.stringify(e.details, null, 2).slice(0, 500));
357
+ process.exit(5);
358
+ }
204
359
 
205
360
  console.log('entities : ' + entities.length);
206
361
  console.log('warnings : ' + warnings.length);
@@ -217,12 +372,28 @@ const code = header +
217
372
  ' entities.map(e => [e.name, e])\n' +
218
373
  ');\n';
219
374
 
220
- writeFileSync('$output_file', code);
221
- console.log('\u2713 Saved : $output_file');
375
+ try {
376
+ writeFileSync('$output_file', code);
377
+ // Also write .json (easier to load from ESM without TS support)
378
+ const jsonFile = '$output_file'.replace(/\.ts$/, '.json');
379
+ writeFileSync(jsonFile, JSON.stringify(entities, null, 2));
380
+ console.log('\u2713 Saved : $output_file');
381
+ console.log('\u2713 Saved : ' + jsonFile);
382
+ } catch (e) {
383
+ console.error('Cannot write output : ' + e.message);
384
+ process.exit(6);
385
+ }
222
386
  EOF
223
387
 
388
+ local rc=0
224
389
  node "$CONFIG_DIR/convert.mjs" 2>&1 | tee "$LOG_DIR/convert.log"
225
- return "${PIPESTATUS[0]}"
390
+ rc=${PIPESTATUS[0]}
391
+ if [[ $rc -ne 0 ]]; then
392
+ err "Conversion exited with code $rc"
393
+ info "See $LOG_DIR/convert.log for details"
394
+ return $rc
395
+ fi
396
+ return 0
226
397
  }
227
398
 
228
399
  # ============================================================
@@ -356,37 +527,43 @@ menu_databases() {
356
527
  header
357
528
  echo -e "${BOLD}${MAGENTA}▶ Database configuration${RESET}"
358
529
  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}"
530
+ echo -e "${DIM}Compatible with @mostajs/orm .env convention (see SecuAccessPro/.env.local)${RESET}"
531
+ echo
532
+ echo -e "${BOLD}Primary DB (single backend 90% of apps) :${RESET}"
533
+ echo -e " ${CYAN}1${RESET}) DB_DIALECT : ${DIM}${DB_DIALECT:-<not set>}${RESET}"
534
+ echo -e " ${CYAN}2${RESET}) SGBD_URI : ${DIM}${SGBD_URI:-<not set>}${RESET}"
535
+ echo -e " ${CYAN}3${RESET}) DB_SCHEMA_STRATEGY : ${DIM}${DB_SCHEMA_STRATEGY:-update}${RESET}"
536
+ echo -e " ${CYAN}4${RESET}) DB_POOL_SIZE : ${DIM}${DB_POOL_SIZE:-20}${RESET}"
537
+ echo -e " ${CYAN}5${RESET}) DB_SHOW_SQL : ${DIM}${DB_SHOW_SQL:-false}${RESET}"
368
538
  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}"
539
+ echo -e "${BOLD}Extra DBs (hybrid apps with Prisma Bridge) :${RESET}"
540
+ echo -e " ${CYAN}a${RESET}) Add extra binding ${DIM}(e.g. MongoDB for audit while PG is primary)${RESET}"
541
+ echo -e " ${CYAN}l${RESET}) List extra bindings : ${DIM}${EXTRA_BINDINGS:-<none>}${RESET}"
542
+ echo
543
+ echo -e "${BOLD}mosta-net + app :${RESET}"
544
+ echo -e " ${CYAN}u${RESET}) MOSTA_NET_URL : ${DIM}${MOSTA_NET_URL:-http://localhost:14488}${RESET}"
545
+ echo -e " ${CYAN}n${RESET}) MOSTA_NET_TRANSPORT : ${DIM}${MOSTA_NET_TRANSPORT:-rest}${RESET}"
546
+ echo -e " ${CYAN}p${RESET}) APP_PORT : ${DIM}${APP_PORT:-3000}${RESET}"
371
547
  echo
372
548
  echo -e " ${CYAN}t${RESET}) Test all connections"
549
+ echo -e " ${CYAN}e${RESET}) Export to .env.local in project"
373
550
  echo -e " ${CYAN}r${RESET}) Reset config"
374
551
  echo -e " ${CYAN}b${RESET}) Back"
375
552
  echo
376
553
  local choice; choice=$(ask "Choice" "1")
377
554
  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}")";;
555
+ 1) prompt_dialect ;;
556
+ 2) save_var SGBD_URI "$(ask 'SGBD_URI (path or connection string)' "${SGBD_URI:-./data.sqlite}")";;
557
+ 3) save_var DB_SCHEMA_STRATEGY "$(ask 'DB_SCHEMA_STRATEGY (update|create|validate|none|create-drop)' "${DB_SCHEMA_STRATEGY:-update}")";;
558
+ 4) save_var DB_POOL_SIZE "$(ask 'DB_POOL_SIZE' "${DB_POOL_SIZE:-20}")";;
559
+ 5) save_var DB_SHOW_SQL "$(ask 'DB_SHOW_SQL (true|false)' "${DB_SHOW_SQL:-false}")";;
560
+ a|A) add_extra_binding ;;
561
+ l|L) list_extra_bindings; pause ;;
562
+ u|U) save_var MOSTA_NET_URL "$(ask 'MOSTA_NET_URL' "${MOSTA_NET_URL:-http://localhost:14488}")";;
563
+ n|N) save_var MOSTA_NET_TRANSPORT "$(ask 'MOSTA_NET_TRANSPORT (rest|sse|graphql|mcp|websocket|jsonrpc|grpc|odata)' "${MOSTA_NET_TRANSPORT:-rest}")";;
564
+ p|P) save_var APP_PORT "$(ask 'APP_PORT' "${APP_PORT:-3000}")";;
389
565
  t|T) action_test_connections; return;;
566
+ e|E) export_env_local ;;
390
567
  r|R) confirm "Really reset config?" && rm -f "$CONFIG_FILE" && ok "Reset";;
391
568
  b|B) return;;
392
569
  *) warn "Unknown";;
@@ -395,38 +572,235 @@ menu_databases() {
395
572
  menu_databases
396
573
  }
397
574
 
575
+ # Prompt user to choose a dialect from a numbered list
576
+ prompt_dialect() {
577
+ echo
578
+ echo "Pick a dialect:"
579
+ local i=1
580
+ local -a dialects=(sqlite postgres mysql mariadb mongodb mssql oracle db2 cockroachdb hana hsqldb spanner sybase)
581
+ for d in "${dialects[@]}"; do
582
+ echo -e " ${CYAN}$i${RESET}) $d"
583
+ i=$((i+1))
584
+ done
585
+ local num; num=$(ask "Number" 1)
586
+ local idx=$((num-1))
587
+ if [[ $idx -ge 0 && $idx -lt ${#dialects[@]} ]]; then
588
+ local d="${dialects[$idx]}"
589
+ save_var DB_DIALECT "$d"
590
+ # Suggest default URI for that dialect if SGBD_URI is empty
591
+ if [[ -z "${SGBD_URI:-}" ]]; then
592
+ local suggest
593
+ case "$d" in
594
+ sqlite) suggest="./data.sqlite" ;;
595
+ postgres) suggest="postgres://user:pw@localhost:5432/app" ;;
596
+ mysql|mariadb) suggest="mysql://user:pw@localhost:3306/app" ;;
597
+ mongodb) suggest="mongodb://localhost:27017/app" ;;
598
+ mssql) suggest="mssql://user:pw@localhost:1433/app" ;;
599
+ oracle) suggest="oracle://user:pw@localhost:1521/ORCLPDB" ;;
600
+ db2) suggest="db2://user:pw@localhost:50000/app" ;;
601
+ cockroachdb) suggest="postgres://user@localhost:26257/app" ;;
602
+ hana) suggest="hana://user:pw@localhost:39041" ;;
603
+ *) suggest="" ;;
604
+ esac
605
+ [[ -n "$suggest" ]] && save_var SGBD_URI "$(ask 'SGBD_URI' "$suggest")"
606
+ fi
607
+ else
608
+ warn "Invalid number"
609
+ fi
610
+ }
611
+
612
+ # Add an extra dialect binding for hybrid apps
613
+ add_extra_binding() {
614
+ local name; name=$(ask "Binding name (e.g. AuditLog, Reports) — used by the Prisma Bridge")
615
+ [[ -z "$name" ]] && return
616
+ local dialect; dialect=$(ask "Dialect for $name (sqlite|postgres|mongodb|oracle|...)")
617
+ [[ -z "$dialect" ]] && return
618
+ local uri; uri=$(ask "URI for $name")
619
+ [[ -z "$uri" ]] && return
620
+ local current="${EXTRA_BINDINGS:-}"
621
+ local new="${name}:${dialect}:${uri}"
622
+ if [[ -z "$current" ]]; then
623
+ save_var EXTRA_BINDINGS "$new"
624
+ else
625
+ save_var EXTRA_BINDINGS "${current};${new}"
626
+ fi
627
+ ok "Added: $name ($dialect @ $uri)"
628
+ }
629
+
630
+ list_extra_bindings() {
631
+ echo
632
+ if [[ -z "${EXTRA_BINDINGS:-}" ]]; then
633
+ dim " (none)"
634
+ return
635
+ fi
636
+ echo -e "${BOLD}Extra bindings:${RESET}"
637
+ local IFS=';'
638
+ for b in $EXTRA_BINDINGS; do
639
+ local name="${b%%:*}"
640
+ local rest="${b#*:}"
641
+ local dialect="${rest%%:*}"
642
+ local uri="${rest#*:}"
643
+ echo -e " ${CYAN}$name${RESET} → ${MAGENTA}$dialect${RESET} @ ${DIM}$uri${RESET}"
644
+ done
645
+ }
646
+
647
+ # Export a .env.local file compatible with @mostajs/orm convention
648
+ export_env_local() {
649
+ local target="$PROJECT_ROOT/.env.mostajs"
650
+ cat > "$target" <<EOF
651
+ # Generated by @mostajs/orm-cli on $(date -u +%Y-%m-%dT%H:%M:%SZ)
652
+ # Primary database
653
+ DB_DIALECT=${DB_DIALECT:-sqlite}
654
+ SGBD_URI=${SGBD_URI:-./data.sqlite}
655
+ DB_SCHEMA_STRATEGY=${DB_SCHEMA_STRATEGY:-update}
656
+ DB_POOL_SIZE=${DB_POOL_SIZE:-20}
657
+ DB_SHOW_SQL=${DB_SHOW_SQL:-false}
658
+
659
+ # mosta-net server
660
+ MOSTA_NET_URL=${MOSTA_NET_URL:-http://localhost:14488}
661
+ MOSTA_NET_TRANSPORT=${MOSTA_NET_TRANSPORT:-rest}
662
+
663
+ # App
664
+ APP_PORT=${APP_PORT:-3000}
665
+
666
+ # Extra bindings for Prisma Bridge (hybrid apps)
667
+ # Format: EXTRA_BINDINGS="ModelName:dialect:uri;OtherModel:dialect:uri"
668
+ EXTRA_BINDINGS=${EXTRA_BINDINGS:-}
669
+ EOF
670
+ ok "Exported : $target"
671
+ info "Review, rename to .env.local, and commit to your .env.example (without secrets)"
672
+ }
673
+
674
+ # Auto-detect mosta-orm dialect from URI scheme
675
+ detect_dialect_from_uri() {
676
+ local uri="$1"
677
+ case "$uri" in
678
+ mongodb://*|mongodb+srv://*) echo "mongodb" ;;
679
+ postgres://*|postgresql://*) echo "postgres" ;;
680
+ mysql://*) echo "mysql" ;;
681
+ mariadb://*) echo "mariadb" ;;
682
+ mssql://*|sqlserver://*) echo "mssql" ;;
683
+ oracle://*) echo "oracle" ;;
684
+ db2://*) echo "db2" ;;
685
+ hana://*) echo "hana" ;;
686
+ cockroachdb://*) echo "cockroachdb" ;;
687
+ spanner://*) echo "spanner" ;;
688
+ sybase://*) echo "sybase" ;;
689
+ sqlite://*|sqlite:*|*.sqlite|*.db|:memory:) echo "sqlite" ;;
690
+ *) echo "unknown" ;;
691
+ esac
692
+ }
693
+
694
+ # Strip scheme prefix from URI (used for SQLite path)
695
+ strip_uri_scheme() {
696
+ local uri="$1"
697
+ case "$uri" in
698
+ sqlite://*) echo "${uri#sqlite://}" ;;
699
+ sqlite:*) echo "${uri#sqlite:}" ;;
700
+ *) echo "$uri" ;;
701
+ esac
702
+ }
703
+
398
704
  action_test_connections() {
399
705
  header
400
- echo -e "${BOLD}${MAGENTA}▶ Testing connections${RESET}"
706
+ echo -e "${BOLD}${MAGENTA}▶ Testing connections (via @mostajs/orm)${RESET}"
401
707
  echo
402
708
  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))
709
+
710
+ # Collect all configured URIs as (dialect, uri) pairs
711
+ local -a pairs=()
712
+
713
+ # Primary DB
714
+ if [[ -n "${DB_DIALECT:-}" && -n "${SGBD_URI:-}" ]]; then
715
+ pairs+=("${DB_DIALECT}|${SGBD_URI}")
716
+ fi
717
+
718
+ # Extra bindings (format: name:dialect:uri;name:dialect:uri)
719
+ if [[ -n "${EXTRA_BINDINGS:-}" ]]; then
720
+ local IFS=';'
721
+ for b in $EXTRA_BINDINGS; do
722
+ local rest="${b#*:}" # strip name
723
+ local dialect="${rest%%:*}"
724
+ local uri="${rest#*:}"
725
+ pairs+=("$dialect|$uri")
726
+ done
727
+ IFS=$' \t\n'
728
+ fi
729
+
730
+ if [[ ${#pairs[@]} -eq 0 ]]; then
731
+ warn "No URIs configured. Go to menu 2 first."
732
+ pause; return
733
+ fi
734
+
735
+ # Ensure @mostajs/orm is installed (test uses its native testConnection)
736
+ info "Checking @mostajs/orm installation..."
737
+ if ! ensure_pkg "@mostajs/orm"; then
738
+ err "Cannot test connections without @mostajs/orm"
739
+ pause; return
740
+ fi
741
+
742
+ # Ensure drivers for each dialect being tested
743
+ info "Checking drivers..."
744
+ for p in "${pairs[@]}"; do
745
+ local d="${p%%|*}"
746
+ ensure_dialect_driver "$d" || warn "Driver for $d may be missing"
427
747
  done
428
748
 
429
- [[ $tested -eq 0 ]] && warn "No URIs configured"
749
+ local orm_path
750
+ orm_path=$(resolve_pkg_path "@mostajs/orm") || {
751
+ err "Cannot resolve @mostajs/orm"
752
+ pause; return
753
+ }
754
+
755
+ # Build a small node script that tests each connection
756
+ local -a args=()
757
+ for p in "${pairs[@]}"; do
758
+ args+=("$p")
759
+ done
760
+
761
+ cat > "$CONFIG_DIR/test-connections.mjs" <<EOF
762
+ import { getDialect } from '$orm_path';
763
+
764
+ const pairs = process.argv.slice(2).map(s => {
765
+ const i = s.indexOf('|');
766
+ return [s.slice(0, i), s.slice(i + 1)];
767
+ });
768
+
769
+ function stripScheme(uri) {
770
+ if (uri.startsWith('sqlite://')) return uri.slice(9);
771
+ if (uri.startsWith('sqlite:')) return uri.slice(7);
772
+ return uri;
773
+ }
774
+
775
+ let ok = 0, fail = 0;
776
+ for (const [dialect, rawUri] of pairs) {
777
+ const uri = dialect === 'sqlite' ? stripScheme(rawUri) : rawUri;
778
+ process.stdout.write(dialect.padEnd(12) + ' ' + rawUri + '\n');
779
+ try {
780
+ const d = await getDialect({ dialect, uri });
781
+ const alive = await d.testConnection();
782
+ if (alive) {
783
+ console.log(' \u2713 reachable');
784
+ ok++;
785
+ } else {
786
+ console.log(' \u2717 testConnection returned false');
787
+ fail++;
788
+ }
789
+ await d.disconnect().catch(() => {});
790
+ } catch (e) {
791
+ console.error(' \u2717 ' + (e.message ?? e));
792
+ if (e.code) console.error(' code : ' + e.code);
793
+ fail++;
794
+ }
795
+ }
796
+ console.log();
797
+ console.log('Results : ' + ok + ' reachable, ' + fail + ' failed');
798
+ process.exit(fail > 0 ? 1 : 0);
799
+ EOF
800
+
801
+ cd "$PROJECT_ROOT"
802
+ node "$CONFIG_DIR/test-connections.mjs" "${args[@]}" 2>&1 | tee "$LOG_DIR/test-connections.log"
803
+ echo
430
804
  pause
431
805
  }
432
806
 
@@ -445,46 +819,139 @@ action_init_dialects() {
445
819
  pause; return
446
820
  fi
447
821
 
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; }
822
+ # Collect configured dialects from DB_DIALECT/SGBD_URI + EXTRA_BINDINGS
823
+ local -a configured=()
824
+ local -a dialect_names=()
825
+
826
+ if [[ -n "${DB_DIALECT:-}" && -n "${SGBD_URI:-}" ]]; then
827
+ configured+=("${DB_DIALECT}:${SGBD_URI}")
828
+ dialect_names+=("$DB_DIALECT")
829
+ fi
830
+
831
+ if [[ -n "${EXTRA_BINDINGS:-}" ]]; then
832
+ local IFS=';'
833
+ for b in $EXTRA_BINDINGS; do
834
+ local rest="${b#*:}"
835
+ local dialect="${rest%%:*}"
836
+ local uri="${rest#*:}"
837
+ configured+=("${dialect}:${uri}")
838
+ dialect_names+=("$dialect")
839
+ done
840
+ IFS=$' \t\n'
841
+ fi
842
+
843
+ if [[ ${#configured[@]} -eq 0 ]]; then
844
+ err "No URIs set. Menu 2 first."
845
+ pause; return
846
+ fi
847
+
848
+ info "Will attempt to initialize:"
849
+ for item in "${configured[@]}"; do
850
+ dim " ${item%%:*} → ${item#*:}"
453
851
  done
454
- [[ $any -eq 0 ]] && { err "No URIs set. Menu 2 first."; pause; return; }
852
+ echo
455
853
 
456
854
  confirm "Proceed?" || return
457
855
 
458
- cat > "$CONFIG_DIR/init-all.mjs" << 'EOF'
856
+ # ---- Step 1 : ensure @mostajs/orm is installed ----
857
+ info "Step 1/3 : checking @mostajs/orm installation..."
858
+ if ! ensure_pkg "@mostajs/orm"; then
859
+ err "Cannot initialize dialects without @mostajs/orm"
860
+ pause; return
861
+ fi
862
+ ok "@mostajs/orm available"
863
+
864
+ # ---- Step 2 : ensure drivers for each dialect are installed ----
865
+ info "Step 2/3 : checking dialect drivers..."
866
+ for dialect in "${dialect_names[@]}"; do
867
+ ensure_dialect_driver "$dialect" || warn "Driver for $dialect may be missing"
868
+ done
869
+ ok "Drivers checked"
870
+
871
+ # ---- Step 3 : resolve absolute path to @mostajs/orm (avoids import resolution issues) ----
872
+ local orm_path
873
+ orm_path=$(resolve_pkg_path "@mostajs/orm") || {
874
+ err "Could not resolve @mostajs/orm path even after install"
875
+ pause; return
876
+ }
877
+ dim "Using @mostajs/orm at : $orm_path"
878
+ echo
879
+
880
+ # Pass each dialect:uri pair as argv to the node script
881
+ cat > "$CONFIG_DIR/init-all.mjs" <<EOF
882
+ // Auto-generated by mostajs-cli — runs from project root
459
883
  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}`);
884
+ import { getDialect } from '$orm_path';
885
+
886
+ let entities;
887
+ try {
888
+ entities = JSON.parse(readFileSync('$GENERATED_DIR/entities.json', 'utf8'));
889
+ } catch (e) {
890
+ console.error('Cannot load entities.json — run menu 1 (Convert) first.');
891
+ console.error('Reason : ' + e.message);
892
+ process.exit(1);
893
+ }
894
+
895
+ function stripScheme(uri) {
896
+ if (uri.startsWith('sqlite://')) return uri.slice(9);
897
+ if (uri.startsWith('sqlite:')) return uri.slice(7);
898
+ return uri;
899
+ }
900
+
901
+ const pairs = process.argv.slice(2).map(s => {
902
+ const i = s.indexOf('|');
903
+ return [s.slice(0, i), s.slice(i + 1)];
904
+ });
905
+
906
+ let ok = 0, fail = 0;
907
+ const schemaStrategy = process.env.DB_SCHEMA_STRATEGY ?? 'update';
908
+ const poolSize = parseInt(process.env.DB_POOL_SIZE ?? '20', 10);
909
+ const showSql = process.env.DB_SHOW_SQL === 'true';
910
+
911
+ for (const [dialect, rawUri] of pairs) {
912
+ const uri = dialect === 'sqlite' ? stripScheme(rawUri) : rawUri;
913
+ process.stdout.write('→ ' + dialect.padEnd(12) + ' : ' + rawUri + '\n');
476
914
  try {
477
- const d = await getDialect({ dialect, uri, schemaStrategy: 'update' });
915
+ const d = await getDialect({ dialect, uri, schemaStrategy, poolSize, showSql });
478
916
  await d.initSchema(entities);
479
- console.log(` \u2713 ${dialect} ready (${entities.length} entities)`);
480
- await d.disconnect();
917
+ console.log(' ' + dialect + ' ready (' + entities.length + ' entities)');
918
+ await d.disconnect().catch(() => {});
919
+ ok++;
481
920
  } catch (e) {
482
- console.error(` \u2717 ${dialect} failed : ${e.message}`);
921
+ console.error(' ' + dialect + ' failed : ' + (e.message ?? e));
922
+ if (e.code) console.error(' code : ' + e.code);
923
+ fail++;
483
924
  }
484
925
  }
926
+ console.log();
927
+ console.log('Summary : ' + ok + ' succeeded, ' + fail + ' failed');
928
+ process.exit(fail > 0 ? 1 : 0);
485
929
  EOF
486
- cd "$PROJECT_ROOT"
487
- node "$CONFIG_DIR/init-all.mjs" 2>&1 | tee "$LOG_DIR/init.log"
930
+
931
+ info "Step 3/3 : running initialization..."
932
+ cd "$PROJECT_ROOT" || return
933
+
934
+ # Pass each dialect:uri as an argv pair
935
+ local -a args=()
936
+ for item in "${configured[@]}"; do
937
+ local d="${item%%:*}"
938
+ local u="${item#*:}"
939
+ args+=("$d|$u")
940
+ done
941
+
942
+ if node "$CONFIG_DIR/init-all.mjs" "${args[@]}" 2>&1 | tee "$LOG_DIR/init.log"; then
943
+ echo
944
+ ok "Initialization complete"
945
+ else
946
+ echo
947
+ warn "One or more dialects failed — check the log above"
948
+ info "Log : $LOG_DIR/init.log"
949
+ echo
950
+ info "Common fixes :"
951
+ dim " - Verify the URI in menu 2 is reachable (menu 2 → T)"
952
+ dim " - Install missing driver : $PKG_MANAGER install <driver>"
953
+ dim " - Oracle/DB2/HANA need native libs installed on your system"
954
+ fi
488
955
  pause
489
956
  }
490
957
 
@@ -587,20 +1054,52 @@ action_curl_test() {
587
1054
  header
588
1055
  echo -e "${BOLD}${MAGENTA}▶ curl smoke test${RESET}"
589
1056
  echo
1057
+
1058
+ if ! command -v curl >/dev/null 2>&1; then
1059
+ err "curl is not installed"
1060
+ info "Install with : sudo apt install curl"
1061
+ return 1
1062
+ fi
1063
+
1064
+ local any_reachable=0
590
1065
  for url in \
591
1066
  "http://localhost:${APP_PORT:-3000}/" \
592
1067
  "http://localhost:${APP_PORT:-3000}/api/health" \
593
1068
  "http://localhost:${MOSTA_NET_PORT:-4447}/" \
594
1069
  "http://localhost:${MOSTA_NET_PORT:-4447}/mcp"; do
595
1070
  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"
1071
+ local output
1072
+ output=$(curl -s -o /dev/null -w " status=%{http_code} time=%{time_total}s" --max-time 5 "$url" 2>&1)
1073
+ local rc=$?
1074
+ if [[ $rc -eq 0 ]]; then
1075
+ echo "$output"
1076
+ any_reachable=1
1077
+ else
1078
+ err " unreachable (curl exit $rc — check service is running)"
1079
+ fi
597
1080
  done
1081
+ [[ $any_reachable -eq 0 ]] && {
1082
+ echo
1083
+ warn "No endpoints reachable. Start services first (menu 5)."
1084
+ }
598
1085
  }
599
1086
 
600
1087
  run_in_project() {
601
- cd "$PROJECT_ROOT"
602
- info "Running: $1"
603
- eval "$1"
1088
+ local cmd="$1"
1089
+ cd "$PROJECT_ROOT" || { err "Cannot cd to project root"; return 1; }
1090
+ info "Running: $cmd"
1091
+ set +e
1092
+ eval "$cmd"
1093
+ local rc=$?
1094
+ set -e
1095
+ if [[ $rc -ne 0 ]]; then
1096
+ err "Command exited with code $rc"
1097
+ info "Check the output above, or common issues :"
1098
+ dim " - Tests failing → fix them, or skip with a flag"
1099
+ dim " - 'command not found' → install missing dev tools"
1100
+ dim " - Port conflict → stop other services first (menu 5 → 3)"
1101
+ fi
1102
+ return $rc
604
1103
  }
605
1104
 
606
1105
  # ============================================================
@@ -982,6 +1481,10 @@ EOF
982
1481
  # MAIN
983
1482
  # ============================================================
984
1483
 
1484
+ # If this script is being *sourced*, stop here — don't start the menu.
1485
+ # This lets other scripts (e.g. tests) reuse helper functions.
1486
+ [[ "${BASH_SOURCE[0]}" != "${0}" ]] && return 0 2>/dev/null
1487
+
985
1488
  # Non-interactive mode if args provided
986
1489
  if [[ $# -gt 0 ]]; then
987
1490
  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.1",
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",
@@ -40,5 +40,9 @@
40
40
  "linux",
41
41
  "darwin",
42
42
  "win32"
43
- ]
43
+ ],
44
+ "dependencies": {
45
+ "@mostajs/orm": "^1.9.2",
46
+ "better-sqlite3": "^12.9.0"
47
+ }
44
48
  }