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.
- checksums.yaml +4 -4
- data/.eksa.json +10 -1
- data/.env.example +8 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +10 -0
- data/README.md +48 -64
- data/_posts/2026-03-15-welcome-to-eksa-framework.md +57 -73
- data/app/views/about.html.erb +3 -3
- data/app/views/docs.html.erb +51 -92
- data/app/views/edit.html.erb +1 -1
- data/app/views/index.html.erb +2 -2
- data/db/eksa_app.db +0 -0
- data/db/setup.rb +4 -4
- data/exe/eksa +135 -12
- data/lib/eksa/database/mongo_adapter.rb +154 -0
- data/lib/eksa/database/sqlite_adapter.rb +26 -0
- data/lib/eksa/database.rb +47 -0
- data/lib/eksa/model.rb +2 -16
- data/lib/eksa/version.rb +1 -1
- data/lib/eksa.rb +1 -2
- metadata +36 -3
data/app/views/docs.html.erb
CHANGED
|
@@ -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="#
|
|
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.
|
|
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="
|
|
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="
|
|
61
|
+
<i data-lucide="shield-check" class="w-6 h-6 text-indigo-400"></i> Setup Keamanan (.env)
|
|
65
62
|
</h2>
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
</
|
|
71
|
-
<
|
|
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>
|
|
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">
|
|
111
|
-
<div class="bg-black/40 rounded-2xl p-6 font-mono text-sm border border-white/10">
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
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="
|
|
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="
|
|
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">
|
|
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">#
|
|
126
|
-
<p class="text-indigo-300">
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
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">#
|
|
141
|
-
<p class="text-indigo-300">eksa g post "Judul Postingan" --category "Tech" --
|
|
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 &
|
|
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">
|
|
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">#
|
|
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">#
|
|
161
|
-
<p class="text-
|
|
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="
|
|
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="
|
|
131
|
+
<i data-lucide="search" class="w-6 h-6 text-indigo-400"></i> SEO & JSON-LD (Structured Data)
|
|
172
132
|
</h2>
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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">
|
|
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.
|
|
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>
|
data/app/views/edit.html.erb
CHANGED
|
@@ -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
|
|
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">
|
data/app/views/index.html.erb
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
1
|
+
require_relative '../lib/eksa'
|
|
2
2
|
|
|
3
|
-
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
|
-
|
|
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
|
|
195
|
-
puts "❌ Error:
|
|
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
|