@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 +21 -0
- package/README.md +532 -0
- package/dist/TableHandler.d.ts +20 -0
- package/dist/TableHandler.js +47 -0
- package/dist/TelegramDB.d.ts +61 -0
- package/dist/TelegramDB.js +411 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +23 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.js +2 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +124 -0
- package/package.json +42 -0
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;
|
package/dist/index.d.ts
ADDED
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);
|
package/dist/types.d.ts
ADDED
|
@@ -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
package/dist/utils.d.ts
ADDED
|
@@ -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
|
+
}
|