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.
- package/LICENSE +21 -0
- package/README.md +326 -0
- package/dist/index.cjs +1100 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +487 -0
- package/dist/index.d.ts +487 -0
- package/dist/index.js +1068 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1068 @@
|
|
|
1
|
+
import { SqlDatabase } from 'remult';
|
|
2
|
+
import { BetterSqlite3DataProvider } from 'remult/remult-better-sqlite3';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as crypto from 'crypto';
|
|
6
|
+
import * as zlib from 'zlib';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
import Database from 'better-sqlite3';
|
|
9
|
+
|
|
10
|
+
// src/index.ts
|
|
11
|
+
var GITHUB_API_BASE = "https://api.github.com";
|
|
12
|
+
var GitHubOperations = class {
|
|
13
|
+
config;
|
|
14
|
+
maxRetries;
|
|
15
|
+
cachedToken = null;
|
|
16
|
+
tokenExpiry = 0;
|
|
17
|
+
onEvent;
|
|
18
|
+
verbose;
|
|
19
|
+
constructor(config, maxRetries = 3, onEvent, verbose = false) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
this.maxRetries = maxRetries;
|
|
22
|
+
this.onEvent = onEvent;
|
|
23
|
+
this.verbose = verbose;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get a file from the repository
|
|
27
|
+
*/
|
|
28
|
+
async getFile(path3) {
|
|
29
|
+
const fullPath = this.getFullPath(path3);
|
|
30
|
+
const url = `${GITHUB_API_BASE}/repos/${this.config.owner}/${this.config.repo}/contents/${fullPath}?ref=${this.config.branch}`;
|
|
31
|
+
const response = await this.withRetry(() => this.fetch(url));
|
|
32
|
+
if (response.status === 404) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Failed to get file ${path3}: ${response.status} ${response.statusText}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
if (data.content) {
|
|
42
|
+
const content = Buffer.from(data.content, "base64");
|
|
43
|
+
return {
|
|
44
|
+
content,
|
|
45
|
+
sha: data.sha,
|
|
46
|
+
size: data.size
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (data.sha && data.size > 0) {
|
|
50
|
+
return this.getBlob(data.sha, data.size);
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get a large file using the blob API
|
|
56
|
+
*/
|
|
57
|
+
async getBlob(sha, size) {
|
|
58
|
+
const url = `${GITHUB_API_BASE}/repos/${this.config.owner}/${this.config.repo}/git/blobs/${sha}`;
|
|
59
|
+
const response = await this.withRetry(() => this.fetch(url));
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Failed to get blob ${sha}: ${response.status} ${response.statusText}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const data = await response.json();
|
|
66
|
+
const content = Buffer.from(data.content, "base64");
|
|
67
|
+
return {
|
|
68
|
+
content,
|
|
69
|
+
sha,
|
|
70
|
+
size
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Create or update a file in the repository
|
|
75
|
+
* Returns the new commit SHA
|
|
76
|
+
*/
|
|
77
|
+
async putFile(path3, content, message, sha) {
|
|
78
|
+
const fullPath = this.getFullPath(path3);
|
|
79
|
+
const url = `${GITHUB_API_BASE}/repos/${this.config.owner}/${this.config.repo}/contents/${fullPath}`;
|
|
80
|
+
const body = {
|
|
81
|
+
message,
|
|
82
|
+
content: content.toString("base64"),
|
|
83
|
+
branch: this.config.branch
|
|
84
|
+
};
|
|
85
|
+
if (sha) {
|
|
86
|
+
body.sha = sha;
|
|
87
|
+
}
|
|
88
|
+
const response = await this.withRetry(
|
|
89
|
+
() => this.fetch(url, {
|
|
90
|
+
method: "PUT",
|
|
91
|
+
body: JSON.stringify(body)
|
|
92
|
+
}),
|
|
93
|
+
true
|
|
94
|
+
// Allow conflict retry
|
|
95
|
+
);
|
|
96
|
+
if (response.status === 409) {
|
|
97
|
+
throw new ConflictError(
|
|
98
|
+
`Conflict updating ${path3}: file was modified concurrently`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
const errorText = await response.text();
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Failed to put file ${path3}: ${response.status} ${response.statusText} - ${errorText}`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const data = await response.json();
|
|
108
|
+
this.emitEvent({
|
|
109
|
+
type: "commit_created",
|
|
110
|
+
sha: data.commit.sha,
|
|
111
|
+
message
|
|
112
|
+
});
|
|
113
|
+
return data.content.sha;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Delete a file from the repository
|
|
117
|
+
*/
|
|
118
|
+
async deleteFile(path3, sha, message) {
|
|
119
|
+
const fullPath = this.getFullPath(path3);
|
|
120
|
+
const url = `${GITHUB_API_BASE}/repos/${this.config.owner}/${this.config.repo}/contents/${fullPath}`;
|
|
121
|
+
const response = await this.withRetry(
|
|
122
|
+
() => this.fetch(url, {
|
|
123
|
+
method: "DELETE",
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
message,
|
|
126
|
+
sha,
|
|
127
|
+
branch: this.config.branch
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
if (!response.ok && response.status !== 404) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Failed to delete file ${path3}: ${response.status} ${response.statusText}`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* List files in a directory
|
|
139
|
+
*/
|
|
140
|
+
async listFiles(path3) {
|
|
141
|
+
const fullPath = this.getFullPath(path3);
|
|
142
|
+
const url = `${GITHUB_API_BASE}/repos/${this.config.owner}/${this.config.repo}/contents/${fullPath}?ref=${this.config.branch}`;
|
|
143
|
+
const response = await this.withRetry(() => this.fetch(url));
|
|
144
|
+
if (response.status === 404) {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`Failed to list files at ${path3}: ${response.status} ${response.statusText}`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const data = await response.json();
|
|
153
|
+
if (!Array.isArray(data)) {
|
|
154
|
+
return [
|
|
155
|
+
{
|
|
156
|
+
name: data.name,
|
|
157
|
+
path: data.path,
|
|
158
|
+
sha: data.sha,
|
|
159
|
+
size: data.size,
|
|
160
|
+
type: data.type
|
|
161
|
+
}
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
return data.map(
|
|
165
|
+
(item) => ({
|
|
166
|
+
name: item.name,
|
|
167
|
+
path: item.path,
|
|
168
|
+
sha: item.sha,
|
|
169
|
+
size: item.size,
|
|
170
|
+
type: item.type
|
|
171
|
+
})
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Check current rate limit status
|
|
176
|
+
*/
|
|
177
|
+
async checkRateLimit() {
|
|
178
|
+
const url = `${GITHUB_API_BASE}/rate_limit`;
|
|
179
|
+
const response = await this.fetch(url);
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
throw new Error(`Failed to check rate limit: ${response.statusText}`);
|
|
182
|
+
}
|
|
183
|
+
const data = await response.json();
|
|
184
|
+
return {
|
|
185
|
+
remaining: data.resources.core.remaining,
|
|
186
|
+
resetAt: new Date(data.resources.core.reset * 1e3),
|
|
187
|
+
limit: data.resources.core.limit
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Ensure the branch exists, create if it doesn't
|
|
192
|
+
* For empty repos, we skip branch creation since files can be pushed directly
|
|
193
|
+
*/
|
|
194
|
+
async ensureBranch() {
|
|
195
|
+
const url = `${GITHUB_API_BASE}/repos/${this.config.owner}/${this.config.repo}/branches/${this.config.branch}`;
|
|
196
|
+
const response = await this.fetch(url);
|
|
197
|
+
if (response.ok) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (response.status !== 404) {
|
|
201
|
+
const errorText = await response.text();
|
|
202
|
+
throw new Error(
|
|
203
|
+
`Failed to check branch '${this.config.branch}': ${response.status} ${response.statusText}. ${errorText}`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const repoUrl = `${GITHUB_API_BASE}/repos/${this.config.owner}/${this.config.repo}`;
|
|
207
|
+
const repoResponse = await this.fetch(repoUrl);
|
|
208
|
+
if (!repoResponse.ok) {
|
|
209
|
+
const errorText = await repoResponse.text();
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Failed to get repository info for '${this.config.owner}/${this.config.repo}': ${repoResponse.status} ${repoResponse.statusText}. ${errorText}`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const repoData = await repoResponse.json();
|
|
215
|
+
const defaultBranch = repoData.default_branch;
|
|
216
|
+
const refUrl = `${GITHUB_API_BASE}/repos/${this.config.owner}/${this.config.repo}/git/refs/heads/${defaultBranch}`;
|
|
217
|
+
const refResponse = await this.fetch(refUrl);
|
|
218
|
+
if (!refResponse.ok) {
|
|
219
|
+
if (refResponse.status === 409 || refResponse.status === 404) {
|
|
220
|
+
this.log(
|
|
221
|
+
`Repository appears to be empty. Branch '${this.config.branch}' will be created on first snapshot.`
|
|
222
|
+
);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const errorText = await refResponse.text();
|
|
226
|
+
throw new Error(
|
|
227
|
+
`Failed to get default branch ref: ${refResponse.status} ${refResponse.statusText}. ${errorText}`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (this.config.branch === defaultBranch) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const refData = await refResponse.json();
|
|
234
|
+
const sha = refData.object.sha;
|
|
235
|
+
const createUrl = `${GITHUB_API_BASE}/repos/${this.config.owner}/${this.config.repo}/git/refs`;
|
|
236
|
+
const createResponse = await this.fetch(createUrl, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
body: JSON.stringify({
|
|
239
|
+
ref: `refs/heads/${this.config.branch}`,
|
|
240
|
+
sha
|
|
241
|
+
})
|
|
242
|
+
});
|
|
243
|
+
if (!createResponse.ok && createResponse.status !== 422) {
|
|
244
|
+
const errorText = await createResponse.text();
|
|
245
|
+
throw new Error(
|
|
246
|
+
`Failed to create branch '${this.config.branch}': ${createResponse.status} ${createResponse.statusText}. ${errorText}`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
this.log(`Created branch: ${this.config.branch}`);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get the full path including configured prefix
|
|
253
|
+
*/
|
|
254
|
+
getFullPath(path3) {
|
|
255
|
+
if (!this.config.path) {
|
|
256
|
+
return path3;
|
|
257
|
+
}
|
|
258
|
+
return `${this.config.path}/${path3}`.replace(/\/+/g, "/");
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Make an authenticated fetch request
|
|
262
|
+
*/
|
|
263
|
+
async fetch(url, options = {}) {
|
|
264
|
+
const token = await this.getAuthToken();
|
|
265
|
+
const headers = {
|
|
266
|
+
Authorization: `Bearer ${token}`,
|
|
267
|
+
Accept: "application/vnd.github+json",
|
|
268
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
269
|
+
...options.headers
|
|
270
|
+
};
|
|
271
|
+
if (options.body) {
|
|
272
|
+
headers["Content-Type"] = "application/json";
|
|
273
|
+
}
|
|
274
|
+
return fetch(url, {
|
|
275
|
+
...options,
|
|
276
|
+
headers
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Get authentication token (PAT or GitHub App installation token)
|
|
281
|
+
*/
|
|
282
|
+
async getAuthToken() {
|
|
283
|
+
if (this.config.token) {
|
|
284
|
+
return this.config.token;
|
|
285
|
+
}
|
|
286
|
+
if (this.cachedToken && Date.now() < this.tokenExpiry) {
|
|
287
|
+
return this.cachedToken;
|
|
288
|
+
}
|
|
289
|
+
if (!this.config.appId || !this.config.privateKey || !this.config.installationId) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
"Either token or GitHub App credentials (appId, privateKey, installationId) must be provided"
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
const jwt = this.generateJWT(this.config.appId, this.config.privateKey);
|
|
295
|
+
const response = await fetch(
|
|
296
|
+
`${GITHUB_API_BASE}/app/installations/${this.config.installationId}/access_tokens`,
|
|
297
|
+
{
|
|
298
|
+
method: "POST",
|
|
299
|
+
headers: {
|
|
300
|
+
Authorization: `Bearer ${jwt}`,
|
|
301
|
+
Accept: "application/vnd.github+json",
|
|
302
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
`Failed to get installation token: ${response.statusText}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
const data = await response.json();
|
|
312
|
+
this.cachedToken = data.token;
|
|
313
|
+
this.tokenExpiry = Date.now() + 55 * 60 * 1e3;
|
|
314
|
+
return this.cachedToken;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Generate a JWT for GitHub App authentication
|
|
318
|
+
*/
|
|
319
|
+
generateJWT(appId, privateKey) {
|
|
320
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
321
|
+
const payload = {
|
|
322
|
+
iat: now - 60,
|
|
323
|
+
// Issued 60 seconds ago to account for clock drift
|
|
324
|
+
exp: now + 10 * 60,
|
|
325
|
+
// Expires in 10 minutes
|
|
326
|
+
iss: appId.toString()
|
|
327
|
+
};
|
|
328
|
+
const header = { alg: "RS256", typ: "JWT" };
|
|
329
|
+
const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
|
|
330
|
+
const encodedPayload = this.base64UrlEncode(JSON.stringify(payload));
|
|
331
|
+
const signatureInput = `${encodedHeader}.${encodedPayload}`;
|
|
332
|
+
const signature = crypto.createSign("RSA-SHA256").update(signatureInput).sign(privateKey, "base64url");
|
|
333
|
+
return `${signatureInput}.${signature}`;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Base64 URL encode a string
|
|
337
|
+
*/
|
|
338
|
+
base64UrlEncode(str) {
|
|
339
|
+
return Buffer.from(str).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Execute a function with retry logic and exponential backoff
|
|
343
|
+
*/
|
|
344
|
+
async withRetry(fn, allowConflict = false) {
|
|
345
|
+
let lastError;
|
|
346
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
347
|
+
try {
|
|
348
|
+
const result = await fn();
|
|
349
|
+
if (result instanceof Response) {
|
|
350
|
+
await this.handleRateLimitHeaders(result);
|
|
351
|
+
}
|
|
352
|
+
return result;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
355
|
+
if (error instanceof ConflictError && !allowConflict) {
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
if (error instanceof Error && error.message.includes("rate limit")) {
|
|
359
|
+
const rateLimit = await this.checkRateLimit();
|
|
360
|
+
const waitTime = Math.max(
|
|
361
|
+
0,
|
|
362
|
+
rateLimit.resetAt.getTime() - Date.now()
|
|
363
|
+
);
|
|
364
|
+
this.emitEvent({
|
|
365
|
+
type: "rate_limit_hit",
|
|
366
|
+
resetAt: rateLimit.resetAt,
|
|
367
|
+
remaining: rateLimit.remaining
|
|
368
|
+
});
|
|
369
|
+
if (waitTime > 0 && waitTime < 6e4) {
|
|
370
|
+
this.log(`Rate limited, waiting ${waitTime}ms`);
|
|
371
|
+
await this.sleep(waitTime);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
this.emitEvent({
|
|
376
|
+
type: "sync_error",
|
|
377
|
+
error: lastError,
|
|
378
|
+
context: "github_operation",
|
|
379
|
+
willRetry: attempt < this.maxRetries
|
|
380
|
+
});
|
|
381
|
+
if (attempt < this.maxRetries) {
|
|
382
|
+
const delay = Math.min(1e3 * Math.pow(2, attempt), 3e4);
|
|
383
|
+
this.log(`Retry attempt ${attempt + 1} after ${delay}ms`);
|
|
384
|
+
await this.sleep(delay);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
throw lastError || new Error("Unknown error");
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Handle rate limit headers from response
|
|
392
|
+
*/
|
|
393
|
+
async handleRateLimitHeaders(response) {
|
|
394
|
+
const remaining = parseInt(
|
|
395
|
+
response.headers.get("x-ratelimit-remaining") || "1000",
|
|
396
|
+
10
|
|
397
|
+
);
|
|
398
|
+
const resetTimestamp = parseInt(
|
|
399
|
+
response.headers.get("x-ratelimit-reset") || "0",
|
|
400
|
+
10
|
|
401
|
+
);
|
|
402
|
+
if (remaining === 0 && resetTimestamp > 0) {
|
|
403
|
+
const resetAt = new Date(resetTimestamp * 1e3);
|
|
404
|
+
const waitTime = Math.max(0, resetAt.getTime() - Date.now());
|
|
405
|
+
this.emitEvent({
|
|
406
|
+
type: "rate_limit_hit",
|
|
407
|
+
resetAt,
|
|
408
|
+
remaining: 0
|
|
409
|
+
});
|
|
410
|
+
if (waitTime > 0 && waitTime < 6e4) {
|
|
411
|
+
this.log(`Rate limit exhausted, waiting ${waitTime}ms`);
|
|
412
|
+
await this.sleep(waitTime);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Sleep for the specified duration
|
|
418
|
+
*/
|
|
419
|
+
sleep(ms) {
|
|
420
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Emit an event if handler is registered
|
|
424
|
+
*/
|
|
425
|
+
emitEvent(event) {
|
|
426
|
+
if (this.onEvent) {
|
|
427
|
+
this.onEvent(event);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Log a message if verbose mode is enabled
|
|
432
|
+
*/
|
|
433
|
+
log(message) {
|
|
434
|
+
if (this.verbose) {
|
|
435
|
+
console.log(`[github-operations] ${message}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
var ConflictError = class extends Error {
|
|
440
|
+
constructor(message) {
|
|
441
|
+
super(message);
|
|
442
|
+
this.name = "ConflictError";
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
var gunzip2 = promisify(zlib.gunzip);
|
|
446
|
+
var SNAPSHOT_DB_NAME = "snapshot.db";
|
|
447
|
+
var SNAPSHOT_DB_GZ_NAME = "snapshot.db.gz";
|
|
448
|
+
var SNAPSHOT_META_NAME = "snapshot.meta.json";
|
|
449
|
+
var RecoveryManager = class {
|
|
450
|
+
github;
|
|
451
|
+
dbPath;
|
|
452
|
+
onEvent;
|
|
453
|
+
verbose;
|
|
454
|
+
constructor(github, dbPath, onEvent, verbose = false) {
|
|
455
|
+
this.github = github;
|
|
456
|
+
this.dbPath = dbPath;
|
|
457
|
+
this.onEvent = onEvent;
|
|
458
|
+
this.verbose = verbose;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Check if local database exists
|
|
462
|
+
*/
|
|
463
|
+
localDatabaseExists() {
|
|
464
|
+
return fs.existsSync(this.dbPath);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Find the latest snapshot in the repository
|
|
468
|
+
*/
|
|
469
|
+
async findLatestSnapshot() {
|
|
470
|
+
try {
|
|
471
|
+
const compressedFile = await this.github.getFile(SNAPSHOT_DB_GZ_NAME);
|
|
472
|
+
if (compressedFile) {
|
|
473
|
+
return {
|
|
474
|
+
path: SNAPSHOT_DB_GZ_NAME,
|
|
475
|
+
sha: compressedFile.sha,
|
|
476
|
+
compressed: true
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
const uncompressedFile = await this.github.getFile(SNAPSHOT_DB_NAME);
|
|
480
|
+
if (uncompressedFile) {
|
|
481
|
+
return {
|
|
482
|
+
path: SNAPSHOT_DB_NAME,
|
|
483
|
+
sha: uncompressedFile.sha,
|
|
484
|
+
compressed: false
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
} catch (error) {
|
|
489
|
+
this.log(`Error finding snapshot: ${error}`);
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Recover database from GitHub snapshot
|
|
495
|
+
*/
|
|
496
|
+
async recover() {
|
|
497
|
+
this.emitEvent({
|
|
498
|
+
type: "recovery_started",
|
|
499
|
+
branch: ""
|
|
500
|
+
// Branch is handled by GitHubOperations
|
|
501
|
+
});
|
|
502
|
+
try {
|
|
503
|
+
const snapshot = await this.findLatestSnapshot();
|
|
504
|
+
if (!snapshot) {
|
|
505
|
+
this.log("No snapshot found in repository");
|
|
506
|
+
return { recovered: false };
|
|
507
|
+
}
|
|
508
|
+
this.log(`Found snapshot: ${snapshot.path}`);
|
|
509
|
+
const file = await this.github.getFile(snapshot.path);
|
|
510
|
+
if (!file) {
|
|
511
|
+
this.log("Failed to download snapshot");
|
|
512
|
+
return { recovered: false };
|
|
513
|
+
}
|
|
514
|
+
let dbContent;
|
|
515
|
+
if (snapshot.compressed) {
|
|
516
|
+
this.log("Decompressing snapshot...");
|
|
517
|
+
dbContent = await gunzip2(file.content);
|
|
518
|
+
} else {
|
|
519
|
+
dbContent = file.content;
|
|
520
|
+
}
|
|
521
|
+
const dir = path.dirname(this.dbPath);
|
|
522
|
+
if (!fs.existsSync(dir)) {
|
|
523
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
524
|
+
}
|
|
525
|
+
fs.writeFileSync(this.dbPath, dbContent);
|
|
526
|
+
this.log(`Database recovered to ${this.dbPath}`);
|
|
527
|
+
await this.getSnapshotMetadata();
|
|
528
|
+
this.emitEvent({
|
|
529
|
+
type: "recovery_completed",
|
|
530
|
+
sha: file.sha
|
|
531
|
+
});
|
|
532
|
+
return {
|
|
533
|
+
recovered: true,
|
|
534
|
+
sha: file.sha,
|
|
535
|
+
size: dbContent.length,
|
|
536
|
+
compressed: snapshot.compressed
|
|
537
|
+
};
|
|
538
|
+
} catch (error) {
|
|
539
|
+
this.log(`Recovery error: ${error}`);
|
|
540
|
+
this.emitEvent({
|
|
541
|
+
type: "sync_error",
|
|
542
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
543
|
+
context: "recovery",
|
|
544
|
+
willRetry: false
|
|
545
|
+
});
|
|
546
|
+
return { recovered: false };
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Get snapshot metadata from repository
|
|
551
|
+
*/
|
|
552
|
+
async getSnapshotMetadata() {
|
|
553
|
+
try {
|
|
554
|
+
const file = await this.github.getFile(SNAPSHOT_META_NAME);
|
|
555
|
+
if (!file) {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
return JSON.parse(file.content.toString("utf-8"));
|
|
559
|
+
} catch {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Get the current snapshot SHA from the repository
|
|
565
|
+
*/
|
|
566
|
+
async getCurrentSnapshotSha() {
|
|
567
|
+
const snapshot = await this.findLatestSnapshot();
|
|
568
|
+
return snapshot?.sha || null;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Emit an event if handler is registered
|
|
572
|
+
*/
|
|
573
|
+
emitEvent(event) {
|
|
574
|
+
if (this.onEvent) {
|
|
575
|
+
this.onEvent(event);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Log a message if verbose mode is enabled
|
|
580
|
+
*/
|
|
581
|
+
log(message) {
|
|
582
|
+
if (this.verbose) {
|
|
583
|
+
console.log(`[recovery] ${message}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// src/sync/sync-manager.ts
|
|
589
|
+
var gzip2 = promisify(zlib.gzip);
|
|
590
|
+
var SNAPSHOT_DB_NAME2 = "snapshot.db";
|
|
591
|
+
var SNAPSHOT_DB_GZ_NAME2 = "snapshot.db.gz";
|
|
592
|
+
var SNAPSHOT_META_NAME2 = "snapshot.meta.json";
|
|
593
|
+
var GitHubSyncManager = class {
|
|
594
|
+
options;
|
|
595
|
+
github;
|
|
596
|
+
recovery;
|
|
597
|
+
db = null;
|
|
598
|
+
hasChanges = false;
|
|
599
|
+
currentSha = null;
|
|
600
|
+
snapshotTimer = null;
|
|
601
|
+
isInitialized = false;
|
|
602
|
+
isClosed = false;
|
|
603
|
+
constructor(options) {
|
|
604
|
+
this.options = options;
|
|
605
|
+
this.github = new GitHubOperations(
|
|
606
|
+
options.github,
|
|
607
|
+
options.sync.maxRetries,
|
|
608
|
+
options.onEvent,
|
|
609
|
+
options.verbose
|
|
610
|
+
);
|
|
611
|
+
this.recovery = new RecoveryManager(
|
|
612
|
+
this.github,
|
|
613
|
+
options.dbPath,
|
|
614
|
+
options.onEvent,
|
|
615
|
+
options.verbose
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Initialize the sync manager: recover from GitHub if needed, open database
|
|
620
|
+
*/
|
|
621
|
+
async initialize() {
|
|
622
|
+
if (this.isInitialized) {
|
|
623
|
+
return this.db;
|
|
624
|
+
}
|
|
625
|
+
this.log("Initializing sync manager...");
|
|
626
|
+
await this.github.ensureBranch();
|
|
627
|
+
let recovered = false;
|
|
628
|
+
if (!this.recovery.localDatabaseExists()) {
|
|
629
|
+
this.log("Local database not found, attempting recovery from GitHub...");
|
|
630
|
+
const result = await this.recovery.recover();
|
|
631
|
+
recovered = result.recovered;
|
|
632
|
+
if (recovered) {
|
|
633
|
+
this.currentSha = await this.recovery.getCurrentSnapshotSha();
|
|
634
|
+
}
|
|
635
|
+
} else {
|
|
636
|
+
this.log("Local database exists, fetching current SHA...");
|
|
637
|
+
this.currentSha = await this.recovery.getCurrentSnapshotSha();
|
|
638
|
+
}
|
|
639
|
+
this.db = this.openDatabase();
|
|
640
|
+
if (this.options.sync.snapshotInterval > 0) {
|
|
641
|
+
this.startSnapshotTimer();
|
|
642
|
+
}
|
|
643
|
+
this.isInitialized = true;
|
|
644
|
+
this.emitEvent({
|
|
645
|
+
type: "initialized",
|
|
646
|
+
recovered
|
|
647
|
+
});
|
|
648
|
+
return this.db;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Open the SQLite database with configured options
|
|
652
|
+
*/
|
|
653
|
+
openDatabase() {
|
|
654
|
+
const dir = path.dirname(this.options.dbPath);
|
|
655
|
+
if (dir && !fs.existsSync(dir)) {
|
|
656
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
657
|
+
this.log(`Created directory: ${dir}`);
|
|
658
|
+
}
|
|
659
|
+
const sqliteOptions = this.options.sqliteOptions || {};
|
|
660
|
+
const db = new Database(this.options.dbPath);
|
|
661
|
+
if (this.options.sync.enableWal) {
|
|
662
|
+
db.pragma("journal_mode = WAL");
|
|
663
|
+
}
|
|
664
|
+
if (sqliteOptions.foreignKeys !== false) {
|
|
665
|
+
db.pragma("foreign_keys = ON");
|
|
666
|
+
}
|
|
667
|
+
if (sqliteOptions.busyTimeout) {
|
|
668
|
+
db.pragma(`busy_timeout = ${sqliteOptions.busyTimeout}`);
|
|
669
|
+
}
|
|
670
|
+
if (sqliteOptions.pragmas) {
|
|
671
|
+
for (const [key, value] of Object.entries(sqliteOptions.pragmas)) {
|
|
672
|
+
db.pragma(`${key} = ${value}`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
this.log("Database opened successfully");
|
|
676
|
+
return db;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Called after database writes to track changes
|
|
680
|
+
*/
|
|
681
|
+
async onWrite() {
|
|
682
|
+
this.hasChanges = true;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Create a snapshot and upload to GitHub
|
|
686
|
+
*/
|
|
687
|
+
async snapshot() {
|
|
688
|
+
if (this.isClosed) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (this.options.sync.snapshotOnChange && !this.hasChanges) {
|
|
692
|
+
this.log("No changes detected, skipping snapshot");
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (!this.db) {
|
|
696
|
+
throw new Error("Database not initialized");
|
|
697
|
+
}
|
|
698
|
+
this.log("Creating snapshot...");
|
|
699
|
+
try {
|
|
700
|
+
if (this.options.sync.enableWal) {
|
|
701
|
+
this.db.pragma("wal_checkpoint(TRUNCATE)");
|
|
702
|
+
}
|
|
703
|
+
const dbContent = fs.readFileSync(this.options.dbPath);
|
|
704
|
+
const checksum = crypto.createHash("sha256").update(dbContent).digest("hex");
|
|
705
|
+
let uploadContent;
|
|
706
|
+
let filename;
|
|
707
|
+
const compressed = this.options.sync.compression;
|
|
708
|
+
if (compressed) {
|
|
709
|
+
uploadContent = await gzip2(dbContent);
|
|
710
|
+
filename = SNAPSHOT_DB_GZ_NAME2;
|
|
711
|
+
this.log(`Compressed ${dbContent.length} -> ${uploadContent.length} bytes`);
|
|
712
|
+
} else {
|
|
713
|
+
uploadContent = dbContent;
|
|
714
|
+
filename = SNAPSHOT_DB_NAME2;
|
|
715
|
+
}
|
|
716
|
+
await this.uploadWithConflictRetry(filename, uploadContent, checksum, compressed);
|
|
717
|
+
this.hasChanges = false;
|
|
718
|
+
this.emitEvent({
|
|
719
|
+
type: "snapshot_uploaded",
|
|
720
|
+
sha: this.currentSha,
|
|
721
|
+
size: uploadContent.length,
|
|
722
|
+
compressed
|
|
723
|
+
});
|
|
724
|
+
this.log(`Snapshot uploaded successfully (${uploadContent.length} bytes)`);
|
|
725
|
+
} catch (error) {
|
|
726
|
+
this.emitEvent({
|
|
727
|
+
type: "sync_error",
|
|
728
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
729
|
+
context: "snapshot",
|
|
730
|
+
willRetry: false
|
|
731
|
+
});
|
|
732
|
+
throw error;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Upload file with automatic conflict resolution
|
|
737
|
+
*/
|
|
738
|
+
async uploadWithConflictRetry(filename, content, checksum, compressed, retries = 3) {
|
|
739
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
740
|
+
try {
|
|
741
|
+
const newSha = await this.github.putFile(
|
|
742
|
+
filename,
|
|
743
|
+
content,
|
|
744
|
+
`Update database snapshot`,
|
|
745
|
+
this.currentSha || void 0
|
|
746
|
+
);
|
|
747
|
+
this.currentSha = newSha;
|
|
748
|
+
const metadata = {
|
|
749
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
750
|
+
size: content.length,
|
|
751
|
+
checksum,
|
|
752
|
+
compressed
|
|
753
|
+
};
|
|
754
|
+
const existingMeta = await this.github.getFile(SNAPSHOT_META_NAME2);
|
|
755
|
+
await this.github.putFile(
|
|
756
|
+
SNAPSHOT_META_NAME2,
|
|
757
|
+
Buffer.from(JSON.stringify(metadata, null, 2)),
|
|
758
|
+
`Update snapshot metadata`,
|
|
759
|
+
existingMeta?.sha
|
|
760
|
+
);
|
|
761
|
+
const oldFilename = compressed ? SNAPSHOT_DB_NAME2 : SNAPSHOT_DB_GZ_NAME2;
|
|
762
|
+
try {
|
|
763
|
+
const oldFile = await this.github.getFile(oldFilename);
|
|
764
|
+
if (oldFile) {
|
|
765
|
+
await this.github.deleteFile(
|
|
766
|
+
oldFilename,
|
|
767
|
+
oldFile.sha,
|
|
768
|
+
`Remove old snapshot format`
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
} catch {
|
|
772
|
+
}
|
|
773
|
+
return;
|
|
774
|
+
} catch (error) {
|
|
775
|
+
if (error instanceof ConflictError && attempt < retries - 1) {
|
|
776
|
+
this.log(`Conflict detected, refetching SHA and retrying (attempt ${attempt + 1})`);
|
|
777
|
+
this.currentSha = await this.recovery.getCurrentSnapshotSha();
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
throw error;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Force an immediate sync (alias for snapshot)
|
|
786
|
+
*/
|
|
787
|
+
async forceSync() {
|
|
788
|
+
const originalOnChange = this.options.sync.snapshotOnChange;
|
|
789
|
+
this.options.sync.snapshotOnChange = false;
|
|
790
|
+
try {
|
|
791
|
+
await this.snapshot();
|
|
792
|
+
} finally {
|
|
793
|
+
this.options.sync.snapshotOnChange = originalOnChange;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Close the sync manager and perform final sync
|
|
798
|
+
*/
|
|
799
|
+
async close() {
|
|
800
|
+
if (this.isClosed) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
this.log("Closing sync manager...");
|
|
804
|
+
this.isClosed = true;
|
|
805
|
+
if (this.snapshotTimer) {
|
|
806
|
+
clearInterval(this.snapshotTimer);
|
|
807
|
+
this.snapshotTimer = null;
|
|
808
|
+
}
|
|
809
|
+
if (this.hasChanges && this.db) {
|
|
810
|
+
try {
|
|
811
|
+
await this.snapshot();
|
|
812
|
+
} catch (error) {
|
|
813
|
+
this.log(`Error during final snapshot: ${error}`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (this.db) {
|
|
817
|
+
this.db.close();
|
|
818
|
+
this.db = null;
|
|
819
|
+
}
|
|
820
|
+
this.log("Sync manager closed");
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Start the automatic snapshot timer
|
|
824
|
+
*/
|
|
825
|
+
startSnapshotTimer() {
|
|
826
|
+
this.snapshotTimer = setInterval(() => {
|
|
827
|
+
this.snapshot().catch((error) => {
|
|
828
|
+
this.log(`Snapshot error: ${error}`);
|
|
829
|
+
});
|
|
830
|
+
}, this.options.sync.snapshotInterval);
|
|
831
|
+
if (this.snapshotTimer.unref) {
|
|
832
|
+
this.snapshotTimer.unref();
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Get the raw database instance
|
|
837
|
+
*/
|
|
838
|
+
get database() {
|
|
839
|
+
return this.db;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Check if there are pending changes
|
|
843
|
+
*/
|
|
844
|
+
get hasPendingChanges() {
|
|
845
|
+
return this.hasChanges;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Check if the manager is initialized
|
|
849
|
+
*/
|
|
850
|
+
get initialized() {
|
|
851
|
+
return this.isInitialized;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Emit an event if handler is registered
|
|
855
|
+
*/
|
|
856
|
+
emitEvent(event) {
|
|
857
|
+
if (this.options.onEvent) {
|
|
858
|
+
this.options.onEvent(event);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Log a message if verbose mode is enabled
|
|
863
|
+
*/
|
|
864
|
+
log(message) {
|
|
865
|
+
if (this.options.verbose) {
|
|
866
|
+
console.log(`[sync-manager] ${message}`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
// src/provider.ts
|
|
872
|
+
var DEFAULT_SYNC_CONFIG = {
|
|
873
|
+
snapshotInterval: 30 * 1e3,
|
|
874
|
+
// 30 seconds
|
|
875
|
+
snapshotOnChange: true,
|
|
876
|
+
enableWal: false,
|
|
877
|
+
walThreshold: 1024 * 1024,
|
|
878
|
+
// 1MB
|
|
879
|
+
maxRetries: 3,
|
|
880
|
+
compression: false
|
|
881
|
+
};
|
|
882
|
+
var BetterSqlite3GitHubDataProvider = class {
|
|
883
|
+
syncManager;
|
|
884
|
+
_isInitialized = false;
|
|
885
|
+
initPromise = null;
|
|
886
|
+
innerProvider = null;
|
|
887
|
+
constructor(options) {
|
|
888
|
+
const githubConfig = {
|
|
889
|
+
owner: options.github.owner,
|
|
890
|
+
repo: options.github.repo,
|
|
891
|
+
branch: options.github.branch || "main",
|
|
892
|
+
path: options.github.path || "",
|
|
893
|
+
token: options.github.token,
|
|
894
|
+
appId: options.github.appId,
|
|
895
|
+
privateKey: options.github.privateKey,
|
|
896
|
+
installationId: options.github.installationId
|
|
897
|
+
};
|
|
898
|
+
const syncConfig = {
|
|
899
|
+
...DEFAULT_SYNC_CONFIG,
|
|
900
|
+
...options.sync
|
|
901
|
+
};
|
|
902
|
+
const managerOptions = {
|
|
903
|
+
dbPath: options.file,
|
|
904
|
+
github: githubConfig,
|
|
905
|
+
sqliteOptions: options.sqliteOptions,
|
|
906
|
+
sync: syncConfig,
|
|
907
|
+
onEvent: options.onEvent,
|
|
908
|
+
verbose: options.verbose || false
|
|
909
|
+
};
|
|
910
|
+
this.syncManager = new GitHubSyncManager(managerOptions);
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Initialize the data provider
|
|
914
|
+
* This must be called before using the provider
|
|
915
|
+
*/
|
|
916
|
+
async init() {
|
|
917
|
+
if (this._isInitialized) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (this.initPromise) {
|
|
921
|
+
return this.initPromise;
|
|
922
|
+
}
|
|
923
|
+
this.initPromise = this.doInit();
|
|
924
|
+
await this.initPromise;
|
|
925
|
+
}
|
|
926
|
+
async doInit() {
|
|
927
|
+
const db = await this.syncManager.initialize();
|
|
928
|
+
this.innerProvider = new BetterSqlite3DataProvider(db);
|
|
929
|
+
this._isInitialized = true;
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Ensure the provider is initialized
|
|
933
|
+
*/
|
|
934
|
+
async ensureInitialized() {
|
|
935
|
+
if (!this._isInitialized) {
|
|
936
|
+
await this.init();
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
getProvider() {
|
|
940
|
+
if (!this.innerProvider) {
|
|
941
|
+
throw new Error(
|
|
942
|
+
"Provider not initialized. Call init() before using the provider."
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
return this.innerProvider;
|
|
946
|
+
}
|
|
947
|
+
// SqlImplementation interface methods
|
|
948
|
+
getLimitSqlSyntax(limit, offset) {
|
|
949
|
+
return this.getProvider().getLimitSqlSyntax(limit, offset);
|
|
950
|
+
}
|
|
951
|
+
createCommand() {
|
|
952
|
+
const command = this.getProvider().createCommand();
|
|
953
|
+
const originalExecute = command.execute.bind(command);
|
|
954
|
+
const syncManager = this.syncManager;
|
|
955
|
+
const isWriteOp = this.isWriteOperation.bind(this);
|
|
956
|
+
command.execute = async (sql) => {
|
|
957
|
+
const result = await originalExecute(sql);
|
|
958
|
+
if (isWriteOp(sql)) {
|
|
959
|
+
await syncManager.onWrite();
|
|
960
|
+
}
|
|
961
|
+
return result;
|
|
962
|
+
};
|
|
963
|
+
return command;
|
|
964
|
+
}
|
|
965
|
+
async transaction(action) {
|
|
966
|
+
await this.ensureInitialized();
|
|
967
|
+
return this.getProvider().transaction(action);
|
|
968
|
+
}
|
|
969
|
+
async entityIsUsedForTheFirstTime(entity) {
|
|
970
|
+
await this.ensureInitialized();
|
|
971
|
+
return this.getProvider().entityIsUsedForTheFirstTime(entity);
|
|
972
|
+
}
|
|
973
|
+
async end() {
|
|
974
|
+
await this.close();
|
|
975
|
+
}
|
|
976
|
+
// Passthrough properties from inner provider
|
|
977
|
+
get orderByNullsFirst() {
|
|
978
|
+
return this.innerProvider?.orderByNullsFirst;
|
|
979
|
+
}
|
|
980
|
+
get afterMutation() {
|
|
981
|
+
return this.innerProvider?.afterMutation;
|
|
982
|
+
}
|
|
983
|
+
get supportsJsonColumnType() {
|
|
984
|
+
return this.innerProvider?.supportsJsonColumnType;
|
|
985
|
+
}
|
|
986
|
+
get doesNotSupportReturningSyntax() {
|
|
987
|
+
return this.innerProvider?.doesNotSupportReturningSyntax ?? true;
|
|
988
|
+
}
|
|
989
|
+
// Additional methods from SqliteCoreDataProvider that may be called
|
|
990
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
991
|
+
async ensureSchema(entities) {
|
|
992
|
+
await this.ensureInitialized();
|
|
993
|
+
return this.getProvider().ensureSchema(entities);
|
|
994
|
+
}
|
|
995
|
+
wrapIdentifier(name) {
|
|
996
|
+
return this.getProvider().wrapIdentifier(name);
|
|
997
|
+
}
|
|
998
|
+
addColumnSqlSyntax(x, dbName, isAlterTable) {
|
|
999
|
+
return this.getProvider().addColumnSqlSyntax(x, dbName, isAlterTable);
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Check if a SQL statement is a write operation
|
|
1003
|
+
*/
|
|
1004
|
+
isWriteOperation(sql) {
|
|
1005
|
+
const upperSql = sql.trim().toUpperCase();
|
|
1006
|
+
return upperSql.startsWith("INSERT") || upperSql.startsWith("UPDATE") || upperSql.startsWith("DELETE") || upperSql.startsWith("CREATE") || upperSql.startsWith("DROP") || upperSql.startsWith("ALTER");
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Force an immediate sync to GitHub
|
|
1010
|
+
*/
|
|
1011
|
+
async forceSync() {
|
|
1012
|
+
await this.ensureInitialized();
|
|
1013
|
+
await this.syncManager.forceSync();
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Create a snapshot and upload to GitHub
|
|
1017
|
+
*/
|
|
1018
|
+
async snapshot() {
|
|
1019
|
+
await this.ensureInitialized();
|
|
1020
|
+
await this.syncManager.snapshot();
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Close the provider and perform final sync
|
|
1024
|
+
*/
|
|
1025
|
+
async close() {
|
|
1026
|
+
await this.syncManager.close();
|
|
1027
|
+
this._isInitialized = false;
|
|
1028
|
+
this.initPromise = null;
|
|
1029
|
+
this.innerProvider = null;
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Get the raw better-sqlite3 database instance
|
|
1033
|
+
*/
|
|
1034
|
+
get rawDatabase() {
|
|
1035
|
+
return this.syncManager.database;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Get the sync manager instance
|
|
1039
|
+
*/
|
|
1040
|
+
get sync() {
|
|
1041
|
+
return this.syncManager;
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Check if the provider is initialized
|
|
1045
|
+
*/
|
|
1046
|
+
get isInitialized() {
|
|
1047
|
+
return this._isInitialized;
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
// src/index.ts
|
|
1052
|
+
function createGitHubDataProvider(options) {
|
|
1053
|
+
let provider = null;
|
|
1054
|
+
let sqlDatabase = null;
|
|
1055
|
+
return async () => {
|
|
1056
|
+
if (sqlDatabase) {
|
|
1057
|
+
return sqlDatabase;
|
|
1058
|
+
}
|
|
1059
|
+
provider = new BetterSqlite3GitHubDataProvider(options);
|
|
1060
|
+
await provider.init();
|
|
1061
|
+
sqlDatabase = new SqlDatabase(provider);
|
|
1062
|
+
return sqlDatabase;
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
export { BetterSqlite3GitHubDataProvider, ConflictError, GitHubOperations, GitHubSyncManager, createGitHubDataProvider };
|
|
1067
|
+
//# sourceMappingURL=index.js.map
|
|
1068
|
+
//# sourceMappingURL=index.js.map
|