gramobase 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/CODE_OF_CONDUCT.md +132 -0
- package/LICENSE +21 -0
- package/README.md +314 -0
- package/dist/BotWorkerPool-9ndHQt2g.d.cts +201 -0
- package/dist/BotWorkerPool-9ndHQt2g.d.ts +201 -0
- package/dist/GramoBaseAuth-00fg0u_b.d.ts +218 -0
- package/dist/GramoBaseAuth-CHNn2_e5.d.cts +218 -0
- package/dist/auth/index.cjs +226 -0
- package/dist/auth/index.cjs.map +1 -0
- package/dist/auth/index.d.cts +5 -0
- package/dist/auth/index.d.ts +5 -0
- package/dist/auth/index.js +203 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/bin/gramobase.cjs +167 -0
- package/dist/bin/gramobase.cjs.map +1 -0
- package/dist/bin/gramobase.d.cts +1 -0
- package/dist/bin/gramobase.d.ts +1 -0
- package/dist/bin/gramobase.js +160 -0
- package/dist/bin/gramobase.js.map +1 -0
- package/dist/index.cjs +1644 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +104 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +1611 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations/index.cjs +98 -0
- package/dist/migrations/index.cjs.map +1 -0
- package/dist/migrations/index.d.cts +23 -0
- package/dist/migrations/index.d.ts +23 -0
- package/dist/migrations/index.js +96 -0
- package/dist/migrations/index.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as members, contributors, and leaders pledge to make participation in the
|
|
6
|
+
**gramobase** community a harassment-free experience for everyone, regardless of
|
|
7
|
+
age, body size, visible or invisible disability, ethnicity, sex characteristics,
|
|
8
|
+
gender identity and expression, level of experience, education, socio-economic
|
|
9
|
+
status, nationality, personal appearance, race, caste, color, religion, or
|
|
10
|
+
sexual identity and orientation.
|
|
11
|
+
|
|
12
|
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
13
|
+
diverse, inclusive, and healthy community.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Our Standards
|
|
18
|
+
|
|
19
|
+
**Examples of behavior that contributes to a positive environment:**
|
|
20
|
+
|
|
21
|
+
- Using welcoming and inclusive language
|
|
22
|
+
- Being respectful of differing viewpoints and experiences
|
|
23
|
+
- Gracefully accepting constructive criticism
|
|
24
|
+
- Focusing on what is best for the community
|
|
25
|
+
- Showing empathy towards other community members
|
|
26
|
+
- Giving credit where credit is due
|
|
27
|
+
|
|
28
|
+
**Examples of unacceptable behavior:**
|
|
29
|
+
|
|
30
|
+
- The use of sexualized language or imagery, and sexual attention or advances of any kind
|
|
31
|
+
- Trolling, insulting or derogatory comments, and personal or political attacks
|
|
32
|
+
- Public or private harassment
|
|
33
|
+
- Publishing others' private information, such as a physical or email address, without explicit permission
|
|
34
|
+
- Deliberate intimidation, stalking, or following
|
|
35
|
+
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Enforcement Responsibilities
|
|
40
|
+
|
|
41
|
+
Community leaders are responsible for clarifying and enforcing our standards of
|
|
42
|
+
acceptable behavior and will take appropriate and fair corrective action in
|
|
43
|
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
44
|
+
or harmful.
|
|
45
|
+
|
|
46
|
+
Community leaders have the right and responsibility to remove, edit, or reject
|
|
47
|
+
comments, commits, code, wiki edits, issues, and other contributions that are
|
|
48
|
+
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
|
49
|
+
decisions when appropriate.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Scope
|
|
54
|
+
|
|
55
|
+
This Code of Conduct applies within all community spaces — including the GitHub
|
|
56
|
+
repository, issue tracker, pull requests, discussions, and any other official
|
|
57
|
+
communication channels — and also applies when an individual is officially
|
|
58
|
+
representing the community in public spaces.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Reporting
|
|
63
|
+
|
|
64
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
65
|
+
reported by opening a **private** GitHub issue or contacting the maintainers
|
|
66
|
+
directly. All complaints will be reviewed and investigated promptly and fairly.
|
|
67
|
+
|
|
68
|
+
All community leaders are obligated to respect the privacy and security of the
|
|
69
|
+
reporter of any incident.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Enforcement Guidelines
|
|
74
|
+
|
|
75
|
+
Community leaders will follow these guidelines in determining the consequences
|
|
76
|
+
for any action they deem in violation of this Code of Conduct:
|
|
77
|
+
|
|
78
|
+
### 1. Correction
|
|
79
|
+
|
|
80
|
+
**Impact:** Use of inappropriate language or other behavior deemed unprofessional.
|
|
81
|
+
|
|
82
|
+
**Consequence:** A private, written warning from community leaders, providing
|
|
83
|
+
clarity around the nature of the violation and an explanation of why the
|
|
84
|
+
behavior was inappropriate. A public apology may be requested.
|
|
85
|
+
|
|
86
|
+
### 2. Warning
|
|
87
|
+
|
|
88
|
+
**Impact:** A violation through a single incident or series of actions.
|
|
89
|
+
|
|
90
|
+
**Consequence:** A warning with consequences for continued behavior. No
|
|
91
|
+
interaction with the people involved — including unsolicited interaction with
|
|
92
|
+
those enforcing the Code of Conduct — for a specified period of time. This
|
|
93
|
+
includes avoiding interactions in community spaces as well as external channels
|
|
94
|
+
like social media. Violating these terms may lead to a temporary or permanent
|
|
95
|
+
ban.
|
|
96
|
+
|
|
97
|
+
### 3. Temporary Ban
|
|
98
|
+
|
|
99
|
+
**Impact:** A serious violation of community standards, including sustained
|
|
100
|
+
inappropriate behavior.
|
|
101
|
+
|
|
102
|
+
**Consequence:** A temporary ban from any sort of interaction or public
|
|
103
|
+
communication with the community for a specified period of time. No public or
|
|
104
|
+
private interaction with the people involved — including unsolicited interaction
|
|
105
|
+
with those enforcing the Code of Conduct — is allowed during this period.
|
|
106
|
+
Violating these terms may lead to a permanent ban.
|
|
107
|
+
|
|
108
|
+
### 4. Permanent Ban
|
|
109
|
+
|
|
110
|
+
**Impact:** Demonstrating a pattern of violation of community standards,
|
|
111
|
+
including sustained inappropriate behavior, harassment of an individual, or
|
|
112
|
+
aggression toward or disparagement of classes of individuals.
|
|
113
|
+
|
|
114
|
+
**Consequence:** A permanent ban from any sort of public interaction within the
|
|
115
|
+
community.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Attribution
|
|
120
|
+
|
|
121
|
+
This Code of Conduct is adapted from the
|
|
122
|
+
[Contributor Covenant](https://www.contributor-covenant.org), version 2.1,
|
|
123
|
+
available at
|
|
124
|
+
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
|
|
125
|
+
|
|
126
|
+
Community Impact Guidelines were inspired by
|
|
127
|
+
[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
|
128
|
+
|
|
129
|
+
For answers to common questions about this code of conduct, see the FAQ at
|
|
130
|
+
[https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq).
|
|
131
|
+
Translations are available at
|
|
132
|
+
[https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gramobase contributors
|
|
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,314 @@
|
|
|
1
|
+
# gramobase
|
|
2
|
+
|
|
3
|
+
[](https://github.com/besaoct/gramobase/actions)
|
|
4
|
+
[](https://www.npmjs.com/package/gramobase)
|
|
5
|
+
[](https://github.com/besaoct/gramobase/blob/main/LICENSE)
|
|
6
|
+
[](https://github.com/besaoct/gramobase/actions)
|
|
7
|
+
[](https://makeapullrequest.com)
|
|
8
|
+
|
|
9
|
+
**Telegram as a free, infinite, production-grade backend database.**
|
|
10
|
+
|
|
11
|
+
Every Telegram channel is a collection. Every message is a document. Zero infrastructure needed — all you need is a free Telegram account.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createClient } from 'gramobase';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
|
|
17
|
+
const db = await createClient({
|
|
18
|
+
botToken: process.env.BOT_TOKEN!,
|
|
19
|
+
channelId: process.env.CHANNEL_ID!,
|
|
20
|
+
}).connect();
|
|
21
|
+
|
|
22
|
+
const users = db.collection('users', {
|
|
23
|
+
schema: z.object({ name: z.string(), email: z.string().email() }),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await users.insertOne({ name: 'Aarav', email: 'aarav@example.com' });
|
|
27
|
+
const user = await users.findOne({ name: { $eq: 'Aarav' } });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Why gramobase?
|
|
33
|
+
|
|
34
|
+
| Feature | gramobase | Firebase free | Supabase free |
|
|
35
|
+
|---|---|---|---|
|
|
36
|
+
| Storage | **Unlimited** | 1GB | 500MB |
|
|
37
|
+
| Reads/writes | 30/s per bot, scales with bot count | 50K/day | 500MB bandwidth |
|
|
38
|
+
| Auth | ✓ built-in | ✓ | ✓ |
|
|
39
|
+
| File storage | **2GB per file** | 1GB total | 1GB total |
|
|
40
|
+
| Realtime | ✓ SSE/webhook | ✓ | ✓ |
|
|
41
|
+
| Infra needed | **None** | Firebase project | Supabase project |
|
|
42
|
+
| Cost | **$0 forever** | Free tier | Free tier |
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install gramobase
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Running Tests
|
|
53
|
+
|
|
54
|
+
To run the suite of 33 unit tests checking the ORM, caching, queue/worker pooling, and authentication:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm run test
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Setup
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx gramobase init
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This walks you through entering your bot token and channel ID, creates `.env` and `gramobase.config.ts`.
|
|
67
|
+
|
|
68
|
+
**Prerequisites:**
|
|
69
|
+
1. Create a bot via [@BotFather](https://t.me/BotFather) on Telegram — takes 30 seconds
|
|
70
|
+
2. Create a private Telegram channel
|
|
71
|
+
3. Add your bot as an **Administrator** with full permissions to the channel
|
|
72
|
+
4. Get your channel ID (forward a message to @userinfobot)
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Core API
|
|
77
|
+
|
|
78
|
+
### Collections (MongoDB-like ORM)
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const PostSchema = z.object({
|
|
82
|
+
title: z.string(),
|
|
83
|
+
body: z.string(),
|
|
84
|
+
views: z.number().default(0),
|
|
85
|
+
tags: z.array(z.string()).default([]),
|
|
86
|
+
published: z.boolean().default(false),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const posts = db.collection('posts', {
|
|
90
|
+
schema: PostSchema,
|
|
91
|
+
indexes: ['title'], // bloom filter index
|
|
92
|
+
encrypt: true, // AES-256 field-level encryption
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Insert
|
|
96
|
+
const post = await posts.insertOne({ title: 'Hello', body: 'World' });
|
|
97
|
+
await posts.insertMany([...]);
|
|
98
|
+
|
|
99
|
+
// Find
|
|
100
|
+
const all = await posts.find();
|
|
101
|
+
const published = await posts.find({ filter: { published: { $eq: true } } });
|
|
102
|
+
const recent = await posts.find({
|
|
103
|
+
filter: { views: { $gte: 100 } },
|
|
104
|
+
sort: { views: -1 },
|
|
105
|
+
limit: 10,
|
|
106
|
+
skip: 0,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Operators: $eq $ne $gt $gte $lt $lte $in $nin $regex $exists $and $or $not
|
|
110
|
+
|
|
111
|
+
// Update
|
|
112
|
+
await posts.findByIdAndUpdate(post._id, {
|
|
113
|
+
$set: { published: true },
|
|
114
|
+
$inc: { views: 1 },
|
|
115
|
+
$push: { tags: 'featured' },
|
|
116
|
+
});
|
|
117
|
+
await posts.updateMany({ published: { $eq: false } }, { $set: { published: true } });
|
|
118
|
+
|
|
119
|
+
// Delete
|
|
120
|
+
await posts.deleteById(post._id);
|
|
121
|
+
await posts.deleteMany({ views: { $eq: 0 } });
|
|
122
|
+
await posts.count({ published: { $eq: true } });
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Authentication
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const auth = db.createAuth({
|
|
129
|
+
jwtSecret: process.env.JWT_SECRET!,
|
|
130
|
+
jwtExpiresIn: '7d',
|
|
131
|
+
bcryptRounds: 12,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const { user, session } = await auth.register('user@example.com', 'password', ['user']);
|
|
135
|
+
const { session: s2 } = await auth.login('user@example.com', 'password');
|
|
136
|
+
|
|
137
|
+
const verified = auth.verifyToken(s2.token);
|
|
138
|
+
auth.requireRole(verified, 'admin'); // throws if not admin
|
|
139
|
+
auth.requireAnyRole(verified, ['mod', 'admin']);
|
|
140
|
+
|
|
141
|
+
await auth.changePassword(user._id, 'old', 'new');
|
|
142
|
+
await auth.updateRoles(user._id, ['user', 'pro']);
|
|
143
|
+
|
|
144
|
+
// Express middleware
|
|
145
|
+
app.use('/api', auth.middleware());
|
|
146
|
+
app.post('/admin', auth.middleware(), auth.requireRoleMiddleware('admin'), handler);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### File Storage
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// Upload any file — images, PDFs, videos up to 2GB (via MTProto)
|
|
153
|
+
const file = await db.uploadFile(buffer, {
|
|
154
|
+
fileName: 'profile.jpg',
|
|
155
|
+
mimeType: 'image/jpeg',
|
|
156
|
+
metadata: { userId: '123' },
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
console.log(file.fileId); // stable Telegram file reference
|
|
160
|
+
console.log(file.url); // CDN-served download URL
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Realtime
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
// Subscribe to collection events
|
|
167
|
+
const unsub = db.realtime.onInsert('orders', (order) => {
|
|
168
|
+
console.log('New order:', order);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
db.realtime.onUpdate('products', (id, changes, doc) => {
|
|
172
|
+
console.log('Updated:', id, changes);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
db.realtime.onDelete('posts', (id) => {
|
|
176
|
+
console.log('Deleted:', id);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Server-Sent Events for browser clients
|
|
180
|
+
app.get('/stream', db.realtime.sseHandler('orders'));
|
|
181
|
+
|
|
182
|
+
// Frontend:
|
|
183
|
+
const es = new EventSource('/stream');
|
|
184
|
+
es.onmessage = (e) => console.log(JSON.parse(e.data));
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Migrations
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const migrations = [
|
|
191
|
+
{
|
|
192
|
+
version: 1,
|
|
193
|
+
name: 'add-slug-field',
|
|
194
|
+
async up(db) {
|
|
195
|
+
const posts = db.collection('posts', { schema: PostSchema });
|
|
196
|
+
const all = await posts.find();
|
|
197
|
+
for (const post of all) {
|
|
198
|
+
await posts.findByIdAndUpdate(post._id, {
|
|
199
|
+
$set: { slug: post.title.toLowerCase().replace(/ /g, '-') },
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
async down(db) {
|
|
204
|
+
await db.collection('posts', { schema: PostSchema })
|
|
205
|
+
.updateMany({}, { $unset: { slug: '' } });
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
await db.migrate(migrations);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Anti-flood bot pool
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
// Pass multiple bot tokens — gramobase round-robins and backs off per token
|
|
217
|
+
const db = await createClient({
|
|
218
|
+
botToken: [
|
|
219
|
+
process.env.BOT_TOKEN_1!,
|
|
220
|
+
process.env.BOT_TOKEN_2!,
|
|
221
|
+
process.env.BOT_TOKEN_3!,
|
|
222
|
+
],
|
|
223
|
+
channelId: process.env.CHANNEL_ID!,
|
|
224
|
+
}).connect();
|
|
225
|
+
|
|
226
|
+
// 3 tokens × 30 req/s = effectively 90 writes/s sustained
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Architecture
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
Developer API (ORM, Auth, Files, Realtime)
|
|
235
|
+
│
|
|
236
|
+
Hot Cache (LRU, 64MB+, O(1) reads)
|
|
237
|
+
│
|
|
238
|
+
State Manager (reactive, optimistic writes)
|
|
239
|
+
│
|
|
240
|
+
Write-Ahead Log (crash recovery, sequence IDs)
|
|
241
|
+
│
|
|
242
|
+
Registry (distributed lease, heartbeat, single writer)
|
|
243
|
+
│
|
|
244
|
+
Bot Worker Pool (round-robin, 429 backoff, retry)
|
|
245
|
+
│
|
|
246
|
+
Telegram Bot API ─────────────────────────────────┐
|
|
247
|
+
│ │
|
|
248
|
+
Private Channel File Storage Realtime
|
|
249
|
+
(messages = docs, (sendDocument, (webhook +
|
|
250
|
+
pinned = index) file_id refs) SSE bridge)
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Storage model
|
|
254
|
+
|
|
255
|
+
- Each collection maps to a private Telegram channel (or shares one via namespaced message tags)
|
|
256
|
+
- A **pinned index message** stores `{ id → msgId }` for O(1) lookups
|
|
257
|
+
- The **Write-Ahead Log** channel stores operation logs for crash recovery
|
|
258
|
+
- A **registry message** acts as a distributed write lock across processes
|
|
259
|
+
|
|
260
|
+
### Limits
|
|
261
|
+
|
|
262
|
+
| Limit | Value |
|
|
263
|
+
|---|---|
|
|
264
|
+
| Telegram rate limit | 30 req/s per bot token (scales with pool size) |
|
|
265
|
+
| Message size | 4096 bytes per message (large docs auto-chunked) |
|
|
266
|
+
| File size (Bot API) | 50MB send, 20MB receive |
|
|
267
|
+
| File size (MTProto/TDLib) | 2GB |
|
|
268
|
+
| Channel message history | Unlimited |
|
|
269
|
+
| Cost | $0 |
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## CLI
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
npx gramobase init # interactive setup wizard
|
|
277
|
+
npx gramobase status # check bot + channel connectivity
|
|
278
|
+
npx gramobase migrate # run pending migrations
|
|
279
|
+
npx gramobase migrate --rollback 1 # rollback last migration
|
|
280
|
+
npx gramobase migrate --status # show migration history
|
|
281
|
+
npx gramobase generate post --fields "title:string,views:number"
|
|
282
|
+
npx gramobase studio # open browser UI (v0.2)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Configuration
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
const db = createClient({
|
|
291
|
+
botToken: string | string[], // single token or pool
|
|
292
|
+
channelId: string, // main storage channel
|
|
293
|
+
walChannelId?: string, // separate WAL channel (optional)
|
|
294
|
+
indexChannelId?: string, // separate index channel (optional)
|
|
295
|
+
encryptionKey?: string, // AES-256 key for encryption at rest
|
|
296
|
+
cacheMaxBytes?: number, // default 64MB
|
|
297
|
+
cacheTtlMs?: number, // default 60s
|
|
298
|
+
concurrency?: number, // max concurrent requests per token, default 25
|
|
299
|
+
webhookUrl?: string, // enables webhook mode for realtime
|
|
300
|
+
debug?: boolean,
|
|
301
|
+
});
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Disclaimer
|
|
307
|
+
|
|
308
|
+
gramobase is designed for prototypes, hobby projects, and small-to-medium applications. It is not a replacement for PostgreSQL or MongoDB in high-traffic production systems. Data lives on Telegram's infrastructure — do not store sensitive PII without encryption.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## License
|
|
313
|
+
|
|
314
|
+
MIT
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import TelegramBot from 'node-telegram-bot-api';
|
|
3
|
+
import EventEmitter from 'eventemitter3';
|
|
4
|
+
|
|
5
|
+
interface GramoBaseDocument {
|
|
6
|
+
_id: string;
|
|
7
|
+
_collection: string;
|
|
8
|
+
_msgId: number;
|
|
9
|
+
_createdAt: string;
|
|
10
|
+
_updatedAt: string;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
type WithId<T> = T & GramoBaseDocument;
|
|
14
|
+
interface CollectionConfig<T extends z.ZodType> {
|
|
15
|
+
schema: T;
|
|
16
|
+
/** Channel override — uses the default if omitted */
|
|
17
|
+
channelId?: string | undefined;
|
|
18
|
+
/** Bloom-filter field list for fast-miss short-circuit */
|
|
19
|
+
indexes?: string[] | undefined;
|
|
20
|
+
/** Encrypt all documents in this collection with AES-256 */
|
|
21
|
+
encrypt?: boolean | undefined;
|
|
22
|
+
/** TTL seconds — documents are automatically expired after this many seconds */
|
|
23
|
+
ttl?: number | undefined;
|
|
24
|
+
}
|
|
25
|
+
type ElemOf<T> = T extends (infer U)[] ? U : T;
|
|
26
|
+
type ComparisonOperator<T> = {
|
|
27
|
+
$eq?: T | ElemOf<T> | undefined;
|
|
28
|
+
$ne?: T | ElemOf<T> | undefined;
|
|
29
|
+
$gt?: T | ElemOf<T> | number | undefined;
|
|
30
|
+
$gte?: T | ElemOf<T> | number | undefined;
|
|
31
|
+
$lt?: T | ElemOf<T> | number | undefined;
|
|
32
|
+
$lte?: T | ElemOf<T> | number | undefined;
|
|
33
|
+
$in?: (T | ElemOf<T>)[] | undefined;
|
|
34
|
+
$nin?: (T | ElemOf<T>)[] | undefined;
|
|
35
|
+
$exists?: boolean | undefined;
|
|
36
|
+
$regex?: RegExp | string | undefined;
|
|
37
|
+
};
|
|
38
|
+
type Filter<T> = {
|
|
39
|
+
[K in keyof T]?: T[K] | ElemOf<T[K]> | ComparisonOperator<T[K]> | undefined;
|
|
40
|
+
} | {
|
|
41
|
+
$and?: Filter<T>[] | undefined;
|
|
42
|
+
$or?: Filter<T>[] | undefined;
|
|
43
|
+
$not?: Filter<T> | undefined;
|
|
44
|
+
};
|
|
45
|
+
interface FindOptions<T> {
|
|
46
|
+
filter?: Filter<T> | undefined;
|
|
47
|
+
sort?: Partial<Record<keyof T | string, 1 | -1>> | undefined;
|
|
48
|
+
limit?: number | undefined;
|
|
49
|
+
skip?: number | undefined;
|
|
50
|
+
projection?: Partial<Record<keyof T | string, 1 | 0>> | undefined;
|
|
51
|
+
useCache?: boolean | undefined;
|
|
52
|
+
}
|
|
53
|
+
interface UpdateOperators<T> {
|
|
54
|
+
$set?: (Partial<T> & Record<string, unknown>) | undefined;
|
|
55
|
+
$unset?: Partial<Record<keyof T | string, '' | true>> | undefined;
|
|
56
|
+
$inc?: Partial<Record<keyof T | string, number>> | undefined;
|
|
57
|
+
$push?: Partial<Record<keyof T | string, unknown>> | undefined;
|
|
58
|
+
}
|
|
59
|
+
type WalOpType = 'INSERT' | 'UPDATE' | 'DELETE';
|
|
60
|
+
interface WalEntry {
|
|
61
|
+
seq: number;
|
|
62
|
+
op: WalOpType;
|
|
63
|
+
collection: string;
|
|
64
|
+
id: string;
|
|
65
|
+
data?: unknown;
|
|
66
|
+
timestamp: string;
|
|
67
|
+
checksum: string;
|
|
68
|
+
}
|
|
69
|
+
interface Lease {
|
|
70
|
+
instanceId: string;
|
|
71
|
+
acquiredAt: number;
|
|
72
|
+
expiresAt: number;
|
|
73
|
+
heartbeatInterval: ReturnType<typeof setInterval> | null;
|
|
74
|
+
}
|
|
75
|
+
interface User {
|
|
76
|
+
_id: string;
|
|
77
|
+
email: string;
|
|
78
|
+
passwordHash: string;
|
|
79
|
+
roles: string[];
|
|
80
|
+
metadata?: Record<string, unknown> | undefined;
|
|
81
|
+
createdAt: string;
|
|
82
|
+
updatedAt: string;
|
|
83
|
+
}
|
|
84
|
+
interface Session {
|
|
85
|
+
userId: string;
|
|
86
|
+
roles: string[];
|
|
87
|
+
expiresAt: number;
|
|
88
|
+
token: string;
|
|
89
|
+
}
|
|
90
|
+
interface AuthConfig {
|
|
91
|
+
jwtSecret: string;
|
|
92
|
+
jwtExpiresIn?: string | undefined;
|
|
93
|
+
bcryptRounds?: number | undefined;
|
|
94
|
+
onSignIn?: ((user: User) => Promise<void>) | undefined;
|
|
95
|
+
onSignOut?: ((userId: string) => Promise<void>) | undefined;
|
|
96
|
+
}
|
|
97
|
+
interface UploadOptions {
|
|
98
|
+
fileName?: string | undefined;
|
|
99
|
+
mimeType?: string | undefined;
|
|
100
|
+
metadata?: Record<string, unknown> | undefined;
|
|
101
|
+
}
|
|
102
|
+
interface FileRecord {
|
|
103
|
+
_id: string;
|
|
104
|
+
fileId: string;
|
|
105
|
+
fileName: string;
|
|
106
|
+
mimeType: string;
|
|
107
|
+
sizeBytes: number;
|
|
108
|
+
url?: string | undefined;
|
|
109
|
+
uploadedAt: string;
|
|
110
|
+
metadata?: Record<string, unknown> | undefined;
|
|
111
|
+
}
|
|
112
|
+
type GramoBaseEvent = {
|
|
113
|
+
type: 'insert';
|
|
114
|
+
collection: string;
|
|
115
|
+
doc: unknown;
|
|
116
|
+
} | {
|
|
117
|
+
type: 'update';
|
|
118
|
+
collection: string;
|
|
119
|
+
id: string;
|
|
120
|
+
changes: unknown;
|
|
121
|
+
doc: unknown;
|
|
122
|
+
} | {
|
|
123
|
+
type: 'delete';
|
|
124
|
+
collection: string;
|
|
125
|
+
id: string;
|
|
126
|
+
} | {
|
|
127
|
+
type: 'worker:rotate';
|
|
128
|
+
tokenIndex: number;
|
|
129
|
+
} | {
|
|
130
|
+
type: 'wal:flush';
|
|
131
|
+
entries: number;
|
|
132
|
+
};
|
|
133
|
+
interface Migration {
|
|
134
|
+
version: number;
|
|
135
|
+
name: string;
|
|
136
|
+
up(db: unknown): Promise<void>;
|
|
137
|
+
down(db: unknown): Promise<void>;
|
|
138
|
+
}
|
|
139
|
+
interface GramoBaseConfig {
|
|
140
|
+
/** Bot token or array of tokens for pool rotation */
|
|
141
|
+
botToken: string | string[];
|
|
142
|
+
/** Primary storage channel ID */
|
|
143
|
+
channelId: string;
|
|
144
|
+
/** Optional separate channel for WAL entries */
|
|
145
|
+
walChannelId?: string | undefined;
|
|
146
|
+
/** Optional separate channel for collection indexes */
|
|
147
|
+
indexChannelId?: string | undefined;
|
|
148
|
+
/** AES-256 encryption key for data at rest */
|
|
149
|
+
encryptionKey?: string | undefined;
|
|
150
|
+
/** LRU cache byte limit (default: 64MB) */
|
|
151
|
+
cacheMaxBytes?: number | undefined;
|
|
152
|
+
/** LRU cache TTL in milliseconds (default: 60s) */
|
|
153
|
+
cacheTtlMs?: number | undefined;
|
|
154
|
+
/** Max concurrent requests per bot token (default: 25) */
|
|
155
|
+
concurrency?: number | undefined;
|
|
156
|
+
/** Webhook URL for realtime events (optional — falls back to polling) */
|
|
157
|
+
webhookUrl?: string | undefined;
|
|
158
|
+
/** Enable verbose debug logging */
|
|
159
|
+
debug?: boolean | undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
interface WorkerStats {
|
|
163
|
+
tokenIndex: number;
|
|
164
|
+
requestCount: number;
|
|
165
|
+
errorCount: number;
|
|
166
|
+
lastUsed: number;
|
|
167
|
+
rateLimitHits: number;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* BotWorkerPool manages a round-robin pool of Telegram bot tokens.
|
|
171
|
+
* Each token gets its own PQueue limited to 25 concurrent requests
|
|
172
|
+
* (safe under Telegram's 30 req/s flood limit with headroom).
|
|
173
|
+
* On 429 responses, the worker is cooled down and the next token takes over.
|
|
174
|
+
*/
|
|
175
|
+
declare class BotWorkerPool extends EventEmitter {
|
|
176
|
+
private bots;
|
|
177
|
+
private queues;
|
|
178
|
+
private stats;
|
|
179
|
+
private currentIndex;
|
|
180
|
+
private debug;
|
|
181
|
+
constructor(tokens: string[], concurrency?: number, debug?: boolean);
|
|
182
|
+
/**
|
|
183
|
+
* Execute a Telegram API call through the pool with automatic retry
|
|
184
|
+
* and token rotation on rate limits.
|
|
185
|
+
*/
|
|
186
|
+
execute<T>(fn: (bot: TelegramBot) => Promise<T>, priority?: number): Promise<T>;
|
|
187
|
+
/**
|
|
188
|
+
* Round-robin with recency bias — prefer the worker that was least recently used.
|
|
189
|
+
*/
|
|
190
|
+
private pickWorker;
|
|
191
|
+
private isFloodError;
|
|
192
|
+
private isRetryableError;
|
|
193
|
+
private extractRetryAfter;
|
|
194
|
+
getBot(index?: number): TelegramBot;
|
|
195
|
+
getStats(): WorkerStats[];
|
|
196
|
+
getQueueSizes(): number[];
|
|
197
|
+
private sleep;
|
|
198
|
+
destroy(): Promise<void>;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export { type AuthConfig as A, BotWorkerPool as B, type CollectionConfig as C, type FileRecord as F, type GramoBaseEvent as G, type Lease as L, type Migration as M, type Session as S, type UploadOptions as U, type WorkerStats as W, type GramoBaseConfig as a, type ComparisonOperator as b, type Filter as c, type FindOptions as d, type GramoBaseDocument as e, type UpdateOperators as f, type User as g, type WalEntry as h, type WalOpType as i, type WithId as j };
|