@mostajs/orm-cli 0.2.3 → 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.
- package/bin/mostajs.sh +502 -4
- 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 ;;
|
|
@@ -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:${
|
|
1371
|
-
echo -e " Dev server (mobile) : ${CYAN}http://${ip}:${
|
|
1372
|
-
echo -e " mosta-net
|
|
1373
|
-
echo -e "
|
|
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,491 @@ 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
|
|
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
|
+
|
|
1467
1965
|
# ============================================================
|
|
1468
1966
|
# ACTION 9 : GENERATE BOILERPLATE
|
|
1469
1967
|
# ============================================================
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/orm-cli",
|
|
3
|
-
"version": "0.
|
|
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",
|