@motiontr/motiondb 1.0.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/README.md ADDED
@@ -0,0 +1,328 @@
1
+ # motiondb
2
+
3
+ Discord botları için **tek dosyalık, atomik ve hızlı** JSON veritabanı.
4
+ `discord.js` ile birlikte kullanılmak üzere tasarlandı, ama framework'ten bağımsızdır.
5
+
6
+ ```json
7
+ {
8
+ "guilds": {
9
+ "123456789012345678": { "prefix": "!", "welcome": { "channelId": "987654321098765432" } }
10
+ },
11
+ "users": {
12
+ "555555555555555555": { "coins": 250, "inventory": ["ticket", "vip"] }
13
+ },
14
+ "cooldowns": { "555555555555555555:daily": 1730000000000 }
15
+ }
16
+ ```
17
+
18
+ ---
19
+
20
+ ## Kurulum
21
+
22
+ ```bash
23
+ npm install
24
+ npm run build # TypeScript derleme -> dist/
25
+ npm test # 49 test (23 çekirdek + 26 API)
26
+ npm run example # examples/basic.js
27
+ npm run example:api # examples/api.js (MotionDB API turu)
28
+ ```
29
+
30
+ Kendi projenizde:
31
+
32
+ ```bash
33
+ npm install @motiontr/motiondb
34
+ ```
35
+
36
+ ```ts
37
+ import { MotionDB, JSONDatabase } from '@motiontr/motiondb';
38
+
39
+ const db = new JSONDatabase({ path: './data/db.json' });
40
+ ```
41
+
42
+ ---
43
+
44
+ ## MotionDB API (asenkron, tablo tabanlı)
45
+
46
+ İkinci katman: tek JSON dosyasında **tablolar**, `await`'li ve dot-notation
47
+ destekli arayüz. Discord botlarında kullanıcı/sunucu verisi için ideal:
48
+
49
+ ```ts
50
+ import { MotionDB } from '@motiontr/motiondb';
51
+
52
+ const db = new MotionDB({ filePath: './data.json' });
53
+ await db.init(); // dosyayı yükler/oluşturur
54
+
55
+ // ---- okuma / yazma ----
56
+ await db.set('myKey', 'myValue'); // → 'myValue'
57
+ await db.get('myKey'); // → 'myValue'
58
+ await db.get('olmayan'); // → null
59
+ await db.has('myKey'); // → true
60
+ await db.delete('myKey'); // → 1 (yoksa 0)
61
+ await db.ensure('ayarlar', { prefix: '!' }); // yoksa oluşturur
62
+
63
+ // ---- dot notation: iç içe nesneler ----
64
+ await db.set('myUser.balance', 700);
65
+ await db.get('myUser.balance'); // → 700
66
+ await db.get('myUser'); // → { balance: 700 }
67
+
68
+ // ---- sayılar ----
69
+ await db.add('myUser.balance', 100); // → 800
70
+ await db.sub('myUser.balance', 50); // → 750
71
+ await db.increment('sayac', 1); // → add kısayolu
72
+ await db.decrement('sayac', 1); // → sub kısayolu
73
+
74
+ // ---- diziler ----
75
+ await db.set('myUser.items', ['Sword', 'Shield']);
76
+ await db.push('myUser.items', 'Armor'); // → [..., 'Armor']
77
+ await db.unshift('myUser.items', 'Helm'); // → başa ekler
78
+ await db.pull('myUser.items', 'Shield'); // → siler
79
+ await db.pull('myUser.items', (i) => i.length < 5, true); // once: ilk eşleşen
80
+ await db.pop('myUser.items'); // → son eleman
81
+ await db.shift('myUser.items'); // → ilk eleman
82
+
83
+ // ---- sorgular ----
84
+ await db.all(); // → [{ id, value }, ...]
85
+ await db.startsWith('userInfo_'); // → [{ id, value }, ...]
86
+ await db.startsWith('x', 'bazıSatırListesi'); // → ikinci parametre anahtar
87
+ await db.count(); // → satır sayısı
88
+
89
+ // ---- tablolar (aynı dosyada ayrı bölüm) ----
90
+ const guilds = db.table('guilds');
91
+ await guilds.set('1552239063984246784.prefix', '!');
92
+ await db.deleteAll(); // → silinen satır sayısı
93
+ db.useNormalKeys(true); // nokta literal anahtar yapsın
94
+ ```
95
+
96
+ ### Seçenekler
97
+
98
+ ```ts
99
+ new MotionDB({
100
+ filePath: './data.json', // dosya yolu (varsayılan './data.json')
101
+ table: 'json', // kök bölüm adı (varsayılan 'json')
102
+ normalKeys: false, // true ise nokta ayracı değil, literal anahtar
103
+ deferWrites: false, // true ise diske yazmayı erteler → flush() gerekir
104
+ spaces: 2, // JSON girintisi
105
+ onCorrupt: 'throw', // 'throw' | 'reset'
106
+ });
107
+ ```
108
+
109
+ ### Metot özeti
110
+
111
+ | Kategori | Metot | Dönüş |
112
+ | --- | --- | --- |
113
+ | Okuma | `get(key)` | değer veya `null` |
114
+ | | `has(key)` | `boolean` |
115
+ | | `all()` | `{ id, value }[]` |
116
+ | | `startsWith(query, key?)` | `{ id, value }[]` |
117
+ | | `count()` | `number` |
118
+ | Yazma | `set(key, value)` | yazılan değer |
119
+ | | `ensure(key, varsayılan)` | değer |
120
+ | | `delete(key)` | `1` / `0` |
121
+ | | `deleteAll()` | silinen satır sayısı |
122
+ | Sayı | `add`, `sub`, `increment`, `decrement` | yeni sayı |
123
+ | Dizi | `push`, `unshift`, `pop`, `shift`, `pull` | dizi / eleman |
124
+ | Tablo | `table(name)` | yeni `MotionDB` |
125
+ | | `createSingleton(options)` | tek instance |
126
+ | | `useNormalKeys(bool)` | `void` |
127
+ | Kalıcılık | `init()`, `flush()`, `flushSync()` | — |
128
+ | Olay | `on('save' \| 'load' \| 'change' \| 'error', fn)` | `this` |
129
+
130
+ **Hata mesajları** (geçersiz kullanımda):
131
+
132
+ - `First argument (key) needs to be a string`
133
+ - `Missing second argument (value)` (`set(key, null)` gibi)
134
+ - `Current value with key: (x) is not an array` (`push` ile dizi olmayana)
135
+ - `Current value with key: (x) is not a number...` (`add` ile sayı olmayana)
136
+
137
+ **Dosya biçimi**
138
+
139
+ ```json
140
+ {
141
+ "json": { "myKey": "myValue", "myUser": { "balance": 750 } },
142
+ "guilds": { "1552239063984246784": { "prefix": "!" } }
143
+ }
144
+ ```
145
+
146
+ > Aynı dosyayı paylaşan tüm `MotionDB` instance'ları (`table()` dahil) tek
147
+ > motor kullanır; birbirinin yazdığını silmezler.
148
+ > `deferWrites: true` ile her işlemde diske yazmayı erteleyip toplu işlerde
149
+ > `await db.flush()` çağırabilirsiniz.
150
+
151
+ ---
152
+
153
+ ## Hızlı başlangıç
154
+
155
+ ```ts
156
+ const db = new JSONDatabase({ path: './data/db.json' });
157
+ db.registerShutdownHooks(); // kapanışta bekleyen yazışları diske atar
158
+
159
+ // oku / yaz (dot-path)
160
+ db.set('guilds.123456789012345678.prefix', '!');
161
+ db.get('guilds.123456789012345678.prefix'); // '!'
162
+ db.get('guilds.olmayan.prefix', '?'); // '?' (fallback)
163
+
164
+ // yoksa oluştur
165
+ db.ensure('users.555555555555555555', { coins: 0, inventory: [] });
166
+
167
+ // sayaç
168
+ db.increment('users.555555555555555555.coins', 250);
169
+
170
+ // dizi
171
+ db.push('guilds.123.roles.moderator', '111111111111111111');
172
+ db.pull('guilds.123.roles.moderator', '111111111111111111');
173
+
174
+ // sil / kontrol
175
+ db.has('users.555555555555555555'); // true
176
+ db.delete('users.555555555555555555.oldKey');
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Seçenekler
182
+
183
+ ```ts
184
+ new JSONDatabase({
185
+ path: './data/bot.json', // dosya yolu (varsayılan './db.json')
186
+ spaces: 2, // girinti sayısı (varsayılan 2)
187
+ debounce: 50, // değişiklikten sonra yazma gecikmesi ms (varsayılan 50)
188
+ autosave: true, // false ise yalnızca save()/flushSync() yazar
189
+ createIfMissing: true, // dosya yoksa oluştur
190
+ onCorrupt: 'throw', // 'throw' | 'reset' (bozuk dosyayı yedekleyip sıfırlar)
191
+ backupOnLoad: false, // ilk yüklemde otomatik yedek al
192
+ backupDir: './data/backups',
193
+ });
194
+ ```
195
+
196
+ > `debounce: 0` yaparsanız her değişiklikte anında yazar. Varsayılan `50` ms,
197
+ > bir komut içindeki 50 yazımı tek dosya işlemine indirger.
198
+
199
+ ---
200
+
201
+ ## API
202
+
203
+ ### Okuma
204
+
205
+ | Metod | Açıklama |
206
+ | --- | --- |
207
+ | `get(path, fallback?)` | Değeri okur, yoksa `fallback` döner |
208
+ | `has(path)` | Yol var mı? |
209
+ | `all()` | Kök nesnenin canlı referansı |
210
+ | `toJSON()` | Kök nesnenin kopyası |
211
+ | `keys()` | Kök kategoriler (`['guilds', 'users', ...]`) |
212
+ | `category(name, fallback?)` | Bir kategorinin tamamı (`db.category('guilds')`) |
213
+
214
+ ### Yazma
215
+
216
+ | Metod | Açıklama |
217
+ | --- | --- |
218
+ | `set(path, value)` | Değer yazar |
219
+ | `ensure(path, defaultValue)` | Yoksa oluşturur, varsa mevcut değeri döner |
220
+ | `delete(path)` | Anahtarı siler, silindiyse `true` |
221
+ | `push(path, ...values)` | Diziye ekler (dizi yoksa oluşturur) |
222
+ | `pull(path, value \| fn)` | Diziden eşleşeni çıkarır (derin karşılaştırma) |
223
+ | `increment(path, amount?)` | Sayısal değeri artırır, yeni değeri döner |
224
+ | `decrement(path, amount?)` | Sayısal değeri azaltır |
225
+ | `edit(fn, path?)` | Ham düzenleme — verinin tamamına erişip değiştirin |
226
+ | `clear()` | Tüm veriyi siler |
227
+
228
+ ### Path sözdizimi
229
+
230
+ ```ts
231
+ db.get('guilds.123.prefix'); // nokta ile
232
+ db.get('items[0].name'); // dizi indeksi
233
+ db.get('users["a.b"].coins'); // nokta içeren anahtar
234
+ db.get(['users', 'a.b', 'coins']); // segment dizisi (en hızlısı)
235
+ ```
236
+
237
+ > **Not:** Discord snowflake ID'leri `Number`'a çevrilmez (yok olurlardı),
238
+ > tüm segmentler string olarak tutulur.
239
+
240
+ ### Kalıcılık
241
+
242
+ | Metod | Açıklama |
243
+ | --- | --- |
244
+ | `save()` / `flush()` | Değişiklikleri atomik olarak yazar (Promise) |
245
+ | `flushSync()` | Anında senkron yazar — çıkış öncesi çağırın |
246
+ | `load(force?)` | Dosyayı okur (ilk çağrıda otomatik) |
247
+ | `reload()` | Bellek değişimlerini atıp dosyayı yeniden okur |
248
+ | `registerShutdownHooks()` | `SIGINT`/`SIGTERM`/`exit` → `flushSync()` |
249
+
250
+ ### Yedekleme
251
+
252
+ ```ts
253
+ const file = db.backup('before-migration'); // path döner
254
+ db.listBackups(); // yeniden eskiye tüm yedekler
255
+ ```
256
+
257
+ ### Olaylar
258
+
259
+ ```ts
260
+ db.on('change', (c) => console.log(c.type, c.path.join('.'))); // set | delete | clear | edit
261
+ db.on('save', (file) => console.log('yazıldı:', file));
262
+ db.on('load', (file) => console.log('yüklendi:', file));
263
+ db.on('corrupt', (backupPath) => console.log('bozuk dosya yedeklendi:', backupPath));
264
+ db.on('error', (err) => console.error(err)); // yazma hataları
265
+ ```
266
+
267
+ ---
268
+
269
+ ## discord.js örneği
270
+
271
+ ```ts
272
+ import { Client, GatewayIntentBits, Events } from 'discord.js';
273
+ import { JSONDatabase } from '@motiontr/motiondb';
274
+
275
+ const db = new JSONDatabase({ path: './data/db.json', backupOnLoad: true });
276
+ db.registerShutdownHooks();
277
+
278
+ const client = new Client({
279
+ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent],
280
+ });
281
+
282
+ client.on(Events.MessageCreate, (message) => {
283
+ if (message.author.bot || !message.guild) return;
284
+
285
+ const prefix = db.get(`guilds.${message.guild.id}.prefix`, '!');
286
+ if (!message.content.startsWith(prefix)) return;
287
+
288
+ const [cmd, ...args] = message.content.slice(prefix.length).trim().split(/\s+/);
289
+
290
+ if (cmd === 'prefix') {
291
+ db.set(`guilds.${message.guild.id}.prefix`, args[0]);
292
+ message.reply(`Ön ek ${args[0]} olarak ayarlandı.`);
293
+ }
294
+ });
295
+ ```
296
+
297
+ ---
298
+
299
+ ## Sınırlar ve iyi pratikler
300
+
301
+ - **Tek süreç içindir.** Aynı dosyayı 2+ process (ör. shard cluster) aynı anda
302
+ yazarsa son yazan kazanır. Sharding yapıyorsanız process başına ayrı dosya
303
+ (`data/shard-0.json`) veya harici bir DB (SQLite/Redis) kullanın.
304
+ - **Orta ölçek için uygundur:** birkaç MB'a kadar veri rahatça çalışır;
305
+ - tüm veri bellekte tutulur, okuma O(1)'dir, yazma ise atomiktir
306
+ (tmp dosya + `rename`, yarım kayıt olmaz).
307
+ - **Büyük veritabanlarda** (on binlerce sunucu + yüksek write I/O) JSON yerine
308
+ SQLite geçmeyi düşünün.
309
+ - Process `SIGKILL` ile öldürülürse son `debounce` penceresindeki değişiklikler
310
+ kaybolabilir; kritik işlemlerden sonra `await db.flush()` çağırın.
311
+
312
+ ---
313
+
314
+ ## Proje yapısı
315
+
316
+ ```
317
+ src/
318
+ index.ts dışa aktarımlar (MotionDB, JSONDatabase)
319
+ MotionDB.ts asenkron tablo API (get/set/push/pull/table/all/...)
320
+ Database.ts JSONDatabase çekirdeği (atomik yazma, yedek, olaylar)
321
+ path.ts dot-path çözümleyici ve okuma/yazma yardımcıları
322
+ examples/
323
+ basic.js çekirdek database özellikleri turu
324
+ api.js MotionDB API turu
325
+ tests/
326
+ db.test.js `npm test` — 23 test (çekirdek)
327
+ motiondb.test.js `npm test` — 26 test (API)
328
+ ```
@@ -0,0 +1,137 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { PathInput, PathSegment } from './path';
3
+ export type CorruptPolicy = 'throw' | 'reset';
4
+ export interface DatabaseOptions {
5
+ /** JSON dosyasının yolu. Varsayılan: `./db.json` */
6
+ path?: string;
7
+ /** Kaydedilirken kullanılacak girinti (boşluk) sayısı. Varsayılan: `2` */
8
+ spaces?: number;
9
+ /**
10
+ * Değişiklikten sonra dosyaya yazılana kadar beklenecek süre (ms).
11
+ * `0` ise her değişiklikte anında yazar. Varsayılan: `50`
12
+ */
13
+ debounce?: number;
14
+ /** `false` ise dosya yalnızca `save()` / `flushSync()` çağrısında yazılır. Varsayılan: `true` */
15
+ autosave?: boolean;
16
+ /** Dosya yoksa oluştur. Varsayılan: `true` */
17
+ createIfMissing?: boolean;
18
+ /** JSON bozulursa ne yapılsın: fırlat (`throw`) veya yedekleyip sıfırla (`reset`). Varsayılan: `throw` */
19
+ onCorrupt?: CorruptPolicy;
20
+ /** İlk yüklemeden önce otomatik yedek al. Varsayılan: `false` */
21
+ backupOnLoad?: boolean;
22
+ /** Yedek dosyalarının konacağı klasör. Varsayılan: dosyanın yanındaki `backups/` */
23
+ backupDir?: string;
24
+ }
25
+ export interface DatabaseChange {
26
+ type: 'set' | 'delete' | 'clear' | 'edit';
27
+ path: PathSegment[];
28
+ value?: unknown;
29
+ }
30
+ export interface DatabaseEvents {
31
+ change: [DatabaseChange];
32
+ save: [string];
33
+ load: [string];
34
+ corrupt: [string];
35
+ error: [Error];
36
+ }
37
+ /**
38
+ * Tek dosyalık, bellekte çalışan JSON veritabanı.
39
+ *
40
+ * Veri tek bir JSON dosyasında "kategoriler" halinde tutulur:
41
+ * `{ "guilds": {...}, "users": {...}, "cooldowns": {...} }`
42
+ *
43
+ * - Tüm değişiklikler bellekte yapılır (çok hızlı), dosyaya atomik yazılır.
44
+ * - Yazma işlemi tmp dosya + `rename` ile yapılır, bu sayede yarım kayıt riski yoktur.
45
+ */
46
+ export declare class JSONDatabase<T extends object = Record<string, unknown>> extends EventEmitter {
47
+ /** Veri dosyasının mutlak yolu */
48
+ readonly file: string;
49
+ /** Yedek dosyalarının klasörü */
50
+ readonly backupDir: string;
51
+ private readonly spaces;
52
+ private readonly debounceMs;
53
+ private readonly autosave;
54
+ private readonly createIfMissing;
55
+ private readonly corruptPolicy;
56
+ private readonly backupOnLoad;
57
+ private store;
58
+ private loaded;
59
+ private dirty;
60
+ private timer;
61
+ private chain;
62
+ constructor(options?: DatabaseOptions);
63
+ /** Dosyayı okur. İlk çağrıda otomatik olarak yapılır. */
64
+ load(force?: boolean): this;
65
+ /** Bellekteki değişiklikleri atıp dosyayı yeniden okur. */
66
+ reload(): this;
67
+ private handleCorrupt;
68
+ /** Değişiklikleri diske yazar (atomik). Yazma sırası korunur. */
69
+ save(): Promise<void>;
70
+ /** `save()` kısayolu. */
71
+ flush(): Promise<void>;
72
+ /** Değişiklikleri hemen senkron olarak diske yazar (ör. kapanış öncesi). */
73
+ flushSync(): this;
74
+ private write;
75
+ private writeSync;
76
+ private markDirty;
77
+ /** Yazmayı planlar: `debounce` süresi içindeki tüm değişiklikler tek dosyaya yazılır. */
78
+ private scheduleSave;
79
+ private cancelTimer;
80
+ private handleError;
81
+ /** Bir değeri okur. Bulunamazsa `fallback` döner. */
82
+ get<D = unknown>(key: PathInput, fallback?: D): D;
83
+ /** Bir değeri yazar ve diske kaydeder. */
84
+ set(key: PathInput, value: unknown): this;
85
+ /** Yol var mı? (undefined olmayan değer) */
86
+ has(key: PathInput): boolean;
87
+ /** Bir anahtarı siler. Silindiyse `true` döner. */
88
+ delete(key: PathInput): boolean;
89
+ /** Değer yoksa varsayılanı yazar ve onu döner (varsayılan nesneler için ideal). */
90
+ ensure<D>(key: PathInput, defaultValue: D): D;
91
+ /** Dizinin sonuna değer(ler) ekler. Dizi yoksa oluşturur. */
92
+ push<D>(key: PathInput, ...values: D[]): D[];
93
+ /** Diziden eşleşen değeri(leri) çıkarır. Fonksiyon verilirse predicate olarak kullanılır. */
94
+ pull(key: PathInput, matcher: unknown | ((value: unknown) => boolean)): unknown[];
95
+ /** Sayısal değeri artırır/sıfırlar. Yeni değeri döner. */
96
+ increment(key: PathInput, amount?: number): number;
97
+ /** Sayaç azaltma kısayolu. */
98
+ decrement(key: PathInput, amount?: number): number;
99
+ /** Kök nesnenin tamamını döner (canlı referans). */
100
+ all(): T;
101
+ /** Kök nesnenin kopyasını döner. */
102
+ toJSON(): T;
103
+ /** Kök anahtarların listesi (kategoriler). */
104
+ keys(): string[];
105
+ /** Bir kategorinin (ör. `guilds`) tamamını döner. */
106
+ category<D = Record<string, unknown>>(name: string, fallback?: D): D;
107
+ /** Tüm veriyi temizler (dosyayı boşaltır). */
108
+ clear(): this;
109
+ /**
110
+ * Ham düzenleme: verinin tamamına erişip doğrudan değiştirin.
111
+ * `path` verilmezse tüm kök değişim olarak işaretlenir.
112
+ *
113
+ * ```ts
114
+ * db.edit((data) => {
115
+ * data.guilds ??= {};
116
+ * data.guilds[id].prefix = '!';
117
+ * });
118
+ * ```
119
+ */
120
+ edit<D = T>(mutator: (data: D) => void, key?: PathInput): this;
121
+ /**
122
+ * Mevcut dosyanın kopyasını alır ve yedek yolunu döner.
123
+ * @param label Dosya adına eklenecek etiket (ör. `"before-migration"`).
124
+ */
125
+ backup(label?: string): string;
126
+ /** Var olan tüm yedek dosyalarını (yeniden eskiye) listeler. */
127
+ listBackups(): string[];
128
+ /**
129
+ * `SIGINT` / `SIGTERM` / `exit` anlarında bekleyen değişiklikleri diske yazar.
130
+ * Botunuzda bir kez çağırın.
131
+ */
132
+ registerShutdownHooks(): this;
133
+ on<K extends keyof DatabaseEvents>(event: K, listener: (...args: DatabaseEvents[K]) => void): this;
134
+ on(event: string | symbol, listener: (...args: any[]) => void): this;
135
+ emit<K extends keyof DatabaseEvents>(event: K, ...args: DatabaseEvents[K]): boolean;
136
+ emit(event: string | symbol, ...args: any[]): boolean;
137
+ }