@mostajs/orm-cli 0.2.2 → 0.3.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 +623 -5
  2. package/package.json +1 -1
package/bin/mostajs.sh CHANGED
@@ -437,6 +437,7 @@ menu_main() {
437
437
  echo -e " ${CYAN}7${RESET}) View logs"
438
438
  echo -e " ${CYAN}8${RESET}) Health checks"
439
439
  echo -e " ${CYAN}9${RESET}) Generate boilerplate (src/db.ts with bridge)"
440
+ echo -e " ${CYAN}s${RESET}) ${BOLD}Seeding${RESET} (upload / validate / apply seed data)"
440
441
  echo -e " ${CYAN}0${RESET}) About / Help"
441
442
  echo
442
443
  echo -e " ${RED}q${RESET}) Quit"
@@ -453,6 +454,7 @@ menu_main() {
453
454
  7) action_logs ;;
454
455
  8) action_healthcheck ;;
455
456
  9) action_generate_boilerplate ;;
457
+ s|S) menu_seeding ;;
456
458
  0) action_about ;;
457
459
  q|Q) exit 0 ;;
458
460
  *) warn "Unknown choice"; pause ;;
@@ -594,7 +596,8 @@ prompt_dialect() {
594
596
  sqlite) suggest="./data.sqlite" ;;
595
597
  postgres) suggest="postgres://user:pw@localhost:5432/app" ;;
596
598
  mysql|mariadb) suggest="mysql://user:pw@localhost:3306/app" ;;
597
- mongodb) suggest="mongodb://localhost:27017/app" ;;
599
+ # MongoDB : include ?authSource=admin (common pitfall without it)
600
+ mongodb) suggest="mongodb://devuser:devpass26@localhost:27017/app?authSource=admin" ;;
598
601
  mssql) suggest="mssql://user:pw@localhost:1433/app" ;;
599
602
  oracle) suggest="oracle://user:pw@localhost:1521/ORCLPDB" ;;
600
603
  db2) suggest="db2://user:pw@localhost:50000/app" ;;
@@ -772,6 +775,46 @@ function stripScheme(uri) {
772
775
  return uri;
773
776
  }
774
777
 
778
+ // Provide dialect-specific hints for common auth / connection errors.
779
+ function hintFor(dialect, rawUri, err) {
780
+ const msg = (err && err.message) ? err.message : '';
781
+ const code = err && (err.code ?? err.codeName);
782
+
783
+ // MongoDB code 18 : Authentication failed → missing ?authSource=admin
784
+ if (dialect === 'mongodb' && (code === 18 || /AuthenticationFailed|18/.test(msg))) {
785
+ if (!/authSource=/.test(rawUri)) {
786
+ return 'MongoDB users are usually declared in the admin DB. Add ?authSource=admin to the URI :\n'
787
+ + ' ' + (rawUri.includes('?') ? rawUri + '&authSource=admin' : rawUri + '?authSource=admin');
788
+ }
789
+ return 'Verify credentials (username / password) in the URI.';
790
+ }
791
+
792
+ // PostgreSQL common errors
793
+ if (dialect === 'postgres' || dialect === 'cockroachdb') {
794
+ if (/password authentication failed/i.test(msg)) return 'Wrong PG password. Check the URI or run: psql "' + rawUri + '" -c "SELECT 1"';
795
+ if (/ECONNREFUSED|connect ECONNREFUSED/i.test(msg)) return 'PG server not running on that host/port. Try: pg_isready -h HOST -p PORT';
796
+ if (/no pg_hba.conf entry/i.test(msg)) return 'Server rejects the connection (pg_hba.conf). Add your IP or use SSL.';
797
+ }
798
+
799
+ // MySQL common errors
800
+ if (dialect === 'mysql' || dialect === 'mariadb') {
801
+ if (/Access denied for user/i.test(msg)) return 'Wrong MySQL password. Try: mysql -u USER -p -h HOST';
802
+ if (/ECONNREFUSED/i.test(msg)) return 'MySQL not running. Try: systemctl status mysql';
803
+ }
804
+
805
+ // SQLite common errors
806
+ if (dialect === 'sqlite') {
807
+ if (/SQLITE_CANTOPEN/i.test(msg)) return 'Cannot open SQLite file. Check the path exists and is writable.';
808
+ }
809
+
810
+ // Generic network errors
811
+ if (/ECONNREFUSED/i.test(msg)) return 'Service is not reachable at that host/port.';
812
+ if (/ENOTFOUND|getaddrinfo/i.test(msg)) return 'DNS resolution failed. Check the hostname.';
813
+ if (/ETIMEDOUT/i.test(msg)) return 'Connection timed out. Check firewall / VPN / host.';
814
+
815
+ return null;
816
+ }
817
+
775
818
  let ok = 0, fail = 0;
776
819
  for (const [dialect, rawUri] of pairs) {
777
820
  const uri = dialect === 'sqlite' ? stripScheme(rawUri) : rawUri;
@@ -790,6 +833,9 @@ for (const [dialect, rawUri] of pairs) {
790
833
  } catch (e) {
791
834
  console.error(' \u2717 ' + (e.message ?? e));
792
835
  if (e.code) console.error(' code : ' + e.code);
836
+ if (e.codeName) console.error(' codeName : ' + e.codeName);
837
+ const hint = hintFor(dialect, rawUri, e);
838
+ if (hint) console.error(' \u2192 ' + hint);
793
839
  fail++;
794
840
  }
795
841
  }
@@ -800,7 +846,66 @@ EOF
800
846
 
801
847
  cd "$PROJECT_ROOT"
802
848
  node "$CONFIG_DIR/test-connections.mjs" "${args[@]}" 2>&1 | tee "$LOG_DIR/test-connections.log"
849
+ local test_rc=${PIPESTATUS[0]}
803
850
  echo
851
+
852
+ # Offer to auto-fix common MongoDB auth issue (missing ?authSource=admin)
853
+ if [[ $test_rc -ne 0 ]] && grep -qE "authSource=admin|AuthenticationFailed|code : 18" "$LOG_DIR/test-connections.log"; then
854
+ echo
855
+ warn "Detected MongoDB authentication failure that is usually fixed by adding"
856
+ warn " ?authSource=admin to the URI."
857
+ if confirm "Append '?authSource=admin' to your MongoDB URI now?"; then
858
+ # Fix the primary SGBD_URI if it's mongodb
859
+ if [[ "${DB_DIALECT:-}" == "mongodb" ]] && [[ -n "${SGBD_URI:-}" ]] && [[ ! "$SGBD_URI" =~ authSource= ]]; then
860
+ local new_uri
861
+ if [[ "$SGBD_URI" =~ \? ]]; then
862
+ new_uri="${SGBD_URI}&authSource=admin"
863
+ else
864
+ new_uri="${SGBD_URI}?authSource=admin"
865
+ fi
866
+ save_var SGBD_URI "$new_uri"
867
+ ok "Updated SGBD_URI : $new_uri"
868
+ fi
869
+ # Fix any MongoDB extra binding missing authSource
870
+ if [[ -n "${EXTRA_BINDINGS:-}" ]]; then
871
+ local new_bindings=""
872
+ local IFS=';'
873
+ for b in $EXTRA_BINDINGS; do
874
+ local name="${b%%:*}"
875
+ local rest="${b#*:}"
876
+ local dialect="${rest%%:*}"
877
+ local uri="${rest#*:}"
878
+ if [[ "$dialect" == "mongodb" ]] && [[ ! "$uri" =~ authSource= ]]; then
879
+ if [[ "$uri" =~ \? ]]; then
880
+ uri="${uri}&authSource=admin"
881
+ else
882
+ uri="${uri}?authSource=admin"
883
+ fi
884
+ fi
885
+ new_bindings+="${new_bindings:+;}${name}:${dialect}:${uri}"
886
+ done
887
+ IFS=$' \t\n'
888
+ save_var EXTRA_BINDINGS "$new_bindings"
889
+ ok "Updated EXTRA_BINDINGS"
890
+ fi
891
+ echo
892
+ info "Re-running the test with the fixed URI..."
893
+ # Rebuild args with fresh env
894
+ load_env
895
+ args=()
896
+ [[ -n "${DB_DIALECT:-}" && -n "${SGBD_URI:-}" ]] && args+=("${DB_DIALECT}|${SGBD_URI}")
897
+ if [[ -n "${EXTRA_BINDINGS:-}" ]]; then
898
+ local IFS=';'
899
+ for b in $EXTRA_BINDINGS; do
900
+ local rest="${b#*:}"
901
+ args+=("${rest%%:*}|${rest#*:}")
902
+ done
903
+ IFS=$' \t\n'
904
+ fi
905
+ node "$CONFIG_DIR/test-connections.mjs" "${args[@]}" 2>&1 | tee "$LOG_DIR/test-connections.log"
906
+ fi
907
+ fi
908
+
804
909
  pause
805
910
  }
806
911
 
@@ -908,6 +1013,21 @@ const schemaStrategy = process.env.DB_SCHEMA_STRATEGY ?? 'update';
908
1013
  const poolSize = parseInt(process.env.DB_POOL_SIZE ?? '20', 10);
909
1014
  const showSql = process.env.DB_SHOW_SQL === 'true';
910
1015
 
1016
+ function hintFor(dialect, rawUri, err) {
1017
+ const msg = (err && err.message) ? err.message : '';
1018
+ const code = err && (err.code ?? err.codeName);
1019
+ if (dialect === 'mongodb' && (code === 18 || /AuthenticationFailed|18/.test(msg))) {
1020
+ if (!/authSource=/.test(rawUri)) {
1021
+ return 'Missing ?authSource=admin on MongoDB URI. Try :\n '
1022
+ + (rawUri.includes('?') ? rawUri + '&authSource=admin' : rawUri + '?authSource=admin');
1023
+ }
1024
+ }
1025
+ if (/ECONNREFUSED/i.test(msg)) return 'Service not running at that host/port';
1026
+ if (/ENOTFOUND/i.test(msg)) return 'DNS resolution failed';
1027
+ if (/ETIMEDOUT/i.test(msg)) return 'Connection timed out';
1028
+ return null;
1029
+ }
1030
+
911
1031
  for (const [dialect, rawUri] of pairs) {
912
1032
  const uri = dialect === 'sqlite' ? stripScheme(rawUri) : rawUri;
913
1033
  process.stdout.write('→ ' + dialect.padEnd(12) + ' : ' + rawUri + '\n');
@@ -920,6 +1040,8 @@ for (const [dialect, rawUri] of pairs) {
920
1040
  } catch (e) {
921
1041
  console.error(' ✗ ' + dialect + ' failed : ' + (e.message ?? e));
922
1042
  if (e.code) console.error(' code : ' + e.code);
1043
+ const hint = hintFor(dialect, rawUri, e);
1044
+ if (hint) console.error(' → ' + hint);
923
1045
  fail++;
924
1046
  }
925
1047
  }
@@ -1245,12 +1367,23 @@ show_urls() {
1245
1367
  load_env
1246
1368
  local ip
1247
1369
  ip=$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost)
1370
+
1371
+ # Parse mosta-net URL from config (full URL, not just port)
1372
+ local mosta_url="${MOSTA_NET_URL:-http://localhost:14488}"
1373
+ local mosta_host="${mosta_url#*://}"
1374
+ mosta_host="${mosta_host%%/*}"
1375
+
1376
+ # App URL : derive from APP_PORT
1377
+ local app_port="${APP_PORT:-3000}"
1378
+
1248
1379
  echo
1249
1380
  echo -e "${BOLD}Access URLs${RESET}"
1250
- echo -e " Dev server (local) : ${CYAN}http://localhost:${APP_PORT:-3000}${RESET}"
1251
- echo -e " Dev server (mobile) : ${CYAN}http://${ip}:${APP_PORT:-3000}${RESET}"
1252
- echo -e " mosta-net : ${CYAN}http://localhost:${MOSTA_NET_PORT:-4447}${RESET}"
1253
- echo -e " MCP endpoint (AI) : ${CYAN}http://localhost:${MOSTA_NET_PORT:-4447}/mcp${RESET}"
1381
+ echo -e " Dev server (local) : ${CYAN}http://localhost:${app_port}${RESET}"
1382
+ echo -e " Dev server (mobile) : ${CYAN}http://${ip}:${app_port}${RESET}"
1383
+ echo -e " mosta-net base : ${CYAN}${mosta_url}${RESET}"
1384
+ echo -e " REST CRUD : ${CYAN}${mosta_url}/api/v1/<collection>${RESET}"
1385
+ echo -e " MCP endpoint (AI) : ${CYAN}${mosta_url}/mcp${RESET}"
1386
+ echo -e " LAN (mobile) : ${CYAN}http://${ip}${mosta_host#localhost}${RESET}"
1254
1387
  }
1255
1388
 
1256
1389
  # ============================================================
@@ -1344,6 +1477,491 @@ action_healthcheck() {
1344
1477
  pause
1345
1478
  }
1346
1479
 
1480
+ # ============================================================
1481
+ # MENU S : SEEDING
1482
+ # ============================================================
1483
+
1484
+ menu_seeding() {
1485
+ load_env
1486
+ header
1487
+ echo -e "${BOLD}${MAGENTA}▶ Seeding — populate databases with test / initial data${RESET}"
1488
+ echo
1489
+ local seed_dir="$CONFIG_DIR/seeds"
1490
+ mkdir -p "$seed_dir"
1491
+ local count
1492
+ count=$(ls -1 "$seed_dir"/*.json 2>/dev/null | wc -l)
1493
+ echo -e " Seed directory : ${DIM}${seed_dir}${RESET}"
1494
+ echo -e " Seed files : ${DIM}${count} .json${RESET}"
1495
+ [[ $count -gt 0 ]] && ls "$seed_dir"/*.json 2>/dev/null | while read f; do
1496
+ local rows; rows=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync('$f','utf8')).length)}catch{console.log('?')}" 2>/dev/null)
1497
+ dim " - $(basename "$f") (${rows} rows)"
1498
+ done
1499
+ echo
1500
+ echo -e "${BOLD}━━━ SEEDING MENU ━━━${RESET}"
1501
+ echo
1502
+ echo -e " ${CYAN}1${RESET}) Upload / import seed file(s)"
1503
+ echo -e " ${CYAN}2${RESET}) Generate seed templates (one empty .json per entity)"
1504
+ echo -e " ${CYAN}3${RESET}) Validate seeds against schema (dry-run, no DB writes)"
1505
+ echo -e " ${CYAN}4${RESET}) Apply seeds to primary DB"
1506
+ echo -e " ${CYAN}5${RESET}) Apply seeds with upsert (insert or update by id)"
1507
+ echo -e " ${CYAN}6${RESET}) Truncate + apply (DESTRUCTIVE — wipes tables first)"
1508
+ echo -e " ${CYAN}7${RESET}) Dump current DB rows → .mostajs/seeds-dump/"
1509
+ echo -e " ${CYAN}8${RESET}) Clear the seeds directory"
1510
+ echo -e " ${CYAN}9${RESET}) Show a seed file"
1511
+ echo
1512
+ echo -e " ${CYAN}b${RESET}) Back"
1513
+ echo
1514
+ local choice
1515
+ choice=$(ask "Choice" "1")
1516
+ case "$choice" in
1517
+ 1) action_seed_upload ;;
1518
+ 2) action_seed_generate_templates ;;
1519
+ 3) action_seed_apply "validate" ;;
1520
+ 4) action_seed_apply "apply" ;;
1521
+ 5) action_seed_apply "upsert" ;;
1522
+ 6) action_seed_apply "truncate-apply" ;;
1523
+ 7) action_seed_dump ;;
1524
+ 8) action_seed_clear ;;
1525
+ 9) action_seed_show ;;
1526
+ b|B) return ;;
1527
+ *) warn "Unknown"; pause ;;
1528
+ esac
1529
+ menu_seeding
1530
+ }
1531
+
1532
+ # ------------------------------------------------------------
1533
+ # seed : upload / import
1534
+ # ------------------------------------------------------------
1535
+
1536
+ action_seed_upload() {
1537
+ header
1538
+ echo -e "${BOLD}${MAGENTA}▶ Upload seed file(s)${RESET}"
1539
+ echo
1540
+ local seed_dir="$CONFIG_DIR/seeds"
1541
+ mkdir -p "$seed_dir"
1542
+
1543
+ echo "Supported forms :"
1544
+ dim " (a) Single file with all collections : seeds.json → { users: [...], posts: [...] }"
1545
+ dim " (b) One file per collection (preferred): seeds/users.json = [...rows...]"
1546
+ dim " (c) CSV (header row = field names) : seeds/users.csv"
1547
+ echo
1548
+ local src
1549
+ src=$(ask "Path to seed file or directory (tilde OK)")
1550
+ [[ -z "$src" ]] && return
1551
+ src="${src/#\~/$HOME}"
1552
+ [[ ! -e "$src" ]] && { err "Path does not exist : $src"; pause; return; }
1553
+
1554
+ if [[ -d "$src" ]]; then
1555
+ local n=0
1556
+ for f in "$src"/*.{json,csv}; do
1557
+ [[ -f "$f" ]] || continue
1558
+ cp "$f" "$seed_dir/" && n=$((n+1))
1559
+ done
1560
+ ok "Imported $n file(s) from $src → $seed_dir"
1561
+ elif [[ "$src" =~ \.json$ ]]; then
1562
+ # Case (a) or (b) — detect
1563
+ local is_map
1564
+ is_map=$(node -e "
1565
+ const d = JSON.parse(require('fs').readFileSync('$src','utf8'));
1566
+ console.log(!Array.isArray(d) && typeof d === 'object' ? 'yes' : 'no');
1567
+ " 2>/dev/null)
1568
+ if [[ "$is_map" == "yes" ]]; then
1569
+ # Split into per-collection files
1570
+ node -e "
1571
+ const { writeFileSync } = require('fs');
1572
+ const d = JSON.parse(require('fs').readFileSync('$src','utf8'));
1573
+ let n = 0;
1574
+ for (const [coll, rows] of Object.entries(d)) {
1575
+ if (!Array.isArray(rows)) continue;
1576
+ writeFileSync('$seed_dir/' + coll + '.json', JSON.stringify(rows, null, 2));
1577
+ console.log(' wrote ' + coll + '.json (' + rows.length + ' rows)');
1578
+ n++;
1579
+ }
1580
+ console.log('split ' + n + ' collections');
1581
+ " 2>&1 | tee -a "$LOG_DIR/seed.log"
1582
+ else
1583
+ # Single-collection array
1584
+ local base; base=$(basename "$src")
1585
+ cp "$src" "$seed_dir/$base"
1586
+ ok "Copied → $seed_dir/$base"
1587
+ fi
1588
+ elif [[ "$src" =~ \.csv$ ]]; then
1589
+ local base; base=$(basename "$src" .csv)
1590
+ # Convert CSV to JSON (minimal — header row + comma-separated values)
1591
+ node -e "
1592
+ const { readFileSync, writeFileSync } = require('fs');
1593
+ const lines = readFileSync('$src','utf8').split(/\r?\n/).filter(Boolean);
1594
+ if (lines.length < 2) { console.error('CSV too small'); process.exit(1); }
1595
+ const headers = lines[0].split(',');
1596
+ const rows = lines.slice(1).map(line => {
1597
+ const vals = line.split(',');
1598
+ const o = {};
1599
+ headers.forEach((h, i) => { o[h.trim()] = vals[i]?.trim() ?? null; });
1600
+ return o;
1601
+ });
1602
+ writeFileSync('$seed_dir/${base}.json', JSON.stringify(rows, null, 2));
1603
+ console.log('Converted ' + rows.length + ' rows → $seed_dir/${base}.json');
1604
+ " 2>&1 | tee -a "$LOG_DIR/seed.log"
1605
+ else
1606
+ err "Unsupported file type — use .json or .csv"
1607
+ fi
1608
+ pause
1609
+ }
1610
+
1611
+ # ------------------------------------------------------------
1612
+ # seed : generate empty templates
1613
+ # ------------------------------------------------------------
1614
+
1615
+ action_seed_generate_templates() {
1616
+ header
1617
+ echo -e "${BOLD}${MAGENTA}▶ Generate empty seed templates${RESET}"
1618
+ echo
1619
+
1620
+ if [[ ! -f "$GENERATED_DIR/entities.json" ]]; then
1621
+ err "No entities.json — run menu 1 (Convert) first"
1622
+ pause; return
1623
+ fi
1624
+
1625
+ local seed_dir="$CONFIG_DIR/seeds"
1626
+ mkdir -p "$seed_dir"
1627
+
1628
+ if ! confirm "Generate a template .json per entity (will NOT overwrite existing files)?"; then
1629
+ return
1630
+ fi
1631
+
1632
+ node -e "
1633
+ const { readFileSync, writeFileSync, existsSync } = require('fs');
1634
+ const entities = JSON.parse(readFileSync('$GENERATED_DIR/entities.json','utf8'));
1635
+ let created = 0, skipped = 0;
1636
+ for (const e of entities) {
1637
+ const file = '$seed_dir/' + e.collection + '.json';
1638
+ if (existsSync(file)) { skipped++; continue; }
1639
+
1640
+ // Build a sample row from field defaults
1641
+ const sample = {};
1642
+ for (const [k, def] of Object.entries(e.fields ?? {})) {
1643
+ if (def.default !== undefined && typeof def.default !== 'object') sample[k] = def.default;
1644
+ else if (def.type === 'string' && def.enum) sample[k] = def.enum[0] ?? '';
1645
+ else if (def.type === 'string') sample[k] = 'example';
1646
+ else if (def.type === 'number') sample[k] = 0;
1647
+ else if (def.type === 'boolean') sample[k] = false;
1648
+ else if (def.type === 'date') sample[k] = new Date().toISOString();
1649
+ else if (def.type === 'json') sample[k] = {};
1650
+ else if (def.type === 'array') sample[k] = [];
1651
+ }
1652
+ writeFileSync(file, JSON.stringify([sample], null, 2));
1653
+ created++;
1654
+ }
1655
+ console.log('Created ' + created + ', skipped ' + skipped + ' (already existed)');
1656
+ " 2>&1 | tee -a "$LOG_DIR/seed.log"
1657
+ pause
1658
+ }
1659
+
1660
+ # ------------------------------------------------------------
1661
+ # seed : validate + apply
1662
+ # ------------------------------------------------------------
1663
+
1664
+ action_seed_apply() {
1665
+ local mode="$1" # validate | apply | upsert | truncate-apply
1666
+ header
1667
+ local title="Validate seeds (dry-run)"
1668
+ case "$mode" in
1669
+ apply) title="Apply seeds (insert)" ;;
1670
+ upsert) title="Apply seeds (upsert by id)" ;;
1671
+ truncate-apply) title="DESTRUCTIVE: truncate + apply" ;;
1672
+ esac
1673
+ echo -e "${BOLD}${MAGENTA}▶ $title${RESET}"
1674
+ echo
1675
+
1676
+ load_env
1677
+
1678
+ if [[ ! -f "$GENERATED_DIR/entities.json" ]]; then
1679
+ err "No entities.json — run menu 1 (Convert) first"
1680
+ pause; return
1681
+ fi
1682
+
1683
+ local seed_dir="$CONFIG_DIR/seeds"
1684
+ local file_count
1685
+ file_count=$(ls -1 "$seed_dir"/*.json 2>/dev/null | wc -l)
1686
+ if [[ $file_count -eq 0 ]]; then
1687
+ err "No seed files in $seed_dir — use menu S → 1 or 2"
1688
+ pause; return
1689
+ fi
1690
+
1691
+ if [[ "$mode" == "truncate-apply" ]]; then
1692
+ warn "This will TRUNCATE all tables listed in seeds before inserting."
1693
+ confirm "Really wipe the tables ?" || return
1694
+ if ! confirm "Are you SURE ? This cannot be undone." ; then return; fi
1695
+ fi
1696
+
1697
+ if [[ "$mode" != "validate" ]]; then
1698
+ if [[ -z "${DB_DIALECT:-}" || -z "${SGBD_URI:-}" ]]; then
1699
+ err "No DB configured (menu 2 first)"
1700
+ pause; return
1701
+ fi
1702
+ # Dialect specific driver
1703
+ ensure_pkg "@mostajs/orm" || return
1704
+ ensure_dialect_driver "$DB_DIALECT" || warn "Driver for $DB_DIALECT may be missing"
1705
+ fi
1706
+
1707
+ local orm_path
1708
+ orm_path=$(resolve_pkg_path "@mostajs/orm" 2>/dev/null)
1709
+ if [[ "$mode" != "validate" ]] && [[ -z "$orm_path" ]]; then
1710
+ err "Cannot resolve @mostajs/orm — install it first"
1711
+ pause; return
1712
+ fi
1713
+
1714
+ cat > "$CONFIG_DIR/seed-runner.mjs" <<EOF
1715
+ import { readFileSync, readdirSync } from 'fs';
1716
+ import { join } from 'path';
1717
+
1718
+ const MODE = process.argv[2] ?? 'validate';
1719
+ const SEEDDIR = process.argv[3];
1720
+ const ENTPATH = process.argv[4];
1721
+ const DIALECT = process.env.DB_DIALECT ?? '';
1722
+ const URI = process.env.SGBD_URI ?? '';
1723
+
1724
+ function stripScheme(u) {
1725
+ if (u.startsWith('sqlite://')) return u.slice(9);
1726
+ if (u.startsWith('sqlite:')) return u.slice(7);
1727
+ return u;
1728
+ }
1729
+
1730
+ const entities = JSON.parse(readFileSync(ENTPATH, 'utf8'));
1731
+ const entityByCollection = Object.fromEntries(entities.map(e => [e.collection, e]));
1732
+ const entityByName = Object.fromEntries(entities.map(e => [e.name, e]));
1733
+
1734
+ // ---------- Load seed files ----------
1735
+ const seeds = {}; // collection -> rows
1736
+ for (const f of readdirSync(SEEDDIR).filter(x => x.endsWith('.json'))) {
1737
+ const coll = f.replace(/\\.json$/, '');
1738
+ let data;
1739
+ try {
1740
+ data = JSON.parse(readFileSync(join(SEEDDIR, f), 'utf8'));
1741
+ } catch (e) {
1742
+ console.error(' \u2717 ' + f + ' : invalid JSON (' + e.message + ')');
1743
+ continue;
1744
+ }
1745
+ if (!Array.isArray(data)) {
1746
+ console.error(' \u2717 ' + f + ' : file is not an array of rows');
1747
+ continue;
1748
+ }
1749
+ seeds[coll] = data;
1750
+ }
1751
+
1752
+ // ---------- Validate ----------
1753
+ function validateRow(row, entity) {
1754
+ const errors = [];
1755
+ const fieldNames = new Set(Object.keys(entity.fields ?? {}));
1756
+ const relationNames = new Set(Object.keys(entity.relations ?? {}));
1757
+ // Required fields
1758
+ for (const [k, def] of Object.entries(entity.fields ?? {})) {
1759
+ if (def.required && (row[k] === undefined || row[k] === null)) {
1760
+ errors.push('missing required field "' + k + '"');
1761
+ }
1762
+ // Enum
1763
+ if (def.enum && row[k] !== undefined && !def.enum.includes(row[k])) {
1764
+ errors.push('field "' + k + '" not in enum ' + JSON.stringify(def.enum) + ' (got: ' + JSON.stringify(row[k]) + ')');
1765
+ }
1766
+ // Type basic check
1767
+ if (row[k] !== undefined && row[k] !== null) {
1768
+ const val = row[k];
1769
+ const t = def.type;
1770
+ if (t === 'number' && typeof val !== 'number') errors.push('field "' + k + '" expected number, got ' + typeof val);
1771
+ if (t === 'boolean' && typeof val !== 'boolean') errors.push('field "' + k + '" expected boolean, got ' + typeof val);
1772
+ if (t === 'string' && typeof val !== 'string') errors.push('field "' + k + '" expected string, got ' + typeof val);
1773
+ if (t === 'date' && typeof val !== 'string' && !(val instanceof Date)) errors.push('field "' + k + '" expected date, got ' + typeof val);
1774
+ }
1775
+ }
1776
+ // Unknown fields (warn-level)
1777
+ const warnings = [];
1778
+ for (const k of Object.keys(row)) {
1779
+ if (!fieldNames.has(k) && !relationNames.has(k) && k !== 'id' && k !== '_id') {
1780
+ warnings.push('unknown field "' + k + '" (not in schema)');
1781
+ }
1782
+ }
1783
+ return { errors, warnings };
1784
+ }
1785
+
1786
+ let totalRows = 0;
1787
+ let totalErrors = 0;
1788
+ let totalWarnings = 0;
1789
+ const reports = [];
1790
+
1791
+ for (const [coll, rows] of Object.entries(seeds)) {
1792
+ const entity = entityByCollection[coll] ?? entityByName[coll];
1793
+ if (!entity) {
1794
+ console.error(' \u2717 ' + coll + ' : no matching entity (collection or name)');
1795
+ continue;
1796
+ }
1797
+ let collErrs = 0, collWarns = 0;
1798
+ rows.forEach((row, i) => {
1799
+ const { errors, warnings } = validateRow(row, entity);
1800
+ if (errors.length) {
1801
+ collErrs += errors.length;
1802
+ for (const e of errors) console.error(' \u2717 ' + coll + '[' + i + '] : ' + e);
1803
+ }
1804
+ collWarns += warnings.length;
1805
+ for (const w of warnings) console.warn(' \u26A0 ' + coll + '[' + i + '] : ' + w);
1806
+ });
1807
+ const mark = collErrs === 0 ? '\u2713' : '\u2717';
1808
+ console.log(' ' + mark + ' ' + coll + ' : ' + rows.length + ' rows, ' + collErrs + ' errors, ' + collWarns + ' warnings');
1809
+ reports.push({ coll, rows: rows.length, errors: collErrs, warnings: collWarns });
1810
+ totalRows += rows.length;
1811
+ totalErrors += collErrs;
1812
+ totalWarnings += collWarns;
1813
+ }
1814
+
1815
+ console.log();
1816
+ console.log('Validation : ' + totalRows + ' rows · ' + totalErrors + ' errors · ' + totalWarnings + ' warnings');
1817
+
1818
+ if (MODE === 'validate') {
1819
+ process.exit(totalErrors > 0 ? 1 : 0);
1820
+ }
1821
+
1822
+ if (totalErrors > 0) {
1823
+ console.error('Refusing to apply : fix validation errors first (run menu S → 3).');
1824
+ process.exit(2);
1825
+ }
1826
+
1827
+ // ---------- Apply ----------
1828
+ const { getDialect } = await import('$orm_path');
1829
+ const uri = DIALECT === 'sqlite' ? stripScheme(URI) : URI;
1830
+ const d = await getDialect({ dialect: DIALECT, uri, schemaStrategy: 'update' });
1831
+ await d.initSchema(entities);
1832
+
1833
+ let inserted = 0, failed = 0;
1834
+
1835
+ for (const [coll, rows] of Object.entries(seeds)) {
1836
+ const entity = entityByCollection[coll] ?? entityByName[coll];
1837
+ if (!entity) continue;
1838
+
1839
+ if (MODE === 'truncate-apply') {
1840
+ try {
1841
+ await d.deleteMany(entity, {});
1842
+ console.log(' \u2205 truncated ' + coll);
1843
+ } catch (e) {
1844
+ console.error(' \u2717 truncate ' + coll + ' : ' + (e.message ?? e));
1845
+ }
1846
+ }
1847
+
1848
+ for (const row of rows) {
1849
+ try {
1850
+ if (MODE === 'upsert' && (row.id || row._id)) {
1851
+ const id = row.id ?? row._id;
1852
+ const existing = await d.findById(entity, String(id)).catch(() => null);
1853
+ if (existing) {
1854
+ await d.update(entity, String(id), row);
1855
+ } else {
1856
+ await d.create(entity, row);
1857
+ }
1858
+ } else {
1859
+ await d.create(entity, row);
1860
+ }
1861
+ inserted++;
1862
+ } catch (e) {
1863
+ failed++;
1864
+ console.error(' \u2717 ' + coll + ' : ' + (e.message ?? e));
1865
+ }
1866
+ }
1867
+ console.log(' \u2713 ' + coll + ' done');
1868
+ }
1869
+
1870
+ console.log();
1871
+ console.log('Applied : ' + inserted + ' inserted · ' + failed + ' failed');
1872
+ await d.disconnect().catch(() => {});
1873
+ process.exit(failed > 0 ? 1 : 0);
1874
+ EOF
1875
+
1876
+ cd "$PROJECT_ROOT" || return
1877
+ export DB_DIALECT="${DB_DIALECT:-}"
1878
+ export SGBD_URI="${SGBD_URI:-}"
1879
+ node "$CONFIG_DIR/seed-runner.mjs" "$mode" "$seed_dir" "$GENERATED_DIR/entities.json" 2>&1 | tee "$LOG_DIR/seed-${mode}.log"
1880
+ echo
1881
+ pause
1882
+ }
1883
+
1884
+ # ------------------------------------------------------------
1885
+ # seed : dump current DB → seed files
1886
+ # ------------------------------------------------------------
1887
+
1888
+ action_seed_dump() {
1889
+ header
1890
+ echo -e "${BOLD}${MAGENTA}▶ Dump current DB rows${RESET}"
1891
+ echo
1892
+ load_env
1893
+ if [[ -z "${DB_DIALECT:-}" || -z "${SGBD_URI:-}" ]]; then
1894
+ err "No DB configured (menu 2 first)"
1895
+ pause; return
1896
+ fi
1897
+ if [[ ! -f "$GENERATED_DIR/entities.json" ]]; then
1898
+ err "No entities.json — run menu 1 (Convert) first"
1899
+ pause; return
1900
+ fi
1901
+ ensure_pkg "@mostajs/orm" || return
1902
+ local orm_path
1903
+ orm_path=$(resolve_pkg_path "@mostajs/orm") || return
1904
+ local dump_dir="$CONFIG_DIR/seeds-dump"
1905
+ mkdir -p "$dump_dir"
1906
+
1907
+ cat > "$CONFIG_DIR/seed-dump.mjs" <<EOF
1908
+ import { readFileSync, writeFileSync } from 'fs';
1909
+ import { getDialect } from '$orm_path';
1910
+
1911
+ const entities = JSON.parse(readFileSync('$GENERATED_DIR/entities.json','utf8'));
1912
+ const DIALECT = process.env.DB_DIALECT;
1913
+ let URI = process.env.SGBD_URI;
1914
+ if (DIALECT === 'sqlite') {
1915
+ if (URI.startsWith('sqlite://')) URI = URI.slice(9);
1916
+ else if (URI.startsWith('sqlite:')) URI = URI.slice(7);
1917
+ }
1918
+ const d = await getDialect({ dialect: DIALECT, uri: URI });
1919
+ await d.initSchema(entities);
1920
+
1921
+ for (const e of entities) {
1922
+ const rows = await d.find(e, {}, { limit: 10000 });
1923
+ writeFileSync('$dump_dir/' + e.collection + '.json', JSON.stringify(rows, null, 2));
1924
+ console.log(' \u2713 ' + e.collection + ' : ' + rows.length + ' rows');
1925
+ }
1926
+ await d.disconnect().catch(() => {});
1927
+ EOF
1928
+ cd "$PROJECT_ROOT"
1929
+ DB_DIALECT="$DB_DIALECT" SGBD_URI="$SGBD_URI" node "$CONFIG_DIR/seed-dump.mjs" 2>&1 | tee "$LOG_DIR/seed-dump.log"
1930
+ echo
1931
+ ok "Dump written to $dump_dir"
1932
+ pause
1933
+ }
1934
+
1935
+ action_seed_clear() {
1936
+ header
1937
+ local seed_dir="$CONFIG_DIR/seeds"
1938
+ local count; count=$(ls -1 "$seed_dir"/*.json 2>/dev/null | wc -l)
1939
+ [[ $count -eq 0 ]] && { warn "Already empty"; pause; return; }
1940
+ if confirm "Delete all $count seed files in $seed_dir ?"; then
1941
+ rm -f "$seed_dir"/*.json
1942
+ ok "Cleared"
1943
+ fi
1944
+ pause
1945
+ }
1946
+
1947
+ action_seed_show() {
1948
+ header
1949
+ local seed_dir="$CONFIG_DIR/seeds"
1950
+ local files=()
1951
+ for f in "$seed_dir"/*.json; do [[ -f "$f" ]] && files+=("$f"); done
1952
+ [[ ${#files[@]} -eq 0 ]] && { warn "No seed files"; pause; return; }
1953
+ echo "Pick a file to display :"
1954
+ local i=1
1955
+ for f in "${files[@]}"; do
1956
+ echo -e " ${CYAN}$i${RESET}) $(basename "$f")"
1957
+ i=$((i+1))
1958
+ done
1959
+ local num; num=$(ask "Number" 1)
1960
+ local idx=$((num-1))
1961
+ [[ $idx -ge 0 && $idx -lt ${#files[@]} ]] && ${PAGER:-less} "${files[$idx]}"
1962
+ pause
1963
+ }
1964
+
1347
1965
  # ============================================================
1348
1966
  # ACTION 9 : GENERATE BOILERPLATE
1349
1967
  # ============================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/orm-cli",
3
- "version": "0.2.2",
3
+ "version": "0.3.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",