local-lite 0.0.12 → 0.0.14

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,276 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseCsv = parseCsv;
4
+ exports.exportTablesToJson = exportTablesToJson;
5
+ exports.importTablesFromJson = importTablesFromJson;
6
+ exports.exportTableToCsv = exportTableToCsv;
7
+ exports.importTableFromCsv = importTableFromCsv;
8
+ const sql_js_1 = require("./sql.js");
9
+ /**
10
+ * Escapes a single CSV cell value according to RFC-4180.
11
+ */
12
+ function escapeCsvValue(val, delimiter) {
13
+ if (val === null || val === undefined) {
14
+ return '';
15
+ }
16
+ let str;
17
+ if (typeof val === 'object') {
18
+ str = JSON.stringify(val);
19
+ }
20
+ else {
21
+ str = String(val);
22
+ }
23
+ // Quote if contains delimiter, double quote, or newlines
24
+ if (str.includes(delimiter) || str.includes('"') || str.includes('\n') || str.includes('\r')) {
25
+ return `"${str.replace(/"/g, '""')}"`;
26
+ }
27
+ return str;
28
+ }
29
+ /**
30
+ * Robust RFC-4180 CSV parser supporting multiline fields and escaped quotes.
31
+ */
32
+ function parseCsv(text, delimiter = ',') {
33
+ const rows = [];
34
+ let currentRow = [];
35
+ let currentVal = '';
36
+ let inQuotes = false;
37
+ let i = 0;
38
+ while (i < text.length) {
39
+ const char = text[i];
40
+ if (inQuotes) {
41
+ if (char === '"') {
42
+ if (i + 1 < text.length && text[i + 1] === '"') {
43
+ // Escaped quote
44
+ currentVal += '"';
45
+ i += 2;
46
+ continue;
47
+ }
48
+ else {
49
+ // End quote
50
+ inQuotes = false;
51
+ i++;
52
+ continue;
53
+ }
54
+ }
55
+ else {
56
+ currentVal += char;
57
+ i++;
58
+ continue;
59
+ }
60
+ }
61
+ if (char === '"') {
62
+ inQuotes = true;
63
+ i++;
64
+ continue;
65
+ }
66
+ if (char === delimiter) {
67
+ currentRow.push(currentVal);
68
+ currentVal = '';
69
+ i++;
70
+ continue;
71
+ }
72
+ if (char === '\r') {
73
+ if (i + 1 < text.length && text[i + 1] === '\n') {
74
+ i++;
75
+ }
76
+ currentRow.push(currentVal);
77
+ currentVal = '';
78
+ rows.push(currentRow);
79
+ currentRow = [];
80
+ i++;
81
+ continue;
82
+ }
83
+ if (char === '\n') {
84
+ currentRow.push(currentVal);
85
+ currentVal = '';
86
+ rows.push(currentRow);
87
+ currentRow = [];
88
+ i++;
89
+ continue;
90
+ }
91
+ currentVal += char;
92
+ i++;
93
+ }
94
+ if (currentVal.length > 0 || currentRow.length > 0) {
95
+ currentRow.push(currentVal);
96
+ rows.push(currentRow);
97
+ }
98
+ return rows;
99
+ }
100
+ /**
101
+ * Exports SQLite tables to a structured JSON string.
102
+ */
103
+ async function exportTablesToJson(client, tables, options = {}) {
104
+ const targetTables = tables && tables.length > 0 ? tables : client.tables;
105
+ const result = {
106
+ version: 1,
107
+ exportedAt: new Date().toISOString(),
108
+ tables: {},
109
+ };
110
+ for (const table of targetTables) {
111
+ try {
112
+ const rows = await client.findMany(table);
113
+ result.tables[table] = rows;
114
+ }
115
+ catch {
116
+ result.tables[table] = [];
117
+ }
118
+ }
119
+ return JSON.stringify(result, null, options.pretty ? 2 : undefined);
120
+ }
121
+ /**
122
+ * Imports tables from a JSON export string into the database.
123
+ */
124
+ async function importTablesFromJson(client, jsonString, options = {}) {
125
+ let parsed;
126
+ try {
127
+ parsed = JSON.parse(jsonString);
128
+ }
129
+ catch (err) {
130
+ throw new Error(`Invalid JSON export format: ${err.message}`);
131
+ }
132
+ if (!parsed || typeof parsed.tables !== 'object') {
133
+ throw new Error('Invalid JSON export format: missing tables object.');
134
+ }
135
+ const mode = options.mode ?? 'merge';
136
+ const skipOutbox = options.skipOutbox ?? false;
137
+ const filterTables = options.tables ? new Set(options.tables) : null;
138
+ const pk = client.getPrimaryKey();
139
+ const importedTables = [];
140
+ const rowCounts = {};
141
+ for (const [table, rows] of Object.entries(parsed.tables)) {
142
+ if (filterTables && !filterTables.has(table)) {
143
+ continue;
144
+ }
145
+ if (!Array.isArray(rows)) {
146
+ continue;
147
+ }
148
+ importedTables.push(table);
149
+ rowCounts[table] = 0;
150
+ await client.transaction(async (tx) => {
151
+ if (mode === 'replace') {
152
+ const safeTable = (0, sql_js_1.escapeIdentifier)(table);
153
+ await tx.query(`DELETE FROM ${safeTable}`);
154
+ }
155
+ for (const row of rows) {
156
+ if (!row || typeof row !== 'object')
157
+ continue;
158
+ const id = String(row[pk] ?? row.id ?? row.ID);
159
+ if (!id)
160
+ continue;
161
+ // Upsert row
162
+ await tx.mutate(table, id, 'INSERT', row, { primaryKey: pk, skipOutbox });
163
+ rowCounts[table]++;
164
+ }
165
+ });
166
+ }
167
+ return { tables: importedTables, rowCounts };
168
+ }
169
+ /**
170
+ * Exports a single SQLite table to an RFC-4180 CSV string.
171
+ */
172
+ async function exportTableToCsv(client, tableName, options = {}) {
173
+ const delimiter = options.delimiter ?? ',';
174
+ const includeHeader = options.includeHeader ?? true;
175
+ const rows = await client.findMany(tableName);
176
+ if (rows.length === 0) {
177
+ if (options.columns && includeHeader) {
178
+ return options.columns.map(c => escapeCsvValue(c, delimiter)).join(delimiter);
179
+ }
180
+ return '';
181
+ }
182
+ // Determine columns
183
+ const columns = options.columns && options.columns.length > 0
184
+ ? options.columns
185
+ : Object.keys(rows[0]);
186
+ const lines = [];
187
+ if (includeHeader) {
188
+ lines.push(columns.map(c => escapeCsvValue(c, delimiter)).join(delimiter));
189
+ }
190
+ for (const row of rows) {
191
+ const values = columns.map(col => escapeCsvValue(row[col], delimiter));
192
+ lines.push(values.join(delimiter));
193
+ }
194
+ return lines.join('\n');
195
+ }
196
+ /**
197
+ * Imports an RFC-4180 CSV string into a SQLite table.
198
+ */
199
+ async function importTableFromCsv(client, tableName, csvString, options = {}) {
200
+ if (!csvString || !csvString.trim()) {
201
+ return { rowCount: 0 };
202
+ }
203
+ const delimiter = options.delimiter ?? ',';
204
+ const hasHeader = options.hasHeader ?? true;
205
+ const mode = options.mode ?? 'merge';
206
+ const skipOutbox = options.skipOutbox ?? false;
207
+ const pk = client.getPrimaryKey();
208
+ const parsedRows = parseCsv(csvString.trim(), delimiter);
209
+ if (parsedRows.length === 0) {
210
+ return { rowCount: 0 };
211
+ }
212
+ let columns;
213
+ let dataRows;
214
+ if (hasHeader) {
215
+ columns = parsedRows[0].map(c => c.trim());
216
+ dataRows = parsedRows.slice(1);
217
+ }
218
+ else {
219
+ if (!options.columns || options.columns.length === 0) {
220
+ throw new Error('Explicit columns must be specified when CSV has no header.');
221
+ }
222
+ columns = options.columns;
223
+ dataRows = parsedRows;
224
+ }
225
+ let importedCount = 0;
226
+ await client.transaction(async (tx) => {
227
+ if (mode === 'replace') {
228
+ const safeTable = (0, sql_js_1.escapeIdentifier)(tableName);
229
+ await tx.query(`DELETE FROM ${safeTable}`);
230
+ }
231
+ for (const rowVals of dataRows) {
232
+ if (rowVals.length === 0 || (rowVals.length === 1 && !rowVals[0].trim())) {
233
+ continue;
234
+ }
235
+ const payload = {};
236
+ for (let i = 0; i < columns.length; i++) {
237
+ const col = columns[i];
238
+ const raw = rowVals[i];
239
+ if (raw === undefined || raw === '') {
240
+ payload[col] = null;
241
+ continue;
242
+ }
243
+ // Auto-detect JSON, boolean, and numeric types
244
+ const trimmed = raw.trim();
245
+ if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
246
+ (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
247
+ try {
248
+ payload[col] = JSON.parse(trimmed);
249
+ continue;
250
+ }
251
+ catch {
252
+ // Keep as string
253
+ }
254
+ }
255
+ if (trimmed.toLowerCase() === 'true') {
256
+ payload[col] = 1;
257
+ }
258
+ else if (trimmed.toLowerCase() === 'false') {
259
+ payload[col] = 0;
260
+ }
261
+ else if (!isNaN(Number(trimmed)) && trimmed !== '') {
262
+ payload[col] = Number(trimmed);
263
+ }
264
+ else {
265
+ payload[col] = raw;
266
+ }
267
+ }
268
+ const id = String(payload[pk] ?? payload.id);
269
+ if (!id)
270
+ continue;
271
+ await tx.mutate(tableName, id, 'INSERT', payload, { primaryKey: pk, skipOutbox });
272
+ importedCount++;
273
+ }
274
+ });
275
+ return { rowCount: importedCount };
276
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-lite",
3
- "version": "0.0.12",
3
+ "version": "0.0.14",
4
4
  "description": "Never let a lost connection break a promise. Local-first sync that faithfully keeps data safe until you're back online.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",