eksa-framework 3.4.3 → 3.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.
@@ -6,7 +6,7 @@
6
6
  </div>
7
7
  <h1 class="text-4xl font-extrabold tracking-tight">Dokumentasi <span class="text-indigo-300">Eksa</span></h1>
8
8
  </div>
9
- <p class="text-white/60 text-lg">Panduan profesional instalasi dan pengembangan aplikasi menggunakan Eksa Framework.</p>
9
+ <p class="text-white/60 text-lg">Panduan profesional instalasi dan pengembangan aplikasi menggunakan Eksa Framework v3.5.0.</p>
10
10
  </div>
11
11
 
12
12
  <div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-12">
@@ -14,6 +14,7 @@
14
14
  <h3 class="font-bold text-indigo-300 uppercase text-xs tracking-widest">Memulai</h3>
15
15
  <ul class="space-y-2 text-sm">
16
16
  <li><a href="#instalasi" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="download" class="w-3 h-3"></i> Instalasi Gem</a></li>
17
+ <li><a href="#keamanan" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="shield" class="w-3 h-3"></i> Setup Keamanan</a></li>
17
18
  <li><a href="#struktur" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="folder" class="w-3 h-3"></i> Struktur Project</a></li>
18
19
  </ul>
19
20
  </div>
@@ -21,16 +22,16 @@
21
22
  <h3 class="font-bold text-indigo-300 uppercase text-xs tracking-widest">Pengembangan</h3>
22
23
  <ul class="space-y-2 text-sm">
23
24
  <li><a href="#routing" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="git-branch" class="w-3 h-3"></i> Routing System</a></li>
24
- <li><a href="#build" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="package" class="w-3 h-3"></i> Build Framework</a></li>
25
+ <li><a href="#database" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="database" class="w-3 h-3"></i> Agnostic Database</a></li>
26
+ <li><a href="#jit" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="zap" class="w-3 h-3"></i> JIT Schema</a></li>
25
27
  </ul>
26
28
  </div>
27
29
  <div class="space-y-4">
28
30
  <h3 class="font-bold text-indigo-300 uppercase text-xs tracking-widest">Fitur Core</h3>
29
31
  <ul class="space-y-2 text-sm">
30
- <li><a href="#database" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="database" class="w-3 h-3"></i> Auto-Migration</a></li>
31
- <li><a href="#flash" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="bell" class="w-3 h-3"></i> Flash UI</a></li>
32
32
  <li><a href="#blog" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="pen-tool" class="w-3 h-3"></i> Markdown Blog</a></li>
33
33
  <li><a href="#cms" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="layout-dashboard" class="w-3 h-3"></i> CMS & Auth</a></li>
34
+ <li><a href="#seo" class="hover:text-indigo-300 transition flex items-center gap-2"><i data-lucide="search" class="w-3 h-3"></i> SEO & JSON-LD</a></li>
34
35
  </ul>
35
36
  </div>
36
37
  </div>
@@ -49,85 +50,54 @@
49
50
  <p class="text-indigo-300">eksa init</p>
50
51
  </div>
51
52
  <div>
52
- <p class="text-white/30"># 3. Install dependensi</p>
53
- <p class="text-indigo-300">bundle install</p>
54
- </div>
55
- <div>
56
- <p class="text-white/30"># 4. Jalankan server</p>
57
- <p class="text-indigo-300">eksa run</p>
53
+ <p class="text-white/30"># 3. Jalankan server</p>
54
+ <p class="text-indigo-300">bundle install && eksa run</p>
58
55
  </div>
59
56
  </div>
60
57
  </section>
61
58
 
62
- <section id="struktur" class="mb-12 scroll-mt-24">
59
+ <section id="keamanan" class="mb-12 scroll-mt-24">
63
60
  <h2 class="text-2xl font-bold mb-4 flex items-center gap-2">
64
- <i data-lucide="folder-tree" class="w-6 h-6 text-indigo-400"></i> Struktur Project
61
+ <i data-lucide="shield-check" class="w-6 h-6 text-indigo-400"></i> Setup Keamanan (.env)
65
62
  </h2>
66
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
67
- <div class="bg-white/5 p-4 rounded-xl border border-white/10">
68
- <code class="text-indigo-300 font-bold">app/</code>
69
- <p class="text-xs text-white/50 mt-1">Berisi Controllers, Models, dan Views aplikasi Anda.</p>
70
- </div>
71
- <div class="bg-white/5 p-4 rounded-xl border border-white/10">
72
- <code class="text-indigo-300 font-bold">db/</code>
73
- <p class="text-xs text-white/50 mt-1">Lokasi penyimpanan database SQLite (Terpisah dari app).</p>
74
- </div>
75
- <div class="bg-white/5 p-4 rounded-xl border border-white/10">
76
- <code class="text-indigo-300 font-bold">lib/eksa/</code>
77
- <p class="text-xs text-white/50 mt-1">Mesin inti (Core Engine) dari Eksa Framework.</p>
78
- </div>
79
- <div class="bg-white/5 p-4 rounded-xl border border-white/10">
80
- <code class="text-indigo-300 font-bold">spec/</code>
81
- <p class="text-xs text-white/50 mt-1">Folder testing otomatis menggunakan RSpec (disertakan saat <code class="text-indigo-200">init</code>).</p>
82
- </div>
83
- <div class="bg-white/5 p-4 rounded-xl border border-white/10">
84
- <code class="text-indigo-300 font-bold">_posts/</code>
85
- <p class="text-xs text-white/50 mt-1">Pusat file markdown untuk konten blog aplikasi.</p>
86
- </div>
87
- <div class="bg-white/5 p-4 rounded-xl border border-white/10">
88
- <code class="text-indigo-300 font-bold">exe/</code>
89
- <p class="text-xs text-white/50 mt-1">Executable files seperti CLI <code class="text-indigo-200">eksa</code>.</p>
90
- </div>
91
- </div>
92
- </section>
93
-
94
- <section id="routing" class="mb-12 scroll-mt-24">
95
- <h2 class="text-2xl font-bold mb-4 flex items-center gap-2">
96
- <i data-lucide="git-branch" class="w-6 h-6 text-indigo-400"></i> Routing System
97
- </h2>
98
- <p class="text-white/60 mb-4 text-sm">Definisikan rute aplikasi Anda di dalam file <code class="text-indigo-200">config.ru</code>. Eksa mendukung pemetaan URL ke Controller dan Action secara eksplisit.</p>
99
- <div class="bg-black/40 rounded-2xl p-6 font-mono text-sm border border-white/10">
100
- <p class="text-white/30"># app.add_route(path, controller_class, action_symbol)</p>
101
- <p class="text-indigo-300 italic">app.add_route "/", PagesController, :index</p>
102
- <p class="text-indigo-300 italic">app.add_route "/about", PagesController, :about</p>
63
+ <p class="text-white/60 mb-4 text-sm">Gunakan file <code class="text-indigo-200">.env</code> untuk menyimpan kredensial sensitif seperti URI MongoDB Atlas dan Secret Session.</p>
64
+ <div class="bg-black/40 rounded-2xl p-6 font-mono text-sm border border-white/10 space-y-2">
65
+ <p class="text-white/30"># Salin template example</p>
66
+ <p class="text-indigo-300">cp .env.example .env</p>
67
+ <p class="text-white/30 mt-4"># Isi EKSA_MONGODB_URI di dalam .env</p>
68
+ <p class="text-indigo-300">EKSA_MONGODB_URI="mongodb+srv://..."</p>
103
69
  </div>
104
70
  </section>
105
71
 
106
72
  <section id="database" class="mb-12 scroll-mt-24">
107
73
  <h2 class="text-2xl font-bold mb-4 flex items-center gap-2">
108
- <i data-lucide="database" class="w-6 h-6 text-indigo-400"></i> Auto-Migration
74
+ <i data-lucide="database" class="w-6 h-6 text-indigo-400"></i> Agnostic Database Engine
109
75
  </h2>
110
- <p class="text-white/60 mb-4 text-sm">Eksa secara otomatis membuat file database dan tabel saat aplikasi pertama kali dijalankan melalui <code class="text-indigo-200">Eksa::Model</code>.</p>
111
- <div class="bg-black/40 rounded-2xl p-6 font-mono text-sm border border-white/10">
112
- <p class="text-indigo-300">def self.setup_initial_schema</p>
113
- <p class="text-white/70 ml-4">@db.execute "CREATE TABLE IF NOT EXISTS pesan (...)"</p>
114
- <p class="text-indigo-300">end</p>
76
+ <p class="text-white/60 mb-4 text-sm">Pindah dari SQLite lokal ke cloud MongoDB Atlas secara instan melalui CLI.</p>
77
+ <div class="bg-black/40 rounded-2xl p-6 font-mono text-sm border border-white/10 space-y-4">
78
+ <div>
79
+ <p class="text-white/30"># Ganti tipe database</p>
80
+ <p class="text-indigo-300">eksa db switch mongo</p>
81
+ </div>
82
+ <div>
83
+ <p class="text-white/30"># Migrasi data otomatis (SQLite -> Mongo)</p>
84
+ <p class="text-indigo-300">eksa db migrate --from sqlite --to mongo</p>
85
+ </div>
115
86
  </div>
116
- <p class="text-xs text-white/40 mt-3 italic">* Database tersimpan secara persisten di folder /db/eksa_app.db</p>
117
87
  </section>
118
88
 
119
- <section id="flash" class="mb-12 scroll-mt-24">
89
+ <section id="jit" class="mb-12 scroll-mt-24">
120
90
  <h2 class="text-2xl font-bold mb-4 flex items-center gap-2">
121
- <i data-lucide="bell" class="w-6 h-6 text-indigo-400"></i> Flash UI Notification
91
+ <i data-lucide="zap" class="w-6 h-6 text-indigo-400"></i> Just-In-Time Schema Initialization
122
92
  </h2>
123
- <p class="text-white/60 mb-4 text-sm">Kirim feedback instan ke user menggunakan fitur redirect dengan notice.</p>
93
+ <p class="text-white/60 mb-4 text-sm">Anda tidak perlu lagi menjalankan migrasi manual. Eksa akan mendeteksi dan membuat tabel/koleksi secara otomatis tepat saat model pertama kali diakses.</p>
124
94
  <div class="bg-black/40 rounded-2xl p-6 font-mono text-sm border border-white/10">
125
- <p class="text-white/30"># Di dalam Controller</p>
126
- <p class="text-indigo-300">redirect_to "/", notice: "Data berhasil disimpan!"</p>
127
- </div>
128
- <div class="mt-4 p-4 bg-emerald-500/10 border border-emerald-500/20 rounded-xl flex items-center gap-3">
129
- <i data-lucide="info" class="w-5 h-5 text-emerald-400"></i>
130
- <p class="text-xs text-emerald-200/80">Sistem Flash Eksa menggunakan cookie sementara yang otomatis dihapus setelah notifikasi muncul (Standar Rack 3).</p>
95
+ <p class="text-white/30"># Definisikan saja di model</p>
96
+ <p class="text-indigo-300">class Post < Eksa::Model</p>
97
+ <p class="text-indigo-300 italic ml-4">def self.setup_schema</p>
98
+ <p class="text-indigo-300 italic ml-8">db.execute "CREATE TABLE IF NOT EXISTS ..."</p>
99
+ <p class="text-indigo-300 italic ml-4">end</p>
100
+ <p class="text-indigo-300">main</p>
131
101
  </div>
132
102
  </section>
133
103
 
@@ -135,55 +105,44 @@
135
105
  <h2 class="text-2xl font-bold mb-4 flex items-center gap-2">
136
106
  <i data-lucide="pen-tool" class="w-6 h-6 text-indigo-400"></i> Markdown Blog Engine
137
107
  </h2>
138
- <p class="text-white/60 mb-4 text-sm">Kelola konten blog dengan file Markdown. Mendukung Front Matter YAML dan rute dinamis.</p>
108
+ <p class="text-white/60 mb-4 text-sm">Kelola konten blog dengan file Markdown. Mendukung Front Matter YAML, Syntax Highlighting, dan SEO otomatis.</p>
139
109
  <div class="bg-black/40 rounded-2xl p-6 font-mono text-sm border border-white/10">
140
- <p class="text-white/30"># Buat post baru lewat CLI (dengan opsi meta)</p>
141
- <p class="text-indigo-300">eksa g post "Judul Postingan" --category "Tech" --author "Nama" --image "url.jpg"</p>
142
- <p class="text-white/30 mt-4"># Akses di URL (Otomatis)</p>
143
- <p class="text-white/70">/posts/:slug</p>
144
- </div>
145
- <div class="mt-4 p-4 bg-indigo-500/10 border border-indigo-500/20 rounded-xl flex items-center gap-3">
146
- <i data-lucide="zap" class="w-5 h-5 text-indigo-400"></i>
147
- <p class="text-xs text-indigo-200/80">Sudah terintegrasi dengan Prism.js untuk highlighting kode otomatis dan tombol copy!</p>
110
+ <p class="text-white/30"># Generate post baru</p>
111
+ <p class="text-indigo-300">eksa g post "Judul Postingan" --category "Tech" --image "url.jpg"</p>
148
112
  </div>
149
113
  </section>
150
114
 
151
115
  <section id="cms" class="mb-12 scroll-mt-24">
152
116
  <h2 class="text-2xl font-bold mb-4 flex items-center gap-2">
153
- <i data-lucide="layout-dashboard" class="w-6 h-6 text-indigo-400"></i> CMS Dashboard & Keamanan
117
+ <i data-lucide="layout-dashboard" class="w-6 h-6 text-indigo-400"></i> CMS Dashboard & Auth
154
118
  </h2>
155
- <p class="text-white/60 mb-4 text-sm">Eksa difasilitasi dengan area protektif yang dapat dilalui pendaftaran mandiri jika belum ada entitas pendaftar, kemudian dikunci menggunakan otentikasi Rack + enkripsi BCrypt.</p>
119
+ <p class="text-white/60 mb-4 text-sm">Gunakan panel admin terintegrasi untuk mengelola postingan, status publikasi, dan keamanan akun.</p>
156
120
  <div class="bg-black/40 rounded-2xl p-6 font-mono text-sm border border-white/10">
157
- <p class="text-white/30"># Menghidupkan Engine Utama dari folder project</p>
121
+ <p class="text-white/30"># Aktivasi fitur</p>
158
122
  <p class="text-indigo-300">eksa feature enable auth</p>
159
123
  <p class="text-indigo-300">eksa feature enable cms</p>
160
- <p class="text-white/30 mt-4"># Lupa sandi masuk halaman CMS?</p>
161
- <p class="text-indigo-300">eksa reset-password usernameanda katasandibaru</p>
162
- </div>
163
- <div class="mt-4 p-4 bg-emerald-500/10 border border-emerald-500/20 rounded-xl flex items-center gap-3">
164
- <i data-lucide="shield-check" class="w-5 h-5 text-emerald-400"></i>
165
- <p class="text-xs text-emerald-200/80">Jika integrasi Auth diaktifkan, masuklah ke URL <code class="text-white">/auth/register</code> saat pertama kali start. Setelah mendaftar The Vault (Dashboard) dapat diakses lewat <code class="text-white">/cms</code></p>
124
+ <p class="text-white/30 mt-4"># Akses Dashboard</p>
125
+ <p class="text-white/70">http://localhost:9292/cms</p>
166
126
  </div>
167
127
  </section>
168
128
 
169
- <section id="build" class="mb-12 scroll-mt-24">
129
+ <section id="seo" class="mb-12 scroll-mt-24">
170
130
  <h2 class="text-2xl font-bold mb-4 flex items-center gap-2">
171
- <i data-lucide="package-check" class="w-6 h-6 text-indigo-400"></i> Build & Publish
131
+ <i data-lucide="search" class="w-6 h-6 text-indigo-400"></i> SEO & JSON-LD (Structured Data)
172
132
  </h2>
173
- <div class="bg-black/40 rounded-2xl p-6 font-mono text-sm border border-white/10">
174
- <p class="text-white/30"># Build gem lokal</p>
175
- <p class="text-indigo-300">gem build eksa-framework.gemspec</p>
176
- <p class="text-white/30 mt-4"># Publish ke RubyGems / GitHub Packages</p>
177
- <p class="text-indigo-300">gem push eksa-framework-3.4.3.gem</p>
133
+ <p class="text-white/60 mb-4 text-sm">Eksa v3.5.0 secara otomatis menyematkan data terstruktur untuk ranking pencarian yang lebih baik.</p>
134
+ <div class="bg-white/5 p-4 rounded-xl border border-white/10 flex items-center gap-3">
135
+ <i data-lucide="check-circle-2" class="w-5 h-5 text-emerald-400"></i>
136
+ <p class="text-xs text-white/70">Mendukung <code class="text-indigo-300">sitemap.xml</code>, <code class="text-indigo-300">robots.txt</code>, dan skema <code class="text-indigo-300">BlogPosting</code> secara dinamis.</p>
178
137
  </div>
179
138
  </section>
180
139
 
181
140
  <div class="mt-12 pt-8 border-t border-white/10 text-center">
182
141
  <div class="flex justify-center gap-4 mb-4">
183
142
  <span class="bg-indigo-500/20 text-indigo-300 text-[10px] px-3 py-1 rounded-full border border-indigo-500/30 uppercase tracking-tighter">Rack 3.0 Compatible</span>
184
- <span class="bg-emerald-500/20 text-emerald-300 text-[10px] px-3 py-1 rounded-full border border-emerald-500/30 uppercase tracking-tighter">SQLite3 Auto-Ready</span>
143
+ <span class="bg-emerald-500/20 text-emerald-300 text-[10px] px-3 py-1 rounded-full border border-emerald-500/30 uppercase tracking-tighter">Production Ready</span>
185
144
  </div>
186
- <p class="text-white/40 text-sm italic">Eksa Framework v3.4.3 Alpha Documentation</p>
145
+ <p class="text-white/40 text-sm italic">Eksa Framework v3.5.0 Professional Documentation</p>
187
146
  <a href="/" class="inline-block mt-6 text-indigo-300 hover:text-white transition font-bold flex items-center justify-center gap-2">
188
147
  <i data-lucide="arrow-left" class="w-4 h-4"></i> Kembali ke Beranda
189
148
  </a>
@@ -10,7 +10,7 @@
10
10
  </div>
11
11
 
12
12
  <p class="text-white/60 mb-8">
13
- Lakukan perubahan pada konten atau pengirim. Perubahan akan langsung diperbarui di database SQLite.
13
+ Lakukan perubahan pada konten atau pengirim. Perubahan akan langsung diperbarui di database.
14
14
  </p>
15
15
 
16
16
  <form action="/edit?id=<%= @id %>" method="post" class="space-y-6">
@@ -2,7 +2,7 @@
2
2
  <div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
3
3
  <div>
4
4
  <span class="inline-block bg-indigo-500/20 text-indigo-200 text-xs font-bold px-3 py-1 rounded-full mb-3 border border-indigo-500/30">
5
- v3.4.3 Alpha
5
+ v3.5.0 Alpha
6
6
  </span>
7
7
  <h1 class="text-4xl font-extrabold tracking-tight">
8
8
  Halo, <span class="text-indigo-300"><%= @nama %></span>!
@@ -40,7 +40,7 @@
40
40
  class="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition placeholder-white/20 h-32 resize-none"></textarea>
41
41
  </div>
42
42
  <button type="submit" class="w-full bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-3 rounded-xl transition duration-300 flex items-center justify-center gap-2 shadow-lg shadow-indigo-900/20">
43
- <i data-lucide="send" class="w-4 h-4"></i> Simpan ke SQLite
43
+ <i data-lucide="send" class="w-4 h-4"></i> Simpan ke Database
44
44
  </button>
45
45
  </form>
46
46
  </div>
data/db/eksa_app.db ADDED
Binary file
data/db/setup.rb CHANGED
@@ -1,13 +1,13 @@
1
- require 'sqlite3'
1
+ require_relative '../lib/eksa'
2
2
 
3
- db = SQLite3::Database.new "eksa_app.db"
3
+ db = Eksa::Database.adapter
4
4
 
5
5
  db.execute <<-SQL
6
6
  CREATE TABLE IF NOT EXISTS pesan (
7
- id INTEGER PRIMARY KEY,
7
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
8
8
  konten TEXT,
9
9
  pengirim TEXT
10
10
  );
11
11
  SQL
12
12
 
13
- puts "Database Eksa berhasil disiapkan!"
13
+ puts "Database Eksa berhasil disiapkan (Engine: #{Eksa::Database.adapter.class})!"
data/exe/eksa CHANGED
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'fileutils'
5
+ require 'json'
5
6
 
6
7
  def usage
7
8
  puts "\n✨ Eksa Framework CLI ✨"
@@ -13,6 +14,8 @@ def usage
13
14
  puts " eksa g post TITLE - Post baru [args: --category, --author, --image]"
14
15
  puts " eksa feature ACTION F - Toggle fitur: action=enable/disable, F=cms/auth"
15
16
  puts " eksa reset-password USR PWD - Reset password akun admin"
17
+ puts " eksa db switch TYPE [URI] - Ganti DB: TYPE=sqlite/mongo"
18
+ puts " eksa db migrate --from TYPE --to TYPE - Pindahkan data antar DB"
16
19
  puts " eksa run - Jalankan server aplikasi"
17
20
  puts "-----------------------------\n"
18
21
  end
@@ -23,7 +26,7 @@ def init_project
23
26
 
24
27
  puts "🚀 Menginisialisasi project di: #{target_dir}"
25
28
 
26
- items_to_init = ['app', 'lib', 'public', 'db', 'spec', '_posts', 'config.ru', 'Gemfile', 'README.md']
29
+ items_to_init = ['app', 'lib', 'public', 'db', 'spec', '_posts', 'config.ru', 'Gemfile', 'README.md', '.env.example']
27
30
 
28
31
  items_to_init.each do |item|
29
32
  source = File.join(gem_root, item)
@@ -179,20 +182,13 @@ end
179
182
 
180
183
  def reset_password(username, password)
181
184
  require 'bcrypt'
182
- require 'sqlite3'
183
- db_path = File.join(Dir.pwd, 'db', 'eksa_app.db')
184
-
185
- unless File.exist?(db_path)
186
- puts "❌ Error: Database tidak ditemukan. Pastikan project sudah diinisialisasi."
187
- return
188
- end
189
-
190
- db = SQLite3::Database.new(db_path)
185
+ require_relative '../lib/eksa/database'
191
186
 
192
187
  begin
188
+ db = Eksa::Database.adapter
193
189
  user = db.execute("SELECT id FROM eksa_users WHERE username = ? LIMIT 1", [username]).first
194
- rescue SQLite3::SQLException
195
- puts "❌ Error: Tabel user tidak ditemukan. Apakah Anda sudah mengaktifkan auth?"
190
+ rescue => e
191
+ puts "❌ Error: Gagal mengakses database: #{e.message}"
196
192
  return
197
193
  end
198
194
 
@@ -205,6 +201,105 @@ def reset_password(username, password)
205
201
  end
206
202
  end
207
203
 
204
+ def switch_database(type, uri = nil)
205
+ valid_types = ['sqlite', 'mongo', 'mongodb']
206
+ unless valid_types.include?(type.downcase)
207
+ puts "❌ Error: Tipe database tidak dikenali. Pilih: sqlite atau mongo."
208
+ return
209
+ end
210
+
211
+ config_path = File.join(Dir.pwd, '.eksa.json')
212
+ begin
213
+ config = File.exist?(config_path) ? JSON.parse(File.read(config_path)) : {}
214
+ rescue
215
+ config = {}
216
+ end
217
+
218
+ config['database'] ||= {}
219
+ config['database']['type'] = type.downcase == 'mongo' ? 'mongodb' : 'sqlite'
220
+
221
+ if type.downcase == 'mongo' || type.downcase == 'mongodb'
222
+ if uri
223
+ config['database']['mongodb'] ||= {}
224
+ config['database']['mongodb']['uri'] = uri
225
+ elsif !ENV['EKSA_MONGODB_URI'] && !ENV['MONGODB_URI'] && !config.dig('database', 'mongodb', 'uri')
226
+ puts "⚠️ Peringatan: Anda memilih MongoDB tapi belum memberikan URI."
227
+ puts " Gunakan: export EKSA_MONGODB_URI=\"YOUR_URI\""
228
+ puts " Atau: eksa db switch mongo \"YOUR_URI\""
229
+ end
230
+ end
231
+
232
+ File.write(config_path, JSON.pretty_generate(config))
233
+ puts "⚙️ Database berhasil diubah ke: #{config['database']['type'].upcase}."
234
+ end
235
+
236
+ def migrate_database(from_type, to_type)
237
+ require_relative '../lib/eksa/database/sqlite_adapter'
238
+ require_relative '../lib/eksa/database/mongo_adapter'
239
+ require 'json'
240
+
241
+ config_path = File.join(Dir.pwd, '.eksa.json')
242
+ config = File.exist?(config_path) ? JSON.parse(File.read(config_path)) : {}
243
+
244
+ puts "🚀 Memulai migrasi data dari #{from_type.upcase} ke #{to_type.upcase}..."
245
+
246
+ source_adapter = create_specific_adapter(from_type, config)
247
+ target_adapter = create_specific_adapter(to_type, config)
248
+
249
+ # Migration logic for User model (the only one currently using Eksa::Model)
250
+ puts " 📦 Memindahkan tabel 'eksa_users'..."
251
+
252
+ begin
253
+ # Source data
254
+ users = source_adapter.execute("SELECT id, username, password_hash FROM eksa_users")
255
+
256
+ # Target setup
257
+ target_adapter.execute("CREATE TABLE IF NOT EXISTS eksa_users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, password_hash TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)")
258
+
259
+ count = 0
260
+ users.each do |user|
261
+ # user is [id, username, password_hash]
262
+ # Check if user already exists in target
263
+ existing = target_adapter.execute("SELECT id FROM eksa_users WHERE username = ?", [user[1]])
264
+
265
+ if existing.empty?
266
+ target_adapter.execute("INSERT INTO eksa_users (username, password_hash) VALUES (?, ?)", [user[1], user[2]])
267
+ count += 1
268
+ else
269
+ puts " ⚠️ User '#{user[1]}' sudah ada di target, melewati..."
270
+ end
271
+ end
272
+
273
+ puts " ✅ Berhasil memindahkan #{count} user."
274
+ puts "\n🎉 Migrasi Selesai!"
275
+ rescue => e
276
+ puts "❌ Terjadi kesalahan saat migrasi: #{e.message}"
277
+ end
278
+ end
279
+
280
+ def create_specific_adapter(type, config)
281
+ case type.downcase
282
+ when 'mongo', 'mongodb'
283
+ require_relative '../lib/eksa/database/mongo_adapter'
284
+ mongo_config = config.dig('database', 'mongodb') || {}
285
+
286
+ # Load .env for CLI
287
+ begin
288
+ require 'dotenv'
289
+ Dotenv.load if File.exist?('.env')
290
+ rescue LoadError
291
+ end
292
+
293
+ env_uri = ENV['EKSA_MONGODB_URI'] || ENV['MONGODB_URI']
294
+ mongo_config['uri'] = env_uri if env_uri
295
+
296
+ Eksa::MongoAdapter.new(mongo_config)
297
+ else
298
+ require_relative '../lib/eksa/database/sqlite_adapter'
299
+ Eksa::SqliteAdapter.new(config.dig('database', 'sqlite'))
300
+ end
301
+ end
302
+
208
303
  def start_server
209
304
  if File.exist?('config.ru')
210
305
  puts "🚀 Memulai Eksa Framework Server..."
@@ -259,6 +354,34 @@ when 'reset-password'
259
354
  else
260
355
  puts "❌ Error: Argumen tidak lengkap. Gunakan: eksa reset-password USERNAME NEW_PASSWORD"
261
356
  end
357
+ when 'db'
358
+ subcommand = ARGV.shift
359
+ case subcommand
360
+ when 'switch'
361
+ type = ARGV.shift
362
+ uri = ARGV.shift
363
+ if type
364
+ switch_database(type, uri)
365
+ else
366
+ puts "❌ Error: Tipe database harus diisi. Gunakan: eksa db switch [sqlite|mongo]"
367
+ end
368
+ when 'migrate'
369
+ from = nil
370
+ to = nil
371
+ while (arg = ARGV.shift)
372
+ case arg
373
+ when '--from' then from = ARGV.shift
374
+ when '--to' then to = ARGV.shift
375
+ end
376
+ end
377
+ if from && to
378
+ migrate_database(from, to)
379
+ else
380
+ puts "❌ Error: Argumen tidak lengkap. Gunakan: eksa db migrate --from TYPE --to TYPE"
381
+ end
382
+ else
383
+ usage
384
+ end
262
385
  else
263
386
  usage
264
387
  end
@@ -0,0 +1,154 @@
1
+ require 'mongo'
2
+
3
+ module Eksa
4
+ class MongoAdapter
5
+ def initialize(config)
6
+ @uri = config&.[]('uri')
7
+ @client = nil
8
+ end
9
+
10
+ def connection
11
+ return @client if @client
12
+ raise "MongoDB URI not configured in .eksa.json" if @uri.nil? || @uri.empty?
13
+
14
+ # Atlas URIs usually contain the database name, if not, we use 'eksa_db'
15
+ @client = Mongo::Client.new(@uri)
16
+ @client
17
+ end
18
+
19
+ def execute(sql, params = [])
20
+ # Minimal SQL-to-Mongo translator for Eksa models
21
+
22
+ # 1. CREATE TABLE IF NOT EXISTS [table]
23
+ if sql =~ /CREATE TABLE IF NOT EXISTS (\w+)/i
24
+ # Mongo creates collections on the fly
25
+ return []
26
+ end
27
+
28
+ # 2. SELECT * FROM [table] ...
29
+ if sql =~ /SELECT (.*) FROM (\w+)/i
30
+ table_name = $2
31
+ query = {}
32
+
33
+ # Handle WHERE clauses
34
+ if sql =~ /WHERE (.*) ORDER BY/i || sql =~ /WHERE (.*) LIMIT/i || sql =~ /WHERE (.*)$/i
35
+ where_clause = $1
36
+
37
+ # Handle OR + LIKE (specific for Search)
38
+ if where_clause =~ /(\w+) LIKE \? OR (\w+) LIKE \?/i
39
+ field1 = $1
40
+ field2 = $2
41
+ # Extract keyword from params (strip % if present)
42
+ term1 = params[0].to_s.gsub('%', '')
43
+ term2 = params[1].to_s.gsub('%', '')
44
+
45
+ query = {
46
+ '$or' => [
47
+ { field1.to_sym => /#{Regexp.escape(term1)}/i },
48
+ { field2.to_sym => /#{Regexp.escape(term2)}/i }
49
+ ]
50
+ }
51
+ # Handle single LIKE
52
+ elsif where_clause =~ /(\w+) LIKE \?/i
53
+ field = $1
54
+ term = params[0].to_s.gsub('%', '')
55
+ query = { field.to_sym => /#{Regexp.escape(term)}/i }
56
+ # Handle simple field = ?
57
+ elsif where_clause =~ /id = \?/i || where_clause =~ /_id = \?/i
58
+ val = params[0]
59
+ query = { _id: val.is_a?(String) ? BSON::ObjectId.from_string(val) : val }
60
+ elsif where_clause =~ /(\w+) = \?/i
61
+ query = { $1.to_sym => params[0] }
62
+ end
63
+ end
64
+
65
+ view = connection[table_name.to_sym].find(query)
66
+
67
+ # Handle ORDER BY
68
+ if sql =~ /ORDER BY (\w+)(?: (ASC|DESC))?/i
69
+ sort_field = $1
70
+ direction = ($2 || 'ASC').upcase == 'DESC' ? -1 : 1
71
+ # Mongo id maps to _id
72
+ sort_field = '_id' if sort_field == 'id'
73
+ view = view.sort({ sort_field => direction })
74
+ end
75
+
76
+ view = view.limit(1) if sql =~ /LIMIT 1/i
77
+ results = view.to_a
78
+
79
+ return results.map do |doc|
80
+ # Try to make it look like a flat array similar to SQLite row
81
+ # We don't know the exact schema, but we can return the values
82
+ # For Eksa::User we specifically need [id, username, password_hash]
83
+ if table_name == 'eksa_users'
84
+ [doc['_id'].to_s, doc['username'], doc['password_hash'], doc['created_at']]
85
+ else
86
+ # For other models, return a hash-like object if possible,
87
+ # or just the values in order. Since User specifically uses indices,
88
+ # and generic models might use indices or keys, this is tricky.
89
+ # Most Eksa models result in array-of-arrays from SQLite gem.
90
+ doc.values.map { |v| v.is_a?(BSON::ObjectId) ? v.to_s : v }
91
+ end
92
+ end
93
+ end
94
+
95
+ # 3. INSERT INTO [table] ([cols]) VALUES ([vals])
96
+ if sql =~ /INSERT INTO (\w+) \((.*)\) VALUES/i
97
+ table_name = $1
98
+ cols = $2.split(',').map(&:strip)
99
+ doc = {}
100
+ cols.each_with_index do |col, i|
101
+ doc[col.to_sym] = params[i]
102
+ end
103
+ doc[:created_at] ||= Time.now
104
+
105
+ connection[table_name.to_sym].insert_one(doc)
106
+ return []
107
+ end
108
+
109
+ # 4. UPDATE [table] SET [col1] = ?, [col2] = ? WHERE [col] = ?
110
+ if sql =~ /UPDATE (\w+) SET (.*) WHERE (\w+) = \?/i
111
+ table_name = $1
112
+ set_clause = $2
113
+ where_col = $3
114
+
115
+ # Parse set assignments (e.g., "col1 = ?, col2 = ?")
116
+ # We assume the order matches params[0...n-1]
117
+ cols = set_clause.split(',').map { |s| s.split('=').first.strip }
118
+
119
+ set_data = {}
120
+ cols.each_with_index do |col, i|
121
+ set_data[col.to_sym] = params[i]
122
+ end
123
+
124
+ # The last parameter is the WHERE value
125
+ where_val = params.last
126
+ selector = if where_col == 'id'
127
+ { _id: where_val.is_a?(String) ? BSON::ObjectId.from_string(where_val) : where_val }
128
+ else
129
+ { where_col.to_sym => where_val }
130
+ end
131
+
132
+ connection[table_name.to_sym].update_one(selector, { '$set' => set_data })
133
+ return []
134
+ end
135
+
136
+ # 5. DELETE FROM [table] WHERE (\w+) = ?
137
+ if sql =~ /DELETE FROM (\w+) WHERE (\w+) = \?/i
138
+ table_name = $1
139
+ where_col = $2
140
+
141
+ selector = if where_col == 'id'
142
+ { _id: params[0].is_a?(String) ? BSON::ObjectId.from_string(params[0]) : params[0] }
143
+ else
144
+ { where_col.to_sym => params[0] }
145
+ end
146
+
147
+ connection[table_name.to_sym].delete_one(selector)
148
+ return []
149
+ end
150
+
151
+ raise "Unsupported SQL query for MongoAdapter: #{sql}"
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,26 @@
1
+ require 'sqlite3'
2
+ require 'fileutils'
3
+
4
+ module Eksa
5
+ class SqliteAdapter
6
+ def initialize(config)
7
+ @path = config&.[]('path') || File.expand_path("../../../db/eksa_app.db", __FILE__)
8
+ ensure_db_dir
9
+ end
10
+
11
+ def connection
12
+ @connection ||= SQLite3::Database.new(@path)
13
+ end
14
+
15
+ def execute(sql, params = [])
16
+ connection.execute(sql, params)
17
+ end
18
+
19
+ private
20
+
21
+ def ensure_db_dir
22
+ db_dir = File.dirname(@path)
23
+ FileUtils.mkdir_p(db_dir) unless Dir.exist?(db_dir)
24
+ end
25
+ end
26
+ end