@mostajs/orm-cli 0.2.3 → 0.3.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 +667 -9
  2. package/package.json +2 -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 ;;
@@ -1365,12 +1367,23 @@ show_urls() {
1365
1367
  load_env
1366
1368
  local ip
1367
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
+
1368
1379
  echo
1369
1380
  echo -e "${BOLD}Access URLs${RESET}"
1370
- echo -e " Dev server (local) : ${CYAN}http://localhost:${APP_PORT:-3000}${RESET}"
1371
- echo -e " Dev server (mobile) : ${CYAN}http://${ip}:${APP_PORT:-3000}${RESET}"
1372
- echo -e " mosta-net : ${CYAN}http://localhost:${MOSTA_NET_PORT:-4447}${RESET}"
1373
- 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}"
1374
1387
  }
1375
1388
 
1376
1389
  # ============================================================
@@ -1464,6 +1477,594 @@ action_healthcheck() {
1464
1477
  pause
1465
1478
  }
1466
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 -e " ${CYAN}h${RESET}) ${BOLD}Hash plain-text passwords in seed files${RESET} (bcrypt)"
1512
+ echo
1513
+ echo -e " ${CYAN}b${RESET}) Back"
1514
+ echo
1515
+ local choice
1516
+ choice=$(ask "Choice" "1")
1517
+ case "$choice" in
1518
+ 1) action_seed_upload ;;
1519
+ 2) action_seed_generate_templates ;;
1520
+ 3) action_seed_apply "validate" ;;
1521
+ 4) action_seed_apply "apply" ;;
1522
+ 5) action_seed_apply "upsert" ;;
1523
+ 6) action_seed_apply "truncate-apply" ;;
1524
+ 7) action_seed_dump ;;
1525
+ 8) action_seed_clear ;;
1526
+ 9) action_seed_show ;;
1527
+ h|H) action_seed_hash_passwords ;;
1528
+ b|B) return ;;
1529
+ *) warn "Unknown"; pause ;;
1530
+ esac
1531
+ menu_seeding
1532
+ }
1533
+
1534
+ # ------------------------------------------------------------
1535
+ # seed : hash plain-text passwords
1536
+ # ------------------------------------------------------------
1537
+
1538
+ action_seed_hash_passwords() {
1539
+ header
1540
+ echo -e "${BOLD}${MAGENTA}▶ Hash plain-text passwords in seed files${RESET}"
1541
+ echo
1542
+
1543
+ local seed_dir="$CONFIG_DIR/seeds"
1544
+ local count
1545
+ count=$(ls -1 "$seed_dir"/*.json 2>/dev/null | wc -l)
1546
+ if [[ $count -eq 0 ]]; then
1547
+ err "No seed files in $seed_dir (menu S → 1 first)"
1548
+ pause; return
1549
+ fi
1550
+
1551
+ echo "Auto-detects fields named :"
1552
+ dim " password, passwordHash, hashedPassword, pwd, userPassword"
1553
+ echo
1554
+ echo "Skips values that are already bcrypt hashes (start with \$2a\$ / \$2b\$ / \$2y\$, 60 chars)"
1555
+ echo
1556
+
1557
+ local cost
1558
+ cost=$(ask "bcrypt cost factor" "${BCRYPT_COST:-10}")
1559
+ if ! confirm "Hash all plain passwords in $seed_dir ?"; then
1560
+ return
1561
+ fi
1562
+
1563
+ # Ensure bcryptjs is available
1564
+ ensure_pkg "bcryptjs" || { pause; return; }
1565
+
1566
+ cat > "$CONFIG_DIR/seed-hash.mjs" <<EOF
1567
+ import { readdirSync, readFileSync, writeFileSync } from 'fs';
1568
+ import { join } from 'path';
1569
+ import bcrypt from 'bcryptjs';
1570
+
1571
+ const SEEDDIR = '$seed_dir';
1572
+ const COST = $cost;
1573
+
1574
+ // Field names commonly used for passwords
1575
+ const PASSWORD_FIELDS = ['password', 'passwordHash', 'hashedPassword', 'pwd', 'userPassword'];
1576
+
1577
+ // Test if a string is already a bcrypt hash
1578
+ const isBcrypt = v => typeof v === 'string' && /^\\\$2[ayb]\\\$\\d{1,2}\\\$/.test(v) && v.length === 60;
1579
+
1580
+ let totalHashed = 0;
1581
+ let totalSkipped = 0;
1582
+ const summary = [];
1583
+
1584
+ for (const f of readdirSync(SEEDDIR).filter(x => x.endsWith('.json'))) {
1585
+ const path = join(SEEDDIR, f);
1586
+ let data;
1587
+ try { data = JSON.parse(readFileSync(path, 'utf8')); }
1588
+ catch { continue; }
1589
+
1590
+ if (!Array.isArray(data)) continue;
1591
+
1592
+ let hashed = 0, skipped = 0;
1593
+ for (const row of data) {
1594
+ for (const field of PASSWORD_FIELDS) {
1595
+ const val = row[field];
1596
+ if (typeof val !== 'string' || val.length === 0) continue;
1597
+ if (isBcrypt(val)) { skipped++; continue; }
1598
+ row[field] = bcrypt.hashSync(val, COST);
1599
+ hashed++;
1600
+ }
1601
+ }
1602
+
1603
+ if (hashed > 0) {
1604
+ writeFileSync(path, JSON.stringify(data, null, 2));
1605
+ summary.push({ file: f, hashed, skipped });
1606
+ }
1607
+ totalHashed += hashed;
1608
+ totalSkipped += skipped;
1609
+ }
1610
+
1611
+ for (const s of summary) {
1612
+ console.log(' \u2713 ' + s.file + ' : ' + s.hashed + ' hashed' + (s.skipped ? ' (+ ' + s.skipped + ' already hashed)' : ''));
1613
+ }
1614
+ console.log();
1615
+ console.log('Total : ' + totalHashed + ' hashed, ' + totalSkipped + ' already-hashed skipped');
1616
+ if (totalHashed === 0 && totalSkipped === 0) {
1617
+ console.log('No password fields found.');
1618
+ }
1619
+ EOF
1620
+ cd "$PROJECT_ROOT"
1621
+ node "$CONFIG_DIR/seed-hash.mjs" 2>&1 | tee "$LOG_DIR/seed-hash.log"
1622
+ echo
1623
+ pause
1624
+ }
1625
+
1626
+ # ------------------------------------------------------------
1627
+ # seed : upload / import
1628
+ # ------------------------------------------------------------
1629
+
1630
+ action_seed_upload() {
1631
+ header
1632
+ echo -e "${BOLD}${MAGENTA}▶ Upload seed file(s)${RESET}"
1633
+ echo
1634
+ local seed_dir="$CONFIG_DIR/seeds"
1635
+ mkdir -p "$seed_dir"
1636
+
1637
+ echo "Supported forms :"
1638
+ dim " (a) Single file with all collections : seeds.json → { users: [...], posts: [...] }"
1639
+ dim " (b) One file per collection (preferred): seeds/users.json = [...rows...]"
1640
+ dim " (c) CSV (header row = field names) : seeds/users.csv"
1641
+ echo
1642
+ local src
1643
+ src=$(ask "Path to seed file or directory (tilde OK)")
1644
+ [[ -z "$src" ]] && return
1645
+ src="${src/#\~/$HOME}"
1646
+ [[ ! -e "$src" ]] && { err "Path does not exist : $src"; pause; return; }
1647
+
1648
+ if [[ -d "$src" ]]; then
1649
+ local n=0
1650
+ for f in "$src"/*.{json,csv}; do
1651
+ [[ -f "$f" ]] || continue
1652
+ cp "$f" "$seed_dir/" && n=$((n+1))
1653
+ done
1654
+ ok "Imported $n file(s) from $src → $seed_dir"
1655
+ elif [[ "$src" =~ \.json$ ]]; then
1656
+ # Case (a) or (b) — detect
1657
+ local is_map
1658
+ is_map=$(node -e "
1659
+ const d = JSON.parse(require('fs').readFileSync('$src','utf8'));
1660
+ console.log(!Array.isArray(d) && typeof d === 'object' ? 'yes' : 'no');
1661
+ " 2>/dev/null)
1662
+ if [[ "$is_map" == "yes" ]]; then
1663
+ # Split into per-collection files
1664
+ node -e "
1665
+ const { writeFileSync } = require('fs');
1666
+ const d = JSON.parse(require('fs').readFileSync('$src','utf8'));
1667
+ let n = 0;
1668
+ for (const [coll, rows] of Object.entries(d)) {
1669
+ if (!Array.isArray(rows)) continue;
1670
+ writeFileSync('$seed_dir/' + coll + '.json', JSON.stringify(rows, null, 2));
1671
+ console.log(' wrote ' + coll + '.json (' + rows.length + ' rows)');
1672
+ n++;
1673
+ }
1674
+ console.log('split ' + n + ' collections');
1675
+ " 2>&1 | tee -a "$LOG_DIR/seed.log"
1676
+ else
1677
+ # Single-collection array
1678
+ local base; base=$(basename "$src")
1679
+ cp "$src" "$seed_dir/$base"
1680
+ ok "Copied → $seed_dir/$base"
1681
+ fi
1682
+ elif [[ "$src" =~ \.csv$ ]]; then
1683
+ local base; base=$(basename "$src" .csv)
1684
+ # Convert CSV to JSON (minimal — header row + comma-separated values)
1685
+ node -e "
1686
+ const { readFileSync, writeFileSync } = require('fs');
1687
+ const lines = readFileSync('$src','utf8').split(/\r?\n/).filter(Boolean);
1688
+ if (lines.length < 2) { console.error('CSV too small'); process.exit(1); }
1689
+ const headers = lines[0].split(',');
1690
+ const rows = lines.slice(1).map(line => {
1691
+ const vals = line.split(',');
1692
+ const o = {};
1693
+ headers.forEach((h, i) => { o[h.trim()] = vals[i]?.trim() ?? null; });
1694
+ return o;
1695
+ });
1696
+ writeFileSync('$seed_dir/${base}.json', JSON.stringify(rows, null, 2));
1697
+ console.log('Converted ' + rows.length + ' rows → $seed_dir/${base}.json');
1698
+ " 2>&1 | tee -a "$LOG_DIR/seed.log"
1699
+ else
1700
+ err "Unsupported file type — use .json or .csv"
1701
+ fi
1702
+ pause
1703
+ }
1704
+
1705
+ # ------------------------------------------------------------
1706
+ # seed : generate empty templates
1707
+ # ------------------------------------------------------------
1708
+
1709
+ action_seed_generate_templates() {
1710
+ header
1711
+ echo -e "${BOLD}${MAGENTA}▶ Generate empty seed templates${RESET}"
1712
+ echo
1713
+
1714
+ if [[ ! -f "$GENERATED_DIR/entities.json" ]]; then
1715
+ err "No entities.json — run menu 1 (Convert) first"
1716
+ pause; return
1717
+ fi
1718
+
1719
+ local seed_dir="$CONFIG_DIR/seeds"
1720
+ mkdir -p "$seed_dir"
1721
+
1722
+ if ! confirm "Generate a template .json per entity (will NOT overwrite existing files)?"; then
1723
+ return
1724
+ fi
1725
+
1726
+ node -e "
1727
+ const { readFileSync, writeFileSync, existsSync } = require('fs');
1728
+ const entities = JSON.parse(readFileSync('$GENERATED_DIR/entities.json','utf8'));
1729
+ let created = 0, skipped = 0;
1730
+ for (const e of entities) {
1731
+ const file = '$seed_dir/' + e.collection + '.json';
1732
+ if (existsSync(file)) { skipped++; continue; }
1733
+
1734
+ // Build a sample row from field defaults
1735
+ const sample = {};
1736
+ for (const [k, def] of Object.entries(e.fields ?? {})) {
1737
+ if (def.default !== undefined && typeof def.default !== 'object') sample[k] = def.default;
1738
+ else if (def.type === 'string' && def.enum) sample[k] = def.enum[0] ?? '';
1739
+ else if (def.type === 'string') sample[k] = 'example';
1740
+ else if (def.type === 'number') sample[k] = 0;
1741
+ else if (def.type === 'boolean') sample[k] = false;
1742
+ else if (def.type === 'date') sample[k] = new Date().toISOString();
1743
+ else if (def.type === 'json') sample[k] = {};
1744
+ else if (def.type === 'array') sample[k] = [];
1745
+ }
1746
+ writeFileSync(file, JSON.stringify([sample], null, 2));
1747
+ created++;
1748
+ }
1749
+ console.log('Created ' + created + ', skipped ' + skipped + ' (already existed)');
1750
+ " 2>&1 | tee -a "$LOG_DIR/seed.log"
1751
+ pause
1752
+ }
1753
+
1754
+ # ------------------------------------------------------------
1755
+ # seed : validate + apply
1756
+ # ------------------------------------------------------------
1757
+
1758
+ action_seed_apply() {
1759
+ local mode="$1" # validate | apply | upsert | truncate-apply
1760
+ header
1761
+ local title="Validate seeds (dry-run)"
1762
+ case "$mode" in
1763
+ apply) title="Apply seeds (insert)" ;;
1764
+ upsert) title="Apply seeds (upsert by id)" ;;
1765
+ truncate-apply) title="DESTRUCTIVE: truncate + apply" ;;
1766
+ esac
1767
+ echo -e "${BOLD}${MAGENTA}▶ $title${RESET}"
1768
+ echo
1769
+
1770
+ load_env
1771
+
1772
+ if [[ ! -f "$GENERATED_DIR/entities.json" ]]; then
1773
+ err "No entities.json — run menu 1 (Convert) first"
1774
+ pause; return
1775
+ fi
1776
+
1777
+ local seed_dir="$CONFIG_DIR/seeds"
1778
+ local file_count
1779
+ file_count=$(ls -1 "$seed_dir"/*.json 2>/dev/null | wc -l)
1780
+ if [[ $file_count -eq 0 ]]; then
1781
+ err "No seed files in $seed_dir — use menu S → 1 or 2"
1782
+ pause; return
1783
+ fi
1784
+
1785
+ if [[ "$mode" == "truncate-apply" ]]; then
1786
+ warn "This will TRUNCATE all tables listed in seeds before inserting."
1787
+ confirm "Really wipe the tables ?" || return
1788
+ if ! confirm "Are you SURE ? This cannot be undone." ; then return; fi
1789
+ fi
1790
+
1791
+ if [[ "$mode" != "validate" ]]; then
1792
+ if [[ -z "${DB_DIALECT:-}" || -z "${SGBD_URI:-}" ]]; then
1793
+ err "No DB configured (menu 2 first)"
1794
+ pause; return
1795
+ fi
1796
+ # Dialect specific driver
1797
+ ensure_pkg "@mostajs/orm" || return
1798
+ ensure_dialect_driver "$DB_DIALECT" || warn "Driver for $DB_DIALECT may be missing"
1799
+ fi
1800
+
1801
+ local orm_path
1802
+ orm_path=$(resolve_pkg_path "@mostajs/orm" 2>/dev/null)
1803
+ if [[ "$mode" != "validate" ]] && [[ -z "$orm_path" ]]; then
1804
+ err "Cannot resolve @mostajs/orm — install it first"
1805
+ pause; return
1806
+ fi
1807
+
1808
+ cat > "$CONFIG_DIR/seed-runner.mjs" <<EOF
1809
+ import { readFileSync, readdirSync } from 'fs';
1810
+ import { join } from 'path';
1811
+
1812
+ const MODE = process.argv[2] ?? 'validate';
1813
+ const SEEDDIR = process.argv[3];
1814
+ const ENTPATH = process.argv[4];
1815
+ const DIALECT = process.env.DB_DIALECT ?? '';
1816
+ const URI = process.env.SGBD_URI ?? '';
1817
+
1818
+ function stripScheme(u) {
1819
+ if (u.startsWith('sqlite://')) return u.slice(9);
1820
+ if (u.startsWith('sqlite:')) return u.slice(7);
1821
+ return u;
1822
+ }
1823
+
1824
+ const entities = JSON.parse(readFileSync(ENTPATH, 'utf8'));
1825
+ const entityByCollection = Object.fromEntries(entities.map(e => [e.collection, e]));
1826
+ const entityByName = Object.fromEntries(entities.map(e => [e.name, e]));
1827
+
1828
+ // ---------- Load seed files ----------
1829
+ const seeds = {}; // collection -> rows
1830
+ for (const f of readdirSync(SEEDDIR).filter(x => x.endsWith('.json'))) {
1831
+ const coll = f.replace(/\\.json$/, '');
1832
+ let data;
1833
+ try {
1834
+ data = JSON.parse(readFileSync(join(SEEDDIR, f), 'utf8'));
1835
+ } catch (e) {
1836
+ console.error(' \u2717 ' + f + ' : invalid JSON (' + e.message + ')');
1837
+ continue;
1838
+ }
1839
+ if (!Array.isArray(data)) {
1840
+ console.error(' \u2717 ' + f + ' : file is not an array of rows');
1841
+ continue;
1842
+ }
1843
+ seeds[coll] = data;
1844
+ }
1845
+
1846
+ // ---------- Validate ----------
1847
+ const PASSWORD_FIELDS = ['password', 'passwordHash', 'hashedPassword', 'pwd', 'userPassword'];
1848
+ const isBcrypt = v => typeof v === 'string' && /^\\\$2[ayb]\\\$\\d{1,2}\\\$/.test(v) && v.length === 60;
1849
+
1850
+ function validateRow(row, entity) {
1851
+ const errors = [];
1852
+ const fieldNames = new Set(Object.keys(entity.fields ?? {}));
1853
+ const relationNames = new Set(Object.keys(entity.relations ?? {}));
1854
+ // Required fields
1855
+ for (const [k, def] of Object.entries(entity.fields ?? {})) {
1856
+ if (def.required && (row[k] === undefined || row[k] === null)) {
1857
+ errors.push('missing required field "' + k + '"');
1858
+ }
1859
+ // Enum
1860
+ if (def.enum && row[k] !== undefined && !def.enum.includes(row[k])) {
1861
+ errors.push('field "' + k + '" not in enum ' + JSON.stringify(def.enum) + ' (got: ' + JSON.stringify(row[k]) + ')');
1862
+ }
1863
+ // Type basic check
1864
+ if (row[k] !== undefined && row[k] !== null) {
1865
+ const val = row[k];
1866
+ const t = def.type;
1867
+ if (t === 'number' && typeof val !== 'number') errors.push('field "' + k + '" expected number, got ' + typeof val);
1868
+ if (t === 'boolean' && typeof val !== 'boolean') errors.push('field "' + k + '" expected boolean, got ' + typeof val);
1869
+ if (t === 'string' && typeof val !== 'string') errors.push('field "' + k + '" expected string, got ' + typeof val);
1870
+ if (t === 'date' && typeof val !== 'string' && !(val instanceof Date)) errors.push('field "' + k + '" expected date, got ' + typeof val);
1871
+ }
1872
+ }
1873
+ // Unknown fields (warn-level)
1874
+ const warnings = [];
1875
+ for (const k of Object.keys(row)) {
1876
+ if (!fieldNames.has(k) && !relationNames.has(k) && k !== 'id' && k !== '_id') {
1877
+ warnings.push('unknown field "' + k + '" (not in schema)');
1878
+ }
1879
+ }
1880
+ // Password fields that look like plain-text (not bcrypt)
1881
+ for (const pf of PASSWORD_FIELDS) {
1882
+ if (row[pf] !== undefined && row[pf] !== null && row[pf] !== '' && !isBcrypt(row[pf])) {
1883
+ warnings.push('field "' + pf + '" does not look like a bcrypt hash — run menu S \u2192 h to hash');
1884
+ }
1885
+ }
1886
+ return { errors, warnings };
1887
+ }
1888
+
1889
+ let totalRows = 0;
1890
+ let totalErrors = 0;
1891
+ let totalWarnings = 0;
1892
+ const reports = [];
1893
+
1894
+ for (const [coll, rows] of Object.entries(seeds)) {
1895
+ const entity = entityByCollection[coll] ?? entityByName[coll];
1896
+ if (!entity) {
1897
+ console.error(' \u2717 ' + coll + ' : no matching entity (collection or name)');
1898
+ continue;
1899
+ }
1900
+ let collErrs = 0, collWarns = 0;
1901
+ rows.forEach((row, i) => {
1902
+ const { errors, warnings } = validateRow(row, entity);
1903
+ if (errors.length) {
1904
+ collErrs += errors.length;
1905
+ for (const e of errors) console.error(' \u2717 ' + coll + '[' + i + '] : ' + e);
1906
+ }
1907
+ collWarns += warnings.length;
1908
+ for (const w of warnings) console.warn(' \u26A0 ' + coll + '[' + i + '] : ' + w);
1909
+ });
1910
+ const mark = collErrs === 0 ? '\u2713' : '\u2717';
1911
+ console.log(' ' + mark + ' ' + coll + ' : ' + rows.length + ' rows, ' + collErrs + ' errors, ' + collWarns + ' warnings');
1912
+ reports.push({ coll, rows: rows.length, errors: collErrs, warnings: collWarns });
1913
+ totalRows += rows.length;
1914
+ totalErrors += collErrs;
1915
+ totalWarnings += collWarns;
1916
+ }
1917
+
1918
+ console.log();
1919
+ console.log('Validation : ' + totalRows + ' rows · ' + totalErrors + ' errors · ' + totalWarnings + ' warnings');
1920
+
1921
+ if (MODE === 'validate') {
1922
+ process.exit(totalErrors > 0 ? 1 : 0);
1923
+ }
1924
+
1925
+ if (totalErrors > 0) {
1926
+ console.error('Refusing to apply : fix validation errors first (run menu S → 3).');
1927
+ process.exit(2);
1928
+ }
1929
+
1930
+ // ---------- Apply ----------
1931
+ const { getDialect } = await import('$orm_path');
1932
+ const uri = DIALECT === 'sqlite' ? stripScheme(URI) : URI;
1933
+ const d = await getDialect({ dialect: DIALECT, uri, schemaStrategy: 'update' });
1934
+ await d.initSchema(entities);
1935
+
1936
+ let inserted = 0, failed = 0;
1937
+
1938
+ for (const [coll, rows] of Object.entries(seeds)) {
1939
+ const entity = entityByCollection[coll] ?? entityByName[coll];
1940
+ if (!entity) continue;
1941
+
1942
+ if (MODE === 'truncate-apply') {
1943
+ try {
1944
+ await d.deleteMany(entity, {});
1945
+ console.log(' \u2205 truncated ' + coll);
1946
+ } catch (e) {
1947
+ console.error(' \u2717 truncate ' + coll + ' : ' + (e.message ?? e));
1948
+ }
1949
+ }
1950
+
1951
+ for (const row of rows) {
1952
+ try {
1953
+ if (MODE === 'upsert' && (row.id || row._id)) {
1954
+ const id = row.id ?? row._id;
1955
+ const existing = await d.findById(entity, String(id)).catch(() => null);
1956
+ if (existing) {
1957
+ await d.update(entity, String(id), row);
1958
+ } else {
1959
+ await d.create(entity, row);
1960
+ }
1961
+ } else {
1962
+ await d.create(entity, row);
1963
+ }
1964
+ inserted++;
1965
+ } catch (e) {
1966
+ failed++;
1967
+ console.error(' \u2717 ' + coll + ' : ' + (e.message ?? e));
1968
+ }
1969
+ }
1970
+ console.log(' \u2713 ' + coll + ' done');
1971
+ }
1972
+
1973
+ console.log();
1974
+ console.log('Applied : ' + inserted + ' inserted · ' + failed + ' failed');
1975
+ await d.disconnect().catch(() => {});
1976
+ process.exit(failed > 0 ? 1 : 0);
1977
+ EOF
1978
+
1979
+ cd "$PROJECT_ROOT" || return
1980
+ export DB_DIALECT="${DB_DIALECT:-}"
1981
+ export SGBD_URI="${SGBD_URI:-}"
1982
+ node "$CONFIG_DIR/seed-runner.mjs" "$mode" "$seed_dir" "$GENERATED_DIR/entities.json" 2>&1 | tee "$LOG_DIR/seed-${mode}.log"
1983
+ echo
1984
+ pause
1985
+ }
1986
+
1987
+ # ------------------------------------------------------------
1988
+ # seed : dump current DB → seed files
1989
+ # ------------------------------------------------------------
1990
+
1991
+ action_seed_dump() {
1992
+ header
1993
+ echo -e "${BOLD}${MAGENTA}▶ Dump current DB rows${RESET}"
1994
+ echo
1995
+ load_env
1996
+ if [[ -z "${DB_DIALECT:-}" || -z "${SGBD_URI:-}" ]]; then
1997
+ err "No DB configured (menu 2 first)"
1998
+ pause; return
1999
+ fi
2000
+ if [[ ! -f "$GENERATED_DIR/entities.json" ]]; then
2001
+ err "No entities.json — run menu 1 (Convert) first"
2002
+ pause; return
2003
+ fi
2004
+ ensure_pkg "@mostajs/orm" || return
2005
+ local orm_path
2006
+ orm_path=$(resolve_pkg_path "@mostajs/orm") || return
2007
+ local dump_dir="$CONFIG_DIR/seeds-dump"
2008
+ mkdir -p "$dump_dir"
2009
+
2010
+ cat > "$CONFIG_DIR/seed-dump.mjs" <<EOF
2011
+ import { readFileSync, writeFileSync } from 'fs';
2012
+ import { getDialect } from '$orm_path';
2013
+
2014
+ const entities = JSON.parse(readFileSync('$GENERATED_DIR/entities.json','utf8'));
2015
+ const DIALECT = process.env.DB_DIALECT;
2016
+ let URI = process.env.SGBD_URI;
2017
+ if (DIALECT === 'sqlite') {
2018
+ if (URI.startsWith('sqlite://')) URI = URI.slice(9);
2019
+ else if (URI.startsWith('sqlite:')) URI = URI.slice(7);
2020
+ }
2021
+ const d = await getDialect({ dialect: DIALECT, uri: URI });
2022
+ await d.initSchema(entities);
2023
+
2024
+ for (const e of entities) {
2025
+ const rows = await d.find(e, {}, { limit: 10000 });
2026
+ writeFileSync('$dump_dir/' + e.collection + '.json', JSON.stringify(rows, null, 2));
2027
+ console.log(' \u2713 ' + e.collection + ' : ' + rows.length + ' rows');
2028
+ }
2029
+ await d.disconnect().catch(() => {});
2030
+ EOF
2031
+ cd "$PROJECT_ROOT"
2032
+ DB_DIALECT="$DB_DIALECT" SGBD_URI="$SGBD_URI" node "$CONFIG_DIR/seed-dump.mjs" 2>&1 | tee "$LOG_DIR/seed-dump.log"
2033
+ echo
2034
+ ok "Dump written to $dump_dir"
2035
+ pause
2036
+ }
2037
+
2038
+ action_seed_clear() {
2039
+ header
2040
+ local seed_dir="$CONFIG_DIR/seeds"
2041
+ local count; count=$(ls -1 "$seed_dir"/*.json 2>/dev/null | wc -l)
2042
+ [[ $count -eq 0 ]] && { warn "Already empty"; pause; return; }
2043
+ if confirm "Delete all $count seed files in $seed_dir ?"; then
2044
+ rm -f "$seed_dir"/*.json
2045
+ ok "Cleared"
2046
+ fi
2047
+ pause
2048
+ }
2049
+
2050
+ action_seed_show() {
2051
+ header
2052
+ local seed_dir="$CONFIG_DIR/seeds"
2053
+ local files=()
2054
+ for f in "$seed_dir"/*.json; do [[ -f "$f" ]] && files+=("$f"); done
2055
+ [[ ${#files[@]} -eq 0 ]] && { warn "No seed files"; pause; return; }
2056
+ echo "Pick a file to display :"
2057
+ local i=1
2058
+ for f in "${files[@]}"; do
2059
+ echo -e " ${CYAN}$i${RESET}) $(basename "$f")"
2060
+ i=$((i+1))
2061
+ done
2062
+ local num; num=$(ask "Number" 1)
2063
+ local idx=$((num-1))
2064
+ [[ $idx -ge 0 && $idx -lt ${#files[@]} ]] && ${PAGER:-less} "${files[$idx]}"
2065
+ pause
2066
+ }
2067
+
1467
2068
  # ============================================================
1468
2069
  # ACTION 9 : GENERATE BOILERPLATE
1469
2070
  # ============================================================
@@ -1624,6 +2225,55 @@ EOF
1624
2225
 
1625
2226
  run_subcommand() {
1626
2227
  case "$1" in
2228
+ hash|h)
2229
+ # mostajs hash <plaintext> [cost]
2230
+ local pw="${2:-}"
2231
+ local cost="${3:-10}"
2232
+ [[ -z "$pw" ]] && { echo "Usage: mostajs hash <password> [cost=10]" >&2; exit 1; }
2233
+ # Try local project, then CLI's own node_modules (bcryptjs is a dep)
2234
+ local bcrypt_dir=""
2235
+ if [[ -d "$PROJECT_ROOT/node_modules/bcryptjs" ]]; then
2236
+ bcrypt_dir="$PROJECT_ROOT/node_modules/bcryptjs"
2237
+ else
2238
+ local cli_dir
2239
+ cli_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
2240
+ [[ -d "$cli_dir/node_modules/bcryptjs" ]] && bcrypt_dir="$cli_dir/node_modules/bcryptjs"
2241
+ fi
2242
+ if [[ -z "$bcrypt_dir" ]]; then
2243
+ ensure_pkg "bcryptjs" >/dev/null 2>&1 || {
2244
+ err "bcryptjs not available. Install it manually : npm install bcryptjs"
2245
+ exit 1
2246
+ }
2247
+ bcrypt_dir="$PROJECT_ROOT/node_modules/bcryptjs"
2248
+ fi
2249
+ BCRYPT_PASSWORD="$pw" BCRYPT_COST="$cost" BCRYPT_DIR="$bcrypt_dir" node -e "
2250
+ const bcrypt = require(process.env.BCRYPT_DIR);
2251
+ const h = bcrypt.hashSync(process.env.BCRYPT_PASSWORD, parseInt(process.env.BCRYPT_COST, 10));
2252
+ console.log(h);
2253
+ "
2254
+ ;;
2255
+ verify|v)
2256
+ # mostajs verify <plaintext> <hash>
2257
+ local pw="${2:-}"
2258
+ local hashval="${3:-}"
2259
+ [[ -z "$pw" || -z "$hashval" ]] && { echo "Usage: mostajs verify <password> <hash>" >&2; exit 1; }
2260
+ local bcrypt_dir=""
2261
+ if [[ -d "$PROJECT_ROOT/node_modules/bcryptjs" ]]; then
2262
+ bcrypt_dir="$PROJECT_ROOT/node_modules/bcryptjs"
2263
+ else
2264
+ local cli_dir
2265
+ cli_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
2266
+ [[ -d "$cli_dir/node_modules/bcryptjs" ]] && bcrypt_dir="$cli_dir/node_modules/bcryptjs"
2267
+ fi
2268
+ if [[ -z "$bcrypt_dir" ]]; then
2269
+ ensure_pkg "bcryptjs" >/dev/null 2>&1 || exit 1
2270
+ bcrypt_dir="$PROJECT_ROOT/node_modules/bcryptjs"
2271
+ fi
2272
+ BCRYPT_PASSWORD="$pw" BCRYPT_HASH="$hashval" BCRYPT_DIR="$bcrypt_dir" node -e "
2273
+ const bcrypt = require(process.env.BCRYPT_DIR);
2274
+ console.log(bcrypt.compareSync(process.env.BCRYPT_PASSWORD, process.env.BCRYPT_HASH) ? 'match' : 'no match');
2275
+ "
2276
+ ;;
1627
2277
  convert|c)
1628
2278
  detect_project
1629
2279
  [[ ${#DETECTED_TYPES[@]} -eq 0 ]] && { err "No schema found"; exit 1; }
@@ -1652,11 +2302,17 @@ run_subcommand() {
1652
2302
  help|-h|--help)
1653
2303
  cat <<EOF
1654
2304
  Usage :
1655
- $CLI_NAME Interactive menu
1656
- $CLI_NAME convert Run conversion (auto-detect schema type)
1657
- $CLI_NAME detect Print detected schemas
1658
- $CLI_NAME health Run health checks
1659
- $CLI_NAME version Print version
2305
+ $CLI_NAME Interactive menu
2306
+ $CLI_NAME convert Run conversion (auto-detect schema type)
2307
+ $CLI_NAME detect Print detected schemas
2308
+ $CLI_NAME health Run health checks
2309
+ $CLI_NAME hash <password> [cost] Hash a password with bcrypt (cost default 10)
2310
+ $CLI_NAME verify <password> <hash> Check if a plain password matches a bcrypt hash
2311
+ $CLI_NAME version Print version
2312
+
2313
+ Examples:
2314
+ $CLI_NAME hash 'Admin@123456' → \$2b\$10\$N9qo8uLOickgx2ZMRZoMyeIjZA...
2315
+ $CLI_NAME verify 'Admin@123456' '\$2b\$10\$N9qo...'
1660
2316
  EOF
1661
2317
  ;;
1662
2318
  *)
@@ -1677,6 +2333,8 @@ EOF
1677
2333
 
1678
2334
  # Non-interactive mode if args provided
1679
2335
  if [[ $# -gt 0 ]]; then
2336
+ detect_project # populates PKG_MANAGER, PROJECT_ROOT, etc.
2337
+ load_env # optional config for subcommands that need it
1680
2338
  run_subcommand "$@"
1681
2339
  exit 0
1682
2340
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/orm-cli",
3
- "version": "0.2.3",
3
+ "version": "0.3.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",
@@ -43,6 +43,7 @@
43
43
  ],
44
44
  "dependencies": {
45
45
  "@mostajs/orm": "^1.9.2",
46
+ "bcryptjs": "^2.4.3",
46
47
  "better-sqlite3": "^12.9.0"
47
48
  }
48
49
  }