@mostajs/orm-cli 0.3.0 → 0.3.2
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.
- package/bin/mostajs.sh +256 -5
- package/package.json +2 -1
package/bin/mostajs.sh
CHANGED
|
@@ -548,6 +548,7 @@ menu_databases() {
|
|
|
548
548
|
echo -e " ${CYAN}p${RESET}) APP_PORT : ${DIM}${APP_PORT:-3000}${RESET}"
|
|
549
549
|
echo
|
|
550
550
|
echo -e " ${CYAN}t${RESET}) Test all connections"
|
|
551
|
+
echo -e " ${CYAN}i${RESET}) ${BOLD}Import from project .env${RESET} (auto-detect app's real DB)"
|
|
551
552
|
echo -e " ${CYAN}e${RESET}) Export to .env.local in project"
|
|
552
553
|
echo -e " ${CYAN}r${RESET}) Reset config"
|
|
553
554
|
echo -e " ${CYAN}b${RESET}) Back"
|
|
@@ -565,6 +566,7 @@ menu_databases() {
|
|
|
565
566
|
n|N) save_var MOSTA_NET_TRANSPORT "$(ask 'MOSTA_NET_TRANSPORT (rest|sse|graphql|mcp|websocket|jsonrpc|grpc|odata)' "${MOSTA_NET_TRANSPORT:-rest}")";;
|
|
566
567
|
p|P) save_var APP_PORT "$(ask 'APP_PORT' "${APP_PORT:-3000}")";;
|
|
567
568
|
t|T) action_test_connections; return;;
|
|
569
|
+
i|I) action_import_env ;;
|
|
568
570
|
e|E) export_env_local ;;
|
|
569
571
|
r|R) confirm "Really reset config?" && rm -f "$CONFIG_FILE" && ok "Reset";;
|
|
570
572
|
b|B) return;;
|
|
@@ -647,6 +649,95 @@ list_extra_bindings() {
|
|
|
647
649
|
done
|
|
648
650
|
}
|
|
649
651
|
|
|
652
|
+
# Import DB config from the project's own .env file.
|
|
653
|
+
# This is CRUCIAL when your app (Prisma, NextAuth, ...) already has a
|
|
654
|
+
# DATABASE_URL / MONGODB_URI / POSTGRES_URL. Seeding the wrong DB leads to
|
|
655
|
+
# the classic "users are in mosta-net /api/v1/users but login fails" bug.
|
|
656
|
+
action_import_env() {
|
|
657
|
+
header
|
|
658
|
+
echo -e "${BOLD}${MAGENTA}▶ Import DB config from project .env${RESET}"
|
|
659
|
+
echo
|
|
660
|
+
|
|
661
|
+
local candidates=(".env.local" ".env" ".env.production" ".env.development")
|
|
662
|
+
local -a available=()
|
|
663
|
+
for f in "${candidates[@]}"; do
|
|
664
|
+
[[ -f "$PROJECT_ROOT/$f" ]] && available+=("$f")
|
|
665
|
+
done
|
|
666
|
+
|
|
667
|
+
if [[ ${#available[@]} -eq 0 ]]; then
|
|
668
|
+
err "No .env* file found in $PROJECT_ROOT"
|
|
669
|
+
pause; return
|
|
670
|
+
fi
|
|
671
|
+
|
|
672
|
+
echo "Found env files :"
|
|
673
|
+
local i=1
|
|
674
|
+
for f in "${available[@]}"; do
|
|
675
|
+
echo -e " ${CYAN}$i${RESET}) $f"
|
|
676
|
+
i=$((i+1))
|
|
677
|
+
done
|
|
678
|
+
echo
|
|
679
|
+
local choice; choice=$(ask "Number" 1)
|
|
680
|
+
local src_file="${available[$((choice-1))]}"
|
|
681
|
+
[[ -z "$src_file" ]] && return
|
|
682
|
+
local src_path="$PROJECT_ROOT/$src_file"
|
|
683
|
+
|
|
684
|
+
# Extract common DB URI variables — priority order matters
|
|
685
|
+
# MONGODB_URI > DATABASE_URL > POSTGRES_URL > MYSQL_URL > SGBD_URI
|
|
686
|
+
local found_uri="" found_var="" found_dialect=""
|
|
687
|
+
for var in SGBD_URI DATABASE_URL MONGODB_URI POSTGRES_URL POSTGRES_URI MYSQL_URL MYSQL_URI POSTGRESQL_URL; do
|
|
688
|
+
# Read value, skip commented lines, strip quotes
|
|
689
|
+
local val
|
|
690
|
+
val=$(grep -E "^${var}=" "$src_path" 2>/dev/null | grep -v "^#" | head -1 | sed "s/^${var}=//" | sed 's/^"\(.*\)"$/\1/' | sed "s/^'\(.*\)'$/\1/")
|
|
691
|
+
if [[ -n "$val" ]]; then
|
|
692
|
+
found_uri="$val"
|
|
693
|
+
found_var="$var"
|
|
694
|
+
break
|
|
695
|
+
fi
|
|
696
|
+
done
|
|
697
|
+
|
|
698
|
+
if [[ -z "$found_uri" ]]; then
|
|
699
|
+
err "No DB URI found in $src_file"
|
|
700
|
+
info "Searched : SGBD_URI, DATABASE_URL, MONGODB_URI, POSTGRES_URL/URI, MYSQL_URL/URI"
|
|
701
|
+
pause; return
|
|
702
|
+
fi
|
|
703
|
+
|
|
704
|
+
# Detect dialect from URI scheme
|
|
705
|
+
found_dialect=$(detect_dialect_from_uri "$found_uri")
|
|
706
|
+
if [[ "$found_dialect" == "unknown" ]]; then
|
|
707
|
+
warn "Could not auto-detect dialect from URI: $found_uri"
|
|
708
|
+
found_dialect=$(ask "Dialect name (mongodb|postgres|mysql|...)" "")
|
|
709
|
+
[[ -z "$found_dialect" ]] && { pause; return; }
|
|
710
|
+
fi
|
|
711
|
+
|
|
712
|
+
info "Detected :"
|
|
713
|
+
echo -e " Variable : ${CYAN}$found_var${RESET}"
|
|
714
|
+
echo -e " URI : ${DIM}$found_uri${RESET}"
|
|
715
|
+
echo -e " Dialect : ${MAGENTA}$found_dialect${RESET}"
|
|
716
|
+
echo
|
|
717
|
+
|
|
718
|
+
# Special Prisma note
|
|
719
|
+
if [[ "$found_var" == "MONGODB_URI" ]] || [[ "$found_var" == "DATABASE_URL" ]]; then
|
|
720
|
+
dim " (This is likely the Prisma datasource — the same DB your app uses for login/auth.)"
|
|
721
|
+
echo
|
|
722
|
+
fi
|
|
723
|
+
|
|
724
|
+
if confirm "Save as DB_DIALECT + SGBD_URI ?"; then
|
|
725
|
+
save_var DB_DIALECT "$found_dialect"
|
|
726
|
+
save_var SGBD_URI "$found_uri"
|
|
727
|
+
# Suggest a strategy based on environment context
|
|
728
|
+
if [[ "$src_file" == ".env.production" ]]; then
|
|
729
|
+
save_var DB_SCHEMA_STRATEGY "validate"
|
|
730
|
+
info "Auto-set DB_SCHEMA_STRATEGY=validate (production)"
|
|
731
|
+
elif [[ -z "${DB_SCHEMA_STRATEGY:-}" ]]; then
|
|
732
|
+
save_var DB_SCHEMA_STRATEGY "update"
|
|
733
|
+
fi
|
|
734
|
+
ok "Config imported"
|
|
735
|
+
echo
|
|
736
|
+
info "Next step : menu 2 → t (test connection), then menu S → 4 (seed)"
|
|
737
|
+
fi
|
|
738
|
+
pause
|
|
739
|
+
}
|
|
740
|
+
|
|
650
741
|
# Export a .env.local file compatible with @mostajs/orm convention
|
|
651
742
|
export_env_local() {
|
|
652
743
|
local target="$PROJECT_ROOT/.env.mostajs"
|
|
@@ -1508,6 +1599,7 @@ menu_seeding() {
|
|
|
1508
1599
|
echo -e " ${CYAN}7${RESET}) Dump current DB rows → .mostajs/seeds-dump/"
|
|
1509
1600
|
echo -e " ${CYAN}8${RESET}) Clear the seeds directory"
|
|
1510
1601
|
echo -e " ${CYAN}9${RESET}) Show a seed file"
|
|
1602
|
+
echo -e " ${CYAN}h${RESET}) ${BOLD}Hash plain-text passwords in seed files${RESET} (bcrypt)"
|
|
1511
1603
|
echo
|
|
1512
1604
|
echo -e " ${CYAN}b${RESET}) Back"
|
|
1513
1605
|
echo
|
|
@@ -1523,12 +1615,105 @@ menu_seeding() {
|
|
|
1523
1615
|
7) action_seed_dump ;;
|
|
1524
1616
|
8) action_seed_clear ;;
|
|
1525
1617
|
9) action_seed_show ;;
|
|
1618
|
+
h|H) action_seed_hash_passwords ;;
|
|
1526
1619
|
b|B) return ;;
|
|
1527
1620
|
*) warn "Unknown"; pause ;;
|
|
1528
1621
|
esac
|
|
1529
1622
|
menu_seeding
|
|
1530
1623
|
}
|
|
1531
1624
|
|
|
1625
|
+
# ------------------------------------------------------------
|
|
1626
|
+
# seed : hash plain-text passwords
|
|
1627
|
+
# ------------------------------------------------------------
|
|
1628
|
+
|
|
1629
|
+
action_seed_hash_passwords() {
|
|
1630
|
+
header
|
|
1631
|
+
echo -e "${BOLD}${MAGENTA}▶ Hash plain-text passwords in seed files${RESET}"
|
|
1632
|
+
echo
|
|
1633
|
+
|
|
1634
|
+
local seed_dir="$CONFIG_DIR/seeds"
|
|
1635
|
+
local count
|
|
1636
|
+
count=$(ls -1 "$seed_dir"/*.json 2>/dev/null | wc -l)
|
|
1637
|
+
if [[ $count -eq 0 ]]; then
|
|
1638
|
+
err "No seed files in $seed_dir (menu S → 1 first)"
|
|
1639
|
+
pause; return
|
|
1640
|
+
fi
|
|
1641
|
+
|
|
1642
|
+
echo "Auto-detects fields named :"
|
|
1643
|
+
dim " password, passwordHash, hashedPassword, pwd, userPassword"
|
|
1644
|
+
echo
|
|
1645
|
+
echo "Skips values that are already bcrypt hashes (start with \$2a\$ / \$2b\$ / \$2y\$, 60 chars)"
|
|
1646
|
+
echo
|
|
1647
|
+
|
|
1648
|
+
local cost
|
|
1649
|
+
cost=$(ask "bcrypt cost factor" "${BCRYPT_COST:-10}")
|
|
1650
|
+
if ! confirm "Hash all plain passwords in $seed_dir ?"; then
|
|
1651
|
+
return
|
|
1652
|
+
fi
|
|
1653
|
+
|
|
1654
|
+
# Ensure bcryptjs is available
|
|
1655
|
+
ensure_pkg "bcryptjs" || { pause; return; }
|
|
1656
|
+
|
|
1657
|
+
cat > "$CONFIG_DIR/seed-hash.mjs" <<EOF
|
|
1658
|
+
import { readdirSync, readFileSync, writeFileSync } from 'fs';
|
|
1659
|
+
import { join } from 'path';
|
|
1660
|
+
import bcrypt from 'bcryptjs';
|
|
1661
|
+
|
|
1662
|
+
const SEEDDIR = '$seed_dir';
|
|
1663
|
+
const COST = $cost;
|
|
1664
|
+
|
|
1665
|
+
// Field names commonly used for passwords
|
|
1666
|
+
const PASSWORD_FIELDS = ['password', 'passwordHash', 'hashedPassword', 'pwd', 'userPassword'];
|
|
1667
|
+
|
|
1668
|
+
// Test if a string is already a bcrypt hash
|
|
1669
|
+
const isBcrypt = v => typeof v === 'string' && /^\\\$2[ayb]\\\$\\d{1,2}\\\$/.test(v) && v.length === 60;
|
|
1670
|
+
|
|
1671
|
+
let totalHashed = 0;
|
|
1672
|
+
let totalSkipped = 0;
|
|
1673
|
+
const summary = [];
|
|
1674
|
+
|
|
1675
|
+
for (const f of readdirSync(SEEDDIR).filter(x => x.endsWith('.json'))) {
|
|
1676
|
+
const path = join(SEEDDIR, f);
|
|
1677
|
+
let data;
|
|
1678
|
+
try { data = JSON.parse(readFileSync(path, 'utf8')); }
|
|
1679
|
+
catch { continue; }
|
|
1680
|
+
|
|
1681
|
+
if (!Array.isArray(data)) continue;
|
|
1682
|
+
|
|
1683
|
+
let hashed = 0, skipped = 0;
|
|
1684
|
+
for (const row of data) {
|
|
1685
|
+
for (const field of PASSWORD_FIELDS) {
|
|
1686
|
+
const val = row[field];
|
|
1687
|
+
if (typeof val !== 'string' || val.length === 0) continue;
|
|
1688
|
+
if (isBcrypt(val)) { skipped++; continue; }
|
|
1689
|
+
row[field] = bcrypt.hashSync(val, COST);
|
|
1690
|
+
hashed++;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if (hashed > 0) {
|
|
1695
|
+
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
1696
|
+
summary.push({ file: f, hashed, skipped });
|
|
1697
|
+
}
|
|
1698
|
+
totalHashed += hashed;
|
|
1699
|
+
totalSkipped += skipped;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
for (const s of summary) {
|
|
1703
|
+
console.log(' \u2713 ' + s.file + ' : ' + s.hashed + ' hashed' + (s.skipped ? ' (+ ' + s.skipped + ' already hashed)' : ''));
|
|
1704
|
+
}
|
|
1705
|
+
console.log();
|
|
1706
|
+
console.log('Total : ' + totalHashed + ' hashed, ' + totalSkipped + ' already-hashed skipped');
|
|
1707
|
+
if (totalHashed === 0 && totalSkipped === 0) {
|
|
1708
|
+
console.log('No password fields found.');
|
|
1709
|
+
}
|
|
1710
|
+
EOF
|
|
1711
|
+
cd "$PROJECT_ROOT"
|
|
1712
|
+
node "$CONFIG_DIR/seed-hash.mjs" 2>&1 | tee "$LOG_DIR/seed-hash.log"
|
|
1713
|
+
echo
|
|
1714
|
+
pause
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1532
1717
|
# ------------------------------------------------------------
|
|
1533
1718
|
# seed : upload / import
|
|
1534
1719
|
# ------------------------------------------------------------
|
|
@@ -1750,6 +1935,9 @@ for (const f of readdirSync(SEEDDIR).filter(x => x.endsWith('.json'))) {
|
|
|
1750
1935
|
}
|
|
1751
1936
|
|
|
1752
1937
|
// ---------- Validate ----------
|
|
1938
|
+
const PASSWORD_FIELDS = ['password', 'passwordHash', 'hashedPassword', 'pwd', 'userPassword'];
|
|
1939
|
+
const isBcrypt = v => typeof v === 'string' && /^\\\$2[ayb]\\\$\\d{1,2}\\\$/.test(v) && v.length === 60;
|
|
1940
|
+
|
|
1753
1941
|
function validateRow(row, entity) {
|
|
1754
1942
|
const errors = [];
|
|
1755
1943
|
const fieldNames = new Set(Object.keys(entity.fields ?? {}));
|
|
@@ -1780,6 +1968,12 @@ function validateRow(row, entity) {
|
|
|
1780
1968
|
warnings.push('unknown field "' + k + '" (not in schema)');
|
|
1781
1969
|
}
|
|
1782
1970
|
}
|
|
1971
|
+
// Password fields that look like plain-text (not bcrypt)
|
|
1972
|
+
for (const pf of PASSWORD_FIELDS) {
|
|
1973
|
+
if (row[pf] !== undefined && row[pf] !== null && row[pf] !== '' && !isBcrypt(row[pf])) {
|
|
1974
|
+
warnings.push('field "' + pf + '" does not look like a bcrypt hash — run menu S \u2192 h to hash');
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1783
1977
|
return { errors, warnings };
|
|
1784
1978
|
}
|
|
1785
1979
|
|
|
@@ -2122,6 +2316,55 @@ EOF
|
|
|
2122
2316
|
|
|
2123
2317
|
run_subcommand() {
|
|
2124
2318
|
case "$1" in
|
|
2319
|
+
hash|h)
|
|
2320
|
+
# mostajs hash <plaintext> [cost]
|
|
2321
|
+
local pw="${2:-}"
|
|
2322
|
+
local cost="${3:-10}"
|
|
2323
|
+
[[ -z "$pw" ]] && { echo "Usage: mostajs hash <password> [cost=10]" >&2; exit 1; }
|
|
2324
|
+
# Try local project, then CLI's own node_modules (bcryptjs is a dep)
|
|
2325
|
+
local bcrypt_dir=""
|
|
2326
|
+
if [[ -d "$PROJECT_ROOT/node_modules/bcryptjs" ]]; then
|
|
2327
|
+
bcrypt_dir="$PROJECT_ROOT/node_modules/bcryptjs"
|
|
2328
|
+
else
|
|
2329
|
+
local cli_dir
|
|
2330
|
+
cli_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
2331
|
+
[[ -d "$cli_dir/node_modules/bcryptjs" ]] && bcrypt_dir="$cli_dir/node_modules/bcryptjs"
|
|
2332
|
+
fi
|
|
2333
|
+
if [[ -z "$bcrypt_dir" ]]; then
|
|
2334
|
+
ensure_pkg "bcryptjs" >/dev/null 2>&1 || {
|
|
2335
|
+
err "bcryptjs not available. Install it manually : npm install bcryptjs"
|
|
2336
|
+
exit 1
|
|
2337
|
+
}
|
|
2338
|
+
bcrypt_dir="$PROJECT_ROOT/node_modules/bcryptjs"
|
|
2339
|
+
fi
|
|
2340
|
+
BCRYPT_PASSWORD="$pw" BCRYPT_COST="$cost" BCRYPT_DIR="$bcrypt_dir" node -e "
|
|
2341
|
+
const bcrypt = require(process.env.BCRYPT_DIR);
|
|
2342
|
+
const h = bcrypt.hashSync(process.env.BCRYPT_PASSWORD, parseInt(process.env.BCRYPT_COST, 10));
|
|
2343
|
+
console.log(h);
|
|
2344
|
+
"
|
|
2345
|
+
;;
|
|
2346
|
+
verify|v)
|
|
2347
|
+
# mostajs verify <plaintext> <hash>
|
|
2348
|
+
local pw="${2:-}"
|
|
2349
|
+
local hashval="${3:-}"
|
|
2350
|
+
[[ -z "$pw" || -z "$hashval" ]] && { echo "Usage: mostajs verify <password> <hash>" >&2; exit 1; }
|
|
2351
|
+
local bcrypt_dir=""
|
|
2352
|
+
if [[ -d "$PROJECT_ROOT/node_modules/bcryptjs" ]]; then
|
|
2353
|
+
bcrypt_dir="$PROJECT_ROOT/node_modules/bcryptjs"
|
|
2354
|
+
else
|
|
2355
|
+
local cli_dir
|
|
2356
|
+
cli_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
2357
|
+
[[ -d "$cli_dir/node_modules/bcryptjs" ]] && bcrypt_dir="$cli_dir/node_modules/bcryptjs"
|
|
2358
|
+
fi
|
|
2359
|
+
if [[ -z "$bcrypt_dir" ]]; then
|
|
2360
|
+
ensure_pkg "bcryptjs" >/dev/null 2>&1 || exit 1
|
|
2361
|
+
bcrypt_dir="$PROJECT_ROOT/node_modules/bcryptjs"
|
|
2362
|
+
fi
|
|
2363
|
+
BCRYPT_PASSWORD="$pw" BCRYPT_HASH="$hashval" BCRYPT_DIR="$bcrypt_dir" node -e "
|
|
2364
|
+
const bcrypt = require(process.env.BCRYPT_DIR);
|
|
2365
|
+
console.log(bcrypt.compareSync(process.env.BCRYPT_PASSWORD, process.env.BCRYPT_HASH) ? 'match' : 'no match');
|
|
2366
|
+
"
|
|
2367
|
+
;;
|
|
2125
2368
|
convert|c)
|
|
2126
2369
|
detect_project
|
|
2127
2370
|
[[ ${#DETECTED_TYPES[@]} -eq 0 ]] && { err "No schema found"; exit 1; }
|
|
@@ -2150,11 +2393,17 @@ run_subcommand() {
|
|
|
2150
2393
|
help|-h|--help)
|
|
2151
2394
|
cat <<EOF
|
|
2152
2395
|
Usage :
|
|
2153
|
-
$CLI_NAME
|
|
2154
|
-
$CLI_NAME convert
|
|
2155
|
-
$CLI_NAME detect
|
|
2156
|
-
$CLI_NAME health
|
|
2157
|
-
$CLI_NAME
|
|
2396
|
+
$CLI_NAME Interactive menu
|
|
2397
|
+
$CLI_NAME convert Run conversion (auto-detect schema type)
|
|
2398
|
+
$CLI_NAME detect Print detected schemas
|
|
2399
|
+
$CLI_NAME health Run health checks
|
|
2400
|
+
$CLI_NAME hash <password> [cost] Hash a password with bcrypt (cost default 10)
|
|
2401
|
+
$CLI_NAME verify <password> <hash> Check if a plain password matches a bcrypt hash
|
|
2402
|
+
$CLI_NAME version Print version
|
|
2403
|
+
|
|
2404
|
+
Examples:
|
|
2405
|
+
$CLI_NAME hash 'Admin@123456' → \$2b\$10\$N9qo8uLOickgx2ZMRZoMyeIjZA...
|
|
2406
|
+
$CLI_NAME verify 'Admin@123456' '\$2b\$10\$N9qo...'
|
|
2158
2407
|
EOF
|
|
2159
2408
|
;;
|
|
2160
2409
|
*)
|
|
@@ -2175,6 +2424,8 @@ EOF
|
|
|
2175
2424
|
|
|
2176
2425
|
# Non-interactive mode if args provided
|
|
2177
2426
|
if [[ $# -gt 0 ]]; then
|
|
2427
|
+
detect_project # populates PKG_MANAGER, PROJECT_ROOT, etc.
|
|
2428
|
+
load_env # optional config for subcommands that need it
|
|
2178
2429
|
run_subcommand "$@"
|
|
2179
2430
|
exit 0
|
|
2180
2431
|
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/orm-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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
|
}
|