@mostajs/orm-cli 0.4.2 → 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.
- package/bin/install-bridge.mjs +49 -12
- package/bin/mostajs.sh +255 -10
- package/package.json +1 -1
package/bin/install-bridge.mjs
CHANGED
|
@@ -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
|
|
34
|
-
const RESTORE
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const
|
|
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
|
-
|
|
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(
|
|
139
|
+
log(` ${c.green('✓')} restored ${c.dim(rel)}`);
|
|
124
140
|
} else {
|
|
125
|
-
log(` ${c.yellow('•')} would restore ${c.dim(
|
|
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('?')} ${
|
|
185
|
+
log(` ${c.yellow('?')} ${rel} — PrismaClient detected but export shape unknown, skipping`);
|
|
157
186
|
continue;
|
|
158
187
|
}
|
|
159
|
-
candidates.push({ path: p, rel
|
|
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
|
}
|
|
@@ -2515,14 +2702,69 @@ run_subcommand() {
|
|
|
2515
2702
|
fi
|
|
2516
2703
|
|
|
2517
2704
|
mkdir -p "$CONFIG_DIR"
|
|
2518
|
-
|
|
2705
|
+
# ── Database choice ──
|
|
2706
|
+
# Resolution order :
|
|
2707
|
+
# 1. existing .mostajs/config.env → honour it (user may have set it with `mostajs` menu 2)
|
|
2708
|
+
# 2. --dialect / --uri / --strategy CLI flags → non-interactive scripting
|
|
2709
|
+
# 3. $DB_DIALECT + $SGBD_URI env vars → non-interactive, no files on disk
|
|
2710
|
+
# 4. interactive prompt (default)
|
|
2711
|
+
if [[ -f "$CONFIG_DIR/config.env" ]]; then
|
|
2712
|
+
load_env
|
|
2713
|
+
info " using existing $CONFIG_DIR/config.env (dialect=$DB_DIALECT uri=$SGBD_URI)"
|
|
2714
|
+
else
|
|
2715
|
+
local bs_dialect="" bs_uri="" bs_strategy="update"
|
|
2716
|
+
for arg in "$@"; do
|
|
2717
|
+
case "$arg" in
|
|
2718
|
+
--dialect=*) bs_dialect="${arg#--dialect=}" ;;
|
|
2719
|
+
--uri=*) bs_uri="${arg#--uri=}" ;;
|
|
2720
|
+
--strategy=*) bs_strategy="${arg#--strategy=}" ;;
|
|
2721
|
+
esac
|
|
2722
|
+
done
|
|
2723
|
+
# Fall back to env vars
|
|
2724
|
+
[[ -z "$bs_dialect" ]] && bs_dialect="${DB_DIALECT:-}"
|
|
2725
|
+
[[ -z "$bs_uri" ]] && bs_uri="${SGBD_URI:-}"
|
|
2726
|
+
|
|
2727
|
+
if [[ -z "$bs_dialect" || -z "$bs_uri" ]]; then
|
|
2728
|
+
echo
|
|
2729
|
+
echo -e "${BOLD}▶ Database choice${RESET} ${DIM}(run \`$CLI_NAME\` then menu 2 beforehand to skip this prompt)${RESET}"
|
|
2730
|
+
echo
|
|
2731
|
+
echo " ${CYAN}1${RESET}) SQLite ${DIM}./data.sqlite (zero setup, recommended for trying out)${RESET}"
|
|
2732
|
+
echo " ${CYAN}2${RESET}) PostgreSQL ${DIM}postgres://user:pass@host:5432/db${RESET}"
|
|
2733
|
+
echo " ${CYAN}3${RESET}) MongoDB ${DIM}mongodb://user:pass@host:27017/db${RESET}"
|
|
2734
|
+
echo " ${CYAN}4${RESET}) MySQL ${DIM}mysql://user:pass@host:3306/db${RESET}"
|
|
2735
|
+
echo " ${CYAN}5${RESET}) MariaDB ${DIM}mariadb://user:pass@host:3306/db${RESET}"
|
|
2736
|
+
echo " ${CYAN}6${RESET}) Oracle ${DIM}oracle://user:pass@host:1521/XE${RESET}"
|
|
2737
|
+
echo " ${CYAN}7${RESET}) SQL Server ${DIM}mssql://user:pass@host:1433/db${RESET}"
|
|
2738
|
+
echo " ${CYAN}8${RESET}) CockroachDB ${DIM}postgresql://user:pass@host:26257/db?sslmode=disable${RESET}"
|
|
2739
|
+
echo " ${CYAN}9${RESET}) DB2 / HANA / HSQLDB / Spanner / Sybase / other (enter manually)"
|
|
2740
|
+
echo
|
|
2741
|
+
local c; c=$(ask "Choice" "1")
|
|
2742
|
+
case "$c" in
|
|
2743
|
+
1) bs_dialect="sqlite"; bs_uri=$(ask "SQLite path" "./data.sqlite") ;;
|
|
2744
|
+
2) bs_dialect="postgres"; bs_uri=$(ask "Postgres URI" "postgres://user:pass@localhost:5432/mydb") ;;
|
|
2745
|
+
3) bs_dialect="mongodb"; bs_uri=$(ask "MongoDB URI" "mongodb://localhost:27017/mydb") ;;
|
|
2746
|
+
4) bs_dialect="mysql"; bs_uri=$(ask "MySQL URI" "mysql://user:pass@localhost:3306/mydb") ;;
|
|
2747
|
+
5) bs_dialect="mariadb"; bs_uri=$(ask "MariaDB URI" "mariadb://user:pass@localhost:3306/mydb") ;;
|
|
2748
|
+
6) bs_dialect="oracle"; bs_uri=$(ask "Oracle URI" "oracle://user:pass@localhost:1521/XE") ;;
|
|
2749
|
+
7) bs_dialect="mssql"; bs_uri=$(ask "SQL Server URI" "mssql://user:pass@localhost:1433/mydb") ;;
|
|
2750
|
+
8) bs_dialect="cockroachdb"; bs_uri=$(ask "CockroachDB URI" "postgresql://root@localhost:26257/mydb?sslmode=disable") ;;
|
|
2751
|
+
9) bs_dialect=$(ask "Dialect (db2|hana|hsqldb|spanner|sybase|other)" "")
|
|
2752
|
+
bs_uri=$(ask "URI" "") ;;
|
|
2753
|
+
*) err "Invalid choice"; exit 1 ;;
|
|
2754
|
+
esac
|
|
2755
|
+
[[ -z "$bs_dialect" || -z "$bs_uri" ]] && { err "dialect and uri are required"; exit 1; }
|
|
2756
|
+
|
|
2757
|
+
local chosen_strategy
|
|
2758
|
+
chosen_strategy=$(ask "Schema strategy (validate|update|create|create-drop|none)" "$bs_strategy")
|
|
2759
|
+
bs_strategy="$chosen_strategy"
|
|
2760
|
+
fi
|
|
2761
|
+
|
|
2519
2762
|
cat > "$CONFIG_DIR/config.env" <<CFG
|
|
2520
|
-
DB_DIALECT
|
|
2521
|
-
SGBD_URI
|
|
2522
|
-
DB_SCHEMA_STRATEGY
|
|
2763
|
+
DB_DIALECT=$bs_dialect
|
|
2764
|
+
SGBD_URI=$bs_uri
|
|
2765
|
+
DB_SCHEMA_STRATEGY=$bs_strategy
|
|
2523
2766
|
CFG
|
|
2524
|
-
ok " wrote $CONFIG_DIR/config.env (
|
|
2525
|
-
# Reload config so action_init_dialects picks up the new values
|
|
2767
|
+
ok " wrote $CONFIG_DIR/config.env (dialect=$bs_dialect)"
|
|
2526
2768
|
load_env
|
|
2527
2769
|
fi
|
|
2528
2770
|
|
|
@@ -2542,7 +2784,7 @@ CFG
|
|
|
2542
2784
|
|
|
2543
2785
|
✓ Bridge installed in-place. Original files backed up as *.prisma.bak
|
|
2544
2786
|
✓ Schema converted : $GENERATED_DIR/entities.json
|
|
2545
|
-
✓ DDL applied (DB_DIALECT
|
|
2787
|
+
✓ DDL applied (DB_DIALECT=$DB_DIALECT SGBD_URI=$SGBD_URI)
|
|
2546
2788
|
|
|
2547
2789
|
Next :
|
|
2548
2790
|
- Add seeds to $CONFIG_DIR/seeds/*.json (one file per entity)
|
|
@@ -2566,8 +2808,11 @@ DONE
|
|
|
2566
2808
|
cat <<EOF
|
|
2567
2809
|
Usage :
|
|
2568
2810
|
$CLI_NAME Interactive menu
|
|
2569
|
-
$CLI_NAME bootstrap
|
|
2570
|
-
|
|
2811
|
+
$CLI_NAME bootstrap One-shot migration : codemod + deps + convert + DDL
|
|
2812
|
+
(interactive database picker unless config exists)
|
|
2813
|
+
$CLI_NAME bootstrap --dialect=postgres --uri=postgres://... --strategy=update
|
|
2814
|
+
Non-interactive bootstrap (for CI / scripts)
|
|
2815
|
+
$CLI_NAME install-bridge Codemod only (dry-run ; add --apply to write)
|
|
2571
2816
|
$CLI_NAME install-bridge --apply Rewrite PrismaClient sites to use @mostajs/orm-bridge
|
|
2572
2817
|
$CLI_NAME install-bridge --restore --apply Undo a prior install-bridge
|
|
2573
2818
|
$CLI_NAME convert Run conversion (auto-detect schema type)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/orm-cli",
|
|
3
|
-
"version": "0.4.
|
|
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",
|