@ozgurv/tg-db 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,532 @@
1
+ # Telegram Chat Database (tg-db)
2
+
3
+ A comprehensive npm package that lets you use Telegram chat as a database. Stores and queries data in chat messages using the Telegram Bot API.
4
+
5
+ ## Features
6
+
7
+ - **CRUD Operations**: Create, Read, Update, Delete
8
+ - **Table/Collection System**: Database-like table structure (similar to SQL tables or MongoDB collections)
9
+ - **Fluent API**: Chainable usage like `db.table('users').find()`
10
+ - **Advanced Queries**: Filter, find, findOne and similar query methods
11
+ - **Batch Operations**: Bulk document insert and update
12
+ - **Query Operators**: $gt, $gte, $lt, $lte, $ne, $in, $nin, $regex support
13
+ - **Automatic Index**: Tracks message IDs automatically
14
+ - **Cache System**: In-memory cache for performance
15
+ - **TypeScript Support**: Full type safety
16
+ - **Error Handling**: Retry mechanism and error handling
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @ozgurv/tg-db
22
+ ```
23
+
24
+ ## Requirements
25
+
26
+ 1. **Telegram Bot Token**: Create a bot via [@BotFather](https://t.me/botfather) and get a token
27
+ 2. **Chat ID**: The chat where data will be stored (add your bot to that chat)
28
+
29
+ ### How to Find Chat ID
30
+
31
+ - **Private Chat**: Start a private chat with your bot; chat ID is usually a positive number
32
+ - **Group/Channel**: Add the bot to the group/channel; chat ID is usually negative (e.g. `-1001234567890`)
33
+
34
+ ## Quick Start
35
+
36
+ ```typescript
37
+ import { TelegramDB } from '@ozgurv/tg-db';
38
+
39
+ // Initialize database
40
+ const db = new TelegramDB({
41
+ botToken: 'YOUR_BOT_TOKEN',
42
+ chatId: 'YOUR_CHAT_ID', // or number: 123456789
43
+ });
44
+
45
+ // Connect
46
+ await db.initialize();
47
+
48
+ // Insert document (table is required)
49
+ const result = await db.insert({
50
+ name: 'John',
51
+ age: 30,
52
+ city: 'New York'
53
+ }, 'users'); // add to 'users' table
54
+
55
+ console.log(result.data); // inserted document
56
+
57
+ // Find documents (table is required)
58
+ const users = await db.find({ city: 'New York' }, 'users');
59
+ console.log(users);
60
+
61
+ // Or use Fluent API (recommended)
62
+ const usersTable = db.table('users');
63
+ const users2 = await usersTable.find({ city: 'New York' });
64
+
65
+ // Update document
66
+ await db.update(
67
+ { name: 'John' },
68
+ { age: 31 },
69
+ 'users'
70
+ );
71
+
72
+ // Delete document
73
+ await db.delete({ name: 'John' }, 'users');
74
+ ```
75
+
76
+ ## API Documentation
77
+
78
+ ### `TelegramDB(config)`
79
+
80
+ Creates the database instance.
81
+
82
+ **Parameters:**
83
+
84
+ - `botToken` (string, required): Telegram bot token
85
+ - `chatId` (string | number, required): Chat ID where data will be stored
86
+ - `messagePrefix` (string, optional): Message prefix (default: "TDB:")
87
+ - `batchDelay` (number, optional): Delay between batch operations in ms (default: 100)
88
+ - `maxRetries` (number, optional): Maximum retry count (default: 3)
89
+
90
+ ### Methods
91
+
92
+ #### `initialize(): Promise<void>`
93
+
94
+ Establishes the database connection. Must be called before use.
95
+
96
+ ```typescript
97
+ await db.initialize();
98
+ ```
99
+
100
+ #### `insert(doc: Partial<Document>, table: string): Promise<OperationResult>`
101
+
102
+ Inserts a new document. **Every document must belong to a table.**
103
+
104
+ ```typescript
105
+ const result = await db.insert({
106
+ name: 'John',
107
+ email: 'john@example.com'
108
+ }, 'users'); // add to 'users' table
109
+
110
+ if (result.success) {
111
+ console.log('Document inserted:', result.data);
112
+ }
113
+
114
+ // Or with Fluent API:
115
+ const users = db.table('users');
116
+ await users.insert({ name: 'John', email: 'john@example.com' });
117
+ ```
118
+
119
+ #### `insertMany(docs: Partial<Document>[], table: string, options?): Promise<OperationResult[]>`
120
+
121
+ Inserts multiple documents.
122
+
123
+ ```typescript
124
+ const results = await db.insertMany([
125
+ { name: 'Alice', age: 25 },
126
+ { name: 'Bob', age: 28 },
127
+ { name: 'Carol', age: 30 }
128
+ ], 'users', {
129
+ delay: 200, // wait 200ms between each insert
130
+ stopOnError: false // continue even on error
131
+ });
132
+
133
+ // Or with Fluent API:
134
+ const users = db.table('users');
135
+ await users.insertMany([...], { delay: 200 });
136
+ ```
137
+
138
+ #### `find(filter?: QueryFilter, table: string): Promise<Document[]>`
139
+
140
+ Finds all documents matching the filter. **Table is required.**
141
+
142
+ ```typescript
143
+ // All documents (from a specific table)
144
+ const allDocs = await db.find({}, 'users');
145
+
146
+ // Simple filter
147
+ const users = await db.find({ city: 'London' }, 'users');
148
+
149
+ // Advanced filters
150
+ const adults = await db.find({
151
+ age: { $gte: 18 }
152
+ }, 'users');
153
+
154
+ // Or with Fluent API (recommended):
155
+ const users = db.table('users');
156
+ const allUsers = await users.find();
157
+ const londonUsers = await users.find({ city: 'London' });
158
+ ```
159
+
160
+ #### `findOne(filter?: QueryFilter, table: string): Promise<Document | null>`
161
+
162
+ Finds the first document matching the filter. **Table is required.**
163
+
164
+ ```typescript
165
+ const user = await db.findOne({ name: 'John' }, 'users');
166
+
167
+ // Or with Fluent API:
168
+ const users = db.table('users');
169
+ const user = await users.findOne({ name: 'John' });
170
+ ```
171
+
172
+ #### `findById(id: string, table: string): Promise<Document | null>`
173
+
174
+ Finds a document by ID. **Table is required.**
175
+
176
+ ```typescript
177
+ const doc = await db.findById('1234567890-abc123', 'users');
178
+
179
+ // Or with Fluent API:
180
+ const users = db.table('users');
181
+ const doc = await users.findById('1234567890-abc123');
182
+ ```
183
+
184
+ #### `update(filter: QueryFilter, update: Partial<Document>, table: string, options?): Promise<OperationResult>`
185
+
186
+ Updates documents. **Table is required.**
187
+
188
+ ```typescript
189
+ // Simple update
190
+ await db.update(
191
+ { name: 'John' },
192
+ { age: 32 },
193
+ 'users'
194
+ );
195
+
196
+ // Upsert (create if not exists)
197
+ await db.update(
198
+ { name: 'New User' },
199
+ { age: 25, city: 'Paris' },
200
+ 'users',
201
+ { upsert: true }
202
+ );
203
+
204
+ // Replace entire document
205
+ await db.update(
206
+ { _id: '123' },
207
+ { name: 'New Name', age: 30 },
208
+ 'users',
209
+ { replace: true }
210
+ );
211
+
212
+ // Or with Fluent API:
213
+ const users = db.table('users');
214
+ await users.update({ name: 'John' }, { age: 32 });
215
+ ```
216
+
217
+ #### `updateById(id: string, update: Partial<Document>, table: string, options?): Promise<OperationResult>`
218
+
219
+ Updates a document by ID. **Table is required.**
220
+
221
+ ```typescript
222
+ await db.updateById('1234567890-abc123', { age: 33 }, 'users');
223
+
224
+ // Or with Fluent API:
225
+ const users = db.table('users');
226
+ await users.updateById('1234567890-abc123', { age: 33 });
227
+ ```
228
+
229
+ #### `delete(filter: QueryFilter, table: string): Promise<OperationResult>`
230
+
231
+ Deletes documents. **Table is required.**
232
+
233
+ ```typescript
234
+ // Delete by filter
235
+ await db.delete({ city: 'London' }, 'users');
236
+
237
+ // Delete all documents (from a specific table)
238
+ await db.deleteAll('users');
239
+
240
+ // Or with Fluent API:
241
+ const users = db.table('users');
242
+ await users.delete({ city: 'London' });
243
+ await users.deleteAll();
244
+ ```
245
+
246
+ #### `deleteById(id: string, table: string): Promise<OperationResult>`
247
+
248
+ Deletes a document by ID. **Table is required.**
249
+
250
+ ```typescript
251
+ await db.deleteById('1234567890-abc123', 'users');
252
+
253
+ // Or with Fluent API:
254
+ const users = db.table('users');
255
+ await users.deleteById('1234567890-abc123');
256
+ ```
257
+
258
+ #### `count(filter?: QueryFilter, table: string): Promise<number>`
259
+
260
+ Returns document count. **Table is required.**
261
+
262
+ ```typescript
263
+ const total = await db.count({}, 'users');
264
+ const londonUsers = await db.count({ city: 'London' }, 'users');
265
+
266
+ // Or with Fluent API:
267
+ const users = db.table('users');
268
+ const total = await users.count();
269
+ const londonUsers = await users.count({ city: 'London' });
270
+ ```
271
+
272
+ #### `getTables(): Promise<string[]>`
273
+
274
+ Returns all table names.
275
+
276
+ ```typescript
277
+ const tables = await db.getTables();
278
+ console.log('Tables:', tables); // ['users', 'products', 'orders']
279
+ ```
280
+
281
+ #### `dropTable(table: string): Promise<OperationResult>`
282
+
283
+ Drops a table completely.
284
+
285
+ ```typescript
286
+ await db.dropTable('old_table');
287
+ ```
288
+
289
+ #### `table(tableName: string): TableHandler`
290
+
291
+ Returns a table handler for Fluent API. **Recommended usage.**
292
+
293
+ ```typescript
294
+ const users = db.table('users');
295
+ await users.insert({ name: 'John' });
296
+ const user = await users.findOne({ name: 'John' });
297
+ await users.update({ name: 'John' }, { age: 30 });
298
+ await users.delete({ name: 'John' });
299
+ ```
300
+
301
+ #### `getStats(table?: string): Promise<DatabaseStats>`
302
+
303
+ Returns database statistics. If table is specified, returns stats for that table only.
304
+
305
+ ```typescript
306
+ // All database statistics
307
+ const stats = await db.getStats();
308
+ console.log(`Total documents: ${stats.totalDocuments}`);
309
+
310
+ // Specific table statistics
311
+ const userStats = await db.getStats('users');
312
+ console.log(`Users table: ${userStats.totalDocuments} documents`);
313
+ ```
314
+
315
+ #### `clear(): Promise<OperationResult>`
316
+
317
+ Clears all data.
318
+
319
+ ```typescript
320
+ await db.clear();
321
+ ```
322
+
323
+ #### `close(): Promise<void>`
324
+
325
+ Closes the database connection.
326
+
327
+ ```typescript
328
+ await db.close();
329
+ ```
330
+
331
+ ## Query Operators
332
+
333
+ Use these operators for advanced queries:
334
+
335
+ ```typescript
336
+ const users = db.table('users');
337
+
338
+ // Greater than
339
+ await users.find({ age: { $gt: 18 } });
340
+
341
+ // Greater than or equal
342
+ await users.find({ age: { $gte: 18 } });
343
+
344
+ // Less than
345
+ await users.find({ age: { $lt: 65 } });
346
+
347
+ // Less than or equal
348
+ await users.find({ age: { $lte: 65 } });
349
+
350
+ // Not equal
351
+ await users.find({ status: { $ne: 'deleted' } });
352
+
353
+ // In
354
+ await users.find({ city: { $in: ['London', 'Paris', 'Berlin'] } });
355
+
356
+ // Not in
357
+ await users.find({ city: { $nin: ['Paris'] } });
358
+
359
+ // Regex
360
+ await users.find({ email: { $regex: '@gmail\\.com$' } });
361
+
362
+ // Exists
363
+ await users.find({ phone: { $exists: true } });
364
+ ```
365
+
366
+ ## Nested Fields
367
+
368
+ Query nested fields using dot notation:
369
+
370
+ ```typescript
371
+ const result = await db.insert({
372
+ name: 'John',
373
+ address: {
374
+ city: 'London',
375
+ district: 'Westminster'
376
+ }
377
+ }, 'users');
378
+
379
+ const docs = await db.find({ 'address.city': 'London' }, 'users');
380
+ ```
381
+
382
+ ## Table/Collection System
383
+
384
+ Every document must belong to a table. This lets you organize data and use it like a real database.
385
+
386
+ ### Two Usage Styles
387
+
388
+ **1. Direct methods (table specified in each call):**
389
+
390
+ ```typescript
391
+ await db.insert({ name: 'John' }, 'users');
392
+ await db.find({}, 'users');
393
+ await db.update({ name: 'John' }, { age: 30 }, 'users');
394
+ await db.delete({ name: 'John' }, 'users');
395
+ ```
396
+
397
+ **2. Fluent API (recommended - cleaner code):**
398
+
399
+ ```typescript
400
+ const users = db.table('users');
401
+ await users.insert({ name: 'John' });
402
+ await users.find({});
403
+ await users.update({ name: 'John' }, { age: 30 });
404
+ await users.delete({ name: 'John' });
405
+ ```
406
+
407
+ ### Table Operations
408
+
409
+ ```typescript
410
+ // List all tables
411
+ const tables = await db.getTables();
412
+ console.log(tables); // ['users', 'products', 'orders']
413
+
414
+ // Drop a table
415
+ await db.dropTable('old_table');
416
+
417
+ // Table statistics
418
+ const stats = await db.getStats('users');
419
+ ```
420
+
421
+ ## Examples
422
+
423
+ ### Todo List Application
424
+
425
+ ```typescript
426
+ import { TelegramDB } from '@ozgurv/tg-db';
427
+
428
+ const db = new TelegramDB({
429
+ botToken: process.env.BOT_TOKEN!,
430
+ chatId: process.env.CHAT_ID!
431
+ });
432
+
433
+ await db.initialize();
434
+
435
+ // Fluent API (recommended)
436
+ const todos = db.table('todos');
437
+
438
+ // Add todo
439
+ await todos.insert({
440
+ task: 'Buy groceries',
441
+ completed: false,
442
+ createdAt: new Date().toISOString()
443
+ });
444
+
445
+ // Get incomplete todos
446
+ const incompleteTodos = await todos.find({ completed: false });
447
+
448
+ // Mark todo as completed
449
+ await todos.update(
450
+ { task: 'Buy groceries' },
451
+ { completed: true }
452
+ );
453
+ ```
454
+
455
+ ### User Management
456
+
457
+ ```typescript
458
+ // Fluent API (recommended)
459
+ const users = db.table('users');
460
+
461
+ // Create user
462
+ await users.insert({
463
+ username: 'johndoe',
464
+ email: 'john@example.com',
465
+ role: 'user',
466
+ createdAt: Date.now()
467
+ });
468
+
469
+ // Find admin users
470
+ const admins = await users.find({ role: 'admin' });
471
+
472
+ // Find user by email
473
+ const user = await users.findOne({ email: 'john@example.com' });
474
+
475
+ // Update user
476
+ await users.update(
477
+ { username: 'johndoe' },
478
+ { role: 'admin' }
479
+ );
480
+ ```
481
+
482
+ ### Multiple Tables
483
+
484
+ ```typescript
485
+ const users = db.table('users');
486
+ const products = db.table('products');
487
+ const orders = db.table('orders');
488
+
489
+ // Add to users table
490
+ await users.insert({ name: 'John', email: 'john@example.com' });
491
+
492
+ // Add to products table
493
+ await products.insert({ name: 'Laptop', price: 1000, stock: 5 });
494
+
495
+ // Add to orders table
496
+ await orders.insert({
497
+ userId: '123',
498
+ productId: '456',
499
+ quantity: 2,
500
+ total: 2000
501
+ });
502
+
503
+ // Each table works independently
504
+ const allUsers = await users.find();
505
+ const allProducts = await products.find();
506
+ const allOrders = await orders.find();
507
+ ```
508
+
509
+ ## Limitations
510
+
511
+ 1. **Telegram API Limits**: The Telegram API has rate limits. Use batch delay for heavy operations.
512
+ 2. **Message Size**: Telegram messages are limited to 4096 characters. Large documents may need to be split.
513
+ 3. **Message History**: The Telegram Bot API cannot fetch old messages directly. A message index is used instead.
514
+ 4. **Bot Permissions**: The bot needs permission to delete messages for delete operations.
515
+
516
+ ## Security
517
+
518
+ - Never share your bot token
519
+ - Keep your chat ID private
520
+ - Do not store sensitive data without encryption
521
+ - Use environment variables:
522
+
523
+ ```typescript
524
+ const db = new TelegramDB({
525
+ botToken: process.env.BOT_TOKEN!,
526
+ chatId: process.env.CHAT_ID!
527
+ });
528
+ ```
529
+
530
+ ## License
531
+
532
+ MIT License
@@ -0,0 +1,20 @@
1
+ import { TelegramDB } from './TelegramDB';
2
+ import { Document, QueryFilter, UpdateOptions, OperationResult, BatchOptions } from './types';
3
+ /** Table/collection handler for fluent API usage */
4
+ export declare class TableHandler {
5
+ private db;
6
+ private tableName;
7
+ constructor(db: TelegramDB, tableName: string);
8
+ insert(doc: Partial<Document>): Promise<OperationResult>;
9
+ insertMany(docs: Partial<Document>[], options?: BatchOptions): Promise<OperationResult[]>;
10
+ find(filter?: QueryFilter): Promise<Document[]>;
11
+ findOne(filter?: QueryFilter): Promise<Document | null>;
12
+ findById(id: string): Promise<Document | null>;
13
+ update(filter: QueryFilter, update: Partial<Document>, options?: UpdateOptions): Promise<OperationResult>;
14
+ updateById(id: string, update: Partial<Document>, options?: UpdateOptions): Promise<OperationResult>;
15
+ delete(filter: QueryFilter): Promise<OperationResult>;
16
+ deleteById(id: string): Promise<OperationResult>;
17
+ deleteAll(): Promise<OperationResult>;
18
+ count(filter?: QueryFilter): Promise<number>;
19
+ getTableName(): string;
20
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TableHandler = void 0;
4
+ /** Table/collection handler for fluent API usage */
5
+ class TableHandler {
6
+ constructor(db, tableName) {
7
+ this.db = db;
8
+ this.tableName = tableName;
9
+ }
10
+ async insert(doc) {
11
+ return this.db.insert(doc, this.tableName);
12
+ }
13
+ async insertMany(docs, options) {
14
+ return this.db.insertMany(docs, this.tableName, options);
15
+ }
16
+ async find(filter = {}) {
17
+ return this.db.find(filter, this.tableName);
18
+ }
19
+ async findOne(filter = {}) {
20
+ return this.db.findOne(filter, this.tableName);
21
+ }
22
+ async findById(id) {
23
+ return this.db.findById(id, this.tableName);
24
+ }
25
+ async update(filter, update, options) {
26
+ return this.db.update(filter, update, this.tableName, options);
27
+ }
28
+ async updateById(id, update, options) {
29
+ return this.db.updateById(id, update, this.tableName, options);
30
+ }
31
+ async delete(filter) {
32
+ return this.db.delete(filter, this.tableName);
33
+ }
34
+ async deleteById(id) {
35
+ return this.db.deleteById(id, this.tableName);
36
+ }
37
+ async deleteAll() {
38
+ return this.db.deleteAll(this.tableName);
39
+ }
40
+ async count(filter = {}) {
41
+ return this.db.count(filter, this.tableName);
42
+ }
43
+ getTableName() {
44
+ return this.tableName;
45
+ }
46
+ }
47
+ exports.TableHandler = TableHandler;
@@ -0,0 +1,61 @@
1
+ import { TelegramDBConfig, Document, QueryFilter, UpdateOptions, OperationResult, BatchOptions, DatabaseStats } from './types';
2
+ import { TableHandler } from './TableHandler';
3
+ /** Uses Telegram chat messages to store and retrieve data */
4
+ export declare class TelegramDB {
5
+ private bot;
6
+ private chatId;
7
+ private prefix;
8
+ private batchDelay;
9
+ private maxRetries;
10
+ private initialized;
11
+ private messageIndex;
12
+ private documentCache;
13
+ private indexMessageId;
14
+ constructor(config: TelegramDBConfig);
15
+ /** Initialize database connection. Must be called before use. */
16
+ initialize(): Promise<void>;
17
+ /** Insert a new document into a table */
18
+ insert(doc: Partial<Document>, table: string): Promise<OperationResult>;
19
+ /** Insert multiple documents into a table */
20
+ insertMany(docs: Partial<Document>[], table: string, options?: BatchOptions): Promise<OperationResult[]>;
21
+ /** Find documents matching the filter in a table */
22
+ find(filter: QueryFilter | undefined, table: string): Promise<Document[]>;
23
+ /** Find a single document matching the filter in a table */
24
+ findOne(filter: QueryFilter | undefined, table: string): Promise<Document | null>;
25
+ /** Find document by ID in a table */
26
+ findById(id: string, table: string): Promise<Document | null>;
27
+ /** Update documents matching the filter in a table */
28
+ update(filter: QueryFilter, update: Partial<Document>, table: string, options?: UpdateOptions): Promise<OperationResult>;
29
+ /** Update a single document by ID in a table */
30
+ updateById(id: string, update: Partial<Document>, table: string, options?: UpdateOptions): Promise<OperationResult>;
31
+ /** Delete documents matching the filter in a table */
32
+ delete(filter: QueryFilter, table: string): Promise<OperationResult>;
33
+ /** Delete a single document by ID in a table */
34
+ deleteById(id: string, table: string): Promise<OperationResult>;
35
+ /** Delete all documents in a table */
36
+ deleteAll(table: string): Promise<OperationResult>;
37
+ /** Count documents matching the filter in a table */
38
+ count(filter: QueryFilter | undefined, table: string): Promise<number>;
39
+ /** Get all table names in the database */
40
+ getTables(): Promise<string[]>;
41
+ /** Tabloyu tamamen siler */
42
+ dropTable(table: string): Promise<OperationResult>;
43
+ /** Belirli tablo veya tüm tablolar için istatistik döner */
44
+ getStats(table?: string): Promise<DatabaseStats>;
45
+ /** Clear all data from the database (all tables) */
46
+ clear(): Promise<OperationResult>;
47
+ /** Get a collection/table handler for fluent API usage */
48
+ table(tableName: string): TableHandler;
49
+ private getAllMessages;
50
+ private reloadCacheFromIndex;
51
+ private loadMessageIndex;
52
+ private saveMessageIndex;
53
+ private setupMessageListener;
54
+ /** Ensure database is initialized */
55
+ private ensureInitialized;
56
+ /** Retry operation with exponential backoff */
57
+ private retryOperation;
58
+ private sleep;
59
+ /** Close database connection */
60
+ close(): Promise<void>;
61
+ }
@@ -0,0 +1,411 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TelegramDB = void 0;
4
+ const telegraf_1 = require("telegraf");
5
+ const utils_1 = require("./utils");
6
+ const TableHandler_1 = require("./TableHandler");
7
+ /** Uses Telegram chat messages to store and retrieve data */
8
+ class TelegramDB {
9
+ constructor(config) {
10
+ this.initialized = false;
11
+ this.messageIndex = new Map();
12
+ this.documentCache = new Map();
13
+ this.indexMessageId = null;
14
+ this.bot = new telegraf_1.Telegraf(config.botToken);
15
+ this.chatId = config.chatId;
16
+ this.prefix = config.messagePrefix || 'TDB:';
17
+ this.batchDelay = config.batchDelay || 100;
18
+ this.maxRetries = config.maxRetries || 3;
19
+ }
20
+ /** Initialize database connection. Must be called before use. */
21
+ async initialize() {
22
+ if (this.initialized) {
23
+ return;
24
+ }
25
+ try {
26
+ await this.bot.telegram.getMe();
27
+ await this.bot.telegram.getChat(this.chatId);
28
+ await this.loadMessageIndex();
29
+ this.setupMessageListener();
30
+ this.initialized = true;
31
+ }
32
+ catch (error) {
33
+ throw new Error(`Failed to initialize Telegram DB: ${error}`);
34
+ }
35
+ }
36
+ /** Insert a new document into a table */
37
+ async insert(doc, table) {
38
+ await this.ensureInitialized();
39
+ try {
40
+ const document = {
41
+ ...doc,
42
+ _id: doc._id || (0, utils_1.generateId)(),
43
+ _table: table,
44
+ };
45
+ const message = (0, utils_1.encodeDocument)(document, this.prefix);
46
+ const sentMessage = await this.retryOperation(() => this.bot.telegram.sendMessage(this.chatId, message), this.maxRetries);
47
+ this.messageIndex.set(document._id, sentMessage.message_id);
48
+ this.documentCache.set(document._id, document);
49
+ await this.saveMessageIndex();
50
+ return {
51
+ success: true,
52
+ data: { ...document, _messageId: sentMessage.message_id },
53
+ message: 'Document inserted successfully',
54
+ };
55
+ }
56
+ catch (error) {
57
+ return {
58
+ success: false,
59
+ error: error,
60
+ message: `Failed to insert document: ${error.message}`,
61
+ };
62
+ }
63
+ }
64
+ /** Insert multiple documents into a table */
65
+ async insertMany(docs, table, options) {
66
+ await this.ensureInitialized();
67
+ const results = [];
68
+ const delay = options?.delay || this.batchDelay;
69
+ const stopOnError = options?.stopOnError || false;
70
+ for (const doc of docs) {
71
+ const result = await this.insert(doc, table);
72
+ results.push(result);
73
+ if (!result.success && stopOnError) {
74
+ break;
75
+ }
76
+ if (delay > 0 && docs.indexOf(doc) < docs.length - 1) {
77
+ await this.sleep(delay);
78
+ }
79
+ }
80
+ return results;
81
+ }
82
+ /** Find documents matching the filter in a table */
83
+ async find(filter = {}, table) {
84
+ await this.ensureInitialized();
85
+ try {
86
+ const documents = [];
87
+ const queryWithTable = { ...filter, _table: table };
88
+ for (const [docId, doc] of this.documentCache.entries()) {
89
+ if ((0, utils_1.matchesFilter)(doc, queryWithTable)) {
90
+ documents.push(doc);
91
+ }
92
+ }
93
+ if (this.documentCache.size === 0) {
94
+ await this.reloadCacheFromIndex();
95
+ for (const [docId, doc] of this.documentCache.entries()) {
96
+ if ((0, utils_1.matchesFilter)(doc, queryWithTable)) {
97
+ documents.push(doc);
98
+ }
99
+ }
100
+ }
101
+ return documents;
102
+ }
103
+ catch (error) {
104
+ throw new Error(`Failed to find documents: ${error.message}`);
105
+ }
106
+ }
107
+ /** Find a single document matching the filter in a table */
108
+ async findOne(filter = {}, table) {
109
+ const results = await this.find(filter, table);
110
+ return results.length > 0 ? results[0] : null;
111
+ }
112
+ /** Find document by ID in a table */
113
+ async findById(id, table) {
114
+ return this.findOne({ _id: id }, table);
115
+ }
116
+ /** Update documents matching the filter in a table */
117
+ async update(filter, update, table, options = {}) {
118
+ await this.ensureInitialized();
119
+ try {
120
+ const queryWithTable = { ...filter, _table: table };
121
+ const documents = await this.find({}, table).then(docs => docs.filter(doc => (0, utils_1.matchesFilter)(doc, queryWithTable)));
122
+ if (documents.length === 0 && options.upsert) {
123
+ const newDoc = (0, utils_1.deepMerge)(filter, update);
124
+ return await this.insert(newDoc, table);
125
+ }
126
+ if (documents.length === 0) {
127
+ return {
128
+ success: false,
129
+ message: 'No documents found to update',
130
+ };
131
+ }
132
+ const updatedDocs = [];
133
+ for (const doc of documents) {
134
+ const updated = options.replace
135
+ ? { ...update, _id: doc._id }
136
+ : (0, utils_1.deepMerge)(doc, { ...update, _id: doc._id });
137
+ const oldMessageId = this.messageIndex.get(doc._id);
138
+ if (oldMessageId) {
139
+ try {
140
+ await this.bot.telegram.deleteMessage(this.chatId, oldMessageId);
141
+ }
142
+ catch {
143
+ // Ignore deletion errors
144
+ }
145
+ }
146
+ const message = (0, utils_1.encodeDocument)(updated, this.prefix);
147
+ const sentMessage = await this.retryOperation(() => this.bot.telegram.sendMessage(this.chatId, message), this.maxRetries);
148
+ this.messageIndex.set(updated._id, sentMessage.message_id);
149
+ this.documentCache.set(updated._id, updated);
150
+ updatedDocs.push(updated);
151
+ }
152
+ await this.saveMessageIndex();
153
+ return {
154
+ success: true,
155
+ data: updatedDocs,
156
+ message: `Updated ${updatedDocs.length} document(s)`,
157
+ };
158
+ }
159
+ catch (error) {
160
+ return {
161
+ success: false,
162
+ error: error,
163
+ message: `Failed to update documents: ${error.message}`,
164
+ };
165
+ }
166
+ }
167
+ /** Update a single document by ID in a table */
168
+ async updateById(id, update, table, options = {}) {
169
+ return this.update({ _id: id }, update, table, options);
170
+ }
171
+ /** Delete documents matching the filter in a table */
172
+ async delete(filter, table) {
173
+ await this.ensureInitialized();
174
+ try {
175
+ const queryWithTable = { ...filter, _table: table };
176
+ const documents = await this.find({}, table).then(docs => docs.filter(doc => (0, utils_1.matchesFilter)(doc, queryWithTable)));
177
+ if (documents.length === 0) {
178
+ return {
179
+ success: false,
180
+ message: 'No documents found to delete',
181
+ };
182
+ }
183
+ let deletedCount = 0;
184
+ for (const doc of documents) {
185
+ const messageId = this.messageIndex.get(doc._id);
186
+ if (messageId) {
187
+ try {
188
+ await this.bot.telegram.deleteMessage(this.chatId, messageId);
189
+ this.messageIndex.delete(doc._id);
190
+ this.documentCache.delete(doc._id);
191
+ deletedCount++;
192
+ }
193
+ catch {
194
+ this.messageIndex.delete(doc._id);
195
+ this.documentCache.delete(doc._id);
196
+ }
197
+ }
198
+ }
199
+ await this.saveMessageIndex();
200
+ return {
201
+ success: true,
202
+ data: { deletedCount },
203
+ message: `Deleted ${deletedCount} document(s)`,
204
+ };
205
+ }
206
+ catch (error) {
207
+ return {
208
+ success: false,
209
+ error: error,
210
+ message: `Failed to delete documents: ${error.message}`,
211
+ };
212
+ }
213
+ }
214
+ /** Delete a single document by ID in a table */
215
+ async deleteById(id, table) {
216
+ return this.delete({ _id: id }, table);
217
+ }
218
+ /** Delete all documents in a table */
219
+ async deleteAll(table) {
220
+ return this.delete({}, table);
221
+ }
222
+ /** Count documents matching the filter in a table */
223
+ async count(filter = {}, table) {
224
+ const queryWithTable = { ...filter, _table: table };
225
+ const documents = await this.find({}, table);
226
+ return documents.filter(doc => (0, utils_1.matchesFilter)(doc, queryWithTable)).length;
227
+ }
228
+ /** Get all table names in the database */
229
+ async getTables() {
230
+ await this.ensureInitialized();
231
+ const tables = new Set();
232
+ for (const [, doc] of this.documentCache.entries()) {
233
+ if (doc._table) {
234
+ tables.add(doc._table);
235
+ }
236
+ }
237
+ return Array.from(tables);
238
+ }
239
+ /** Tabloyu tamamen siler */
240
+ async dropTable(table) {
241
+ return this.deleteAll(table);
242
+ }
243
+ /** Belirli tablo veya tüm tablolar için istatistik döner */
244
+ async getStats(table) {
245
+ await this.ensureInitialized();
246
+ const documents = table
247
+ ? await this.find({}, table)
248
+ : Array.from(this.documentCache.values());
249
+ const messages = await this.getAllMessages();
250
+ let oldestDoc;
251
+ let newestDoc;
252
+ if (documents.length > 0) {
253
+ oldestDoc = documents.reduce((oldest, current) => {
254
+ const oldestId = oldest._id.split('-')[0];
255
+ const currentId = current._id.split('-')[0];
256
+ return parseInt(currentId) < parseInt(oldestId) ? current : oldest;
257
+ });
258
+ newestDoc = documents.reduce((newest, current) => {
259
+ const newestId = newest._id.split('-')[0];
260
+ const currentId = current._id.split('-')[0];
261
+ return parseInt(currentId) > parseInt(newestId) ? current : newest;
262
+ });
263
+ }
264
+ return {
265
+ totalDocuments: documents.length,
266
+ totalMessages: messages.length,
267
+ oldestDocument: oldestDoc,
268
+ newestDocument: newestDoc,
269
+ };
270
+ }
271
+ /** Clear all data from the database (all tables) */
272
+ async clear() {
273
+ await this.ensureInitialized();
274
+ const tables = await this.getTables();
275
+ const results = [];
276
+ for (const table of tables) {
277
+ const result = await this.deleteAll(table);
278
+ results.push(result);
279
+ }
280
+ return {
281
+ success: results.every(r => r.success),
282
+ data: results,
283
+ message: `Cleared ${tables.length} table(s)`,
284
+ };
285
+ }
286
+ /** Get a collection/table handler for fluent API usage */
287
+ table(tableName) {
288
+ return new TableHandler_1.TableHandler(this, tableName);
289
+ }
290
+ async getAllMessages() {
291
+ const messages = [];
292
+ for (const [docId, messageId] of this.messageIndex.entries()) {
293
+ messages.push({
294
+ message_id: messageId,
295
+ text: '', // Text is stored in cache, not needed here
296
+ });
297
+ }
298
+ return messages;
299
+ }
300
+ async reloadCacheFromIndex() {
301
+ try {
302
+ if (this.indexMessageId) {
303
+ // Cache populated as operations happen
304
+ }
305
+ }
306
+ catch {
307
+ // Ignore errors
308
+ }
309
+ }
310
+ async loadMessageIndex() {
311
+ try {
312
+ this.messageIndex.clear();
313
+ this.documentCache.clear();
314
+ }
315
+ catch {
316
+ this.messageIndex.clear();
317
+ this.documentCache.clear();
318
+ }
319
+ }
320
+ async saveMessageIndex() {
321
+ try {
322
+ const indexData = {
323
+ _id: '__INDEX__',
324
+ _table: '__SYSTEM__',
325
+ messageIndex: Array.from(this.messageIndex.entries()),
326
+ documents: Array.from(this.documentCache.values()),
327
+ updatedAt: Date.now(),
328
+ };
329
+ const indexMessage = (0, utils_1.encodeDocument)(indexData, `${this.prefix}INDEX:`);
330
+ if (this.indexMessageId) {
331
+ try {
332
+ await this.bot.telegram.deleteMessage(this.chatId, this.indexMessageId);
333
+ }
334
+ catch {
335
+ // Ignore errors
336
+ }
337
+ }
338
+ if (this.messageIndex.size > 0) {
339
+ try {
340
+ const sentMessage = await this.bot.telegram.sendMessage(this.chatId, indexMessage);
341
+ this.indexMessageId = sentMessage.message_id;
342
+ }
343
+ catch {
344
+ // Fail silently if message too long
345
+ }
346
+ }
347
+ }
348
+ catch (error) {
349
+ console.warn('Failed to save message index:', error);
350
+ }
351
+ }
352
+ setupMessageListener() {
353
+ this.bot.on('message', async (ctx) => {
354
+ const message = ctx.message;
355
+ if (!message || !('text' in message) || !message.text)
356
+ return;
357
+ const text = message.text;
358
+ if (String(ctx.chat?.id) !== String(this.chatId))
359
+ return;
360
+ if (text.startsWith(this.prefix) && !text.startsWith(`${this.prefix}INDEX:`)) {
361
+ const doc = (0, utils_1.decodeDocument)(text, this.prefix);
362
+ if (doc && doc._id) {
363
+ this.messageIndex.set(doc._id, message.message_id);
364
+ this.documentCache.set(doc._id, doc);
365
+ }
366
+ }
367
+ else if (text.startsWith(`${this.prefix}INDEX:`)) {
368
+ const indexData = (0, utils_1.decodeDocument)(text, `${this.prefix}INDEX:`);
369
+ if (indexData && indexData._id === '__INDEX__' && indexData.messageIndex && indexData.documents) {
370
+ this.messageIndex = new Map(indexData.messageIndex);
371
+ indexData.documents.forEach((doc) => {
372
+ this.documentCache.set(doc._id, doc);
373
+ });
374
+ this.indexMessageId = message.message_id;
375
+ }
376
+ }
377
+ });
378
+ this.bot.launch().catch(() => { });
379
+ }
380
+ /** Ensure database is initialized */
381
+ async ensureInitialized() {
382
+ if (!this.initialized) {
383
+ await this.initialize();
384
+ }
385
+ }
386
+ /** Retry operation with exponential backoff */
387
+ async retryOperation(operation, maxRetries) {
388
+ let lastError;
389
+ for (let i = 0; i < maxRetries; i++) {
390
+ try {
391
+ return await operation();
392
+ }
393
+ catch (error) {
394
+ lastError = error;
395
+ if (i < maxRetries - 1) {
396
+ await this.sleep(1000 * (i + 1));
397
+ }
398
+ }
399
+ }
400
+ throw lastError;
401
+ }
402
+ sleep(ms) {
403
+ return new Promise(resolve => setTimeout(resolve, ms));
404
+ }
405
+ /** Close database connection */
406
+ async close() {
407
+ this.bot.stop();
408
+ this.initialized = false;
409
+ }
410
+ }
411
+ exports.TelegramDB = TelegramDB;
@@ -0,0 +1,4 @@
1
+ export { TelegramDB } from './TelegramDB';
2
+ export { TableHandler } from './TableHandler';
3
+ export * from './types';
4
+ export * from './utils';
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.TableHandler = exports.TelegramDB = void 0;
18
+ var TelegramDB_1 = require("./TelegramDB");
19
+ Object.defineProperty(exports, "TelegramDB", { enumerable: true, get: function () { return TelegramDB_1.TelegramDB; } });
20
+ var TableHandler_1 = require("./TableHandler");
21
+ Object.defineProperty(exports, "TableHandler", { enumerable: true, get: function () { return TableHandler_1.TableHandler; } });
22
+ __exportStar(require("./types"), exports);
23
+ __exportStar(require("./utils"), exports);
@@ -0,0 +1,35 @@
1
+ export interface TelegramDBConfig {
2
+ botToken: string;
3
+ chatId: string | number;
4
+ messagePrefix?: string;
5
+ batchDelay?: number;
6
+ maxRetries?: number;
7
+ }
8
+ export interface Document {
9
+ _id: string;
10
+ _table: string;
11
+ [key: string]: any;
12
+ }
13
+ export interface QueryFilter {
14
+ [key: string]: any;
15
+ }
16
+ export interface UpdateOptions {
17
+ upsert?: boolean;
18
+ replace?: boolean;
19
+ }
20
+ export interface OperationResult {
21
+ success: boolean;
22
+ message?: string;
23
+ data?: any;
24
+ error?: Error;
25
+ }
26
+ export interface BatchOptions {
27
+ delay?: number;
28
+ stopOnError?: boolean;
29
+ }
30
+ export interface DatabaseStats {
31
+ totalDocuments: number;
32
+ totalMessages: number;
33
+ oldestDocument?: Document;
34
+ newestDocument?: Document;
35
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ import { Document, QueryFilter } from './types';
2
+ export declare function encodeDocument(doc: Document, prefix?: string): string;
3
+ export declare function decodeDocument(messageText: string, prefix?: string): Document | null;
4
+ export declare function matchesFilter(doc: Document, filter: QueryFilter): boolean;
5
+ export declare function generateId(): string;
6
+ export declare function deepMerge(target: any, source: any): any;
package/dist/utils.js ADDED
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.encodeDocument = encodeDocument;
4
+ exports.decodeDocument = decodeDocument;
5
+ exports.matchesFilter = matchesFilter;
6
+ exports.generateId = generateId;
7
+ exports.deepMerge = deepMerge;
8
+ function encodeDocument(doc, prefix = "TDB:") {
9
+ const json = JSON.stringify(doc);
10
+ return `${prefix}${json}`;
11
+ }
12
+ function decodeDocument(messageText, prefix = "TDB:") {
13
+ if (!messageText.startsWith(prefix)) {
14
+ return null;
15
+ }
16
+ try {
17
+ const json = messageText.substring(prefix.length);
18
+ return JSON.parse(json);
19
+ }
20
+ catch (error) {
21
+ return null;
22
+ }
23
+ }
24
+ function matchesFilter(doc, filter) {
25
+ for (const [key, value] of Object.entries(filter)) {
26
+ if (key === '_id' && doc._id !== value) {
27
+ return false;
28
+ }
29
+ const docValue = getNestedValue(doc, key);
30
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
31
+ if (!matchesOperators(docValue, value)) {
32
+ return false;
33
+ }
34
+ }
35
+ else if (Array.isArray(value)) {
36
+ if (!value.includes(docValue)) {
37
+ return false;
38
+ }
39
+ }
40
+ else {
41
+ if (docValue !== value) {
42
+ return false;
43
+ }
44
+ }
45
+ }
46
+ return true;
47
+ }
48
+ function getNestedValue(obj, path) {
49
+ return path.split('.').reduce((current, key) => current?.[key], obj);
50
+ }
51
+ function matchesOperators(docValue, operators) {
52
+ for (const [op, opValue] of Object.entries(operators)) {
53
+ switch (op) {
54
+ case '$gt':
55
+ if (!(docValue > opValue))
56
+ return false;
57
+ break;
58
+ case '$gte':
59
+ if (!(docValue >= opValue))
60
+ return false;
61
+ break;
62
+ case '$lt':
63
+ if (!(docValue < opValue))
64
+ return false;
65
+ break;
66
+ case '$lte':
67
+ if (!(docValue <= opValue))
68
+ return false;
69
+ break;
70
+ case '$ne':
71
+ if (docValue === opValue)
72
+ return false;
73
+ break;
74
+ case '$in':
75
+ if (!Array.isArray(opValue) || !opValue.includes(docValue))
76
+ return false;
77
+ break;
78
+ case '$nin':
79
+ if (Array.isArray(opValue) && opValue.includes(docValue))
80
+ return false;
81
+ break;
82
+ case '$regex':
83
+ if (typeof docValue !== 'string')
84
+ return false;
85
+ const regex = new RegExp(opValue);
86
+ if (!regex.test(docValue))
87
+ return false;
88
+ break;
89
+ case '$exists':
90
+ const exists = docValue !== undefined && docValue !== null;
91
+ if (opValue !== exists)
92
+ return false;
93
+ break;
94
+ default:
95
+ break;
96
+ }
97
+ }
98
+ return true;
99
+ }
100
+ function generateId() {
101
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
102
+ }
103
+ function deepMerge(target, source) {
104
+ const output = { ...target };
105
+ if (isObject(target) && isObject(source)) {
106
+ Object.keys(source).forEach(key => {
107
+ if (isObject(source[key])) {
108
+ if (!(key in target)) {
109
+ Object.assign(output, { [key]: source[key] });
110
+ }
111
+ else {
112
+ output[key] = deepMerge(target[key], source[key]);
113
+ }
114
+ }
115
+ else {
116
+ Object.assign(output, { [key]: source[key] });
117
+ }
118
+ });
119
+ }
120
+ return output;
121
+ }
122
+ function isObject(item) {
123
+ return item && typeof item === 'object' && !Array.isArray(item);
124
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@ozgurv/tg-db",
3
+ "version": "1.0.0",
4
+ "description": "Use Telegram chat as a database - Store and query data in Telegram conversations",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch",
10
+ "prepublishOnly": "npm run build",
11
+ "test": "jest"
12
+ },
13
+ "keywords": [
14
+ "telegram",
15
+ "database",
16
+ "bot",
17
+ "storage",
18
+ "chat-db",
19
+ "telegram-bot"
20
+ ],
21
+ "author": "",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "telegraf": "^4.15.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^20.10.0",
28
+ "typescript": "^5.3.3"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "repository": {
36
+ "type": "git",
37
+ "url": ""
38
+ },
39
+ "engines": {
40
+ "node": ">=16.0.0"
41
+ }
42
+ }