gramobase 1.0.9 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,20 +4,47 @@
4
4
  [![CI/CD Status](https://github.com/besaoct/gramobase/actions/workflows/build.yml/badge.svg)](https://github.com/besaoct/gramobase/actions)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-orange.svg)](https://github.com/besaoct/gramobase/blob/main/LICENSE)
6
6
  [![Tests Passed](https://img.shields.io/badge/Tests-40%2F40%20Passed-brightgreen.svg)](https://github.com/besaoct/gramobase/actions)
7
- [![Coverage: 100%](https://img.shields.io/badge/Coverage-100%25-brightgreen.svg)](https://github.com/besaoct/gramobase/actions)
7
+ [![Coverage](https://codecov.io/gh/besaoct/gramobase/branch/main/graph/badge.svg)](https://codecov.io/gh/besaoct/gramobase)
8
8
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/besaoct/gramobase/pulls)
9
9
 
10
10
  **Telegram as a free, infinite, production-grade backend database.**
11
11
 
12
- Every Telegram channel is a collection. Every message is a document. Zero infrastructure needed — all you need is a free Telegram account.
12
+ Every Telegram channel is a collection. Every message is a document. Zero infrastructure needed — all you need a free Telegram account.
13
+
14
+ ---
15
+
16
+ ## Quick Start
17
+
18
+ ### 1. Installation
19
+
20
+ ```bash
21
+ npm install gramobase
22
+ ```
23
+
24
+ ### 2. Setup
25
+
26
+ Initialize your project with the auto-detect wizard:
27
+
28
+ ```bash
29
+ npx gramobase init
30
+ ```
31
+
32
+ This interactive wizard walks you through setting up your backend. It features **Auto-Detect** technology and **Anti-Flood** scaling: it will ask you how many Bot Tokens you want to configure (for 30 req/s scaling per bot). Provide your tokens, and leave the Channel ID blank! By sending a message in your channel, `gramobase` will automatically fetch your hidden Telegram Channel ID for you and generate your `.env` and `gramobase.config.ts`.
33
+
34
+ **Prerequisites:**
35
+ 1. Create a bot via [@BotFather](https://t.me/BotFather) on Telegram — takes 30 seconds
36
+ 2. Create a private Telegram channel
37
+ 3. Add your bot as an **Administrator** with full permissions to the channel
38
+
39
+ ### 3. Usage
13
40
 
14
41
  ```ts
15
42
  import { createClient } from 'gramobase';
16
43
  import { z } from 'zod';
17
44
 
18
45
  const db = await createClient({
19
- botToken: process.env.BOT_TOKEN!,
20
- channelId: process.env.CHANNEL_ID!,
46
+ botToken: process.env.GRAMOBASE_BOT_TOKEN_1!,
47
+ channelId: process.env.GRAMOBASE_CHANNEL_ID!,
21
48
  }).connect();
22
49
 
23
50
  const users = db.collection('users', {
@@ -28,6 +55,44 @@ await users.insertOne({ name: 'Aarav', email: 'aarav@example.com' });
28
55
  const user = await users.findOne({ name: { $eq: 'Aarav' } });
29
56
  ```
30
57
 
58
+ > [!IMPORTANT]
59
+ > **Next.js & Hot-Reloading Environments:**
60
+ > In development environments that support hot-reloading (like Next.js), the module cache is frequently cleared, which can instantiate multiple database clients and trigger write lease lock collisions. To prevent this, choose one of the following methods to cache your client:
61
+ >
62
+ > **Option A: Built-in `global` config option (Recommended)**
63
+ > Pass `global: true` in your client config to automatically handle global caching inside the package:
64
+ >
65
+ > ```ts
66
+ > import { createClient } from 'gramobase';
67
+ >
68
+ > const client = createClient({
69
+ > botToken: process.env.GRAMOBASE_BOT_TOKEN_1!,
70
+ > channelId: process.env.GRAMOBASE_CHANNEL_ID!,
71
+ > global: true, // Automatically caches the client instance globally
72
+ > });
73
+ > const db = await client.connect();
74
+ > ```
75
+ >
76
+ > **Option B: Manual `globalThis` caching**
77
+ > Alternatively, you can manage caching manually on the global scope:
78
+ >
79
+ > ```ts
80
+ > import { createClient } from 'gramobase';
81
+ >
82
+ > const globalForDb = globalThis as unknown as { dbClient: any };
83
+ >
84
+ > export async function getDb() {
85
+ > if (!globalForDb.dbClient) {
86
+ > const client = createClient({
87
+ > botToken: process.env.GRAMOBASE_BOT_TOKEN_1!,
88
+ > channelId: process.env.GRAMOBASE_CHANNEL_ID!,
89
+ > });
90
+ > globalForDb.dbClient = await client.connect();
91
+ > }
92
+ > return globalForDb.dbClient;
93
+ > }
94
+ > ```
95
+
31
96
  ---
32
97
 
33
98
  ## Why gramobase?
@@ -45,12 +110,6 @@ const user = await users.findOne({ name: { $eq: 'Aarav' } });
45
110
 
46
111
  ---
47
112
 
48
- ## Installation
49
-
50
- ```bash
51
- npm install gramobase
52
- ```
53
-
54
113
  ### Running Tests
55
114
 
56
115
  To run the suite of 40 unit tests checking the ORM, caching, queue/worker pooling, and authentication:
@@ -59,20 +118,6 @@ To run the suite of 40 unit tests checking the ORM, caching, queue/worker poolin
59
118
  npm run test
60
119
  ```
61
120
 
62
- ### Setup
63
-
64
- ```bash
65
- npx gramobase init
66
- ```
67
-
68
- This interactive wizard walks you through setting up your backend.
69
- It features **Auto-Detect** technology and **Anti-Flood** scaling: it will ask you how many Bot Tokens you want to configure (for 30 req/s scaling per bot). Provide your tokens, and leave the Channel ID blank! By sending a message in your channel, `gramobase` will automatically fetch your hidden Telegram Channel ID for you and generate your `.env` and `gramobase.config.ts`.
70
-
71
- **Prerequisites:**
72
- 1. Create a bot via [@BotFather](https://t.me/BotFather) on Telegram — takes 30 seconds
73
- 2. Create a private Telegram channel
74
- 3. Add your bot as an **Administrator** with full permissions to the channel
75
-
76
121
  ---
77
122
 
78
123
  ## Core API
@@ -274,17 +319,17 @@ Developer API (ORM, Auth, Files, Realtime)
274
319
 
275
320
  Telegram Bot API ─────────────────────────────────┐
276
321
  │ │
277
- Private Channel File Storage Realtime
278
- (messages = docs, (sendDocument, (webhook +
279
- pinned = index) file_id refs) SSE bridge)
322
+ Private Channel File Storage Realtime
323
+ (messages = docs, (sendDocument, (webhook +
324
+ pinned = registry) file_id refs) SSE bridge)
280
325
  ```
281
326
 
282
327
  ### Storage model
283
328
 
284
329
  - Each collection maps to a private Telegram channel (or shares one via namespaced message tags)
285
- - A **pinned index message** stores `{ idmsgId }` for O(1) lookups
330
+ - A **pinned registry message** acts as a distributed write lock across processes and stores the master index mapping of collection names to their respective index message IDs (`{ collectionNameindexMsgId }`)
331
+ - Individual **index messages** (unpinned) store `{ id → msgId }` for O(1) document lookups
286
332
  - The **Write-Ahead Log** channel stores operation logs for crash recovery
287
- - A **registry message** acts as a distributed write lock across processes
288
333
 
289
334
  ### Limits
290
335
 
@@ -308,11 +353,27 @@ npx gramobase migrate # run pending migrations
308
353
  npx gramobase migrate --rollback 1 # rollback last migration
309
354
  npx gramobase migrate --status # show migration history
310
355
  npx gramobase generate post --fields "title:string,views:number"
311
- npx gramobase studio # open browser UI (v0.2)
356
+ npx gramobase studio # open browser UI (see below)
357
+ npx gramobase studio --port 9000 # custom port
312
358
  ```
313
359
 
314
360
  ---
315
361
 
362
+ ## gramobase Studio
363
+
364
+ `npx gramobase studio` spins up a local browser admin panel at **http://localhost:4242**. Zero extra dependencies — it reads your `.env` and starts a Node HTTP server backed by a real live `GramoBase` client.
365
+
366
+ **Features:**
367
+ - 🔍 **Collection Browser** — Paginated table view of every document in any collection
368
+ - 📋 **Sortable Columns** — Click any column header to sort ASC/DESC
369
+ - 🔎 **Filter Bar** — Filter with `field:value` syntax or free-text regex search
370
+ - 📄 **JSON Inspector** — Click any row to open a full syntax-highlighted document drawer
371
+ - 📡 **Realtime Feed** — Live SSE stream of all insert/update/delete/WAL events
372
+ - ⚡ **Stats Dashboard** — Cache hit rate, bytes used, worker pool status, token count
373
+ - 🤖 **Bot Info Panel** — Bot username, channel ID, token pool capacity
374
+
375
+ ---
376
+
316
377
  ## Configuration
317
378
 
318
379
  ```ts
@@ -327,6 +388,7 @@ const db = createClient({
327
388
  concurrency?: number, // max concurrent requests per token, default 25
328
389
  webhookUrl?: string, // enables webhook mode for realtime
329
390
  debug?: boolean,
391
+ global?: boolean, // auto-cache client globally (default: false)
330
392
  });
331
393
  ```
332
394
 
@@ -1,4 +1,3 @@
1
- import { z } from 'zod';
2
1
  import TelegramBot from 'node-telegram-bot-api';
3
2
  import EventEmitter from 'eventemitter3';
4
3
 
@@ -11,7 +10,11 @@ interface GramoBaseDocument {
11
10
  [key: string]: unknown;
12
11
  }
13
12
  type WithId<T> = T & GramoBaseDocument;
14
- interface CollectionConfig<T extends z.ZodType> {
13
+ interface SchemaLike<Output = any> {
14
+ parse(data: unknown): Output;
15
+ }
16
+ type InferSchema<T extends SchemaLike> = ReturnType<T['parse']>;
17
+ interface CollectionConfig<T extends SchemaLike> {
15
18
  schema: T;
16
19
  /** Channel override — uses the default if omitted */
17
20
  channelId?: string | undefined;
@@ -157,6 +160,8 @@ interface GramoBaseConfig {
157
160
  webhookUrl?: string | undefined;
158
161
  /** Enable verbose debug logging */
159
162
  debug?: boolean | undefined;
163
+ /** Auto-cache client globally on globalThis in dev mode to prevent lease collisions in serverless/hot-reloading environments */
164
+ global?: boolean | undefined;
160
165
  }
161
166
 
162
167
  interface WorkerStats {
@@ -198,4 +203,4 @@ declare class BotWorkerPool extends EventEmitter {
198
203
  destroy(): Promise<void>;
199
204
  }
200
205
 
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 };
206
+ export { type AuthConfig as A, BotWorkerPool as B, type CollectionConfig as C, type FileRecord as F, type GramoBaseEvent as G, type InferSchema as I, type Lease as L, type Migration as M, type SchemaLike 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 Session as f, type UpdateOperators as g, type User as h, type WalEntry as i, type WalOpType as j, type WithId as k };
@@ -1,4 +1,3 @@
1
- import { z } from 'zod';
2
1
  import TelegramBot from 'node-telegram-bot-api';
3
2
  import EventEmitter from 'eventemitter3';
4
3
 
@@ -11,7 +10,11 @@ interface GramoBaseDocument {
11
10
  [key: string]: unknown;
12
11
  }
13
12
  type WithId<T> = T & GramoBaseDocument;
14
- interface CollectionConfig<T extends z.ZodType> {
13
+ interface SchemaLike<Output = any> {
14
+ parse(data: unknown): Output;
15
+ }
16
+ type InferSchema<T extends SchemaLike> = ReturnType<T['parse']>;
17
+ interface CollectionConfig<T extends SchemaLike> {
15
18
  schema: T;
16
19
  /** Channel override — uses the default if omitted */
17
20
  channelId?: string | undefined;
@@ -157,6 +160,8 @@ interface GramoBaseConfig {
157
160
  webhookUrl?: string | undefined;
158
161
  /** Enable verbose debug logging */
159
162
  debug?: boolean | undefined;
163
+ /** Auto-cache client globally on globalThis in dev mode to prevent lease collisions in serverless/hot-reloading environments */
164
+ global?: boolean | undefined;
160
165
  }
161
166
 
162
167
  interface WorkerStats {
@@ -198,4 +203,4 @@ declare class BotWorkerPool extends EventEmitter {
198
203
  destroy(): Promise<void>;
199
204
  }
200
205
 
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 };
206
+ export { type AuthConfig as A, BotWorkerPool as B, type CollectionConfig as C, type FileRecord as F, type GramoBaseEvent as G, type InferSchema as I, type Lease as L, type Migration as M, type SchemaLike 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 Session as f, type UpdateOperators as g, type User as h, type WalEntry as i, type WalOpType as j, type WithId as k };
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { B as BotWorkerPool, e as GramoBaseDocument, i as WalOpType, h as WalEntry, C as CollectionConfig, j as WithId, c as Filter, d as FindOptions, f as UpdateOperators, A as AuthConfig, g as User, S as Session } from './BotWorkerPool-9ndHQt2g.js';
2
+ import { B as BotWorkerPool, L as Lease, e as GramoBaseDocument, j as WalOpType, i as WalEntry, S as SchemaLike, C as CollectionConfig, I as InferSchema, k as WithId, c as Filter, d as FindOptions, g as UpdateOperators, A as AuthConfig, h as User, f as Session } from './BotWorkerPool-h_8a20dt.js';
3
3
  import EventEmitter from 'eventemitter3';
4
4
 
5
5
  /**
@@ -41,6 +41,41 @@ declare class HotCache extends EventEmitter {
41
41
  private docKey;
42
42
  }
43
43
 
44
+ /**
45
+ * Registry uses a pinned Telegram message as a distributed lock.
46
+ *
47
+ * When a gramobase instance starts up, it reads the registry message.
48
+ * If no lease exists or the existing lease is expired, it writes a new
49
+ * lease with its own instanceId and begins sending heartbeats.
50
+ *
51
+ * This prevents multiple writer processes from corrupting the index
52
+ * (last-write-wins races on the pinned index message).
53
+ *
54
+ * Read-only operations are always permitted. Only index mutations
55
+ * require holding the write lease.
56
+ */
57
+ declare class Registry {
58
+ private pool;
59
+ private channelId;
60
+ private debug;
61
+ private state;
62
+ private readonly instanceId;
63
+ constructor(pool: BotWorkerPool, channelId: string, debug?: boolean);
64
+ acquireWriteLease(options?: {
65
+ wait?: boolean;
66
+ }): Promise<Lease>;
67
+ releaseWriteLease(): Promise<void>;
68
+ forceRelease(): Promise<void>;
69
+ isWriteLeaseHeld(): Promise<boolean>;
70
+ private heartbeat;
71
+ private readRegistryMessage;
72
+ private writeRegistryMessage;
73
+ getCollectionIndexMsgId(collection: string): Promise<number | null>;
74
+ setCollectionIndexMsgId(collection: string, msgId: number): Promise<void>;
75
+ getInstanceId(): string;
76
+ getCurrentLease(): Lease | null;
77
+ }
78
+
44
79
  interface IndexMessage {
45
80
  collection: string;
46
81
  entries: Record<string, number>;
@@ -60,10 +95,12 @@ interface IndexMessage {
60
95
  declare class TelegramStorage {
61
96
  private pool;
62
97
  private defaultChannelId;
98
+ private registry;
63
99
  private debug;
64
100
  private encryptionKey;
65
101
  private indexMsgIds;
66
- constructor(pool: BotWorkerPool, defaultChannelId: string, encryptionKey?: string, debug?: boolean);
102
+ constructor(pool: BotWorkerPool, defaultChannelId: string, registry: Registry, encryptionKey?: string, debug?: boolean);
103
+ private readRawMessageText;
67
104
  loadIndex(collection: string, channelId?: string): Promise<IndexMessage>;
68
105
  saveIndex(index: IndexMessage, channelId?: string): Promise<void>;
69
106
  writeDocument(doc: GramoBaseDocument, channelId?: string): Promise<number>;
@@ -122,7 +159,7 @@ declare class WriteAheadLog {
122
159
  getCurrentSeq(): number;
123
160
  }
124
161
 
125
- type DocOf<T extends z.ZodType> = WithId<z.infer<T>>;
162
+ type DocOf<T extends SchemaLike> = WithId<InferSchema<T>>;
126
163
  /**
127
164
  * Collection<T> is the main ORM interface.
128
165
  *
@@ -133,7 +170,7 @@ type DocOf<T extends z.ZodType> = WithId<z.infer<T>>;
133
170
  * - Index management (id → msgId map, stored as a pinned message)
134
171
  * - Filter/sort/skip/limit in memory after cache warm-up
135
172
  */
136
- declare class Collection<T extends z.ZodType> {
173
+ declare class Collection<T extends SchemaLike> {
137
174
  private name;
138
175
  private config;
139
176
  private cache;
@@ -143,18 +180,18 @@ declare class Collection<T extends z.ZodType> {
143
180
  private indexLoaded;
144
181
  constructor(name: string, config: CollectionConfig<T>, cache: HotCache, storage: TelegramStorage, wal: WriteAheadLog, defaultChannelId: string);
145
182
  ensureIndexLoaded(): Promise<void>;
146
- insertOne(data: z.infer<T>): Promise<DocOf<T>>;
147
- insertMany(items: z.infer<T>[]): Promise<DocOf<T>[]>;
183
+ insertOne(data: InferSchema<T>): Promise<DocOf<T>>;
184
+ insertMany(items: InferSchema<T>[]): Promise<DocOf<T>[]>;
148
185
  findById(id: string): Promise<DocOf<T> | null>;
149
- findOne(filter?: Filter<z.infer<T>>): Promise<DocOf<T> | null>;
150
- find(options?: FindOptions<z.infer<T>>): Promise<DocOf<T>[]>;
151
- count(filter?: Filter<z.infer<T>>): Promise<number>;
152
- updateOne(filter: Filter<z.infer<T>>, update: UpdateOperators<z.infer<T>>): Promise<DocOf<T> | null>;
153
- updateMany(filter: Filter<z.infer<T>>, update: UpdateOperators<z.infer<T>>): Promise<DocOf<T>[]>;
154
- findByIdAndUpdate(id: string, update: UpdateOperators<z.infer<T>>): Promise<DocOf<T> | null>;
186
+ findOne(filter?: Filter<InferSchema<T>>): Promise<DocOf<T> | null>;
187
+ find(options?: FindOptions<InferSchema<T>>): Promise<DocOf<T>[]>;
188
+ count(filter?: Filter<InferSchema<T>>): Promise<number>;
189
+ updateOne(filter: Filter<InferSchema<T>>, update: UpdateOperators<InferSchema<T>>): Promise<DocOf<T> | null>;
190
+ updateMany(filter: Filter<InferSchema<T>>, update: UpdateOperators<InferSchema<T>>): Promise<DocOf<T>[]>;
191
+ findByIdAndUpdate(id: string, update: UpdateOperators<InferSchema<T>>): Promise<DocOf<T> | null>;
155
192
  private applyUpdate;
156
- deleteOne(filter: Filter<z.infer<T>>): Promise<boolean>;
157
- deleteMany(filter: Filter<z.infer<T>>): Promise<number>;
193
+ deleteOne(filter: Filter<InferSchema<T>>): Promise<boolean>;
194
+ deleteMany(filter: Filter<InferSchema<T>>): Promise<number>;
158
195
  deleteById(id: string): Promise<boolean>;
159
196
  private flushIndex;
160
197
  private matchesFilter;
@@ -215,4 +252,4 @@ declare class GramoBaseAuth {
215
252
  requireRoleMiddleware(role: string): (req: any, res: any, next: any) => void;
216
253
  }
217
254
 
218
- export { Collection as C, GramoBaseAuth as G };
255
+ export { Collection as C, GramoBaseAuth as G, Registry as R };
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { B as BotWorkerPool, e as GramoBaseDocument, i as WalOpType, h as WalEntry, C as CollectionConfig, j as WithId, c as Filter, d as FindOptions, f as UpdateOperators, A as AuthConfig, g as User, S as Session } from './BotWorkerPool-9ndHQt2g.cjs';
2
+ import { B as BotWorkerPool, L as Lease, e as GramoBaseDocument, j as WalOpType, i as WalEntry, S as SchemaLike, C as CollectionConfig, I as InferSchema, k as WithId, c as Filter, d as FindOptions, g as UpdateOperators, A as AuthConfig, h as User, f as Session } from './BotWorkerPool-h_8a20dt.cjs';
3
3
  import EventEmitter from 'eventemitter3';
4
4
 
5
5
  /**
@@ -41,6 +41,41 @@ declare class HotCache extends EventEmitter {
41
41
  private docKey;
42
42
  }
43
43
 
44
+ /**
45
+ * Registry uses a pinned Telegram message as a distributed lock.
46
+ *
47
+ * When a gramobase instance starts up, it reads the registry message.
48
+ * If no lease exists or the existing lease is expired, it writes a new
49
+ * lease with its own instanceId and begins sending heartbeats.
50
+ *
51
+ * This prevents multiple writer processes from corrupting the index
52
+ * (last-write-wins races on the pinned index message).
53
+ *
54
+ * Read-only operations are always permitted. Only index mutations
55
+ * require holding the write lease.
56
+ */
57
+ declare class Registry {
58
+ private pool;
59
+ private channelId;
60
+ private debug;
61
+ private state;
62
+ private readonly instanceId;
63
+ constructor(pool: BotWorkerPool, channelId: string, debug?: boolean);
64
+ acquireWriteLease(options?: {
65
+ wait?: boolean;
66
+ }): Promise<Lease>;
67
+ releaseWriteLease(): Promise<void>;
68
+ forceRelease(): Promise<void>;
69
+ isWriteLeaseHeld(): Promise<boolean>;
70
+ private heartbeat;
71
+ private readRegistryMessage;
72
+ private writeRegistryMessage;
73
+ getCollectionIndexMsgId(collection: string): Promise<number | null>;
74
+ setCollectionIndexMsgId(collection: string, msgId: number): Promise<void>;
75
+ getInstanceId(): string;
76
+ getCurrentLease(): Lease | null;
77
+ }
78
+
44
79
  interface IndexMessage {
45
80
  collection: string;
46
81
  entries: Record<string, number>;
@@ -60,10 +95,12 @@ interface IndexMessage {
60
95
  declare class TelegramStorage {
61
96
  private pool;
62
97
  private defaultChannelId;
98
+ private registry;
63
99
  private debug;
64
100
  private encryptionKey;
65
101
  private indexMsgIds;
66
- constructor(pool: BotWorkerPool, defaultChannelId: string, encryptionKey?: string, debug?: boolean);
102
+ constructor(pool: BotWorkerPool, defaultChannelId: string, registry: Registry, encryptionKey?: string, debug?: boolean);
103
+ private readRawMessageText;
67
104
  loadIndex(collection: string, channelId?: string): Promise<IndexMessage>;
68
105
  saveIndex(index: IndexMessage, channelId?: string): Promise<void>;
69
106
  writeDocument(doc: GramoBaseDocument, channelId?: string): Promise<number>;
@@ -122,7 +159,7 @@ declare class WriteAheadLog {
122
159
  getCurrentSeq(): number;
123
160
  }
124
161
 
125
- type DocOf<T extends z.ZodType> = WithId<z.infer<T>>;
162
+ type DocOf<T extends SchemaLike> = WithId<InferSchema<T>>;
126
163
  /**
127
164
  * Collection<T> is the main ORM interface.
128
165
  *
@@ -133,7 +170,7 @@ type DocOf<T extends z.ZodType> = WithId<z.infer<T>>;
133
170
  * - Index management (id → msgId map, stored as a pinned message)
134
171
  * - Filter/sort/skip/limit in memory after cache warm-up
135
172
  */
136
- declare class Collection<T extends z.ZodType> {
173
+ declare class Collection<T extends SchemaLike> {
137
174
  private name;
138
175
  private config;
139
176
  private cache;
@@ -143,18 +180,18 @@ declare class Collection<T extends z.ZodType> {
143
180
  private indexLoaded;
144
181
  constructor(name: string, config: CollectionConfig<T>, cache: HotCache, storage: TelegramStorage, wal: WriteAheadLog, defaultChannelId: string);
145
182
  ensureIndexLoaded(): Promise<void>;
146
- insertOne(data: z.infer<T>): Promise<DocOf<T>>;
147
- insertMany(items: z.infer<T>[]): Promise<DocOf<T>[]>;
183
+ insertOne(data: InferSchema<T>): Promise<DocOf<T>>;
184
+ insertMany(items: InferSchema<T>[]): Promise<DocOf<T>[]>;
148
185
  findById(id: string): Promise<DocOf<T> | null>;
149
- findOne(filter?: Filter<z.infer<T>>): Promise<DocOf<T> | null>;
150
- find(options?: FindOptions<z.infer<T>>): Promise<DocOf<T>[]>;
151
- count(filter?: Filter<z.infer<T>>): Promise<number>;
152
- updateOne(filter: Filter<z.infer<T>>, update: UpdateOperators<z.infer<T>>): Promise<DocOf<T> | null>;
153
- updateMany(filter: Filter<z.infer<T>>, update: UpdateOperators<z.infer<T>>): Promise<DocOf<T>[]>;
154
- findByIdAndUpdate(id: string, update: UpdateOperators<z.infer<T>>): Promise<DocOf<T> | null>;
186
+ findOne(filter?: Filter<InferSchema<T>>): Promise<DocOf<T> | null>;
187
+ find(options?: FindOptions<InferSchema<T>>): Promise<DocOf<T>[]>;
188
+ count(filter?: Filter<InferSchema<T>>): Promise<number>;
189
+ updateOne(filter: Filter<InferSchema<T>>, update: UpdateOperators<InferSchema<T>>): Promise<DocOf<T> | null>;
190
+ updateMany(filter: Filter<InferSchema<T>>, update: UpdateOperators<InferSchema<T>>): Promise<DocOf<T>[]>;
191
+ findByIdAndUpdate(id: string, update: UpdateOperators<InferSchema<T>>): Promise<DocOf<T> | null>;
155
192
  private applyUpdate;
156
- deleteOne(filter: Filter<z.infer<T>>): Promise<boolean>;
157
- deleteMany(filter: Filter<z.infer<T>>): Promise<number>;
193
+ deleteOne(filter: Filter<InferSchema<T>>): Promise<boolean>;
194
+ deleteMany(filter: Filter<InferSchema<T>>): Promise<number>;
158
195
  deleteById(id: string): Promise<boolean>;
159
196
  private flushIndex;
160
197
  private matchesFilter;
@@ -215,4 +252,4 @@ declare class GramoBaseAuth {
215
252
  requireRoleMiddleware(role: string): (req: any, res: any, next: any) => void;
216
253
  }
217
254
 
218
- export { Collection as C, GramoBaseAuth as G };
255
+ export { Collection as C, GramoBaseAuth as G, Registry as R };
@@ -1,5 +1,5 @@
1
1
  import 'zod';
2
- import '../BotWorkerPool-9ndHQt2g.cjs';
3
- export { G as GramoBaseAuth } from '../GramoBaseAuth-CHNn2_e5.cjs';
2
+ import '../BotWorkerPool-h_8a20dt.cjs';
3
+ export { G as GramoBaseAuth } from '../GramoBaseAuth-ObeOxqKj.cjs';
4
4
  import 'node-telegram-bot-api';
5
5
  import 'eventemitter3';
@@ -1,5 +1,5 @@
1
1
  import 'zod';
2
- import '../BotWorkerPool-9ndHQt2g.js';
3
- export { G as GramoBaseAuth } from '../GramoBaseAuth-00fg0u_b.js';
2
+ import '../BotWorkerPool-h_8a20dt.js';
3
+ export { G as GramoBaseAuth } from '../GramoBaseAuth-DfRKq2yW.js';
4
4
  import 'node-telegram-bot-api';
5
5
  import 'eventemitter3';
@@ -154,9 +154,8 @@ export default config;
154
154
  ${chalk__default.default.gray("\u2514\u2500")} gramobase/migrations/
155
155
 
156
156
  ${chalk__default.default.bold("Next steps:")}
157
- ${chalk__default.default.cyan("1.")} Add your bot token and channel ID to .env
158
- ${chalk__default.default.cyan("2.")} Run ${chalk__default.default.bold("gramobase migrate")} to initialize the database
159
- ${chalk__default.default.cyan("3.")} Import and use: ${chalk__default.default.gray("import { createClient } from 'gramobase'")}
157
+ ${chalk__default.default.cyan("1.")} Run ${chalk__default.default.bold("npx gramobase migrate")} to initialize the database
158
+ ${chalk__default.default.cyan("2.")} Import and use: ${chalk__default.default.gray("import { createClient } from 'gramobase'")}
160
159
  `);
161
160
  });
162
161
  program.command("migrate").description("Run pending migrations").option("--rollback <steps>", "Rollback N migration steps", "0").option("--status", "Show migration status").action(async (opts) => {
@@ -229,18 +228,25 @@ export type ${capitalize(safeName)} = z.infer<typeof ${safeName}Schema>;
229
228
  ${chalk__default.default.green("\u2713")} Generated ${chalk__default.default.cyan(`gramobase/${safeName}.schema.ts`)}
230
229
  `);
231
230
  });
232
- program.command("studio").description("Open the gramobase browser studio UI").option("--port <port>", "Port to listen on", "4242").action((opts) => {
231
+ program.command("studio").description("Open the gramobase browser studio UI").option("--port <port>", "Port to listen on", "4242").action(async (opts) => {
233
232
  const port = parseInt(opts.port, 10);
234
233
  if (isNaN(port) || port < 1 || port > 65535) {
235
234
  console.error(chalk__default.default.red(" Error: Invalid port number"));
236
235
  process.exit(1);
237
236
  }
238
- console.log(`
239
- ${chalk__default.default.bold.cyan("gramobase studio")}
240
- `);
241
- console.log(` ${chalk__default.default.gray("Open")} ${chalk__default.default.cyan(`http://localhost:${port}`)} ${chalk__default.default.gray("in your browser")}
237
+ const spinner = ora__default.default("Starting gramobase studio...").start();
238
+ try {
239
+ const { startStudio } = await import(new URL("../../dist/studio/server.js", (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('gramobase.cjs', document.baseURI).href))).href);
240
+ await startStudio(port, process.cwd());
241
+ spinner.succeed(chalk__default.default.green("gramobase studio is running!"));
242
+ console.log(`
243
+ ${chalk__default.default.bold("Studio")} ${chalk__default.default.cyan(`http://localhost:${port}`)}
244
+ ${chalk__default.default.gray("Press Ctrl+C to stop.")}
242
245
  `);
243
- console.log(chalk__default.default.yellow(" Studio UI coming in v0.2.0 \u2014 contribute at github.com/yourusername/gramobase\n"));
246
+ } catch (e) {
247
+ spinner.fail(chalk__default.default.red("Failed to start studio: " + (e?.message || String(e))));
248
+ process.exit(1);
249
+ }
244
250
  });
245
251
  function capitalize(s) {
246
252
  return s.charAt(0).toUpperCase() + s.slice(1);