remult-sqlite-github 0.1.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.
@@ -0,0 +1,487 @@
1
+ import { SqlImplementation, SqlCommand, EntityMetadata, FieldMetadata, SqlDatabase } from 'remult';
2
+ import Database from 'better-sqlite3';
3
+
4
+ /**
5
+ * Database type from better-sqlite3
6
+ */
7
+ type DatabaseType = Database.Database;
8
+ /**
9
+ * SQLite configuration options
10
+ */
11
+ interface SqliteOptions {
12
+ /**
13
+ * Enable foreign key constraints
14
+ * @default true
15
+ */
16
+ foreignKeys?: boolean;
17
+ /**
18
+ * SQLite busy timeout in milliseconds
19
+ * @default 5000
20
+ */
21
+ busyTimeout?: number;
22
+ /**
23
+ * Additional PRAGMA statements to execute on connection
24
+ */
25
+ pragmas?: Record<string, string | number | boolean>;
26
+ }
27
+ /**
28
+ * GitHub authentication configuration
29
+ */
30
+ interface GitHubAuthConfig {
31
+ /**
32
+ * Personal Access Token for authentication
33
+ * Use either token OR appId/privateKey/installationId
34
+ */
35
+ token?: string;
36
+ /**
37
+ * GitHub App ID for app-based authentication
38
+ */
39
+ appId?: number;
40
+ /**
41
+ * GitHub App private key (PEM format)
42
+ */
43
+ privateKey?: string;
44
+ /**
45
+ * GitHub App installation ID
46
+ */
47
+ installationId?: number;
48
+ }
49
+ /**
50
+ * GitHub repository configuration
51
+ */
52
+ interface GitHubRepoConfig extends GitHubAuthConfig {
53
+ /**
54
+ * Repository owner (username or organization)
55
+ */
56
+ owner: string;
57
+ /**
58
+ * Repository name
59
+ */
60
+ repo: string;
61
+ /**
62
+ * Branch to use for storage
63
+ * @default "main"
64
+ */
65
+ branch?: string;
66
+ /**
67
+ * Path prefix in repository
68
+ * @default ""
69
+ */
70
+ path?: string;
71
+ }
72
+ /**
73
+ * Sync configuration options
74
+ */
75
+ interface SyncConfig {
76
+ /**
77
+ * Interval between automatic snapshots in milliseconds
78
+ * @default 30000 (30 seconds)
79
+ */
80
+ snapshotInterval?: number;
81
+ /**
82
+ * Only create snapshot if there are changes
83
+ * @default true
84
+ */
85
+ snapshotOnChange?: boolean;
86
+ /**
87
+ * Enable WAL mode segmentation for incremental uploads
88
+ * @default false
89
+ */
90
+ enableWal?: boolean;
91
+ /**
92
+ * WAL file size threshold before creating new segment (bytes)
93
+ * Only applies when enableWal is true
94
+ * @default 1048576 (1MB)
95
+ */
96
+ walThreshold?: number;
97
+ /**
98
+ * Maximum retry attempts for failed operations
99
+ * @default 3
100
+ */
101
+ maxRetries?: number;
102
+ /**
103
+ * Enable gzip compression for uploads
104
+ * @default false
105
+ */
106
+ compression?: boolean;
107
+ }
108
+ /**
109
+ * Main configuration options for the GitHub data provider
110
+ */
111
+ interface BetterSqlite3GitHubOptions {
112
+ /**
113
+ * Local SQLite database file path
114
+ */
115
+ file: string;
116
+ /**
117
+ * SQLite configuration options
118
+ */
119
+ sqliteOptions?: SqliteOptions;
120
+ /**
121
+ * GitHub repository configuration
122
+ */
123
+ github: GitHubRepoConfig;
124
+ /**
125
+ * Sync configuration options
126
+ */
127
+ sync?: SyncConfig;
128
+ /**
129
+ * Event callback for sync events
130
+ */
131
+ onEvent?: (event: GitHubSyncEvent) => void;
132
+ /**
133
+ * Enable verbose logging
134
+ * @default false
135
+ */
136
+ verbose?: boolean;
137
+ }
138
+ /**
139
+ * Events emitted during sync operations
140
+ */
141
+ type GitHubSyncEvent = {
142
+ type: "snapshot_uploaded";
143
+ sha: string;
144
+ size: number;
145
+ compressed: boolean;
146
+ } | {
147
+ type: "wal_uploaded";
148
+ index: number;
149
+ size: number;
150
+ } | {
151
+ type: "recovery_started";
152
+ branch: string;
153
+ } | {
154
+ type: "recovery_completed";
155
+ sha: string;
156
+ } | {
157
+ type: "commit_created";
158
+ sha: string;
159
+ message: string;
160
+ } | {
161
+ type: "sync_error";
162
+ error: Error;
163
+ context: string;
164
+ willRetry: boolean;
165
+ } | {
166
+ type: "rate_limit_hit";
167
+ resetAt: Date;
168
+ remaining: number;
169
+ } | {
170
+ type: "initialized";
171
+ recovered: boolean;
172
+ };
173
+ /**
174
+ * Internal configuration for GitHubOperations
175
+ */
176
+ interface GitHubConfig {
177
+ owner: string;
178
+ repo: string;
179
+ branch: string;
180
+ path: string;
181
+ token?: string;
182
+ appId?: number;
183
+ privateKey?: string;
184
+ installationId?: number;
185
+ }
186
+ /**
187
+ * Result of a file retrieval operation
188
+ */
189
+ interface GitHubFileResult {
190
+ content: Buffer;
191
+ sha: string;
192
+ size: number;
193
+ }
194
+ /**
195
+ * File listing entry
196
+ */
197
+ interface GitHubFileEntry {
198
+ name: string;
199
+ path: string;
200
+ sha: string;
201
+ size: number;
202
+ type: "file" | "dir";
203
+ }
204
+ /**
205
+ * Rate limit information
206
+ */
207
+ interface RateLimitInfo {
208
+ remaining: number;
209
+ resetAt: Date;
210
+ limit: number;
211
+ }
212
+ /**
213
+ * Result of a recovery operation
214
+ */
215
+ interface RecoveryResult {
216
+ recovered: boolean;
217
+ sha?: string;
218
+ size?: number;
219
+ compressed?: boolean;
220
+ }
221
+ /**
222
+ * Snapshot metadata stored alongside the database file
223
+ */
224
+ interface SnapshotMetadata {
225
+ timestamp: string;
226
+ size: number;
227
+ checksum: string;
228
+ compressed: boolean;
229
+ walIndex?: number;
230
+ }
231
+ /**
232
+ * Internal sync manager options
233
+ */
234
+ interface GitHubSyncManagerOptions {
235
+ dbPath: string;
236
+ github: GitHubConfig;
237
+ sqliteOptions?: SqliteOptions;
238
+ sync: Required<SyncConfig>;
239
+ onEvent?: (event: GitHubSyncEvent) => void;
240
+ verbose: boolean;
241
+ }
242
+
243
+ /**
244
+ * Orchestrates database sync operations with GitHub
245
+ */
246
+ declare class GitHubSyncManager {
247
+ private options;
248
+ private github;
249
+ private recovery;
250
+ private db;
251
+ private hasChanges;
252
+ private currentSha;
253
+ private snapshotTimer;
254
+ private isInitialized;
255
+ private isClosed;
256
+ constructor(options: GitHubSyncManagerOptions);
257
+ /**
258
+ * Initialize the sync manager: recover from GitHub if needed, open database
259
+ */
260
+ initialize(): Promise<DatabaseType>;
261
+ /**
262
+ * Open the SQLite database with configured options
263
+ */
264
+ private openDatabase;
265
+ /**
266
+ * Called after database writes to track changes
267
+ */
268
+ onWrite(): Promise<void>;
269
+ /**
270
+ * Create a snapshot and upload to GitHub
271
+ */
272
+ snapshot(): Promise<void>;
273
+ /**
274
+ * Upload file with automatic conflict resolution
275
+ */
276
+ private uploadWithConflictRetry;
277
+ /**
278
+ * Force an immediate sync (alias for snapshot)
279
+ */
280
+ forceSync(): Promise<void>;
281
+ /**
282
+ * Close the sync manager and perform final sync
283
+ */
284
+ close(): Promise<void>;
285
+ /**
286
+ * Start the automatic snapshot timer
287
+ */
288
+ private startSnapshotTimer;
289
+ /**
290
+ * Get the raw database instance
291
+ */
292
+ get database(): DatabaseType | null;
293
+ /**
294
+ * Check if there are pending changes
295
+ */
296
+ get hasPendingChanges(): boolean;
297
+ /**
298
+ * Check if the manager is initialized
299
+ */
300
+ get initialized(): boolean;
301
+ /**
302
+ * Emit an event if handler is registered
303
+ */
304
+ private emitEvent;
305
+ /**
306
+ * Log a message if verbose mode is enabled
307
+ */
308
+ private log;
309
+ }
310
+
311
+ /**
312
+ * GitHub REST API operations wrapper with retry logic and rate limit handling
313
+ */
314
+ declare class GitHubOperations {
315
+ private config;
316
+ private maxRetries;
317
+ private cachedToken;
318
+ private tokenExpiry;
319
+ private onEvent?;
320
+ private verbose;
321
+ constructor(config: GitHubConfig, maxRetries?: number, onEvent?: (event: GitHubSyncEvent) => void, verbose?: boolean);
322
+ /**
323
+ * Get a file from the repository
324
+ */
325
+ getFile(path: string): Promise<GitHubFileResult | null>;
326
+ /**
327
+ * Get a large file using the blob API
328
+ */
329
+ private getBlob;
330
+ /**
331
+ * Create or update a file in the repository
332
+ * Returns the new commit SHA
333
+ */
334
+ putFile(path: string, content: Buffer, message: string, sha?: string): Promise<string>;
335
+ /**
336
+ * Delete a file from the repository
337
+ */
338
+ deleteFile(path: string, sha: string, message: string): Promise<void>;
339
+ /**
340
+ * List files in a directory
341
+ */
342
+ listFiles(path: string): Promise<GitHubFileEntry[]>;
343
+ /**
344
+ * Check current rate limit status
345
+ */
346
+ checkRateLimit(): Promise<RateLimitInfo>;
347
+ /**
348
+ * Ensure the branch exists, create if it doesn't
349
+ * For empty repos, we skip branch creation since files can be pushed directly
350
+ */
351
+ ensureBranch(): Promise<void>;
352
+ /**
353
+ * Get the full path including configured prefix
354
+ */
355
+ private getFullPath;
356
+ /**
357
+ * Make an authenticated fetch request
358
+ */
359
+ private fetch;
360
+ /**
361
+ * Get authentication token (PAT or GitHub App installation token)
362
+ */
363
+ private getAuthToken;
364
+ /**
365
+ * Generate a JWT for GitHub App authentication
366
+ */
367
+ private generateJWT;
368
+ /**
369
+ * Base64 URL encode a string
370
+ */
371
+ private base64UrlEncode;
372
+ /**
373
+ * Execute a function with retry logic and exponential backoff
374
+ */
375
+ private withRetry;
376
+ /**
377
+ * Handle rate limit headers from response
378
+ */
379
+ private handleRateLimitHeaders;
380
+ /**
381
+ * Sleep for the specified duration
382
+ */
383
+ private sleep;
384
+ /**
385
+ * Emit an event if handler is registered
386
+ */
387
+ private emitEvent;
388
+ /**
389
+ * Log a message if verbose mode is enabled
390
+ */
391
+ private log;
392
+ }
393
+ /**
394
+ * Custom error for conflict (SHA mismatch) situations
395
+ */
396
+ declare class ConflictError extends Error {
397
+ constructor(message: string);
398
+ }
399
+
400
+ /**
401
+ * A Remult data provider that syncs SQLite to GitHub
402
+ */
403
+ declare class BetterSqlite3GitHubDataProvider implements SqlImplementation {
404
+ private syncManager;
405
+ private _isInitialized;
406
+ private initPromise;
407
+ private innerProvider;
408
+ constructor(options: BetterSqlite3GitHubOptions);
409
+ /**
410
+ * Initialize the data provider
411
+ * This must be called before using the provider
412
+ */
413
+ init(): Promise<void>;
414
+ private doInit;
415
+ /**
416
+ * Ensure the provider is initialized
417
+ */
418
+ private ensureInitialized;
419
+ private getProvider;
420
+ getLimitSqlSyntax(limit: number, offset: number): string;
421
+ createCommand(): SqlCommand;
422
+ transaction(action: (sql: SqlImplementation) => Promise<void>): Promise<void>;
423
+ entityIsUsedForTheFirstTime(entity: EntityMetadata): Promise<void>;
424
+ end(): Promise<void>;
425
+ get orderByNullsFirst(): boolean | undefined;
426
+ get afterMutation(): VoidFunction | undefined;
427
+ get supportsJsonColumnType(): boolean | undefined;
428
+ get doesNotSupportReturningSyntax(): boolean;
429
+ ensureSchema(entities: EntityMetadata<any>[]): Promise<void>;
430
+ wrapIdentifier(name: string): string;
431
+ addColumnSqlSyntax(x: FieldMetadata, dbName: string, isAlterTable: boolean): string;
432
+ /**
433
+ * Check if a SQL statement is a write operation
434
+ */
435
+ private isWriteOperation;
436
+ /**
437
+ * Force an immediate sync to GitHub
438
+ */
439
+ forceSync(): Promise<void>;
440
+ /**
441
+ * Create a snapshot and upload to GitHub
442
+ */
443
+ snapshot(): Promise<void>;
444
+ /**
445
+ * Close the provider and perform final sync
446
+ */
447
+ close(): Promise<void>;
448
+ /**
449
+ * Get the raw better-sqlite3 database instance
450
+ */
451
+ get rawDatabase(): DatabaseType | null;
452
+ /**
453
+ * Get the sync manager instance
454
+ */
455
+ get sync(): GitHubSyncManager;
456
+ /**
457
+ * Check if the provider is initialized
458
+ */
459
+ get isInitialized(): boolean;
460
+ }
461
+
462
+ /**
463
+ * Create a GitHub-synced SQLite data provider for Remult
464
+ *
465
+ * @param options - Configuration options for the provider
466
+ * @returns A factory function that creates an initialized SqlDatabase
467
+ *
468
+ * @example
469
+ * ```typescript
470
+ * import { createGitHubDataProvider } from "remult-sqlite-github";
471
+ *
472
+ * const api = remultApi({
473
+ * dataProvider: createGitHubDataProvider({
474
+ * file: "./mydb.sqlite",
475
+ * github: {
476
+ * owner: "your-username",
477
+ * repo: "your-database-repo",
478
+ * token: process.env.GITHUB_TOKEN,
479
+ * },
480
+ * }),
481
+ * entities: [Task],
482
+ * });
483
+ * ```
484
+ */
485
+ declare function createGitHubDataProvider(options: BetterSqlite3GitHubOptions): () => Promise<SqlDatabase>;
486
+
487
+ export { BetterSqlite3GitHubDataProvider, type BetterSqlite3GitHubOptions, ConflictError, type DatabaseType, type GitHubAuthConfig, GitHubOperations, type GitHubRepoConfig, type GitHubSyncEvent, GitHubSyncManager, type RateLimitInfo, type RecoveryResult, type SnapshotMetadata, type SqliteOptions, type SyncConfig, createGitHubDataProvider };