@mostajs/orm-cli 0.4.3 → 0.4.5

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
@@ -1653,6 +1653,9 @@ menu_seeding() {
1653
1653
  echo -e " ${CYAN}8${RESET}) Clear the seeds directory"
1654
1654
  echo -e " ${CYAN}9${RESET}) Show a seed file"
1655
1655
  echo -e " ${CYAN}h${RESET}) ${BOLD}Hash plain-text passwords in seed files${RESET} (bcrypt)"
1656
+ echo -e " ${CYAN}r${RESET}) ${BOLD}Restore seed scripts${RESET} from ${DIM}*.prisma.bak${RESET} (undo install-bridge on seeds)"
1657
+ echo -e " ${CYAN}s${RESET}) ${BOLD}Run seed scripts${RESET} (${DIM}scripts/seed-*.ts | prisma/seed.ts${RESET}) via tsx"
1658
+ echo -e " ${RED}d${RESET}) ${BOLD}Drop table(s)${RESET} — pick one / many / all, then DROP TABLE (DESTRUCTIVE)"
1656
1659
  echo
1657
1660
  echo -e " ${CYAN}b${RESET}) Back"
1658
1661
  echo
@@ -1669,12 +1672,187 @@ menu_seeding() {
1669
1672
  8) action_seed_clear ;;
1670
1673
  9) action_seed_show ;;
1671
1674
  h|H) action_seed_hash_passwords ;;
1675
+ r|R) action_seed_restore_scripts ;;
1676
+ s|S) action_seed_run_scripts ;;
1677
+ d|D) action_drop_tables ;;
1672
1678
  b|B) return ;;
1673
1679
  *) warn "Unknown"; pause ;;
1674
1680
  esac
1675
1681
  menu_seeding
1676
1682
  }
1677
1683
 
1684
+ # ------------------------------------------------------------
1685
+ # seed : drop tables (interactive picker)
1686
+ # ------------------------------------------------------------
1687
+ action_drop_tables() {
1688
+ header
1689
+ echo -e "${BOLD}${RED}▶ Drop table(s) — DESTRUCTIVE${RESET}"
1690
+ echo
1691
+ load_env
1692
+ local seed_dir="$CONFIG_DIR/seeds"
1693
+ local entities_json="$GENERATED_DIR/entities.json"
1694
+ if [[ ! -f "$entities_json" ]]; then
1695
+ warn "No entities.json found at $entities_json — run menu 1 (Convert) first."
1696
+ pause; return
1697
+ fi
1698
+
1699
+ # Run a Node helper that lists live tables, lets the user pick, then drops via dialect
1700
+ node --input-type=module -e "
1701
+ import { readFileSync } from 'node:fs';
1702
+ import { createInterface } from 'node:readline/promises';
1703
+ import { stdin, stdout } from 'node:process';
1704
+ import { getDialect } from '${PROJECT_ROOT}/node_modules/@mostajs/orm/dist/index.js';
1705
+
1706
+ const env = readFileSync('${PROJECT_ROOT}/.mostajs/config.env', 'utf8');
1707
+ for (const line of env.split('\n')) {
1708
+ const [k, v] = line.split('=');
1709
+ if (k && v) process.env[k.trim()] = v.trim();
1710
+ }
1711
+
1712
+ const entities = JSON.parse(readFileSync('${PROJECT_ROOT}/.mostajs/generated/entities.json', 'utf8'));
1713
+ const tableSet = new Set(entities.map(e => e.collection));
1714
+ // Add junction tables (many-to-many.through)
1715
+ for (const e of entities) {
1716
+ for (const r of Object.values(e.relations || {})) {
1717
+ if (r && r.type === 'many-to-many' && r.through) tableSet.add(r.through);
1718
+ }
1719
+ }
1720
+
1721
+ const d = await getDialect({
1722
+ dialect: process.env.DB_DIALECT,
1723
+ uri: process.env.SGBD_URI,
1724
+ schemaStrategy: 'none',
1725
+ });
1726
+
1727
+ // Try to list live tables (dialects that expose getTableListQuery via internal call)
1728
+ let live = [];
1729
+ try {
1730
+ const sql = d.getTableListQuery && d.getTableListQuery();
1731
+ if (sql) {
1732
+ const rows = await d.executeQuery(sql, []);
1733
+ live = rows.map(r => r.name || r.TABLE_NAME || r.table_name || Object.values(r)[0]).filter(Boolean);
1734
+ }
1735
+ } catch {}
1736
+ // Intersect with schema-known tables (only show ones we own)
1737
+ const owned = (live.length ? live : Array.from(tableSet)).filter(t => tableSet.has(t)).sort();
1738
+
1739
+ if (owned.length === 0) {
1740
+ console.log(' No tables found that match this project\'s entities.');
1741
+ await d.disconnect();
1742
+ process.exit(0);
1743
+ }
1744
+
1745
+ console.log(' Live tables in this project :');
1746
+ owned.forEach((t, i) => console.log(' ' + (i + 1).toString().padStart(2) + ') ' + t));
1747
+ console.log(' a) ALL of the above');
1748
+ console.log(' q) Cancel');
1749
+
1750
+ const rl = createInterface({ input: stdin, output: stdout });
1751
+ const pick = (await rl.question(' Pick (number, comma-separated, or a) : ')).trim();
1752
+ if (!pick || pick.toLowerCase() === 'q') { rl.close(); await d.disconnect(); console.log(' Cancelled.'); process.exit(0); }
1753
+
1754
+ const targets = pick.toLowerCase() === 'a'
1755
+ ? owned
1756
+ : pick.split(',').map(s => s.trim()).map(s => owned[parseInt(s, 10) - 1]).filter(Boolean);
1757
+
1758
+ if (targets.length === 0) { rl.close(); await d.disconnect(); console.log(' Nothing to drop.'); process.exit(0); }
1759
+
1760
+ console.log(' About to DROP : ' + targets.join(', '));
1761
+ const confirm = (await rl.question(' Type DROP to confirm : ')).trim();
1762
+ rl.close();
1763
+ if (confirm !== 'DROP') { await d.disconnect(); console.log(' Aborted.'); process.exit(0); }
1764
+
1765
+ let ok = 0, fail = 0;
1766
+ for (const t of targets) {
1767
+ try {
1768
+ await d.dropTable(t);
1769
+ console.log(' ✓ dropped ' + t);
1770
+ ok++;
1771
+ } catch (e) {
1772
+ console.error(' ✗ ' + t + ' : ' + (e.message ?? e));
1773
+ fail++;
1774
+ }
1775
+ }
1776
+ await d.disconnect();
1777
+ console.log('\nDropped : ' + ok + ' · failed : ' + fail);
1778
+ " 2>&1
1779
+ echo
1780
+ pause
1781
+ }
1782
+
1783
+ # ------------------------------------------------------------
1784
+ # seed : restore TS seed scripts from .prisma.bak (undo install-bridge)
1785
+ # ------------------------------------------------------------
1786
+ action_seed_restore_scripts() {
1787
+ header
1788
+ echo -e "${BOLD}${MAGENTA}▶ Restore seed scripts from *.prisma.bak${RESET}"
1789
+ echo
1790
+ echo -e " ${DIM}Scans for seed-like .prisma.bak files that were rewritten by install-bridge${RESET}"
1791
+ echo -e " ${DIM}and moves each backup back to its original filename.${RESET}"
1792
+ echo
1793
+ local cli_dir
1794
+ cli_dir="$(cd "$(dirname "$0")/.." && pwd)"
1795
+ echo -e "${DIM}\$ node install-bridge.mjs --restore-seeds --project \"$PROJECT_ROOT\"${RESET}"
1796
+ node "$cli_dir/bin/install-bridge.mjs" --restore-seeds --project "$PROJECT_ROOT"
1797
+ echo
1798
+ if confirm "Apply the restoration above?"; then
1799
+ node "$cli_dir/bin/install-bridge.mjs" --restore-seeds --apply --project "$PROJECT_ROOT"
1800
+ else
1801
+ dim " Skipped."
1802
+ fi
1803
+ pause
1804
+ }
1805
+
1806
+ # ------------------------------------------------------------
1807
+ # seed : run TS seed scripts via tsx (or node --loader ts-node)
1808
+ # ------------------------------------------------------------
1809
+ action_seed_run_scripts() {
1810
+ header
1811
+ echo -e "${BOLD}${MAGENTA}▶ Run seed scripts${RESET}"
1812
+ echo
1813
+ # Candidate scripts : prisma/seed.ts, scripts/seed*.{ts,js}, scripts/seed.ts
1814
+ local -a candidates=()
1815
+ [[ -f "$PROJECT_ROOT/prisma/seed.ts" ]] && candidates+=("$PROJECT_ROOT/prisma/seed.ts")
1816
+ [[ -f "$PROJECT_ROOT/prisma/seed.js" ]] && candidates+=("$PROJECT_ROOT/prisma/seed.js")
1817
+ if [[ -d "$PROJECT_ROOT/scripts" ]]; then
1818
+ while IFS= read -r f; do candidates+=("$f"); done < <(
1819
+ 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
1820
+ )
1821
+ fi
1822
+ if [[ ${#candidates[@]} -eq 0 ]]; then
1823
+ warn "No seed scripts found under prisma/ or scripts/."
1824
+ dim " Expected : prisma/seed.ts | scripts/seed.ts | scripts/seed-*.ts (or .js)"
1825
+ pause
1826
+ return
1827
+ fi
1828
+ echo -e " Found ${CYAN}${#candidates[@]}${RESET} seed script(s):"
1829
+ local i=1
1830
+ for f in "${candidates[@]}"; do
1831
+ echo -e " ${CYAN}$i${RESET}) ${f#$PROJECT_ROOT/}"
1832
+ ((i++))
1833
+ done
1834
+ echo -e " ${CYAN}a${RESET}) Run ALL sequentially"
1835
+ echo
1836
+ local pick
1837
+ pick=$(ask "Choice" "a")
1838
+ local runner="npx --yes tsx"
1839
+ command -v tsx >/dev/null && runner="tsx"
1840
+ if [[ "$pick" == "a" || "$pick" == "A" ]]; then
1841
+ for f in "${candidates[@]}"; do
1842
+ echo -e "${CYAN}▶ ${runner} ${f#$PROJECT_ROOT/}${RESET}"
1843
+ (cd "$PROJECT_ROOT" && $runner "$f") || { warn "Script failed: $f"; pause; return; }
1844
+ done
1845
+ ok "All seed scripts ran successfully."
1846
+ elif [[ "$pick" =~ ^[0-9]+$ ]] && (( pick >= 1 && pick <= ${#candidates[@]} )); then
1847
+ local f="${candidates[$((pick-1))]}"
1848
+ echo -e "${CYAN}▶ ${runner} ${f#$PROJECT_ROOT/}${RESET}"
1849
+ (cd "$PROJECT_ROOT" && $runner "$f") && ok "Done." || warn "Script failed."
1850
+ else
1851
+ warn "Unknown choice"
1852
+ fi
1853
+ pause
1854
+ }
1855
+
1678
1856
  # ------------------------------------------------------------
1679
1857
  # seed : hash plain-text passwords
1680
1858
  # ------------------------------------------------------------
@@ -1995,6 +2173,15 @@ function validateRow(row, entity) {
1995
2173
  const errors = [];
1996
2174
  const fieldNames = new Set(Object.keys(entity.fields ?? {}));
1997
2175
  const relationNames = new Set(Object.keys(entity.relations ?? {}));
2176
+ // FK columns generated by relations (many-to-one / one-to-one joinColumn, plus
2177
+ // the conventional <relName>Id fallback). These appear in seed JSONs as direct
2178
+ // FK values ("userId": "user-admin") and must NOT be flagged as "unknown field".
2179
+ const relationFkColumns = new Set();
2180
+ for (const [relName, rel] of Object.entries(entity.relations ?? {})) {
2181
+ if (rel && (rel.type === 'many-to-one' || rel.type === 'one-to-one')) {
2182
+ relationFkColumns.add(rel.joinColumn || (relName + 'Id'));
2183
+ }
2184
+ }
1998
2185
  // Required fields
1999
2186
  for (const [k, def] of Object.entries(entity.fields ?? {})) {
2000
2187
  if (def.required && (row[k] === undefined || row[k] === null)) {
@@ -2017,7 +2204,7 @@ function validateRow(row, entity) {
2017
2204
  // Unknown fields (warn-level)
2018
2205
  const warnings = [];
2019
2206
  for (const k of Object.keys(row)) {
2020
- if (!fieldNames.has(k) && !relationNames.has(k) && k !== 'id' && k !== '_id') {
2207
+ if (!fieldNames.has(k) && !relationNames.has(k) && !relationFkColumns.has(k) && k !== 'id' && k !== '_id') {
2021
2208
  warnings.push('unknown field "' + k + '" (not in schema)');
2022
2209
  }
2023
2210
  }
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.5",
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",