@mostajs/orm-cli 0.4.3 → 0.4.6

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.
@@ -30,11 +30,13 @@ const argv = process.argv.slice(2);
30
30
  const flag = (name) => argv.includes(`--${name}`);
31
31
  const val = (name) => { const i = argv.indexOf(`--${name}`); return i >= 0 ? argv[i + 1] : null; };
32
32
 
33
- const APPLY = flag('apply');
34
- const RESTORE = flag('restore');
35
- const ONE_FILE = val('file');
36
- const ROOT = resolve(val('project') ?? process.cwd());
37
- const QUIET = flag('quiet');
33
+ const APPLY = flag('apply');
34
+ const RESTORE = flag('restore');
35
+ const RESTORE_SEEDS = flag('restore-seeds'); // NEW : restore only seed-like .prisma.bak files
36
+ const ONE_FILE = val('file');
37
+ const ROOT = resolve(val('project') ?? process.cwd());
38
+ const QUIET = flag('quiet');
39
+ const REWRITE_SEEDS = flag('rewrite-seeds'); // NEW : force rewriting seed scripts (legacy behavior)
38
40
 
39
41
  const log = (...a) => { if (!QUIET) console.log(...a); };
40
42
  const c = { cyan: s => `\x1b[36m${s}\x1b[0m`, yellow: s => `\x1b[33m${s}\x1b[0m`, green: s => `\x1b[32m${s}\x1b[0m`, red: s => `\x1b[31m${s}\x1b[0m`, bold: s => `\x1b[1m${s}\x1b[0m`, dim: s => `\x1b[2m${s}\x1b[0m` };
@@ -43,6 +45,17 @@ const c = { cyan: s => `\x1b[36m${s}\x1b[0m`, yellow: s => `\x1b[33m${s}\x1b[0
43
45
  const SKIP_DIRS = new Set(['node_modules', '.next', '.svelte-kit', 'dist', 'build', '.turbo', '.vercel', '.cache', 'coverage', '.git', '.vscode', '.idea']);
44
46
  const EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.js', '.jsx', '.mjs']);
45
47
 
48
+ // Paths that look like seed SCRIPTS (not db modules). These must NOT be
49
+ // rewritten — their `new PrismaClient()` is a local instance scoped to the
50
+ // script, not a reusable export. Rewriting them turns the script into a
51
+ // 2-line stub and destroys the seed logic.
52
+ //
53
+ // Heuristic : last path segment starts with `seed` or `seeder`, OR file sits
54
+ // inside a `seeds/` or `fixtures/` directory, OR the path is Prisma's
55
+ // canonical `prisma/seed.(ts|js)`.
56
+ const RX_SEED_PATH = /(^|[\\\/])(seeds?|fixtures|seeders?)[\\\/]|(^|[\\\/])(seed[^\\\/]*|seeder[^\\\/]*)\.(ts|tsx|mts|js|jsx|mjs)$|(^|[\\\/])prisma[\\\/]seed\.(ts|tsx|mts|js|jsx|mjs)$/i;
57
+ function looksLikeSeedScript(relPath) { return RX_SEED_PATH.test(relPath); }
58
+
46
59
  function* walk(dir) {
47
60
  let entries;
48
61
  try { entries = readdirSync(dir); } catch { return; }
@@ -52,7 +65,8 @@ function* walk(dir) {
52
65
  let st;
53
66
  try { st = statSync(p); } catch { continue; }
54
67
  if (st.isDirectory()) yield* walk(p);
55
- else if (EXTENSIONS.has(extname(name))) yield p;
68
+ // Source files we might rewrite, OR .prisma.bak we might restore.
69
+ else if (EXTENSIONS.has(extname(name)) || name.endsWith('.prisma.bak')) yield p;
56
70
  }
57
71
  }
58
72
 
@@ -113,16 +127,18 @@ function buildReplacement(shape) {
113
127
  }
114
128
 
115
129
  // ---------- Restore ----------
116
- function restoreBackups() {
130
+ function restoreBackups({ onlySeeds = false } = {}) {
117
131
  const restored = [];
118
132
  for (const p of walk(ROOT)) {
119
133
  if (!p.endsWith('.prisma.bak')) continue;
120
134
  const original = p.slice(0, -'.prisma.bak'.length);
135
+ const rel = relative(ROOT, original);
136
+ if (onlySeeds && !looksLikeSeedScript(rel)) continue;
121
137
  if (APPLY) {
122
138
  renameSync(p, original);
123
- log(` ${c.green('✓')} restored ${c.dim(relative(ROOT, original))}`);
139
+ log(` ${c.green('✓')} restored ${c.dim(rel)}`);
124
140
  } else {
125
- log(` ${c.yellow('•')} would restore ${c.dim(relative(ROOT, original))}`);
141
+ log(` ${c.yellow('•')} would restore ${c.dim(rel)}`);
126
142
  }
127
143
  restored.push(original);
128
144
  }
@@ -130,6 +146,12 @@ function restoreBackups() {
130
146
  }
131
147
 
132
148
  // ---------- Main ----------
149
+ if (RESTORE_SEEDS) {
150
+ log(c.bold('▶ Restoring seed-looking .prisma.bak files only' + (APPLY ? '' : c.yellow(' (dry-run, use --apply)'))));
151
+ const r = restoreBackups({ onlySeeds: true });
152
+ log(`\n${r.length} seed file(s) ${APPLY ? 'restored' : 'would be restored'}`);
153
+ process.exit(0);
154
+ }
133
155
  if (RESTORE) {
134
156
  log(c.bold('▶ Restoring .prisma.bak files' + (APPLY ? '' : c.yellow(' (dry-run, use --apply)'))));
135
157
  const r = restoreBackups();
@@ -141,6 +163,7 @@ log(c.bold(`▶ mostajs install-bridge — scanning ${c.cyan(ROOT)}`));
141
163
  log('');
142
164
 
143
165
  const candidates = [];
166
+ const skippedSeeds = [];
144
167
  const iter = ONE_FILE ? [resolve(ROOT, ONE_FILE)] : walk(ROOT);
145
168
  for (const p of iter) {
146
169
  let src;
@@ -151,12 +174,18 @@ for (const p of iter) {
151
174
  continue;
152
175
  }
153
176
  if (!RX_NEW.test(src)) continue; // imports only = not an instantiation site
177
+ const rel = relative(ROOT, p);
178
+ if (looksLikeSeedScript(rel) && !REWRITE_SEEDS) {
179
+ skippedSeeds.push(rel);
180
+ log(` ${c.yellow('⚠ skip seed script')} ${c.dim(rel)}`);
181
+ continue;
182
+ }
154
183
  const shape = detectExportShape(src);
155
184
  if (!shape) {
156
- log(` ${c.yellow('?')} ${relative(ROOT, p)} — PrismaClient detected but export shape unknown, skipping`);
185
+ log(` ${c.yellow('?')} ${rel} — PrismaClient detected but export shape unknown, skipping`);
157
186
  continue;
158
187
  }
159
- candidates.push({ path: p, rel: relative(ROOT, p), shape, src });
188
+ candidates.push({ path: p, rel, shape, src });
160
189
  }
161
190
 
162
191
  if (candidates.length === 0) {
@@ -171,6 +200,13 @@ log(c.bold(`Found ${candidates.length} PrismaClient instantiation site(s):`));
171
200
  for (const ca of candidates) {
172
201
  log(` ${c.green('→')} ${ca.rel} ${c.dim(`(${ca.shape.kind}${ca.shape.name ? ' ' + ca.shape.name : ''})`)}`);
173
202
  }
203
+ if (skippedSeeds.length) {
204
+ log('');
205
+ log(c.yellow(`${skippedSeeds.length} seed script(s) were skipped (preserved as-is).`));
206
+ log(c.dim(' These files keep their original content. To run them with the bridge,'));
207
+ log(c.dim(' either: (a) re-link @prisma/client, (b) rewrite them with --rewrite-seeds,'));
208
+ log(c.dim(' (c) use the mostajs seed system (menu S) with JSON fixtures.'));
209
+ }
174
210
  log('');
175
211
 
176
212
  if (!APPLY) {
@@ -201,4 +237,5 @@ log(` 2. ${c.cyan('npx @mostajs/orm-cli')} → menu 1 (Convert Prisma → ent
201
237
  log(` 3. Set ${c.cyan('DB_DIALECT')} + ${c.cyan('SGBD_URI')} in .env (or: menu 2 → i to import)`);
202
238
  log(` 4. ${c.cyan('npm run dev')} and test.`);
203
239
  log('');
204
- log(`To undo : ${c.cyan('mostajs install-bridge --restore --apply')}`);
240
+ log(`To undo all : ${c.cyan('mostajs install-bridge --restore --apply')}`);
241
+ log(`To restore seeds : ${c.cyan('mostajs install-bridge --restore-seeds --apply')}`);
package/bin/mostajs.sh CHANGED
@@ -98,13 +98,32 @@ ensure_pkg() {
98
98
  warn "Missing package(s): ${missing[*]}"
99
99
  if confirm "Install now with $PKG_MANAGER?"; then
100
100
  cd "$PROJECT_ROOT" || return 1
101
+ local log_file="/tmp/mostajs-install-$$.log"
101
102
  case "$PKG_MANAGER" in
102
- pnpm) pnpm add "${missing[@]}" 2>&1 | tail -5 ;;
103
- yarn) yarn add "${missing[@]}" 2>&1 | tail -5 ;;
104
- bun) bun add "${missing[@]}" 2>&1 | tail -5 ;;
105
- *) npm install --save "${missing[@]}" --legacy-peer-deps 2>&1 | tail -5 ;;
103
+ pnpm) pnpm add "${missing[@]}" >"$log_file" 2>&1 & ;;
104
+ yarn) yarn add "${missing[@]}" >"$log_file" 2>&1 & ;;
105
+ bun) bun add "${missing[@]}" >"$log_file" 2>&1 & ;;
106
+ *) npm install --save "${missing[@]}" --legacy-peer-deps >"$log_file" 2>&1 & ;;
106
107
  esac
107
- local rc=${PIPESTATUS[0]}
108
+ local install_pid=$!
109
+ # Braille spinner — visual feedback while the install runs in background
110
+ local frames='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
111
+ local tick=0
112
+ while kill -0 "$install_pid" 2>/dev/null; do
113
+ local f="${frames:$((tick % 10)):1}"
114
+ local secs=$((tick / 5))
115
+ printf "\r ${YELLOW}%s${RESET} installing ${CYAN}%s${RESET} ${DIM}(%ds)${RESET} " \
116
+ "$f" "${missing[*]}" "$secs"
117
+ tick=$(( tick + 1 ))
118
+ sleep 0.2
119
+ done
120
+ wait "$install_pid"
121
+ local rc=$?
122
+ # Clear the spinner line
123
+ printf "\r%80s\r" ""
124
+ # Show the last lines of the install log (errors or summary)
125
+ tail -5 "$log_file"
126
+ rm -f "$log_file"
108
127
  if [[ $rc -ne 0 ]]; then
109
128
  err "Install failed"
110
129
  return $rc
@@ -1653,6 +1672,9 @@ menu_seeding() {
1653
1672
  echo -e " ${CYAN}8${RESET}) Clear the seeds directory"
1654
1673
  echo -e " ${CYAN}9${RESET}) Show a seed file"
1655
1674
  echo -e " ${CYAN}h${RESET}) ${BOLD}Hash plain-text passwords in seed files${RESET} (bcrypt)"
1675
+ echo -e " ${CYAN}r${RESET}) ${BOLD}Restore seed scripts${RESET} from ${DIM}*.prisma.bak${RESET} (undo install-bridge on seeds)"
1676
+ echo -e " ${CYAN}s${RESET}) ${BOLD}Run seed scripts${RESET} (${DIM}scripts/seed-*.ts | prisma/seed.ts${RESET}) via tsx"
1677
+ echo -e " ${RED}d${RESET}) ${BOLD}Drop table(s)${RESET} — pick one / many / all, then DROP TABLE (DESTRUCTIVE)"
1656
1678
  echo
1657
1679
  echo -e " ${CYAN}b${RESET}) Back"
1658
1680
  echo
@@ -1669,12 +1691,187 @@ menu_seeding() {
1669
1691
  8) action_seed_clear ;;
1670
1692
  9) action_seed_show ;;
1671
1693
  h|H) action_seed_hash_passwords ;;
1694
+ r|R) action_seed_restore_scripts ;;
1695
+ s|S) action_seed_run_scripts ;;
1696
+ d|D) action_drop_tables ;;
1672
1697
  b|B) return ;;
1673
1698
  *) warn "Unknown"; pause ;;
1674
1699
  esac
1675
1700
  menu_seeding
1676
1701
  }
1677
1702
 
1703
+ # ------------------------------------------------------------
1704
+ # seed : drop tables (interactive picker)
1705
+ # ------------------------------------------------------------
1706
+ action_drop_tables() {
1707
+ header
1708
+ echo -e "${BOLD}${RED}▶ Drop table(s) — DESTRUCTIVE${RESET}"
1709
+ echo
1710
+ load_env
1711
+ local seed_dir="$CONFIG_DIR/seeds"
1712
+ local entities_json="$GENERATED_DIR/entities.json"
1713
+ if [[ ! -f "$entities_json" ]]; then
1714
+ warn "No entities.json found at $entities_json — run menu 1 (Convert) first."
1715
+ pause; return
1716
+ fi
1717
+
1718
+ # Run a Node helper that lists live tables, lets the user pick, then drops via dialect
1719
+ node --input-type=module -e "
1720
+ import { readFileSync } from 'node:fs';
1721
+ import { createInterface } from 'node:readline/promises';
1722
+ import { stdin, stdout } from 'node:process';
1723
+ import { getDialect } from '${PROJECT_ROOT}/node_modules/@mostajs/orm/dist/index.js';
1724
+
1725
+ const env = readFileSync('${PROJECT_ROOT}/.mostajs/config.env', 'utf8');
1726
+ for (const line of env.split('\n')) {
1727
+ const [k, v] = line.split('=');
1728
+ if (k && v) process.env[k.trim()] = v.trim();
1729
+ }
1730
+
1731
+ const entities = JSON.parse(readFileSync('${PROJECT_ROOT}/.mostajs/generated/entities.json', 'utf8'));
1732
+ const tableSet = new Set(entities.map(e => e.collection));
1733
+ // Add junction tables (many-to-many.through)
1734
+ for (const e of entities) {
1735
+ for (const r of Object.values(e.relations || {})) {
1736
+ if (r && r.type === 'many-to-many' && r.through) tableSet.add(r.through);
1737
+ }
1738
+ }
1739
+
1740
+ const d = await getDialect({
1741
+ dialect: process.env.DB_DIALECT,
1742
+ uri: process.env.SGBD_URI,
1743
+ schemaStrategy: 'none',
1744
+ });
1745
+
1746
+ // Try to list live tables (dialects that expose getTableListQuery via internal call)
1747
+ let live = [];
1748
+ try {
1749
+ const sql = d.getTableListQuery && d.getTableListQuery();
1750
+ if (sql) {
1751
+ const rows = await d.executeQuery(sql, []);
1752
+ live = rows.map(r => r.name || r.TABLE_NAME || r.table_name || Object.values(r)[0]).filter(Boolean);
1753
+ }
1754
+ } catch {}
1755
+ // Intersect with schema-known tables (only show ones we own)
1756
+ const owned = (live.length ? live : Array.from(tableSet)).filter(t => tableSet.has(t)).sort();
1757
+
1758
+ if (owned.length === 0) {
1759
+ console.log(' No tables found that match this project\'s entities.');
1760
+ await d.disconnect();
1761
+ process.exit(0);
1762
+ }
1763
+
1764
+ console.log(' Live tables in this project :');
1765
+ owned.forEach((t, i) => console.log(' ' + (i + 1).toString().padStart(2) + ') ' + t));
1766
+ console.log(' a) ALL of the above');
1767
+ console.log(' q) Cancel');
1768
+
1769
+ const rl = createInterface({ input: stdin, output: stdout });
1770
+ const pick = (await rl.question(' Pick (number, comma-separated, or a) : ')).trim();
1771
+ if (!pick || pick.toLowerCase() === 'q') { rl.close(); await d.disconnect(); console.log(' Cancelled.'); process.exit(0); }
1772
+
1773
+ const targets = pick.toLowerCase() === 'a'
1774
+ ? owned
1775
+ : pick.split(',').map(s => s.trim()).map(s => owned[parseInt(s, 10) - 1]).filter(Boolean);
1776
+
1777
+ if (targets.length === 0) { rl.close(); await d.disconnect(); console.log(' Nothing to drop.'); process.exit(0); }
1778
+
1779
+ console.log(' About to DROP : ' + targets.join(', '));
1780
+ const confirm = (await rl.question(' Type DROP to confirm : ')).trim();
1781
+ rl.close();
1782
+ if (confirm !== 'DROP') { await d.disconnect(); console.log(' Aborted.'); process.exit(0); }
1783
+
1784
+ let ok = 0, fail = 0;
1785
+ for (const t of targets) {
1786
+ try {
1787
+ await d.dropTable(t);
1788
+ console.log(' ✓ dropped ' + t);
1789
+ ok++;
1790
+ } catch (e) {
1791
+ console.error(' ✗ ' + t + ' : ' + (e.message ?? e));
1792
+ fail++;
1793
+ }
1794
+ }
1795
+ await d.disconnect();
1796
+ console.log('\nDropped : ' + ok + ' · failed : ' + fail);
1797
+ " 2>&1
1798
+ echo
1799
+ pause
1800
+ }
1801
+
1802
+ # ------------------------------------------------------------
1803
+ # seed : restore TS seed scripts from .prisma.bak (undo install-bridge)
1804
+ # ------------------------------------------------------------
1805
+ action_seed_restore_scripts() {
1806
+ header
1807
+ echo -e "${BOLD}${MAGENTA}▶ Restore seed scripts from *.prisma.bak${RESET}"
1808
+ echo
1809
+ echo -e " ${DIM}Scans for seed-like .prisma.bak files that were rewritten by install-bridge${RESET}"
1810
+ echo -e " ${DIM}and moves each backup back to its original filename.${RESET}"
1811
+ echo
1812
+ local cli_dir
1813
+ cli_dir="$(cd "$(dirname "$0")/.." && pwd)"
1814
+ echo -e "${DIM}\$ node install-bridge.mjs --restore-seeds --project \"$PROJECT_ROOT\"${RESET}"
1815
+ node "$cli_dir/bin/install-bridge.mjs" --restore-seeds --project "$PROJECT_ROOT"
1816
+ echo
1817
+ if confirm "Apply the restoration above?"; then
1818
+ node "$cli_dir/bin/install-bridge.mjs" --restore-seeds --apply --project "$PROJECT_ROOT"
1819
+ else
1820
+ dim " Skipped."
1821
+ fi
1822
+ pause
1823
+ }
1824
+
1825
+ # ------------------------------------------------------------
1826
+ # seed : run TS seed scripts via tsx (or node --loader ts-node)
1827
+ # ------------------------------------------------------------
1828
+ action_seed_run_scripts() {
1829
+ header
1830
+ echo -e "${BOLD}${MAGENTA}▶ Run seed scripts${RESET}"
1831
+ echo
1832
+ # Candidate scripts : prisma/seed.ts, scripts/seed*.{ts,js}, scripts/seed.ts
1833
+ local -a candidates=()
1834
+ [[ -f "$PROJECT_ROOT/prisma/seed.ts" ]] && candidates+=("$PROJECT_ROOT/prisma/seed.ts")
1835
+ [[ -f "$PROJECT_ROOT/prisma/seed.js" ]] && candidates+=("$PROJECT_ROOT/prisma/seed.js")
1836
+ if [[ -d "$PROJECT_ROOT/scripts" ]]; then
1837
+ while IFS= read -r f; do candidates+=("$f"); done < <(
1838
+ find "$PROJECT_ROOT/scripts" -maxdepth 2 \( -name 'seed-*.ts' -o -name 'seed-*.js' -o -name 'seed.ts' -o -name 'seed.js' \) 2>/dev/null | sort
1839
+ )
1840
+ fi
1841
+ if [[ ${#candidates[@]} -eq 0 ]]; then
1842
+ warn "No seed scripts found under prisma/ or scripts/."
1843
+ dim " Expected : prisma/seed.ts | scripts/seed.ts | scripts/seed-*.ts (or .js)"
1844
+ pause
1845
+ return
1846
+ fi
1847
+ echo -e " Found ${CYAN}${#candidates[@]}${RESET} seed script(s):"
1848
+ local i=1
1849
+ for f in "${candidates[@]}"; do
1850
+ echo -e " ${CYAN}$i${RESET}) ${f#$PROJECT_ROOT/}"
1851
+ ((i++))
1852
+ done
1853
+ echo -e " ${CYAN}a${RESET}) Run ALL sequentially"
1854
+ echo
1855
+ local pick
1856
+ pick=$(ask "Choice" "a")
1857
+ local runner="npx --yes tsx"
1858
+ command -v tsx >/dev/null && runner="tsx"
1859
+ if [[ "$pick" == "a" || "$pick" == "A" ]]; then
1860
+ for f in "${candidates[@]}"; do
1861
+ echo -e "${CYAN}▶ ${runner} ${f#$PROJECT_ROOT/}${RESET}"
1862
+ (cd "$PROJECT_ROOT" && $runner "$f") || { warn "Script failed: $f"; pause; return; }
1863
+ done
1864
+ ok "All seed scripts ran successfully."
1865
+ elif [[ "$pick" =~ ^[0-9]+$ ]] && (( pick >= 1 && pick <= ${#candidates[@]} )); then
1866
+ local f="${candidates[$((pick-1))]}"
1867
+ echo -e "${CYAN}▶ ${runner} ${f#$PROJECT_ROOT/}${RESET}"
1868
+ (cd "$PROJECT_ROOT" && $runner "$f") && ok "Done." || warn "Script failed."
1869
+ else
1870
+ warn "Unknown choice"
1871
+ fi
1872
+ pause
1873
+ }
1874
+
1678
1875
  # ------------------------------------------------------------
1679
1876
  # seed : hash plain-text passwords
1680
1877
  # ------------------------------------------------------------
@@ -1995,6 +2192,15 @@ function validateRow(row, entity) {
1995
2192
  const errors = [];
1996
2193
  const fieldNames = new Set(Object.keys(entity.fields ?? {}));
1997
2194
  const relationNames = new Set(Object.keys(entity.relations ?? {}));
2195
+ // FK columns generated by relations (many-to-one / one-to-one joinColumn, plus
2196
+ // the conventional <relName>Id fallback). These appear in seed JSONs as direct
2197
+ // FK values ("userId": "user-admin") and must NOT be flagged as "unknown field".
2198
+ const relationFkColumns = new Set();
2199
+ for (const [relName, rel] of Object.entries(entity.relations ?? {})) {
2200
+ if (rel && (rel.type === 'many-to-one' || rel.type === 'one-to-one')) {
2201
+ relationFkColumns.add(rel.joinColumn || (relName + 'Id'));
2202
+ }
2203
+ }
1998
2204
  // Required fields
1999
2205
  for (const [k, def] of Object.entries(entity.fields ?? {})) {
2000
2206
  if (def.required && (row[k] === undefined || row[k] === null)) {
@@ -2017,7 +2223,7 @@ function validateRow(row, entity) {
2017
2223
  // Unknown fields (warn-level)
2018
2224
  const warnings = [];
2019
2225
  for (const k of Object.keys(row)) {
2020
- if (!fieldNames.has(k) && !relationNames.has(k) && k !== 'id' && k !== '_id') {
2226
+ if (!fieldNames.has(k) && !relationNames.has(k) && !relationFkColumns.has(k) && k !== 'id' && k !== '_id') {
2021
2227
  warnings.push('unknown field "' + k + '" (not in schema)');
2022
2228
  }
2023
2229
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/orm-cli",
3
- "version": "0.4.3",
3
+ "version": "0.4.6",
4
4
  "description": "Universal CLI to integrate @mostajs/orm into any project — one-shot `mostajs bootstrap` migrates a Prisma project (codemod + deps + schema convert + DDL) to 13 databases with zero code change.",
5
5
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
6
  "license": "AGPL-3.0-or-later",