local-lite 0.0.7 → 0.0.10
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 +36 -0
- package/dist/backups/cloud-storage.d.ts +61 -0
- package/dist/backups/cloud-storage.js +295 -0
- package/dist/backups/index.d.ts +14 -0
- package/dist/backups/index.js +32 -0
- package/dist/config/index.js +1 -0
- package/dist/config/types.d.ts +9 -0
- package/dist/core/client.d.ts +22 -0
- package/dist/core/client.js +26 -13
- package/dist/core/schema-manager.d.ts +6 -0
- package/dist/core/schema-manager.js +25 -0
- package/dist/core/table-manager.d.ts +9 -0
- package/dist/core/table-manager.js +13 -0
- package/package.json +146 -140
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ Built around the **Transactional Outbox Pattern**, `local-lite` ensures instant,
|
|
|
19
19
|
* **[Configuration Reference](https://georgechitechi.github.io/local-lite/#/configuration)** - Options for `defineSyncConfig()`, retries, and loggers
|
|
20
20
|
* **[Local SQLite Adapters](https://georgechitechi.github.io/local-lite/#/adapters-local)** - Expo SQLite, OP-SQLite, better-sqlite3, node:sqlite, and LibSQL
|
|
21
21
|
* **[Remote Cloud Adapters](https://georgechitechi.github.io/local-lite/#/adapters-remote)** - Supabase, Convex, Cloud Firestore, and custom adapters
|
|
22
|
+
* **[Database Backups](https://georgechitechi.github.io/local-lite/#/adapters-local?id=database-backups)** - SQLite snapshots with a Google Drive OAuth/PKCE client
|
|
22
23
|
* **[React & Expo Hooks](https://georgechitechi.github.io/local-lite/#/react-hooks)** - `useLiveQuery()` and `useSyncStatus()`
|
|
23
24
|
* **[Conflict Resolution](https://georgechitechi.github.io/local-lite/#/conflict-resolution)** - LWW, Client-Wins, Server-Wins, and custom resolvers
|
|
24
25
|
|
|
@@ -85,6 +86,7 @@ This recovers sync state; it does not repair corrupt SQLite files or cancel hung
|
|
|
85
86
|
* **🛡️ Conflict Resolution:** Built-in strategies including **Last-Write-Wins (LWW)**, Server-Wins, Client-Wins, or custom resolver functions.
|
|
86
87
|
* **📶 Network-Aware:** Automatically pauses polling when offline and triggers immediate sync flushes the instant connectivity is restored.
|
|
87
88
|
* **🔄 Infinite-Loop Safe:** Incoming remote deltas update local tables and fire reactive UI listeners without echoing back into the outbox.
|
|
89
|
+
* **☁️ Database Backups:** Export and restore SQLite snapshots independently of your live sync backend.
|
|
88
90
|
|
|
89
91
|
---
|
|
90
92
|
|
|
@@ -249,6 +251,40 @@ export function TodoScreen() {
|
|
|
249
251
|
To synchronize with Supabase using zero-clock-skew delta queries and `deleted: true` soft deletes, execute the following script in your Supabase project (**Dashboard → SQL Editor**):
|
|
250
252
|
|
|
251
253
|
```sql
|
|
254
|
+
-- Ensure every application table has the soft-delete column expected by local-lite.
|
|
255
|
+
DO $$
|
|
256
|
+
DECLARE
|
|
257
|
+
table_record RECORD;
|
|
258
|
+
BEGIN
|
|
259
|
+
FOR table_record IN
|
|
260
|
+
SELECT table_schema, table_name
|
|
261
|
+
FROM information_schema.tables
|
|
262
|
+
WHERE table_schema = 'public'
|
|
263
|
+
AND table_type = 'BASE TABLE'
|
|
264
|
+
LOOP
|
|
265
|
+
EXECUTE format(
|
|
266
|
+
'ALTER TABLE %I.%I ADD COLUMN IF NOT EXISTS deleted BOOLEAN',
|
|
267
|
+
table_record.table_schema,
|
|
268
|
+
table_record.table_name
|
|
269
|
+
);
|
|
270
|
+
EXECUTE format(
|
|
271
|
+
'UPDATE %I.%I SET deleted = FALSE WHERE deleted IS NULL',
|
|
272
|
+
table_record.table_schema,
|
|
273
|
+
table_record.table_name
|
|
274
|
+
);
|
|
275
|
+
EXECUTE format(
|
|
276
|
+
'ALTER TABLE %I.%I ALTER COLUMN deleted SET DEFAULT FALSE',
|
|
277
|
+
table_record.table_schema,
|
|
278
|
+
table_record.table_name
|
|
279
|
+
);
|
|
280
|
+
EXECUTE format(
|
|
281
|
+
'ALTER TABLE %I.%I ALTER COLUMN deleted SET NOT NULL',
|
|
282
|
+
table_record.table_schema,
|
|
283
|
+
table_record.table_name
|
|
284
|
+
);
|
|
285
|
+
END LOOP;
|
|
286
|
+
END $$;
|
|
287
|
+
|
|
252
288
|
-- 1. Automatic Timestamp Trigger
|
|
253
289
|
CREATE OR REPLACE FUNCTION public.set_sync_updated_at()
|
|
254
290
|
RETURNS TRIGGER AS $$
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { BackupStorage } from './index.js';
|
|
2
|
+
export interface CloudBackupFile {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
modifiedAt?: string;
|
|
6
|
+
size?: number;
|
|
7
|
+
}
|
|
8
|
+
export type BackupAccessTokenProvider = () => Promise<string>;
|
|
9
|
+
export interface GoogleDriveTokens {
|
|
10
|
+
accessToken: string;
|
|
11
|
+
refreshToken?: string;
|
|
12
|
+
expiresAt: number;
|
|
13
|
+
}
|
|
14
|
+
export interface GoogleDriveTokenStore {
|
|
15
|
+
get(): Promise<GoogleDriveTokens | null>;
|
|
16
|
+
set(tokens: GoogleDriveTokens): Promise<void>;
|
|
17
|
+
remove(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export interface GoogleDriveKeyValueStorage {
|
|
20
|
+
getItem(key: string): Promise<unknown>;
|
|
21
|
+
setItem(key: string, value: unknown): Promise<void>;
|
|
22
|
+
removeItem(key: string): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export interface GoogleDriveAuthorizationRequest {
|
|
25
|
+
authorizationUrl: string;
|
|
26
|
+
redirectUri: string;
|
|
27
|
+
}
|
|
28
|
+
export interface GoogleDriveAuthorizationResponse {
|
|
29
|
+
code: string;
|
|
30
|
+
state: string;
|
|
31
|
+
}
|
|
32
|
+
export interface GoogleDriveBackupClientOptions {
|
|
33
|
+
clientId: string;
|
|
34
|
+
redirectUri: string;
|
|
35
|
+
authorize: (request: GoogleDriveAuthorizationRequest) => Promise<GoogleDriveAuthorizationResponse | null>;
|
|
36
|
+
tokenStore: GoogleDriveTokenStore;
|
|
37
|
+
randomBytes?: (length: number) => Uint8Array;
|
|
38
|
+
fetch?: typeof fetch;
|
|
39
|
+
}
|
|
40
|
+
export declare function createGoogleDriveTokenStore(storage: GoogleDriveKeyValueStorage, key?: string): GoogleDriveTokenStore;
|
|
41
|
+
export declare class GoogleDriveBackupStorage implements BackupStorage {
|
|
42
|
+
private readonly getAccessToken;
|
|
43
|
+
private readonly fetcher;
|
|
44
|
+
constructor(getAccessToken: BackupAccessTokenProvider, fetcher?: typeof fetch);
|
|
45
|
+
save(fileName: string, data: Uint8Array): Promise<void>;
|
|
46
|
+
read(fileId: string): Promise<Uint8Array>;
|
|
47
|
+
list(): Promise<CloudBackupFile[]>;
|
|
48
|
+
}
|
|
49
|
+
export declare class GoogleDriveBackupClient implements BackupStorage {
|
|
50
|
+
private readonly options;
|
|
51
|
+
private readonly storage;
|
|
52
|
+
private readonly fetcher;
|
|
53
|
+
constructor(options: GoogleDriveBackupClientOptions);
|
|
54
|
+
connect(): Promise<boolean>;
|
|
55
|
+
disconnect(): Promise<void>;
|
|
56
|
+
isConnected(): Promise<boolean>;
|
|
57
|
+
getValidAccessToken(): Promise<string>;
|
|
58
|
+
save(fileName: string, data: Uint8Array): Promise<void>;
|
|
59
|
+
read(fileId: string): Promise<Uint8Array>;
|
|
60
|
+
list(): Promise<CloudBackupFile[]>;
|
|
61
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GoogleDriveBackupClient = exports.GoogleDriveBackupStorage = void 0;
|
|
4
|
+
exports.createGoogleDriveTokenStore = createGoogleDriveTokenStore;
|
|
5
|
+
const sha2_js_1 = require("@noble/hashes/sha2.js");
|
|
6
|
+
const utils_js_1 = require("@noble/hashes/utils.js");
|
|
7
|
+
const GOOGLE_API = 'https://www.googleapis.com/drive/v3';
|
|
8
|
+
const GOOGLE_UPLOAD_API = 'https://www.googleapis.com/upload/drive/v3';
|
|
9
|
+
const GOOGLE_AUTHORIZATION_ENDPOINT = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
10
|
+
const GOOGLE_TOKEN_ENDPOINT = 'https://oauth2.googleapis.com/token';
|
|
11
|
+
const GOOGLE_DRIVE_SCOPE = 'https://www.googleapis.com/auth/drive.file';
|
|
12
|
+
const BACKUP_FOLDER = 'backups';
|
|
13
|
+
function secureRandomBytes(length) {
|
|
14
|
+
const crypto = globalThis.crypto;
|
|
15
|
+
if (!crypto?.getRandomValues) {
|
|
16
|
+
throw new Error('Secure random number generation is unavailable.');
|
|
17
|
+
}
|
|
18
|
+
return crypto.getRandomValues(new Uint8Array(length));
|
|
19
|
+
}
|
|
20
|
+
function base64UrlEncode(bytes) {
|
|
21
|
+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
22
|
+
let result = '';
|
|
23
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
24
|
+
const first = bytes[index];
|
|
25
|
+
const second = bytes[index + 1];
|
|
26
|
+
const third = bytes[index + 2];
|
|
27
|
+
result += alphabet[first >> 2];
|
|
28
|
+
result += alphabet[((first & 3) << 4) | (second === undefined ? 0 : second >> 4)];
|
|
29
|
+
if (second !== undefined) {
|
|
30
|
+
result += alphabet[((second & 15) << 2) | (third === undefined ? 0 : third >> 6)];
|
|
31
|
+
}
|
|
32
|
+
if (third !== undefined)
|
|
33
|
+
result += alphabet[third & 63];
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
function validateGoogleTokens(value) {
|
|
38
|
+
if (!value || typeof value !== 'object')
|
|
39
|
+
return false;
|
|
40
|
+
const tokens = value;
|
|
41
|
+
return (typeof tokens.accessToken === 'string' &&
|
|
42
|
+
typeof tokens.expiresAt === 'number' &&
|
|
43
|
+
(tokens.refreshToken === undefined || typeof tokens.refreshToken === 'string'));
|
|
44
|
+
}
|
|
45
|
+
function createGoogleDriveTokenStore(storage, key = 'local-lite-google-drive-backup-tokens') {
|
|
46
|
+
return {
|
|
47
|
+
get: async () => {
|
|
48
|
+
const tokens = await storage.getItem(key);
|
|
49
|
+
return validateGoogleTokens(tokens) ? tokens : null;
|
|
50
|
+
},
|
|
51
|
+
set: tokens => storage.setItem(key, tokens),
|
|
52
|
+
remove: () => storage.removeItem(key),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function parseTokenResponse(response) {
|
|
56
|
+
try {
|
|
57
|
+
return (await response.json());
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new Error(`Google OAuth request failed (${response.status}).`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function createStoredTokens(payload, prior) {
|
|
64
|
+
if (!payload.access_token) {
|
|
65
|
+
throw new Error('Google OAuth did not return an access token.');
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
accessToken: payload.access_token,
|
|
69
|
+
refreshToken: payload.refresh_token ?? prior?.refreshToken,
|
|
70
|
+
expiresAt: Date.now() + (payload.expires_in ?? 3600) * 1000,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function throwTokenError(payload, status) {
|
|
74
|
+
throw new Error(payload.error_description ||
|
|
75
|
+
payload.error ||
|
|
76
|
+
`Google OAuth request failed (${status}).`);
|
|
77
|
+
}
|
|
78
|
+
async function parseApiError(response) {
|
|
79
|
+
let detail = '';
|
|
80
|
+
try {
|
|
81
|
+
const body = (await response.json());
|
|
82
|
+
detail =
|
|
83
|
+
(typeof body.error === 'object' ? body.error?.message : body.error) ??
|
|
84
|
+
body.error_description ??
|
|
85
|
+
body.message ??
|
|
86
|
+
'';
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
detail = response.statusText;
|
|
90
|
+
}
|
|
91
|
+
return new Error(`Google Drive request failed (${response.status})${detail ? `: ${detail}` : ''}`);
|
|
92
|
+
}
|
|
93
|
+
async function getGoogleFolderId(token, fetcher) {
|
|
94
|
+
const query = encodeURIComponent(`name = '${BACKUP_FOLDER}' and mimeType = 'application/vnd.google-apps.folder' and trashed = false`);
|
|
95
|
+
const lookup = await fetcher(`${GOOGLE_API}/files?q=${query}&spaces=drive&fields=files(id,name)&pageSize=100`, { headers: { Authorization: `Bearer ${token}` } });
|
|
96
|
+
if (!lookup.ok)
|
|
97
|
+
throw await parseApiError(lookup);
|
|
98
|
+
const result = (await lookup.json());
|
|
99
|
+
if (result.files?.[0]?.id)
|
|
100
|
+
return result.files[0].id;
|
|
101
|
+
const create = await fetcher(`${GOOGLE_API}/files?fields=id`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: {
|
|
104
|
+
Authorization: `Bearer ${token}`,
|
|
105
|
+
'Content-Type': 'application/json',
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
name: BACKUP_FOLDER,
|
|
109
|
+
mimeType: 'application/vnd.google-apps.folder',
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
if (!create.ok)
|
|
113
|
+
throw await parseApiError(create);
|
|
114
|
+
const folder = (await create.json());
|
|
115
|
+
if (!folder.id)
|
|
116
|
+
throw new Error('Google Drive did not return a backup folder ID.');
|
|
117
|
+
return folder.id;
|
|
118
|
+
}
|
|
119
|
+
class GoogleDriveBackupStorage {
|
|
120
|
+
getAccessToken;
|
|
121
|
+
fetcher;
|
|
122
|
+
constructor(getAccessToken, fetcher = fetch) {
|
|
123
|
+
this.getAccessToken = getAccessToken;
|
|
124
|
+
this.fetcher = fetcher;
|
|
125
|
+
}
|
|
126
|
+
async save(fileName, data) {
|
|
127
|
+
const token = await this.getAccessToken();
|
|
128
|
+
const folderId = await getGoogleFolderId(token, this.fetcher);
|
|
129
|
+
const session = await this.fetcher(`${GOOGLE_UPLOAD_API}/files?uploadType=resumable&fields=id`, {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: {
|
|
132
|
+
Authorization: `Bearer ${token}`,
|
|
133
|
+
'Content-Type': 'application/json; charset=UTF-8',
|
|
134
|
+
'X-Upload-Content-Type': 'application/octet-stream',
|
|
135
|
+
'X-Upload-Content-Length': String(data.byteLength),
|
|
136
|
+
},
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
name: fileName,
|
|
139
|
+
mimeType: 'application/octet-stream',
|
|
140
|
+
parents: [folderId],
|
|
141
|
+
}),
|
|
142
|
+
});
|
|
143
|
+
if (!session.ok)
|
|
144
|
+
throw await parseApiError(session);
|
|
145
|
+
const uploadUrl = session.headers.get('Location');
|
|
146
|
+
if (!uploadUrl) {
|
|
147
|
+
throw new Error('Google Drive did not return a resumable upload URL.');
|
|
148
|
+
}
|
|
149
|
+
const uploaded = await this.fetcher(uploadUrl, {
|
|
150
|
+
method: 'PUT',
|
|
151
|
+
headers: {
|
|
152
|
+
Authorization: `Bearer ${token}`,
|
|
153
|
+
'Content-Type': 'application/octet-stream',
|
|
154
|
+
'Content-Range': `bytes 0-${data.byteLength - 1}/${data.byteLength}`,
|
|
155
|
+
},
|
|
156
|
+
body: data.slice().buffer,
|
|
157
|
+
});
|
|
158
|
+
if (!uploaded.ok)
|
|
159
|
+
throw await parseApiError(uploaded);
|
|
160
|
+
}
|
|
161
|
+
async read(fileId) {
|
|
162
|
+
const token = await this.getAccessToken();
|
|
163
|
+
const response = await this.fetcher(`${GOOGLE_API}/files/${encodeURIComponent(fileId)}?alt=media`, { headers: { Authorization: `Bearer ${token}` } });
|
|
164
|
+
if (!response.ok)
|
|
165
|
+
throw await parseApiError(response);
|
|
166
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
167
|
+
}
|
|
168
|
+
async list() {
|
|
169
|
+
const token = await this.getAccessToken();
|
|
170
|
+
const folderId = await getGoogleFolderId(token, this.fetcher);
|
|
171
|
+
const query = encodeURIComponent(`'${folderId}' in parents and trashed = false`);
|
|
172
|
+
const response = await this.fetcher(`${GOOGLE_API}/files?q=${query}&fields=files(id,name,size,modifiedTime)&orderBy=modifiedTime%20desc&pageSize=100`, { headers: { Authorization: `Bearer ${token}` } });
|
|
173
|
+
if (!response.ok)
|
|
174
|
+
throw await parseApiError(response);
|
|
175
|
+
const result = (await response.json());
|
|
176
|
+
return (result.files ?? [])
|
|
177
|
+
.filter(file => file.name.endsWith('.db'))
|
|
178
|
+
.map(file => ({
|
|
179
|
+
id: file.id,
|
|
180
|
+
name: file.name,
|
|
181
|
+
modifiedAt: file.modifiedTime,
|
|
182
|
+
size: file.size ? Number(file.size) : undefined,
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
exports.GoogleDriveBackupStorage = GoogleDriveBackupStorage;
|
|
187
|
+
class GoogleDriveBackupClient {
|
|
188
|
+
options;
|
|
189
|
+
storage;
|
|
190
|
+
fetcher;
|
|
191
|
+
constructor(options) {
|
|
192
|
+
this.options = options;
|
|
193
|
+
if (!options.clientId.trim())
|
|
194
|
+
throw new Error('Google OAuth client ID is required.');
|
|
195
|
+
if (!options.redirectUri.trim())
|
|
196
|
+
throw new Error('Google OAuth redirect URI is required.');
|
|
197
|
+
this.fetcher = options.fetch ?? fetch;
|
|
198
|
+
this.storage = new GoogleDriveBackupStorage(() => this.getValidAccessToken(), this.fetcher);
|
|
199
|
+
}
|
|
200
|
+
async connect() {
|
|
201
|
+
const randomBytes = this.options.randomBytes ?? secureRandomBytes;
|
|
202
|
+
const verifierBytes = randomBytes(32);
|
|
203
|
+
const stateBytes = randomBytes(32);
|
|
204
|
+
if (verifierBytes.length !== 32 || stateBytes.length !== 32) {
|
|
205
|
+
throw new Error('The randomBytes provider returned an invalid number of bytes.');
|
|
206
|
+
}
|
|
207
|
+
const codeVerifier = base64UrlEncode(verifierBytes);
|
|
208
|
+
const codeChallenge = base64UrlEncode((0, sha2_js_1.sha256)((0, utils_js_1.utf8ToBytes)(codeVerifier)));
|
|
209
|
+
const state = base64UrlEncode(stateBytes);
|
|
210
|
+
const authorizationParams = new URLSearchParams({
|
|
211
|
+
client_id: this.options.clientId,
|
|
212
|
+
redirect_uri: this.options.redirectUri,
|
|
213
|
+
response_type: 'code',
|
|
214
|
+
scope: GOOGLE_DRIVE_SCOPE,
|
|
215
|
+
access_type: 'offline',
|
|
216
|
+
prompt: 'consent',
|
|
217
|
+
code_challenge: codeChallenge,
|
|
218
|
+
code_challenge_method: 'S256',
|
|
219
|
+
state,
|
|
220
|
+
});
|
|
221
|
+
const authorization = await this.options.authorize({
|
|
222
|
+
authorizationUrl: `${GOOGLE_AUTHORIZATION_ENDPOINT}?${authorizationParams}`,
|
|
223
|
+
redirectUri: this.options.redirectUri,
|
|
224
|
+
});
|
|
225
|
+
if (!authorization)
|
|
226
|
+
return false;
|
|
227
|
+
if (authorization.state !== state) {
|
|
228
|
+
throw new Error('Google OAuth state validation failed. Please try connecting again.');
|
|
229
|
+
}
|
|
230
|
+
if (!authorization.code) {
|
|
231
|
+
throw new Error('Google OAuth did not return an authorization code.');
|
|
232
|
+
}
|
|
233
|
+
const response = await this.fetcher(GOOGLE_TOKEN_ENDPOINT, {
|
|
234
|
+
method: 'POST',
|
|
235
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
236
|
+
body: new URLSearchParams({
|
|
237
|
+
client_id: this.options.clientId,
|
|
238
|
+
code: authorization.code,
|
|
239
|
+
code_verifier: codeVerifier,
|
|
240
|
+
grant_type: 'authorization_code',
|
|
241
|
+
redirect_uri: this.options.redirectUri,
|
|
242
|
+
}).toString(),
|
|
243
|
+
});
|
|
244
|
+
const payload = await parseTokenResponse(response);
|
|
245
|
+
if (!response.ok)
|
|
246
|
+
throwTokenError(payload, response.status);
|
|
247
|
+
const prior = await this.options.tokenStore.get();
|
|
248
|
+
await this.options.tokenStore.set(createStoredTokens(payload, prior));
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
async disconnect() {
|
|
252
|
+
await this.options.tokenStore.remove();
|
|
253
|
+
}
|
|
254
|
+
async isConnected() {
|
|
255
|
+
const tokens = await this.options.tokenStore.get();
|
|
256
|
+
return validateGoogleTokens(tokens);
|
|
257
|
+
}
|
|
258
|
+
async getValidAccessToken() {
|
|
259
|
+
const tokens = await this.options.tokenStore.get();
|
|
260
|
+
if (!validateGoogleTokens(tokens)) {
|
|
261
|
+
throw new Error('Connect Google Drive before using cloud backups.');
|
|
262
|
+
}
|
|
263
|
+
if (tokens.expiresAt > Date.now() + 60_000)
|
|
264
|
+
return tokens.accessToken;
|
|
265
|
+
if (!tokens.refreshToken) {
|
|
266
|
+
throw new Error('Your Google Drive sign-in expired. Disconnect and connect the account again.');
|
|
267
|
+
}
|
|
268
|
+
const response = await this.fetcher(GOOGLE_TOKEN_ENDPOINT, {
|
|
269
|
+
method: 'POST',
|
|
270
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
271
|
+
body: new URLSearchParams({
|
|
272
|
+
client_id: this.options.clientId,
|
|
273
|
+
grant_type: 'refresh_token',
|
|
274
|
+
refresh_token: tokens.refreshToken,
|
|
275
|
+
scope: GOOGLE_DRIVE_SCOPE,
|
|
276
|
+
}).toString(),
|
|
277
|
+
});
|
|
278
|
+
const payload = await parseTokenResponse(response);
|
|
279
|
+
if (!response.ok)
|
|
280
|
+
throwTokenError(payload, response.status);
|
|
281
|
+
const updatedTokens = createStoredTokens(payload, tokens);
|
|
282
|
+
await this.options.tokenStore.set(updatedTokens);
|
|
283
|
+
return updatedTokens.accessToken;
|
|
284
|
+
}
|
|
285
|
+
save(fileName, data) {
|
|
286
|
+
return this.storage.save(fileName, data);
|
|
287
|
+
}
|
|
288
|
+
read(fileId) {
|
|
289
|
+
return this.storage.read(fileId);
|
|
290
|
+
}
|
|
291
|
+
list() {
|
|
292
|
+
return this.storage.list();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
exports.GoogleDriveBackupClient = GoogleDriveBackupClient;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { type BackupAccessTokenProvider, type CloudBackupFile, type GoogleDriveAuthorizationRequest, type GoogleDriveAuthorizationResponse, type GoogleDriveBackupClientOptions, type GoogleDriveKeyValueStorage, type GoogleDriveTokenStore, type GoogleDriveTokens, GoogleDriveBackupClient, GoogleDriveBackupStorage, createGoogleDriveTokenStore, } from './cloud-storage.js';
|
|
2
|
+
export interface BackupDatabase {
|
|
3
|
+
exportDatabase(): Promise<Uint8Array>;
|
|
4
|
+
importDatabase(data: Uint8Array): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export interface BackupStorage {
|
|
7
|
+
save(fileName: string, data: Uint8Array): Promise<void>;
|
|
8
|
+
read(fileName: string): Promise<Uint8Array>;
|
|
9
|
+
}
|
|
10
|
+
export interface CreateDatabaseBackupOptions {
|
|
11
|
+
fileName?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function createDatabaseBackup(database: BackupDatabase, storage: BackupStorage, options?: CreateDatabaseBackupOptions): Promise<string>;
|
|
14
|
+
export declare function restoreDatabaseBackup(database: BackupDatabase, storage: BackupStorage, fileName: string): Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createGoogleDriveTokenStore = exports.GoogleDriveBackupStorage = exports.GoogleDriveBackupClient = void 0;
|
|
4
|
+
exports.createDatabaseBackup = createDatabaseBackup;
|
|
5
|
+
exports.restoreDatabaseBackup = restoreDatabaseBackup;
|
|
6
|
+
var cloud_storage_js_1 = require("./cloud-storage.js");
|
|
7
|
+
Object.defineProperty(exports, "GoogleDriveBackupClient", { enumerable: true, get: function () { return cloud_storage_js_1.GoogleDriveBackupClient; } });
|
|
8
|
+
Object.defineProperty(exports, "GoogleDriveBackupStorage", { enumerable: true, get: function () { return cloud_storage_js_1.GoogleDriveBackupStorage; } });
|
|
9
|
+
Object.defineProperty(exports, "createGoogleDriveTokenStore", { enumerable: true, get: function () { return cloud_storage_js_1.createGoogleDriveTokenStore; } });
|
|
10
|
+
function validateFileName(fileName) {
|
|
11
|
+
if (!fileName || /[\\/]/.test(fileName)) {
|
|
12
|
+
throw new Error('Backup file name must be a single file name.');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async function createDatabaseBackup(database, storage, options = {}) {
|
|
16
|
+
const fileName = options.fileName ?? `local-lite-${new Date().toISOString().replace(/[:.]/g, '-')}.db`;
|
|
17
|
+
validateFileName(fileName);
|
|
18
|
+
const databaseBytes = await database.exportDatabase();
|
|
19
|
+
if (databaseBytes.length === 0) {
|
|
20
|
+
throw new Error('Cannot create a backup from an empty database.');
|
|
21
|
+
}
|
|
22
|
+
await storage.save(fileName, databaseBytes);
|
|
23
|
+
return fileName;
|
|
24
|
+
}
|
|
25
|
+
async function restoreDatabaseBackup(database, storage, fileName) {
|
|
26
|
+
validateFileName(fileName);
|
|
27
|
+
const databaseBytes = await storage.read(fileName);
|
|
28
|
+
if (databaseBytes.length === 0) {
|
|
29
|
+
throw new Error('Cannot restore an empty database backup.');
|
|
30
|
+
}
|
|
31
|
+
await database.importDatabase(databaseBytes);
|
|
32
|
+
}
|
package/dist/config/index.js
CHANGED
|
@@ -89,6 +89,7 @@ async function createSyncClient(config) {
|
|
|
89
89
|
autoHydrateJSON: config.autoHydrateJSON,
|
|
90
90
|
syncScope: config.syncScope,
|
|
91
91
|
autoPauseOnBackground: config.autoPauseOnBackground,
|
|
92
|
+
touchUpdatedAt: config.touchUpdatedAt,
|
|
92
93
|
});
|
|
93
94
|
// 5. Execute Migrations & Auto-initialize
|
|
94
95
|
if (config.autoInitialize !== false) {
|
package/dist/config/types.d.ts
CHANGED
|
@@ -181,4 +181,13 @@ export interface LocalLiteConfig {
|
|
|
181
181
|
* Scoped synchronization filter (e.g. { org_id: '...' } or dynamic resolver callback).
|
|
182
182
|
*/
|
|
183
183
|
syncScope?: Record<string, any> | (() => Record<string, any> | Promise<Record<string, any>>);
|
|
184
|
+
/**
|
|
185
|
+
* Stamp an "updated at" column on every local INSERT/UPDATE so reactive queries
|
|
186
|
+
* (which compare rows by `id` + `updated_at`/`updatedAt`) re-render after offline edits.
|
|
187
|
+
* Only the local SQLite row is stamped; payloads pushed to the remote are unchanged.
|
|
188
|
+
* - `true` (default): column inferred from the remote adapter's `updatedAtColumn`, else 'updated_at'
|
|
189
|
+
* - `string`: explicit column name
|
|
190
|
+
* - `false`: disabled
|
|
191
|
+
*/
|
|
192
|
+
touchUpdatedAt?: boolean | string;
|
|
184
193
|
}
|
package/dist/core/client.d.ts
CHANGED
|
@@ -27,6 +27,15 @@ export interface LocalFirstClientConfig {
|
|
|
27
27
|
autoHydrateJSON?: boolean;
|
|
28
28
|
syncScope?: Record<string, any> | (() => Record<string, any> | Promise<Record<string, any>>);
|
|
29
29
|
autoPauseOnBackground?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Stamp an "updated at" column on every local INSERT/UPDATE write so reactive
|
|
32
|
+
* queries (which compare rows by `id` + `updated_at`/`updatedAt`) detect offline edits.
|
|
33
|
+
* Only the local SQLite row is stamped; outbox payloads sent to the remote are unchanged.
|
|
34
|
+
* - `true` (default): column inferred from the remote adapter's `updatedAtColumn`, else 'updated_at'
|
|
35
|
+
* - `string`: explicit column name
|
|
36
|
+
* - `false`: disabled
|
|
37
|
+
*/
|
|
38
|
+
touchUpdatedAt?: boolean | string;
|
|
30
39
|
}
|
|
31
40
|
export declare class TransactionalClient {
|
|
32
41
|
private parent;
|
|
@@ -67,12 +76,25 @@ export declare class LocalFirstClient {
|
|
|
67
76
|
tables: string[];
|
|
68
77
|
jsonColumns: Record<string, string[]>;
|
|
69
78
|
autoHydrateJSON: boolean;
|
|
79
|
+
/** Local column stamped on INSERT/UPDATE writes, or null when disabled. */
|
|
80
|
+
touchColumn: string | null;
|
|
70
81
|
private primaryKey;
|
|
71
82
|
private migrations;
|
|
72
83
|
private autoStart;
|
|
73
84
|
private initialization?;
|
|
74
85
|
constructor(config: LocalFirstClientConfig);
|
|
75
86
|
getPrimaryKey(): string;
|
|
87
|
+
/**
|
|
88
|
+
* Options for TableManager.applyMutation on locally-originated writes.
|
|
89
|
+
* The touch value uses the mutation timestamp so it matches what remote
|
|
90
|
+
* adapters (e.g. Supabase) write for the same operation on push.
|
|
91
|
+
*/
|
|
92
|
+
localMutationOptions(timestamp: number): {
|
|
93
|
+
autoSchema: boolean;
|
|
94
|
+
schemaManager: SchemaManager;
|
|
95
|
+
touchColumn: string | null;
|
|
96
|
+
touchValue: string;
|
|
97
|
+
};
|
|
76
98
|
/**
|
|
77
99
|
* Automatically deserializes JSON string columns into JavaScript objects/arrays.
|
|
78
100
|
*/
|
package/dist/core/client.js
CHANGED
|
@@ -74,10 +74,7 @@ class TransactionalClient {
|
|
|
74
74
|
async mutate(tableName, rowId, operation, payload, options = {}) {
|
|
75
75
|
const pk = options.primaryKey ?? this.parent.getPrimaryKey();
|
|
76
76
|
const timestamp = options.customTimestamp ?? Date.now();
|
|
77
|
-
await table_manager_js_1.TableManager.applyMutation(this.tx, tableName, rowId, operation, payload, pk,
|
|
78
|
-
autoSchema: this.parent.autoSchema,
|
|
79
|
-
schemaManager: this.parent.schemaManager,
|
|
80
|
-
});
|
|
77
|
+
await table_manager_js_1.TableManager.applyMutation(this.tx, tableName, rowId, operation, payload, pk, this.parent.localMutationOptions(timestamp));
|
|
81
78
|
if (!options.skipOutbox) {
|
|
82
79
|
const outboxId = await this.parent.recordOutboxMutation(this.tx, tableName, rowId, operation, payload, timestamp);
|
|
83
80
|
if (outboxId) {
|
|
@@ -157,6 +154,8 @@ class LocalFirstClient {
|
|
|
157
154
|
tables;
|
|
158
155
|
jsonColumns;
|
|
159
156
|
autoHydrateJSON;
|
|
157
|
+
/** Local column stamped on INSERT/UPDATE writes, or null when disabled. */
|
|
158
|
+
touchColumn;
|
|
160
159
|
primaryKey;
|
|
161
160
|
migrations;
|
|
162
161
|
autoStart;
|
|
@@ -173,6 +172,13 @@ class LocalFirstClient {
|
|
|
173
172
|
this.autoStart = config.autoStart ?? true;
|
|
174
173
|
this.jsonColumns = config.jsonColumns ?? {};
|
|
175
174
|
this.autoHydrateJSON = config.autoHydrateJSON ?? false;
|
|
175
|
+
const touch = config.touchUpdatedAt ?? true;
|
|
176
|
+
this.touchColumn =
|
|
177
|
+
touch === false
|
|
178
|
+
? null
|
|
179
|
+
: typeof touch === 'string'
|
|
180
|
+
? touch
|
|
181
|
+
: this.remote?.updatedAtColumn || 'updated_at';
|
|
176
182
|
this.schemaManager = new schema_manager_js_1.SchemaManager();
|
|
177
183
|
const workerOptions = {
|
|
178
184
|
network: config.network,
|
|
@@ -195,6 +201,19 @@ class LocalFirstClient {
|
|
|
195
201
|
getPrimaryKey() {
|
|
196
202
|
return this.primaryKey;
|
|
197
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Options for TableManager.applyMutation on locally-originated writes.
|
|
206
|
+
* The touch value uses the mutation timestamp so it matches what remote
|
|
207
|
+
* adapters (e.g. Supabase) write for the same operation on push.
|
|
208
|
+
*/
|
|
209
|
+
localMutationOptions(timestamp) {
|
|
210
|
+
return {
|
|
211
|
+
autoSchema: this.autoSchema,
|
|
212
|
+
schemaManager: this.schemaManager,
|
|
213
|
+
touchColumn: this.touchColumn,
|
|
214
|
+
touchValue: new Date(timestamp).toISOString(),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
198
217
|
/**
|
|
199
218
|
* Automatically deserializes JSON string columns into JavaScript objects/arrays.
|
|
200
219
|
*/
|
|
@@ -345,10 +364,7 @@ class LocalFirstClient {
|
|
|
345
364
|
let affectedOutboxId;
|
|
346
365
|
const executeInTransaction = async (tx) => {
|
|
347
366
|
// 1. Mutate local table
|
|
348
|
-
await table_manager_js_1.TableManager.applyMutation(tx, tableName, rowId, operation, payload, pk,
|
|
349
|
-
autoSchema: this.autoSchema,
|
|
350
|
-
schemaManager: this.schemaManager,
|
|
351
|
-
});
|
|
367
|
+
await table_manager_js_1.TableManager.applyMutation(tx, tableName, rowId, operation, payload, pk, this.localMutationOptions(timestamp));
|
|
352
368
|
// 2. Insert into sync_outbox (if not skipped)
|
|
353
369
|
if (!options.skipOutbox) {
|
|
354
370
|
affectedOutboxId = await this.recordOutboxMutation(tx, tableName, rowId, operation, payload, timestamp);
|
|
@@ -400,13 +416,10 @@ class LocalFirstClient {
|
|
|
400
416
|
? String(payload[pk])
|
|
401
417
|
: (0, uuid_js_1.generateUUID)());
|
|
402
418
|
generatedIds.push(rowId);
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
schemaManager: this.schemaManager,
|
|
406
|
-
});
|
|
419
|
+
const timestamp = m.customTimestamp ?? options.customTimestamp ?? Date.now();
|
|
420
|
+
await table_manager_js_1.TableManager.applyMutation(tx, m.tableName, rowId, m.operation, payload, pk, this.localMutationOptions(timestamp));
|
|
407
421
|
const skipOutbox = m.skipOutbox ?? options.skipOutbox ?? false;
|
|
408
422
|
if (!skipOutbox) {
|
|
409
|
-
const timestamp = m.customTimestamp ?? options.customTimestamp ?? Date.now();
|
|
410
423
|
if (this.coalesceOutbox && m.operation !== 'INSERT') {
|
|
411
424
|
const outboxId = await this.recordOutboxMutation(tx, m.tableName, rowId, m.operation, payload, timestamp, pendingOutboxMap);
|
|
412
425
|
if (outboxId) {
|
|
@@ -17,6 +17,12 @@ export declare class SchemaManager {
|
|
|
17
17
|
private knownIndexes;
|
|
18
18
|
clearCache(): void;
|
|
19
19
|
hasColumn(tableName: string, column: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Returns whether `column` exists on `tableName`, introspecting via PRAGMA table_info
|
|
22
|
+
* at most once per table (cached). Unlike ensureTableIntrospected, this has no
|
|
23
|
+
* side effects (no auto-indexing), so it is safe on hand-written schemas.
|
|
24
|
+
*/
|
|
25
|
+
tableHasColumn(db: DBExecutable, tableName: string, column: string): Promise<boolean>;
|
|
20
26
|
/**
|
|
21
27
|
* Introspects table columns via PRAGMA table_info if not previously cached.
|
|
22
28
|
*/
|
|
@@ -42,6 +42,31 @@ class SchemaManager {
|
|
|
42
42
|
hasColumn(tableName, column) {
|
|
43
43
|
return this.knownColumns.has(`${tableName}:${column.toLowerCase()}`);
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Returns whether `column` exists on `tableName`, introspecting via PRAGMA table_info
|
|
47
|
+
* at most once per table (cached). Unlike ensureTableIntrospected, this has no
|
|
48
|
+
* side effects (no auto-indexing), so it is safe on hand-written schemas.
|
|
49
|
+
*/
|
|
50
|
+
async tableHasColumn(db, tableName, column) {
|
|
51
|
+
if (this.hasColumn(tableName, column))
|
|
52
|
+
return true;
|
|
53
|
+
const probeKey = `${tableName}:__probed__`;
|
|
54
|
+
if (!this.knownColumns.has(probeKey)) {
|
|
55
|
+
try {
|
|
56
|
+
const tableInfo = await db.execute(`PRAGMA table_info(${(0, sql_js_1.escapeIdentifier)(tableName)});`);
|
|
57
|
+
if (Array.isArray(tableInfo) && tableInfo.length > 0) {
|
|
58
|
+
// Only cache once the table exists, so tables created later get re-probed.
|
|
59
|
+
this.knownColumns.add(probeKey);
|
|
60
|
+
for (const col of tableInfo) {
|
|
61
|
+
if (col?.name)
|
|
62
|
+
this.knownColumns.add(`${tableName}:${String(col.name).toLowerCase()}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch { }
|
|
67
|
+
}
|
|
68
|
+
return this.hasColumn(tableName, column);
|
|
69
|
+
}
|
|
45
70
|
/**
|
|
46
71
|
* Introspects table columns via PRAGMA table_info if not previously cached.
|
|
47
72
|
*/
|
|
@@ -4,6 +4,15 @@ import { SchemaManager } from './schema-manager.js';
|
|
|
4
4
|
export interface TableMutationOptions {
|
|
5
5
|
autoSchema?: boolean;
|
|
6
6
|
schemaManager?: SchemaManager;
|
|
7
|
+
/**
|
|
8
|
+
* Column to stamp with `touchValue` on local writes (e.g. 'updated_at').
|
|
9
|
+
* - UPDATE: always overwritten (mirrors what remote adapters do on push).
|
|
10
|
+
* - INSERT: only set when the payload does not already provide a value.
|
|
11
|
+
* Skipped when the column does not exist and autoSchema is disabled.
|
|
12
|
+
*/
|
|
13
|
+
touchColumn?: string | null;
|
|
14
|
+
/** Value written to `touchColumn`. Default: current time as an ISO string. */
|
|
15
|
+
touchValue?: any;
|
|
7
16
|
}
|
|
8
17
|
export declare class TableManager {
|
|
9
18
|
/**
|
|
@@ -13,6 +13,16 @@ function formatPayload(payload) {
|
|
|
13
13
|
}
|
|
14
14
|
return formatted;
|
|
15
15
|
}
|
|
16
|
+
async function withTouchedColumn(tx, sm, tableName, operation, payload, options) {
|
|
17
|
+
const col = options.touchColumn;
|
|
18
|
+
if (operation === 'INSERT' && payload && payload[col] !== undefined && payload[col] !== null) {
|
|
19
|
+
return payload;
|
|
20
|
+
}
|
|
21
|
+
if (!options.autoSchema && !(await sm.tableHasColumn(tx, tableName, col))) {
|
|
22
|
+
return payload;
|
|
23
|
+
}
|
|
24
|
+
return { ...(payload || {}), [col]: options.touchValue ?? new Date().toISOString() };
|
|
25
|
+
}
|
|
16
26
|
class TableManager {
|
|
17
27
|
/**
|
|
18
28
|
* Applies an insert, update, or delete mutation on a local data table.
|
|
@@ -30,6 +40,9 @@ class TableManager {
|
|
|
30
40
|
await tx.execute(`DELETE FROM ${safeTable} WHERE ${safePk} = ?`, [rowId]);
|
|
31
41
|
return;
|
|
32
42
|
}
|
|
43
|
+
if (options?.touchColumn) {
|
|
44
|
+
payload = await withTouchedColumn(tx, sm, tableName, operation, payload, options);
|
|
45
|
+
}
|
|
33
46
|
const formatted = options?.autoSchema
|
|
34
47
|
? await sm.ensureColumns(tx, tableName, payload, primaryKey)
|
|
35
48
|
: formatPayload(payload);
|
package/package.json
CHANGED
|
@@ -1,144 +1,150 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "local-lite",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Never let a lost connection break a promise. Local-first sync that faithfully keeps data safe until you're back online.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"main": "dist/index.js",
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
8
|
-
"files": [
|
|
9
|
-
"dist",
|
|
10
|
-
"README.md",
|
|
11
|
-
"LICENSE"
|
|
12
|
-
],
|
|
13
|
-
"exports": {
|
|
14
|
-
".": {
|
|
15
|
-
"types": "./dist/index.d.ts",
|
|
16
|
-
"import": "./dist/index.js",
|
|
17
|
-
"default": "./dist/index.js"
|
|
18
|
-
},
|
|
19
|
-
"./react": {
|
|
20
|
-
"types": "./dist/react/index.d.ts",
|
|
21
|
-
"import": "./dist/react/index.js",
|
|
22
|
-
"default": "./dist/react/index.js"
|
|
23
|
-
},
|
|
24
|
-
"./config": {
|
|
25
|
-
"types": "./dist/config/index.d.ts",
|
|
26
|
-
"import": "./dist/config/index.js",
|
|
27
|
-
"default": "./dist/config/index.js"
|
|
28
|
-
},
|
|
29
|
-
"./schema": {
|
|
30
|
-
"types": "./dist/schema/index.d.ts",
|
|
31
|
-
"import": "./dist/schema/index.js",
|
|
32
|
-
"default": "./dist/schema/index.js"
|
|
33
|
-
},
|
|
34
|
-
"./supabase": {
|
|
35
|
-
"types": "./dist/adapters/remote/supabase.d.ts",
|
|
36
|
-
"import": "./dist/adapters/remote/supabase.js",
|
|
37
|
-
"default": "./dist/adapters/remote/supabase.js"
|
|
38
|
-
},
|
|
39
|
-
"./convex": {
|
|
40
|
-
"types": "./dist/adapters/remote/convex.d.ts",
|
|
41
|
-
"import": "./dist/adapters/remote/convex.js",
|
|
42
|
-
"default": "./dist/adapters/remote/convex.js"
|
|
43
|
-
},
|
|
44
|
-
"./firebase": {
|
|
45
|
-
"types": "./dist/adapters/remote/firestore.d.ts",
|
|
46
|
-
"import": "./dist/adapters/remote/firestore.js",
|
|
47
|
-
"default": "./dist/adapters/remote/firestore.js"
|
|
48
|
-
},
|
|
49
|
-
"./
|
|
50
|
-
"types": "./dist/
|
|
51
|
-
"import": "./dist/
|
|
52
|
-
"default": "./dist/
|
|
53
|
-
},
|
|
54
|
-
"./
|
|
55
|
-
"types": "./dist/adapters/local/
|
|
56
|
-
"import": "./dist/adapters/local/
|
|
57
|
-
"default": "./dist/adapters/local/
|
|
58
|
-
},
|
|
59
|
-
"./
|
|
60
|
-
"types": "./dist/adapters/local/
|
|
61
|
-
"import": "./dist/adapters/local/
|
|
62
|
-
"default": "./dist/adapters/local/
|
|
63
|
-
},
|
|
64
|
-
"./
|
|
65
|
-
"types": "./dist/adapters/local/
|
|
66
|
-
"import": "./dist/adapters/local/
|
|
67
|
-
"default": "./dist/adapters/local/
|
|
68
|
-
},
|
|
69
|
-
"./
|
|
70
|
-
"types": "./dist/adapters/local/
|
|
71
|
-
"import": "./dist/adapters/local/
|
|
72
|
-
"default": "./dist/adapters/local/
|
|
73
|
-
},
|
|
74
|
-
"./
|
|
75
|
-
"types": "./dist/adapters/local/
|
|
76
|
-
"import": "./dist/adapters/local/
|
|
77
|
-
"default": "./dist/adapters/local/
|
|
78
|
-
},
|
|
79
|
-
"./
|
|
80
|
-
"types": "./dist/
|
|
81
|
-
"import": "./dist/
|
|
82
|
-
"default": "./dist/
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "local-lite",
|
|
3
|
+
"version": "0.0.10",
|
|
4
|
+
"description": "Never let a lost connection break a promise. Local-first sync that faithfully keeps data safe until you're back online.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./react": {
|
|
20
|
+
"types": "./dist/react/index.d.ts",
|
|
21
|
+
"import": "./dist/react/index.js",
|
|
22
|
+
"default": "./dist/react/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./config": {
|
|
25
|
+
"types": "./dist/config/index.d.ts",
|
|
26
|
+
"import": "./dist/config/index.js",
|
|
27
|
+
"default": "./dist/config/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./schema": {
|
|
30
|
+
"types": "./dist/schema/index.d.ts",
|
|
31
|
+
"import": "./dist/schema/index.js",
|
|
32
|
+
"default": "./dist/schema/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./supabase": {
|
|
35
|
+
"types": "./dist/adapters/remote/supabase.d.ts",
|
|
36
|
+
"import": "./dist/adapters/remote/supabase.js",
|
|
37
|
+
"default": "./dist/adapters/remote/supabase.js"
|
|
38
|
+
},
|
|
39
|
+
"./convex": {
|
|
40
|
+
"types": "./dist/adapters/remote/convex.d.ts",
|
|
41
|
+
"import": "./dist/adapters/remote/convex.js",
|
|
42
|
+
"default": "./dist/adapters/remote/convex.js"
|
|
43
|
+
},
|
|
44
|
+
"./firebase": {
|
|
45
|
+
"types": "./dist/adapters/remote/firestore.d.ts",
|
|
46
|
+
"import": "./dist/adapters/remote/firestore.js",
|
|
47
|
+
"default": "./dist/adapters/remote/firestore.js"
|
|
48
|
+
},
|
|
49
|
+
"./backups": {
|
|
50
|
+
"types": "./dist/backups/index.d.ts",
|
|
51
|
+
"import": "./dist/backups/index.js",
|
|
52
|
+
"default": "./dist/backups/index.js"
|
|
53
|
+
},
|
|
54
|
+
"./expo-sqlite": {
|
|
55
|
+
"types": "./dist/adapters/local/expo-sqlite.d.ts",
|
|
56
|
+
"import": "./dist/adapters/local/expo-sqlite.js",
|
|
57
|
+
"default": "./dist/adapters/local/expo-sqlite.js"
|
|
58
|
+
},
|
|
59
|
+
"./op-sqlite": {
|
|
60
|
+
"types": "./dist/adapters/local/op-sqlite.d.ts",
|
|
61
|
+
"import": "./dist/adapters/local/op-sqlite.js",
|
|
62
|
+
"default": "./dist/adapters/local/op-sqlite.js"
|
|
63
|
+
},
|
|
64
|
+
"./better-sqlite3": {
|
|
65
|
+
"types": "./dist/adapters/local/better-sqlite3.d.ts",
|
|
66
|
+
"import": "./dist/adapters/local/better-sqlite3.js",
|
|
67
|
+
"default": "./dist/adapters/local/better-sqlite3.js"
|
|
68
|
+
},
|
|
69
|
+
"./libsql": {
|
|
70
|
+
"types": "./dist/adapters/local/libsql.d.ts",
|
|
71
|
+
"import": "./dist/adapters/local/libsql.js",
|
|
72
|
+
"default": "./dist/adapters/local/libsql.js"
|
|
73
|
+
},
|
|
74
|
+
"./sql-js": {
|
|
75
|
+
"types": "./dist/adapters/local/sql-js.d.ts",
|
|
76
|
+
"import": "./dist/adapters/local/sql-js.js",
|
|
77
|
+
"default": "./dist/adapters/local/sql-js.js"
|
|
78
|
+
},
|
|
79
|
+
"./node-sqlite": {
|
|
80
|
+
"types": "./dist/adapters/local/node-sqlite.d.ts",
|
|
81
|
+
"import": "./dist/adapters/local/node-sqlite.js",
|
|
82
|
+
"default": "./dist/adapters/local/node-sqlite.js"
|
|
83
|
+
},
|
|
84
|
+
"./core": {
|
|
85
|
+
"types": "./dist/core/index.d.ts",
|
|
86
|
+
"import": "./dist/core/index.js",
|
|
87
|
+
"default": "./dist/core/index.js"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"scripts": {
|
|
91
|
+
"build": "tsc",
|
|
92
|
+
"test": "vitest run",
|
|
93
|
+
"test:watch": "vitest",
|
|
94
|
+
"prepublishOnly": "npm run test && npm run build"
|
|
95
|
+
},
|
|
96
|
+
"peerDependencies": {
|
|
97
|
+
"@libsql/client": "^0.6.0",
|
|
98
|
+
"@op-engineering/op-sqlite": "^0.1.0",
|
|
99
|
+
"@supabase/supabase-js": "^2.0.0",
|
|
100
|
+
"better-sqlite3": "^8.0.0",
|
|
101
|
+
"convex": ">=1.0.0",
|
|
102
|
+
"expo-sqlite": ">=57.0.0",
|
|
103
|
+
"firebase": ">=9.0.0",
|
|
104
|
+
"react": ">=18.0.0",
|
|
105
|
+
"sql.js": "^1.8.0"
|
|
106
|
+
},
|
|
107
|
+
"peerDependenciesMeta": {
|
|
108
|
+
"react": {
|
|
109
|
+
"optional": true
|
|
110
|
+
},
|
|
111
|
+
"@supabase/supabase-js": {
|
|
112
|
+
"optional": true
|
|
113
|
+
},
|
|
114
|
+
"convex": {
|
|
115
|
+
"optional": true
|
|
116
|
+
},
|
|
117
|
+
"firebase": {
|
|
118
|
+
"optional": true
|
|
119
|
+
},
|
|
120
|
+
"expo-sqlite": {
|
|
121
|
+
"optional": true
|
|
122
|
+
},
|
|
123
|
+
"@op-engineering/op-sqlite": {
|
|
124
|
+
"optional": true
|
|
125
|
+
},
|
|
126
|
+
"better-sqlite3": {
|
|
127
|
+
"optional": true
|
|
128
|
+
},
|
|
129
|
+
"@libsql/client": {
|
|
130
|
+
"optional": true
|
|
131
|
+
},
|
|
132
|
+
"sql.js": {
|
|
133
|
+
"optional": true
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"devDependencies": {
|
|
132
137
|
"@supabase/supabase-js": "^2.118.0",
|
|
133
138
|
"@types/node": "^26.6.5",
|
|
134
139
|
"@types/react": "^19.3.0",
|
|
135
|
-
"@types/uuid": "^11.0.0",
|
|
136
|
-
"expo-sqlite": "^58.0.13",
|
|
137
|
-
"react": "^19.3.1",
|
|
138
|
-
"typescript": "^7.0.2",
|
|
139
|
-
"vitest": "^5.0.5"
|
|
140
|
-
},
|
|
141
|
-
"dependencies": {
|
|
142
|
-
"
|
|
143
|
-
|
|
140
|
+
"@types/uuid": "^11.0.0",
|
|
141
|
+
"expo-sqlite": "^58.0.13",
|
|
142
|
+
"react": "^19.3.1",
|
|
143
|
+
"typescript": "^7.0.2",
|
|
144
|
+
"vitest": "^5.0.5"
|
|
145
|
+
},
|
|
146
|
+
"dependencies": {
|
|
147
|
+
"@noble/hashes": "^2.4.0",
|
|
148
|
+
"uuid": "^14.0.3"
|
|
149
|
+
}
|
|
144
150
|
}
|