@mostajs/orm-cli 0.4.6 → 0.5.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 +456 -2
- package/package.json +1 -1
package/bin/mostajs.sh
CHANGED
|
@@ -27,7 +27,7 @@ CLI_NAME="mostajs"
|
|
|
27
27
|
# PATHS — relative to the CALLER's CWD, not the script
|
|
28
28
|
# ============================================================
|
|
29
29
|
|
|
30
|
-
PROJECT_ROOT="$(pwd)"
|
|
30
|
+
PROJECT_ROOT="${PROJECT_ROOT:-$(pwd)}"
|
|
31
31
|
CONFIG_DIR="$PROJECT_ROOT/.mostajs"
|
|
32
32
|
CONFIG_FILE="$CONFIG_DIR/config.env"
|
|
33
33
|
LOG_DIR="$CONFIG_DIR/logs"
|
|
@@ -214,7 +214,16 @@ header() {
|
|
|
214
214
|
# ============================================================
|
|
215
215
|
|
|
216
216
|
load_env() {
|
|
217
|
-
|
|
217
|
+
# Source .mostajs/config.env WITHOUT overriding env vars that are already
|
|
218
|
+
# set (CLI invocation with DB_DIALECT=... mostajs ... takes precedence).
|
|
219
|
+
[[ -f "$CONFIG_FILE" ]] || return
|
|
220
|
+
while IFS='=' read -r key value; do
|
|
221
|
+
[[ -z "$key" || "$key" =~ ^# ]] && continue
|
|
222
|
+
# Trim whitespace from key and strip surrounding quotes from value
|
|
223
|
+
key="${key// /}"
|
|
224
|
+
value="${value%\"}"; value="${value#\"}"
|
|
225
|
+
[[ -z "${!key+x}" ]] && export "$key=$value"
|
|
226
|
+
done < "$CONFIG_FILE"
|
|
218
227
|
}
|
|
219
228
|
|
|
220
229
|
save_var() {
|
|
@@ -457,6 +466,7 @@ menu_main() {
|
|
|
457
466
|
echo -e " ${CYAN}8${RESET}) Health checks"
|
|
458
467
|
echo -e " ${CYAN}9${RESET}) Generate boilerplate (src/db.ts with bridge)"
|
|
459
468
|
echo -e " ${CYAN}s${RESET}) ${BOLD}Seeding${RESET} (upload / validate / apply seed data)"
|
|
469
|
+
echo -e " ${CYAN}e${RESET}) ${BOLD}Export entities${RESET} → Prisma / JSON Schema / OpenAPI / Native"
|
|
460
470
|
echo -e " ${GREEN}b${RESET}) ${BOLD}Bootstrap${RESET} — one-shot migration of a Prisma project"
|
|
461
471
|
echo -e " ${GREEN}i${RESET}) ${BOLD}Install bridge${RESET} — codemod PrismaClient → bridge (dry-run / apply / restore)"
|
|
462
472
|
echo -e " ${CYAN}0${RESET}) About / Help"
|
|
@@ -476,6 +486,7 @@ menu_main() {
|
|
|
476
486
|
8) action_healthcheck ;;
|
|
477
487
|
9) action_generate_boilerplate ;;
|
|
478
488
|
s|S) menu_seeding ;;
|
|
489
|
+
e|E) action_export_entities ;;
|
|
479
490
|
b|B) menu_bootstrap ;;
|
|
480
491
|
i|I) menu_install_bridge ;;
|
|
481
492
|
0) action_about ;;
|
|
@@ -1700,6 +1711,71 @@ menu_seeding() {
|
|
|
1700
1711
|
menu_seeding
|
|
1701
1712
|
}
|
|
1702
1713
|
|
|
1714
|
+
# ------------------------------------------------------------
|
|
1715
|
+
# Export entities → Prisma / JSON Schema / OpenAPI / Native TS
|
|
1716
|
+
# ------------------------------------------------------------
|
|
1717
|
+
action_export_entities() {
|
|
1718
|
+
header
|
|
1719
|
+
echo -e "${BOLD}${MAGENTA}▶ Export entities to another schema format${RESET}"
|
|
1720
|
+
echo
|
|
1721
|
+
local ent_json="$GENERATED_DIR/entities.json"
|
|
1722
|
+
if [[ ! -f "$ent_json" ]]; then
|
|
1723
|
+
err "No entities.json found — run menu 1 (Convert) first."
|
|
1724
|
+
pause; return
|
|
1725
|
+
fi
|
|
1726
|
+
local count
|
|
1727
|
+
count=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$ent_json','utf8')).length)" 2>/dev/null || echo 0)
|
|
1728
|
+
echo -e " Source : ${DIM}${ent_json}${RESET} (${CYAN}${count}${RESET} entities)"
|
|
1729
|
+
echo
|
|
1730
|
+
echo -e " ${CYAN}1${RESET}) Prisma → ${DIM}schema.prisma${RESET}"
|
|
1731
|
+
echo -e " ${CYAN}2${RESET}) JSON Schema → ${DIM}schema.json${RESET} (2020-12)"
|
|
1732
|
+
echo -e " ${CYAN}3${RESET}) OpenAPI 3.1 → ${DIM}openapi.json${RESET}"
|
|
1733
|
+
echo -e " ${CYAN}4${RESET}) Native (TS) → ${DIM}src/schemas.ts${RESET}"
|
|
1734
|
+
echo
|
|
1735
|
+
echo -e " ${CYAN}b${RESET}) Back"
|
|
1736
|
+
echo
|
|
1737
|
+
local choice
|
|
1738
|
+
choice=$(ask "Format" "1")
|
|
1739
|
+
local format="" default_out=""
|
|
1740
|
+
case "$choice" in
|
|
1741
|
+
1) format="prisma"; default_out="$PROJECT_ROOT/prisma/schema.prisma" ;;
|
|
1742
|
+
2) format="jsonschema"; default_out="$PROJECT_ROOT/schema.json" ;;
|
|
1743
|
+
3) format="openapi"; default_out="$PROJECT_ROOT/openapi.json" ;;
|
|
1744
|
+
4) format="native"; default_out="$PROJECT_ROOT/src/schemas.ts" ;;
|
|
1745
|
+
b|B) return ;;
|
|
1746
|
+
*) warn "Unknown"; pause; return ;;
|
|
1747
|
+
esac
|
|
1748
|
+
local out
|
|
1749
|
+
out=$(ask "Output file" "$default_out")
|
|
1750
|
+
mkdir -p "$(dirname "$out")"
|
|
1751
|
+
|
|
1752
|
+
# Spawn Node to run the adapter's fromEntitySchema
|
|
1753
|
+
node --input-type=module -e "
|
|
1754
|
+
import('@mostajs/orm-adapter').then(async ({ PrismaAdapter, JsonSchemaAdapter, OpenApiAdapter, NativeAdapter }) => {
|
|
1755
|
+
const entities = JSON.parse(await (await import('fs/promises')).readFile('$ent_json', 'utf8'));
|
|
1756
|
+
let out;
|
|
1757
|
+
switch ('$format') {
|
|
1758
|
+
case 'prisma': out = await new PrismaAdapter().fromEntitySchema(entities); break;
|
|
1759
|
+
case 'jsonschema': out = JSON.stringify(await new JsonSchemaAdapter().fromEntitySchema(entities), null, 2); break;
|
|
1760
|
+
case 'openapi': out = JSON.stringify(await new OpenApiAdapter().fromEntitySchema(entities), null, 2); break;
|
|
1761
|
+
case 'native':
|
|
1762
|
+
out = '// Auto-generated by mostajs export → Native (EntitySchema TS)\n'
|
|
1763
|
+
+ '// Author: @mostajs/orm-cli\n\n'
|
|
1764
|
+
+ \"import type { EntitySchema } from '@mostajs/orm';\n\n\"
|
|
1765
|
+
+ 'export const schemas: EntitySchema[] = '
|
|
1766
|
+
+ JSON.stringify(entities, null, 2) + ';\n\n'
|
|
1767
|
+
+ '// Named exports for convenience\n'
|
|
1768
|
+
+ entities.map(e => 'export const ' + e.name + 'Schema: EntitySchema = schemas.find(s => s.name === \"' + e.name + '\")!;').join('\n')
|
|
1769
|
+
+ '\n';
|
|
1770
|
+
break;
|
|
1771
|
+
}
|
|
1772
|
+
await (await import('fs/promises')).writeFile('$out', out);
|
|
1773
|
+
console.log(' ✓ written ' + '$out' + ' (' + (out.length/1024).toFixed(1) + ' KB)');
|
|
1774
|
+
}).catch(e => { console.error(' ✗', e.message); process.exit(1); });
|
|
1775
|
+
" 2>&1
|
|
1776
|
+
pause
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1703
1779
|
# ------------------------------------------------------------
|
|
1704
1780
|
# seed : drop tables (interactive picker)
|
|
1705
1781
|
# ------------------------------------------------------------
|
|
@@ -2569,6 +2645,364 @@ EOF
|
|
|
2569
2645
|
pause
|
|
2570
2646
|
}
|
|
2571
2647
|
|
|
2648
|
+
# ============================================================
|
|
2649
|
+
# `mostajs init` — scaffold a new project
|
|
2650
|
+
# ============================================================
|
|
2651
|
+
#
|
|
2652
|
+
# Creates every file a fresh project needs to run on the bridge :
|
|
2653
|
+
# - .env with PORT / DB_DIALECT / SGBD_URI / AUTH_SECRET
|
|
2654
|
+
# - prisma/schema.prisma (minimal User model — starting point)
|
|
2655
|
+
# - src/lib/db.ts (createPrismaLikeDb)
|
|
2656
|
+
# - .mostajs/config.env (mirrors .env for the seed-runner)
|
|
2657
|
+
# - .mostajs/generated/entities.json (empty array, filled by menu 1)
|
|
2658
|
+
#
|
|
2659
|
+
# Dialect defaults to sqlite ./data.sqlite. Pass --dialect=postgres etc.
|
|
2660
|
+
# Refuses to overwrite existing files unless --force.
|
|
2661
|
+
|
|
2662
|
+
action_cli_init() {
|
|
2663
|
+
local dialect="sqlite"
|
|
2664
|
+
local uri=""
|
|
2665
|
+
local force=0
|
|
2666
|
+
while [[ $# -gt 0 ]]; do
|
|
2667
|
+
case "$1" in
|
|
2668
|
+
--dialect) dialect="$2"; shift 2 ;;
|
|
2669
|
+
--dialect=*) dialect="${1#*=}"; shift ;;
|
|
2670
|
+
--uri) uri="$2"; shift 2 ;;
|
|
2671
|
+
--uri=*) uri="${1#*=}"; shift ;;
|
|
2672
|
+
--force|-f) force=1; shift ;;
|
|
2673
|
+
*) warn "Unknown flag: $1"; shift ;;
|
|
2674
|
+
esac
|
|
2675
|
+
done
|
|
2676
|
+
|
|
2677
|
+
# Default URIs per dialect
|
|
2678
|
+
if [[ -z "$uri" ]]; then
|
|
2679
|
+
case "$dialect" in
|
|
2680
|
+
sqlite) uri="./data.sqlite" ;;
|
|
2681
|
+
postgres) uri="postgres://user:pass@localhost:5432/mydb" ;;
|
|
2682
|
+
mysql) uri="mysql://user:pass@localhost:3306/mydb" ;;
|
|
2683
|
+
mariadb) uri="mariadb://user:pass@localhost:3306/mydb" ;;
|
|
2684
|
+
mongodb) uri="mongodb://user:pass@localhost:27017/mydb" ;;
|
|
2685
|
+
oracle) uri="oracle://user:pass@localhost:1521/XE" ;;
|
|
2686
|
+
mssql) uri="mssql://user:pass@localhost:1433/mydb" ;;
|
|
2687
|
+
cockroachdb) uri="postgresql://user:pass@localhost:26257/mydb?sslmode=disable" ;;
|
|
2688
|
+
*) uri="./data.sqlite"; dialect="sqlite" ;;
|
|
2689
|
+
esac
|
|
2690
|
+
fi
|
|
2691
|
+
|
|
2692
|
+
header
|
|
2693
|
+
echo -e "${BOLD}${MAGENTA}▶ mostajs init — scaffold a bridge-ready project${RESET}"
|
|
2694
|
+
echo
|
|
2695
|
+
echo -e " Dialect : ${CYAN}${dialect}${RESET}"
|
|
2696
|
+
echo -e " URI : ${DIM}${uri}${RESET}"
|
|
2697
|
+
echo -e " Root : ${DIM}${PROJECT_ROOT}${RESET}"
|
|
2698
|
+
echo
|
|
2699
|
+
|
|
2700
|
+
local created=0 skipped=0
|
|
2701
|
+
|
|
2702
|
+
write_if_missing() {
|
|
2703
|
+
local path="$1"; local content="$2"
|
|
2704
|
+
if [[ -f "$PROJECT_ROOT/$path" && $force -eq 0 ]]; then
|
|
2705
|
+
dim " - skip $path (exists — use --force to overwrite)"
|
|
2706
|
+
((skipped++))
|
|
2707
|
+
return
|
|
2708
|
+
fi
|
|
2709
|
+
mkdir -p "$(dirname "$PROJECT_ROOT/$path")"
|
|
2710
|
+
printf '%s' "$content" > "$PROJECT_ROOT/$path"
|
|
2711
|
+
ok "created $path"
|
|
2712
|
+
((created++))
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
# --- .env ---
|
|
2716
|
+
local secret
|
|
2717
|
+
secret=$(node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" 2>/dev/null || echo 'CHANGE-ME-IN-PROD')
|
|
2718
|
+
write_if_missing ".env" "\
|
|
2719
|
+
# Port — used by next dev / start (reads PORT from here)
|
|
2720
|
+
PORT=3000
|
|
2721
|
+
|
|
2722
|
+
# Database — consumed by @mostajs/orm-bridge (createPrismaLikeDb)
|
|
2723
|
+
DB_DIALECT=${dialect}
|
|
2724
|
+
SGBD_URI=${uri}
|
|
2725
|
+
DB_SCHEMA_STRATEGY=update
|
|
2726
|
+
|
|
2727
|
+
# NextAuth (if you use it)
|
|
2728
|
+
NEXTAUTH_URL=http://localhost:3000
|
|
2729
|
+
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
2730
|
+
AUTH_SECRET=${secret}
|
|
2731
|
+
"
|
|
2732
|
+
|
|
2733
|
+
# --- .mostajs/config.env (mirror for the seed-runner) ---
|
|
2734
|
+
write_if_missing ".mostajs/config.env" "\
|
|
2735
|
+
DB_DIALECT=${dialect}
|
|
2736
|
+
SGBD_URI=${uri}
|
|
2737
|
+
DB_SCHEMA_STRATEGY=update
|
|
2738
|
+
APP_PORT=3000
|
|
2739
|
+
"
|
|
2740
|
+
|
|
2741
|
+
# --- .mostajs/generated/entities.json (empty — filled by menu 1) ---
|
|
2742
|
+
write_if_missing ".mostajs/generated/entities.json" "[]
|
|
2743
|
+
"
|
|
2744
|
+
|
|
2745
|
+
# --- prisma/schema.prisma (minimal starter) ---
|
|
2746
|
+
# Prisma's valid providers : sqlite, postgresql, mysql, mongodb, sqlserver, cockroachdb
|
|
2747
|
+
local provider="$dialect"
|
|
2748
|
+
case "$dialect" in
|
|
2749
|
+
postgres|postgresql) provider="postgresql" ;;
|
|
2750
|
+
mssql) provider="sqlserver" ;;
|
|
2751
|
+
mariadb) provider="mysql" ;;
|
|
2752
|
+
oracle|db2|hana|hsqldb|spanner|sybase) provider="sqlite" ;; # Prisma has no native provider — keep sqlite placeholder
|
|
2753
|
+
esac
|
|
2754
|
+
write_if_missing "prisma/schema.prisma" "\
|
|
2755
|
+
// Minimal starter — edit freely. Run 'mostajs' menu 1 to convert to EntitySchema.
|
|
2756
|
+
generator client {
|
|
2757
|
+
provider = \"prisma-client-js\"
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
datasource db {
|
|
2761
|
+
provider = \"${provider}\"
|
|
2762
|
+
url = env(\"DATABASE_URL\")
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
model User {
|
|
2766
|
+
id String @id @default(uuid())
|
|
2767
|
+
email String @unique
|
|
2768
|
+
password String
|
|
2769
|
+
name String?
|
|
2770
|
+
createdAt DateTime @default(now())
|
|
2771
|
+
updatedAt DateTime @updatedAt
|
|
2772
|
+
}
|
|
2773
|
+
"
|
|
2774
|
+
|
|
2775
|
+
# --- src/lib/db.ts (createPrismaLikeDb) ---
|
|
2776
|
+
write_if_missing "src/lib/db.ts" "\
|
|
2777
|
+
// Generated by 'mostajs init' — @mostajs/orm-bridge entry point.
|
|
2778
|
+
// Every Prisma-style db.User.findUnique(...) call below is routed to
|
|
2779
|
+
// @mostajs/orm (13 dialects). Edit DB_DIALECT / SGBD_URI in .env to switch.
|
|
2780
|
+
import { createPrismaLikeDb } from '@mostajs/orm-bridge/prisma-client'
|
|
2781
|
+
|
|
2782
|
+
export const db = createPrismaLikeDb()
|
|
2783
|
+
"
|
|
2784
|
+
|
|
2785
|
+
echo
|
|
2786
|
+
echo -e " ${BOLD}${created}${RESET} file(s) created, ${DIM}${skipped}${RESET} skipped"
|
|
2787
|
+
echo
|
|
2788
|
+
echo -e " ${BOLD}Next steps${RESET} :"
|
|
2789
|
+
echo -e " ${CYAN}1.${RESET} npm install @mostajs/orm @mostajs/orm-bridge @mostajs/orm-cli --legacy-peer-deps"
|
|
2790
|
+
echo -e " ${CYAN}2.${RESET} Edit ${DIM}prisma/schema.prisma${RESET} — add your models"
|
|
2791
|
+
echo -e " ${CYAN}3.${RESET} ${CYAN}mostajs${RESET} → menu 1 (Convert) → menu 3 (init DDL)"
|
|
2792
|
+
echo -e " ${CYAN}4.${RESET} ${CYAN}mostajs${RESET} → menu S (Seeds) — populate, hash, apply"
|
|
2793
|
+
echo -e " ${CYAN}5.${RESET} ${CYAN}npm run dev${RESET}"
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
# ============================================================
|
|
2797
|
+
# `mostajs migrate` — incremental DDL diff / apply / status
|
|
2798
|
+
# ============================================================
|
|
2799
|
+
#
|
|
2800
|
+
# Subcommands :
|
|
2801
|
+
# diff — list ALTERs needed to make the live DB match entities.json
|
|
2802
|
+
# apply — execute those ALTERs (prompts for confirmation, --yes to skip)
|
|
2803
|
+
# status — show entities.json count + live tables count + missing columns
|
|
2804
|
+
|
|
2805
|
+
action_cli_migrate() {
|
|
2806
|
+
local sub="${1:-}"
|
|
2807
|
+
[[ -z "$sub" ]] && { action_migrate_help; return; }
|
|
2808
|
+
shift
|
|
2809
|
+
case "$sub" in
|
|
2810
|
+
diff|d) action_migrate_diff "$@" ;;
|
|
2811
|
+
apply|a) action_migrate_apply "$@" ;;
|
|
2812
|
+
status|s) action_migrate_status "$@" ;;
|
|
2813
|
+
help|h|--help) action_migrate_help ;;
|
|
2814
|
+
*) err "Unknown migrate subcommand: $sub"; action_migrate_help; return 1 ;;
|
|
2815
|
+
esac
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
action_migrate_help() {
|
|
2819
|
+
cat <<EOF
|
|
2820
|
+
|
|
2821
|
+
${BOLD}mostajs migrate${RESET} — incremental schema migration
|
|
2822
|
+
|
|
2823
|
+
${CYAN}diff${RESET} show ALTER statements the DB needs to match entities.json
|
|
2824
|
+
${CYAN}apply${RESET} execute those ALTERs (prompts for confirmation)
|
|
2825
|
+
flags : --yes (skip confirmation)
|
|
2826
|
+
${CYAN}status${RESET} show live-vs-schema summary per entity
|
|
2827
|
+
|
|
2828
|
+
Every subcommand honors DB_DIALECT + SGBD_URI from ${DIM}.mostajs/config.env${RESET}.
|
|
2829
|
+
|
|
2830
|
+
EOF
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
# Node helper : compare live columns vs schema.fields and emit ALTER plan as JSON.
|
|
2834
|
+
# Outputs to stdout : { changes: [{ table, column, sql }], ok: bool }
|
|
2835
|
+
_migrate_compute_plan() {
|
|
2836
|
+
load_env
|
|
2837
|
+
local entities_json="$GENERATED_DIR/entities.json"
|
|
2838
|
+
if [[ ! -f "$entities_json" ]]; then
|
|
2839
|
+
err "No entities.json — run menu 1 (Convert) first."
|
|
2840
|
+
return 1
|
|
2841
|
+
fi
|
|
2842
|
+
ENT_PATH="$entities_json" DIALECT="$DB_DIALECT" URI="$SGBD_URI" \
|
|
2843
|
+
node --input-type=module -e "
|
|
2844
|
+
import { readFileSync } from 'node:fs';
|
|
2845
|
+
import { getDialect } from '${PROJECT_ROOT}/node_modules/@mostajs/orm/dist/index.js';
|
|
2846
|
+
const entities = JSON.parse(readFileSync(process.env.ENT_PATH, 'utf8'));
|
|
2847
|
+
const d = await getDialect({ dialect: process.env.DIALECT, uri: process.env.URI, schemaStrategy: 'none' });
|
|
2848
|
+
|
|
2849
|
+
// Use the dialect's own introspection — protected method, exposed via cast
|
|
2850
|
+
const changes = [];
|
|
2851
|
+
for (const e of entities) {
|
|
2852
|
+
let live;
|
|
2853
|
+
try {
|
|
2854
|
+
live = await (d).getExistingColumns(e.collection);
|
|
2855
|
+
} catch {
|
|
2856
|
+
changes.push({ table: e.collection, column: '*', sql: '-- (cannot introspect — run menu 3 first)' });
|
|
2857
|
+
continue;
|
|
2858
|
+
}
|
|
2859
|
+
const hasCol = (name) => {
|
|
2860
|
+
const lc = name.toLowerCase();
|
|
2861
|
+
for (const c of live) if (c.toLowerCase() === lc) return true;
|
|
2862
|
+
return false;
|
|
2863
|
+
};
|
|
2864
|
+
// Field columns
|
|
2865
|
+
for (const [name, f] of Object.entries(e.fields || {})) {
|
|
2866
|
+
if (name === '_id') continue;
|
|
2867
|
+
if (hasCol(name)) continue;
|
|
2868
|
+
// Reconstruct the ALTER — d has fieldToSqlType + getIdColumnType + quoteIdentifier
|
|
2869
|
+
const q = (n) => (d).quoteIdentifier(n);
|
|
2870
|
+
let sql;
|
|
2871
|
+
if (name === 'id') {
|
|
2872
|
+
sql = 'ALTER TABLE ' + q(e.collection) + ' ADD ' + q('id') + ' ' + (d).getIdColumnType();
|
|
2873
|
+
} else {
|
|
2874
|
+
sql = 'ALTER TABLE ' + q(e.collection) + ' ADD ' + q(name) + ' ' + (d).fieldToSqlType(f);
|
|
2875
|
+
}
|
|
2876
|
+
changes.push({ table: e.collection, column: name, sql });
|
|
2877
|
+
}
|
|
2878
|
+
// Relation FK columns
|
|
2879
|
+
for (const [rname, rel] of Object.entries(e.relations || {})) {
|
|
2880
|
+
if (rel.type !== 'many-to-one' && rel.type !== 'one-to-one') continue;
|
|
2881
|
+
const colName = rel.joinColumn || (rname + 'Id');
|
|
2882
|
+
if (hasCol(colName)) continue;
|
|
2883
|
+
const q = (n) => (d).quoteIdentifier(n);
|
|
2884
|
+
changes.push({
|
|
2885
|
+
table: e.collection, column: colName,
|
|
2886
|
+
sql: 'ALTER TABLE ' + q(e.collection) + ' ADD ' + q(colName) + ' ' + (d).getIdColumnType(),
|
|
2887
|
+
});
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
await d.disconnect();
|
|
2891
|
+
console.log(JSON.stringify({ ok: true, changes }));
|
|
2892
|
+
"
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
action_migrate_diff() {
|
|
2896
|
+
header
|
|
2897
|
+
echo -e "${BOLD}${MAGENTA}▶ mostajs migrate diff${RESET}"
|
|
2898
|
+
echo
|
|
2899
|
+
local plan_json
|
|
2900
|
+
plan_json=$(_migrate_compute_plan) || { pause; return 1; }
|
|
2901
|
+
local count
|
|
2902
|
+
count=$(echo "$plan_json" | node -e "process.stdin.on('data',d=>{console.log(JSON.parse(d).changes.length)})" 2>/dev/null || echo '?')
|
|
2903
|
+
if [[ "$count" == "0" ]]; then
|
|
2904
|
+
ok "Schema is up to date — nothing to ALTER."
|
|
2905
|
+
return 0
|
|
2906
|
+
fi
|
|
2907
|
+
echo -e " ${BOLD}${count}${RESET} pending change(s) :"
|
|
2908
|
+
echo
|
|
2909
|
+
echo "$plan_json" | node -e "
|
|
2910
|
+
let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{
|
|
2911
|
+
const p = JSON.parse(d);
|
|
2912
|
+
for (const ch of p.changes) console.log(' ' + ch.sql + ';');
|
|
2913
|
+
});
|
|
2914
|
+
"
|
|
2915
|
+
echo
|
|
2916
|
+
echo -e " Run ${CYAN}mostajs migrate apply${RESET} to execute these statements."
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
action_migrate_apply() {
|
|
2920
|
+
local auto_yes=0
|
|
2921
|
+
[[ "${1:-}" == "--yes" || "${1:-}" == "-y" ]] && auto_yes=1
|
|
2922
|
+
header
|
|
2923
|
+
echo -e "${BOLD}${MAGENTA}▶ mostajs migrate apply${RESET}"
|
|
2924
|
+
echo
|
|
2925
|
+
local plan_json
|
|
2926
|
+
plan_json=$(_migrate_compute_plan) || return 1
|
|
2927
|
+
local count
|
|
2928
|
+
count=$(echo "$plan_json" | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d).changes.length))" 2>/dev/null || echo 0)
|
|
2929
|
+
if [[ "$count" == "0" ]]; then
|
|
2930
|
+
ok "Schema is up to date — nothing to ALTER."
|
|
2931
|
+
return 0
|
|
2932
|
+
fi
|
|
2933
|
+
echo " Pending : ${BOLD}${count}${RESET} statement(s)"
|
|
2934
|
+
echo
|
|
2935
|
+
echo "$plan_json" | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{for(const ch of JSON.parse(d).changes) console.log(' ' + ch.sql + ';')})"
|
|
2936
|
+
echo
|
|
2937
|
+
if [[ $auto_yes -eq 0 ]]; then
|
|
2938
|
+
if ! confirm "Execute these ALTER statements?"; then
|
|
2939
|
+
dim " Aborted."
|
|
2940
|
+
return
|
|
2941
|
+
fi
|
|
2942
|
+
fi
|
|
2943
|
+
|
|
2944
|
+
# Execute
|
|
2945
|
+
load_env
|
|
2946
|
+
PLAN="$plan_json" DIALECT="$DB_DIALECT" URI="$SGBD_URI" \
|
|
2947
|
+
node --input-type=module -e "
|
|
2948
|
+
import { getDialect } from '${PROJECT_ROOT}/node_modules/@mostajs/orm/dist/index.js';
|
|
2949
|
+
const plan = JSON.parse(process.env.PLAN);
|
|
2950
|
+
const d = await getDialect({ dialect: process.env.DIALECT, uri: process.env.URI, schemaStrategy: 'none' });
|
|
2951
|
+
let ok = 0, fail = 0;
|
|
2952
|
+
for (const ch of plan.changes) {
|
|
2953
|
+
try {
|
|
2954
|
+
await d.executeRun(ch.sql, []);
|
|
2955
|
+
console.log(' ✓ ' + ch.table + '.' + ch.column);
|
|
2956
|
+
ok++;
|
|
2957
|
+
} catch (e) {
|
|
2958
|
+
console.error(' ✗ ' + ch.table + '.' + ch.column + ' : ' + e.message);
|
|
2959
|
+
fail++;
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
await d.disconnect();
|
|
2963
|
+
console.log('\nApplied : ' + ok + ' ok, ' + fail + ' failed');
|
|
2964
|
+
process.exit(fail > 0 ? 1 : 0);
|
|
2965
|
+
"
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
action_migrate_status() {
|
|
2969
|
+
header
|
|
2970
|
+
echo -e "${BOLD}${MAGENTA}▶ mostajs migrate status${RESET}"
|
|
2971
|
+
echo
|
|
2972
|
+
load_env
|
|
2973
|
+
local ent_json="$GENERATED_DIR/entities.json"
|
|
2974
|
+
if [[ ! -f "$ent_json" ]]; then
|
|
2975
|
+
err "No entities.json — run menu 1 (Convert) first."
|
|
2976
|
+
return 1
|
|
2977
|
+
fi
|
|
2978
|
+
ENT_PATH="$ent_json" DIALECT="$DB_DIALECT" URI="$SGBD_URI" \
|
|
2979
|
+
node --input-type=module -e "
|
|
2980
|
+
import { readFileSync } from 'node:fs';
|
|
2981
|
+
import { getDialect } from '${PROJECT_ROOT}/node_modules/@mostajs/orm/dist/index.js';
|
|
2982
|
+
const entities = JSON.parse(readFileSync(process.env.ENT_PATH, 'utf8'));
|
|
2983
|
+
const d = await getDialect({ dialect: process.env.DIALECT, uri: process.env.URI, schemaStrategy: 'none' });
|
|
2984
|
+
let existing = 0, missing = 0, lagging = 0;
|
|
2985
|
+
for (const e of entities) {
|
|
2986
|
+
let live;
|
|
2987
|
+
try { live = await (d).getExistingColumns(e.collection); }
|
|
2988
|
+
catch { live = new Set(); }
|
|
2989
|
+
if (!live || live.size === 0) { console.log(' ✗ ' + e.collection + ' — table not found'); missing++; continue; }
|
|
2990
|
+
const hasCol = (n) => { const lc = n.toLowerCase(); for (const c of live) if (c.toLowerCase() === lc) return true; return false; };
|
|
2991
|
+
const schemaCols = Object.keys(e.fields || {});
|
|
2992
|
+
const need = schemaCols.filter(c => !hasCol(c));
|
|
2993
|
+
if (need.length) {
|
|
2994
|
+
console.log(' ⚠ ' + e.collection + ' — missing ' + need.length + ' column(s) : ' + need.join(', '));
|
|
2995
|
+
lagging++;
|
|
2996
|
+
} else {
|
|
2997
|
+
console.log(' ✓ ' + e.collection + ' (' + live.size + ' cols live, ' + schemaCols.length + ' in schema)');
|
|
2998
|
+
existing++;
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
await d.disconnect();
|
|
3002
|
+
console.log('\n ' + existing + ' up-to-date · ' + lagging + ' need migrate · ' + missing + ' missing');
|
|
3003
|
+
"
|
|
3004
|
+
}
|
|
3005
|
+
|
|
2572
3006
|
# ============================================================
|
|
2573
3007
|
# CLI SUBCOMMANDS (non-interactive)
|
|
2574
3008
|
# ============================================================
|
|
@@ -2654,6 +3088,26 @@ run_subcommand() {
|
|
|
2654
3088
|
health|h)
|
|
2655
3089
|
action_healthcheck
|
|
2656
3090
|
;;
|
|
3091
|
+
init)
|
|
3092
|
+
# mostajs init [--dialect sqlite|postgres|mongodb|...] [--force]
|
|
3093
|
+
# Scaffold a fresh project with bridge-ready layout :
|
|
3094
|
+
# .env (PORT, DB_DIALECT, SGBD_URI, AUTH_SECRET)
|
|
3095
|
+
# prisma/schema.prisma (minimal — User model only)
|
|
3096
|
+
# src/lib/db.ts (createPrismaLikeDb)
|
|
3097
|
+
# .mostajs/config.env (mirrors .env for the runner)
|
|
3098
|
+
# .mostajs/generated/entities.json (empty array)
|
|
3099
|
+
shift
|
|
3100
|
+
action_cli_init "$@"
|
|
3101
|
+
;;
|
|
3102
|
+
migrate|mig|m)
|
|
3103
|
+
# mostajs migrate <subcommand> [options]
|
|
3104
|
+
# Subcommands :
|
|
3105
|
+
# diff — show ALTER statements the target DB needs to match entities.json
|
|
3106
|
+
# apply — execute those ALTERs (with confirmation)
|
|
3107
|
+
# status — show what's in entities.json vs what's live in the DB
|
|
3108
|
+
shift
|
|
3109
|
+
action_cli_migrate "$@"
|
|
3110
|
+
;;
|
|
2657
3111
|
install-bridge|ib)
|
|
2658
3112
|
# mostajs install-bridge [--apply] [--file X] [--project P] [--restore]
|
|
2659
3113
|
# Codemod : scans the project for `new PrismaClient(...)` sites and rewrites
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/orm-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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",
|