@mostajs/orm-cli 0.4.7 → 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.
Files changed (2) hide show
  1. package/bin/mostajs.sh +389 -2
  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
- [[ -f "$CONFIG_FILE" ]] && { set -a; source "$CONFIG_FILE"; set +a; }
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() {
@@ -2636,6 +2645,364 @@ EOF
2636
2645
  pause
2637
2646
  }
2638
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
+
2639
3006
  # ============================================================
2640
3007
  # CLI SUBCOMMANDS (non-interactive)
2641
3008
  # ============================================================
@@ -2721,6 +3088,26 @@ run_subcommand() {
2721
3088
  health|h)
2722
3089
  action_healthcheck
2723
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
+ ;;
2724
3111
  install-bridge|ib)
2725
3112
  # mostajs install-bridge [--apply] [--file X] [--project P] [--restore]
2726
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.4.7",
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",