@ariadng/sheets 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,691 @@
1
+ // src/plus/batch.ts
2
+ var BatchOperations = class {
3
+ constructor(client) {
4
+ this.client = client;
5
+ // Google Sheets allows up to 100 operations per batch
6
+ this.MAX_BATCH_SIZE = 100;
7
+ }
8
+ /**
9
+ * Execute multiple write operations efficiently
10
+ * Automatically splits into optimal batch sizes
11
+ */
12
+ async batchWrite(spreadsheetId, operations) {
13
+ const batches = this.chunk(operations, this.MAX_BATCH_SIZE);
14
+ const results = [];
15
+ for (const batch of batches) {
16
+ const result = await this.client.batchWrite(spreadsheetId, batch);
17
+ results.push(result);
18
+ }
19
+ return results;
20
+ }
21
+ /**
22
+ * Execute multiple clear operations efficiently
23
+ */
24
+ async batchClear(spreadsheetId, ranges) {
25
+ const batches = this.chunk(ranges, this.MAX_BATCH_SIZE);
26
+ const results = [];
27
+ for (const batch of batches) {
28
+ const result = await this.client.batchClear(spreadsheetId, batch);
29
+ results.push(result);
30
+ }
31
+ return results;
32
+ }
33
+ /**
34
+ * Execute multiple read operations efficiently
35
+ */
36
+ async batchRead(spreadsheetId, ranges) {
37
+ const batches = this.chunk(ranges, this.MAX_BATCH_SIZE);
38
+ const results = [];
39
+ for (const batch of batches) {
40
+ const batchResult = await this.client.batchRead(spreadsheetId, batch);
41
+ results.push(...batchResult);
42
+ }
43
+ return results;
44
+ }
45
+ /**
46
+ * Execute a mixed batch of operations
47
+ */
48
+ async executeBatch(spreadsheetId, operations) {
49
+ const results = {};
50
+ const promises = [];
51
+ if (operations.writes) {
52
+ promises.push(
53
+ this.batchWrite(spreadsheetId, operations.writes).then((r) => {
54
+ results.writeResults = r;
55
+ })
56
+ );
57
+ }
58
+ if (operations.clears) {
59
+ promises.push(
60
+ this.batchClear(spreadsheetId, operations.clears).then((r) => {
61
+ results.clearResults = r;
62
+ })
63
+ );
64
+ }
65
+ if (operations.reads) {
66
+ promises.push(
67
+ this.batchRead(spreadsheetId, operations.reads).then((r) => {
68
+ results.readResults = r;
69
+ })
70
+ );
71
+ }
72
+ await Promise.all(promises);
73
+ return results;
74
+ }
75
+ /**
76
+ * Helper to chunk arrays into smaller batches
77
+ */
78
+ chunk(array, size) {
79
+ const chunks = [];
80
+ for (let i = 0; i < array.length; i += size) {
81
+ chunks.push(array.slice(i, i + size));
82
+ }
83
+ return chunks;
84
+ }
85
+ };
86
+
87
+ // src/plus/cache.ts
88
+ var SimpleCache = class {
89
+ constructor(config) {
90
+ this.cache = /* @__PURE__ */ new Map();
91
+ this.config = {
92
+ ttlSeconds: config?.ttlSeconds ?? 60,
93
+ maxEntries: config?.maxEntries ?? 100
94
+ };
95
+ }
96
+ get(key) {
97
+ const entry = this.cache.get(key);
98
+ if (!entry) return null;
99
+ if (Date.now() > entry.expiry) {
100
+ this.cache.delete(key);
101
+ return null;
102
+ }
103
+ return entry.value;
104
+ }
105
+ set(key, value, ttlOverride) {
106
+ if (this.cache.size >= this.config.maxEntries) {
107
+ const firstKey = this.cache.keys().next().value;
108
+ if (firstKey) {
109
+ this.cache.delete(firstKey);
110
+ }
111
+ }
112
+ const ttl = ttlOverride ?? this.config.ttlSeconds;
113
+ this.cache.set(key, {
114
+ value,
115
+ expiry: Date.now() + ttl * 1e3
116
+ });
117
+ }
118
+ invalidate(pattern) {
119
+ if (!pattern) {
120
+ this.cache.clear();
121
+ return;
122
+ }
123
+ const regex = new RegExp(pattern.replace("*", ".*"));
124
+ for (const key of this.cache.keys()) {
125
+ if (regex.test(key)) {
126
+ this.cache.delete(key);
127
+ }
128
+ }
129
+ }
130
+ size() {
131
+ return this.cache.size;
132
+ }
133
+ clear() {
134
+ this.cache.clear();
135
+ }
136
+ };
137
+ function withCache(client, config) {
138
+ const cache = new SimpleCache(config);
139
+ const wrappedClient = Object.create(client);
140
+ const originalRead = client.read.bind(client);
141
+ wrappedClient.read = async function(spreadsheetId, range) {
142
+ const cacheKey = `${spreadsheetId}:${range}`;
143
+ const cached = cache.get(cacheKey);
144
+ if (cached !== null) {
145
+ return cached;
146
+ }
147
+ const result = await originalRead(spreadsheetId, range);
148
+ cache.set(cacheKey, result);
149
+ return result;
150
+ };
151
+ const originalBatchRead = client.batchRead.bind(client);
152
+ wrappedClient.batchRead = async function(spreadsheetId, ranges) {
153
+ const uncachedRanges = [];
154
+ const cachedResults = /* @__PURE__ */ new Map();
155
+ for (const range of ranges) {
156
+ const cacheKey = `${spreadsheetId}:${range}`;
157
+ const cached = cache.get(cacheKey);
158
+ if (cached !== null) {
159
+ cachedResults.set(range, {
160
+ range,
161
+ values: cached
162
+ });
163
+ } else {
164
+ uncachedRanges.push(range);
165
+ }
166
+ }
167
+ let freshResults = [];
168
+ if (uncachedRanges.length > 0) {
169
+ freshResults = await originalBatchRead(spreadsheetId, uncachedRanges);
170
+ for (const result of freshResults) {
171
+ if (result.range) {
172
+ const cacheKey = `${spreadsheetId}:${result.range}`;
173
+ cache.set(cacheKey, result.values || []);
174
+ }
175
+ }
176
+ }
177
+ const results = [];
178
+ for (const range of ranges) {
179
+ const cached = cachedResults.get(range);
180
+ if (cached) {
181
+ results.push(cached);
182
+ } else {
183
+ const fresh = freshResults.find((r) => r.range === range);
184
+ if (fresh) {
185
+ results.push(fresh);
186
+ }
187
+ }
188
+ }
189
+ return results;
190
+ };
191
+ const originalWrite = client.write.bind(client);
192
+ wrappedClient.write = async function(spreadsheetId, range, values) {
193
+ const result = await originalWrite(spreadsheetId, range, values);
194
+ cache.invalidate(`${spreadsheetId}:${range}*`);
195
+ return result;
196
+ };
197
+ const originalAppend = client.append.bind(client);
198
+ wrappedClient.append = async function(spreadsheetId, range, values) {
199
+ const result = await originalAppend(spreadsheetId, range, values);
200
+ cache.invalidate(`${spreadsheetId}:*`);
201
+ return result;
202
+ };
203
+ const originalClear = client.clear.bind(client);
204
+ wrappedClient.clear = async function(spreadsheetId, range) {
205
+ const result = await originalClear(spreadsheetId, range);
206
+ cache.invalidate(`${spreadsheetId}:${range}*`);
207
+ return result;
208
+ };
209
+ wrappedClient.cache = cache;
210
+ return wrappedClient;
211
+ }
212
+
213
+ // src/plus/types.ts
214
+ var A1 = class _A1 {
215
+ /**
216
+ * Convert column letter to index (A=0, B=1, etc)
217
+ */
218
+ static columnToIndex(column) {
219
+ let index = 0;
220
+ for (let i = 0; i < column.length; i++) {
221
+ index = index * 26 + (column.charCodeAt(i) - 64);
222
+ }
223
+ return index - 1;
224
+ }
225
+ /**
226
+ * Convert index to column letter (0=A, 1=B, etc)
227
+ */
228
+ static indexToColumn(index) {
229
+ let column = "";
230
+ index++;
231
+ while (index > 0) {
232
+ const remainder = (index - 1) % 26;
233
+ column = String.fromCharCode(65 + remainder) + column;
234
+ index = Math.floor((index - 1) / 26);
235
+ }
236
+ return column;
237
+ }
238
+ /**
239
+ * Parse A1 notation to components
240
+ */
241
+ static parse(notation) {
242
+ const match = notation.match(
243
+ /^(?:(?:'([^']+)'|([^!]+))!)?([A-Z]+)(\d+)(?::([A-Z]+)(\d+))?$/
244
+ );
245
+ if (!match) {
246
+ throw new Error(`Invalid A1 notation: ${notation}`);
247
+ }
248
+ const [, quotedSheet, unquotedSheet, startCol, startRow, endCol, endRow] = match;
249
+ return {
250
+ sheet: quotedSheet || unquotedSheet || void 0,
251
+ startCol,
252
+ startRow: parseInt(startRow, 10),
253
+ endCol: endCol || void 0,
254
+ endRow: endRow ? parseInt(endRow, 10) : void 0
255
+ };
256
+ }
257
+ /**
258
+ * Build A1 notation from components
259
+ */
260
+ static build(sheet, startCol, startRow, endCol, endRow) {
261
+ let sheetPrefix = "";
262
+ if (sheet) {
263
+ sheetPrefix = /[^a-zA-Z0-9]/.test(sheet) ? `'${sheet}'!` : `${sheet}!`;
264
+ }
265
+ const start = `${startCol}${startRow}`;
266
+ if (endCol && endRow) {
267
+ return `${sheetPrefix}${start}:${endCol}${endRow}`;
268
+ }
269
+ return `${sheetPrefix}${start}`;
270
+ }
271
+ /**
272
+ * Get range dimensions
273
+ */
274
+ static getDimensions(notation) {
275
+ const parsed = _A1.parse(notation);
276
+ const rows = parsed.endRow ? parsed.endRow - parsed.startRow + 1 : 1;
277
+ const columns = parsed.endCol ? _A1.columnToIndex(parsed.endCol) - _A1.columnToIndex(parsed.startCol) + 1 : 1;
278
+ return { rows, columns };
279
+ }
280
+ /**
281
+ * Offset a range by rows and columns
282
+ */
283
+ static offset(notation, rowOffset, colOffset) {
284
+ const parsed = _A1.parse(notation);
285
+ const newStartCol = _A1.indexToColumn(
286
+ _A1.columnToIndex(parsed.startCol) + colOffset
287
+ );
288
+ const newStartRow = parsed.startRow + rowOffset;
289
+ if (newStartRow < 1) {
290
+ throw new Error("Row offset results in invalid range");
291
+ }
292
+ let newEndCol;
293
+ let newEndRow;
294
+ if (parsed.endCol && parsed.endRow) {
295
+ newEndCol = _A1.indexToColumn(
296
+ _A1.columnToIndex(parsed.endCol) + colOffset
297
+ );
298
+ newEndRow = parsed.endRow + rowOffset;
299
+ if (newEndRow < 1) {
300
+ throw new Error("Row offset results in invalid range");
301
+ }
302
+ }
303
+ return _A1.build(parsed.sheet, newStartCol, newStartRow, newEndCol, newEndRow);
304
+ }
305
+ };
306
+ var TypedSheets = class {
307
+ constructor(client) {
308
+ this.client = client;
309
+ }
310
+ async read(spreadsheetId, range, parser) {
311
+ const data = await this.client.read(spreadsheetId, range);
312
+ return parser ? parser(data) : data;
313
+ }
314
+ async write(spreadsheetId, range, data, serializer) {
315
+ const values = serializer ? serializer(data) : data;
316
+ await this.client.write(spreadsheetId, range, values);
317
+ }
318
+ async append(spreadsheetId, range, data, serializer) {
319
+ const values = serializer ? serializer(data) : data;
320
+ await this.client.append(spreadsheetId, range, values);
321
+ }
322
+ };
323
+ var Parsers = {
324
+ /**
325
+ * Parse rows as objects using first row as headers
326
+ */
327
+ rowsToObjects(data) {
328
+ if (data.length < 2) return [];
329
+ const [headers, ...rows] = data;
330
+ return rows.map((row) => {
331
+ const obj = {};
332
+ headers?.forEach((header, i) => {
333
+ obj[header] = row[i];
334
+ });
335
+ return obj;
336
+ });
337
+ },
338
+ /**
339
+ * Parse as simple 2D array with type coercion to numbers
340
+ */
341
+ asNumbers(data) {
342
+ return data.map((row) => row.map((cell) => parseFloat(cell) || 0));
343
+ },
344
+ /**
345
+ * Parse as strings, handling empty cells
346
+ */
347
+ asStrings(data) {
348
+ return data.map((row) => row.map((cell) => String(cell || "")));
349
+ },
350
+ /**
351
+ * Parse as key-value pairs from two columns
352
+ */
353
+ asMap(data) {
354
+ const map = /* @__PURE__ */ new Map();
355
+ for (const row of data) {
356
+ if (row.length >= 2) {
357
+ map.set(String(row[0]), row[1]);
358
+ }
359
+ }
360
+ return map;
361
+ },
362
+ /**
363
+ * Parse single column as array
364
+ */
365
+ column(data, columnIndex = 0) {
366
+ return data.map((row) => row[columnIndex]).filter((val) => val !== void 0);
367
+ }
368
+ };
369
+ var Serializers = {
370
+ /**
371
+ * Convert objects to rows with headers
372
+ */
373
+ objectsToRows(objects, headers) {
374
+ if (objects.length === 0) return [];
375
+ const keys = headers || Object.keys(objects[0]);
376
+ const headerRow = keys.map(String);
377
+ const dataRows = objects.map((obj) => keys.map((key) => obj[key]));
378
+ return [headerRow, ...dataRows];
379
+ },
380
+ /**
381
+ * Convert Map to two-column format
382
+ */
383
+ mapToRows(map) {
384
+ const rows = [];
385
+ for (const [key, value] of map.entries()) {
386
+ rows.push([key, value]);
387
+ }
388
+ return rows;
389
+ },
390
+ /**
391
+ * Convert array to single column
392
+ */
393
+ arrayToColumn(array) {
394
+ return array.map((item) => [item]);
395
+ },
396
+ /**
397
+ * Transpose rows and columns
398
+ */
399
+ transpose(data) {
400
+ if (data.length === 0) return [];
401
+ const maxLength = Math.max(...data.map((row) => row.length));
402
+ const result = [];
403
+ for (let col = 0; col < maxLength; col++) {
404
+ const newRow = [];
405
+ for (let row = 0; row < data.length; row++) {
406
+ newRow.push(data[row]?.[col] ?? "");
407
+ }
408
+ result.push(newRow);
409
+ }
410
+ return result;
411
+ }
412
+ };
413
+
414
+ // src/core/client.ts
415
+ import { google } from "googleapis";
416
+
417
+ // src/core/errors.ts
418
+ var GoogleSheetsError = class extends Error {
419
+ constructor(originalError) {
420
+ const message = originalError.response?.data?.error?.message || originalError.message || "Unknown error";
421
+ super(message);
422
+ this.name = "GoogleSheetsError";
423
+ this.code = originalError.response?.status || originalError.code;
424
+ this.originalError = originalError;
425
+ const retryableCodes = [429, 500, 502, 503, 504, "ECONNRESET", "ETIMEDOUT", "ENOTFOUND"];
426
+ this.isRetryable = retryableCodes.includes(this.code);
427
+ if (originalError.stack) {
428
+ this.stack = originalError.stack;
429
+ }
430
+ }
431
+ /**
432
+ * Check if error is a rate limit error
433
+ */
434
+ isRateLimitError() {
435
+ return this.code === 429;
436
+ }
437
+ /**
438
+ * Check if error is a permission error
439
+ */
440
+ isPermissionError() {
441
+ return this.code === 403;
442
+ }
443
+ /**
444
+ * Check if error is a not found error
445
+ */
446
+ isNotFoundError() {
447
+ return this.code === 404;
448
+ }
449
+ /**
450
+ * Get a user-friendly error message
451
+ */
452
+ getUserMessage() {
453
+ if (this.isPermissionError()) {
454
+ return "Permission denied. Please ensure the spreadsheet is shared with the service account or you have proper OAuth permissions.";
455
+ }
456
+ if (this.isRateLimitError()) {
457
+ return "Rate limit exceeded. Please wait before making more requests.";
458
+ }
459
+ if (this.isNotFoundError()) {
460
+ return "Spreadsheet or range not found. Please check the ID and range are correct.";
461
+ }
462
+ return this.message;
463
+ }
464
+ };
465
+
466
+ // src/core/client.ts
467
+ var GoogleSheetsCore = class {
468
+ constructor(config) {
469
+ this.sheets = google.sheets({
470
+ version: "v4",
471
+ auth: config.auth
472
+ });
473
+ this.retryConfig = {
474
+ maxAttempts: config.retryConfig?.maxAttempts ?? 3,
475
+ maxDelay: config.retryConfig?.maxDelay ?? 1e4,
476
+ initialDelay: config.retryConfig?.initialDelay ?? 1e3
477
+ };
478
+ }
479
+ /**
480
+ * Read values from a spreadsheet
481
+ * @param spreadsheetId The spreadsheet ID
482
+ * @param range A1 notation range (e.g., 'Sheet1!A1:B10')
483
+ * @returns 2D array of values
484
+ */
485
+ async read(spreadsheetId, range) {
486
+ return this.withRetry(async () => {
487
+ const response = await this.sheets.spreadsheets.values.get({
488
+ spreadsheetId,
489
+ range
490
+ });
491
+ return response.data.values || [];
492
+ });
493
+ }
494
+ /**
495
+ * Write values to a spreadsheet
496
+ * @param spreadsheetId The spreadsheet ID
497
+ * @param range A1 notation range
498
+ * @param values 2D array of values to write
499
+ */
500
+ async write(spreadsheetId, range, values) {
501
+ return this.withRetry(async () => {
502
+ const response = await this.sheets.spreadsheets.values.update({
503
+ spreadsheetId,
504
+ range,
505
+ valueInputOption: "USER_ENTERED",
506
+ requestBody: { values }
507
+ });
508
+ return response.data;
509
+ });
510
+ }
511
+ /**
512
+ * Append values to a spreadsheet
513
+ */
514
+ async append(spreadsheetId, range, values) {
515
+ return this.withRetry(async () => {
516
+ const response = await this.sheets.spreadsheets.values.append({
517
+ spreadsheetId,
518
+ range,
519
+ valueInputOption: "USER_ENTERED",
520
+ insertDataOption: "INSERT_ROWS",
521
+ requestBody: { values }
522
+ });
523
+ return response.data;
524
+ });
525
+ }
526
+ /**
527
+ * Clear values in a range
528
+ */
529
+ async clear(spreadsheetId, range) {
530
+ return this.withRetry(async () => {
531
+ const response = await this.sheets.spreadsheets.values.clear({
532
+ spreadsheetId,
533
+ range
534
+ });
535
+ return response.data;
536
+ });
537
+ }
538
+ /**
539
+ * Batch read multiple ranges
540
+ */
541
+ async batchRead(spreadsheetId, ranges) {
542
+ return this.withRetry(async () => {
543
+ const response = await this.sheets.spreadsheets.values.batchGet({
544
+ spreadsheetId,
545
+ ranges
546
+ });
547
+ return response.data.valueRanges || [];
548
+ });
549
+ }
550
+ /**
551
+ * Batch update multiple ranges
552
+ */
553
+ async batchWrite(spreadsheetId, data) {
554
+ return this.withRetry(async () => {
555
+ const response = await this.sheets.spreadsheets.values.batchUpdate({
556
+ spreadsheetId,
557
+ requestBody: {
558
+ data: data.map((item) => ({
559
+ range: item.range,
560
+ values: item.values
561
+ })),
562
+ valueInputOption: "USER_ENTERED"
563
+ }
564
+ });
565
+ return response.data;
566
+ });
567
+ }
568
+ /**
569
+ * Batch clear multiple ranges
570
+ */
571
+ async batchClear(spreadsheetId, ranges) {
572
+ return this.withRetry(async () => {
573
+ const response = await this.sheets.spreadsheets.values.batchClear({
574
+ spreadsheetId,
575
+ requestBody: { ranges }
576
+ });
577
+ return response.data;
578
+ });
579
+ }
580
+ /**
581
+ * Get spreadsheet metadata
582
+ */
583
+ async getSpreadsheet(spreadsheetId) {
584
+ return this.withRetry(async () => {
585
+ const response = await this.sheets.spreadsheets.get({
586
+ spreadsheetId
587
+ });
588
+ return response.data;
589
+ });
590
+ }
591
+ /**
592
+ * Get the underlying Sheets API instance for advanced usage
593
+ */
594
+ getApi() {
595
+ return this.sheets;
596
+ }
597
+ /**
598
+ * Simple exponential backoff retry logic
599
+ */
600
+ async withRetry(fn) {
601
+ let lastError;
602
+ for (let attempt = 0; attempt < this.retryConfig.maxAttempts; attempt++) {
603
+ try {
604
+ return await fn();
605
+ } catch (error) {
606
+ lastError = error;
607
+ if (!this.isRetryable(error) || attempt === this.retryConfig.maxAttempts - 1) {
608
+ throw new GoogleSheetsError(error);
609
+ }
610
+ const baseDelay = Math.min(
611
+ this.retryConfig.initialDelay * Math.pow(2, attempt),
612
+ this.retryConfig.maxDelay
613
+ );
614
+ const jitter = Math.random() * 1e3;
615
+ const delay = baseDelay + jitter;
616
+ await new Promise((resolve) => setTimeout(resolve, delay));
617
+ }
618
+ }
619
+ throw new GoogleSheetsError(lastError);
620
+ }
621
+ isRetryable(error) {
622
+ const retryableCodes = [429, 500, 502, 503, 504];
623
+ const retryableErrors = ["ECONNRESET", "ETIMEDOUT", "ENOTFOUND"];
624
+ return retryableCodes.includes(error.code) || retryableCodes.includes(error.response?.status) || retryableErrors.includes(error.code);
625
+ }
626
+ };
627
+
628
+ // src/core/auth.ts
629
+ import { GoogleAuth, OAuth2Client, JWT } from "google-auth-library";
630
+ import * as fs from "fs/promises";
631
+ async function createServiceAccountAuth(keyFile) {
632
+ const key = typeof keyFile === "string" ? JSON.parse(await fs.readFile(keyFile, "utf8")) : keyFile;
633
+ const jwt = new JWT({
634
+ email: key.client_email,
635
+ key: key.private_key,
636
+ scopes: ["https://www.googleapis.com/auth/spreadsheets"]
637
+ });
638
+ return jwt;
639
+ }
640
+ async function createOAuth2Client(credentials, tokenPath) {
641
+ const client = new OAuth2Client(
642
+ credentials.client_id,
643
+ credentials.client_secret,
644
+ credentials.redirect_uris[0]
645
+ );
646
+ if (tokenPath) {
647
+ try {
648
+ const token = JSON.parse(await fs.readFile(tokenPath, "utf8"));
649
+ client.setCredentials(token);
650
+ } catch {
651
+ }
652
+ }
653
+ return client;
654
+ }
655
+ function generateAuthUrl(client, scopes = ["https://www.googleapis.com/auth/spreadsheets"]) {
656
+ return client.generateAuthUrl({
657
+ access_type: "offline",
658
+ scope: scopes
659
+ });
660
+ }
661
+ async function getTokenFromCode(client, code) {
662
+ const { tokens } = await client.getToken(code);
663
+ client.setCredentials(tokens);
664
+ return tokens;
665
+ }
666
+ async function saveToken(tokens, path) {
667
+ await fs.writeFile(path, JSON.stringify(tokens, null, 2));
668
+ }
669
+ function createAuth(auth) {
670
+ if (auth instanceof GoogleAuth || auth instanceof OAuth2Client || auth instanceof JWT) {
671
+ return auth;
672
+ }
673
+ return createServiceAccountAuth(auth);
674
+ }
675
+ export {
676
+ A1,
677
+ BatchOperations,
678
+ GoogleSheetsCore,
679
+ GoogleSheetsError,
680
+ Parsers,
681
+ Serializers,
682
+ SimpleCache,
683
+ TypedSheets,
684
+ createAuth,
685
+ createOAuth2Client,
686
+ createServiceAccountAuth,
687
+ generateAuthUrl,
688
+ getTokenFromCode,
689
+ saveToken,
690
+ withCache
691
+ };