bodevops-features 1.0.0 → 1.0.8

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/dist/esm/index.js CHANGED
@@ -1,11 +1,1687 @@
1
- const test = () => {
2
- console.log('test');
3
- };
1
+ import { google } from 'googleapis';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ /**
6
+ * Google Drive Feature Library - Configuration Module
7
+ * @description Configuration management for Google Drive client initialization and authentication.
8
+ * @module gg-drive/config
9
+ */
10
+ /**
11
+ * Default OAuth scopes required for Google Drive operations.
12
+ * These scopes provide full access to Drive files.
13
+ */
14
+ const DEFAULT_DRIVE_SCOPES = [
15
+ 'https://www.googleapis.com/auth/drive',
16
+ 'https://www.googleapis.com/auth/drive.file',
17
+ ];
18
+ /**
19
+ * Google Drive Configuration Manager.
20
+ * Handles validation and normalization of configuration options for the Google Drive client.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * // Using key file path
25
+ * const config = new GoogleDriveConfig({
26
+ * keyFilePath: './service-account.json'
27
+ * });
28
+ *
29
+ * // Using credentials object
30
+ * const config = new GoogleDriveConfig({
31
+ * credentials: {
32
+ * type: 'service_account',
33
+ * project_id: 'my-project',
34
+ * private_key: '-----BEGIN PRIVATE KEY-----\n...',
35
+ * client_email: 'service-account@my-project.iam.gserviceaccount.com',
36
+ * // ... other fields
37
+ * }
38
+ * });
39
+ * ```
40
+ */
41
+ class GoogleDriveConfig {
42
+ /**
43
+ * Creates a new GoogleDriveConfig instance.
44
+ *
45
+ * @param config - Configuration options for Google Drive client
46
+ * @throws Error if neither keyFilePath nor credentials is provided
47
+ */
48
+ constructor({ keyFilePath, credentials, scopes }) {
49
+ // Validate that at least one authentication method is provided
50
+ if (!keyFilePath && !credentials) {
51
+ throw new Error('GoogleDriveConfig: Either keyFilePath or credentials must be provided');
52
+ }
53
+ this.keyFilePath = keyFilePath;
54
+ this.credentials = credentials;
55
+ this.scopes = scopes || DEFAULT_DRIVE_SCOPES;
56
+ // Validate credentials structure if provided
57
+ if (credentials) {
58
+ this.validateCredentials(credentials);
59
+ }
60
+ }
61
+ /**
62
+ * Validates that the credentials object contains all required fields.
63
+ *
64
+ * @param credentials - The credentials object to validate
65
+ * @throws Error if required fields are missing
66
+ */
67
+ validateCredentials(credentials) {
68
+ const requiredFields = [
69
+ 'type',
70
+ 'project_id',
71
+ 'private_key',
72
+ 'client_email',
73
+ ];
74
+ for (const field of requiredFields) {
75
+ if (!credentials[field]) {
76
+ throw new Error(`GoogleDriveConfig: Missing required credential field: ${field}`);
77
+ }
78
+ }
79
+ if (credentials.type !== 'service_account') {
80
+ throw new Error(`GoogleDriveConfig: Invalid credential type. Expected 'service_account', got '${credentials.type}'`);
81
+ }
82
+ }
83
+ /**
84
+ * Returns the authentication configuration object suitable for googleapis.
85
+ *
86
+ * @returns Authentication options for Google Auth
87
+ */
88
+ getAuthOptions() {
89
+ if (this.keyFilePath) {
90
+ return {
91
+ keyFile: this.keyFilePath,
92
+ scopes: this.scopes,
93
+ };
94
+ }
95
+ return {
96
+ credentials: this.credentials,
97
+ scopes: this.scopes,
98
+ };
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Google Drive Feature Library - Utility Functions
104
+ * @description Helper functions for file handling, formatting, and validation in Google Drive operations.
105
+ * @module gg-drive/utils
106
+ */
107
+ /**
108
+ * Size units for human-readable byte formatting.
109
+ */
110
+ const SIZE_UNITS = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
111
+ /**
112
+ * Base value for byte conversions (1024 bytes = 1 KB).
113
+ */
114
+ const BYTE_BASE = 1024;
115
+ /**
116
+ * Converts a byte count into a human-readable string format.
117
+ *
118
+ * @param bytes - The number of bytes to format
119
+ * @returns A human-readable string representation (e.g., "1.5 GB")
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * formatBytes(0); // "0 Bytes"
124
+ * formatBytes(1024); // "1 KB"
125
+ * formatBytes(1536); // "1.5 KB"
126
+ * formatBytes(1073741824); // "1 GB"
127
+ * ```
128
+ */
129
+ function formatBytes({ bytes }) {
130
+ if (bytes === 0) {
131
+ return '0 Bytes';
132
+ }
133
+ const exponent = Math.floor(Math.log(bytes) / Math.log(BYTE_BASE));
134
+ const value = bytes / Math.pow(BYTE_BASE, exponent);
135
+ const formattedValue = parseFloat(value.toFixed(2));
136
+ return `${formattedValue} ${SIZE_UNITS[exponent]}`;
137
+ }
138
+ /**
139
+ * Normalizes a file path to use the correct path separators for the current OS.
140
+ * Also resolves relative paths to absolute paths.
141
+ *
142
+ * @param filePath - The file path to normalize
143
+ * @returns The normalized absolute file path
144
+ *
145
+ * @example
146
+ * ```typescript
147
+ * normalizeFilePath({ filePath: './documents/file.pdf' });
148
+ * // Returns: "D:\\MyFolder\\documents\\file.pdf" (on Windows)
149
+ * ```
150
+ */
151
+ function normalizeFilePath({ filePath }) {
152
+ // Resolve to absolute path if relative
153
+ const absolutePath = path.resolve(filePath);
154
+ // Normalize path separators for current OS
155
+ return path.normalize(absolutePath);
156
+ }
157
+ /**
158
+ * Validates that a file exists at the specified path.
159
+ *
160
+ * @param filePath - The absolute path to the file to validate
161
+ * @returns True if the file exists, false otherwise
162
+ *
163
+ * @example
164
+ * ```typescript
165
+ * const exists = validateFileExists({ filePath: 'D:\\MyFolder\\file.pdf' });
166
+ * if (!exists) {
167
+ * console.log('File not found!');
168
+ * }
169
+ * ```
170
+ */
171
+ function validateFileExists({ filePath }) {
172
+ try {
173
+ return fs.existsSync(filePath);
174
+ }
175
+ catch {
176
+ return false;
177
+ }
178
+ }
179
+ /**
180
+ * Gets detailed information about a local file.
181
+ *
182
+ * @param filePath - The absolute path to the file
183
+ * @returns File information object or null if file doesn't exist
184
+ *
185
+ * @example
186
+ * ```typescript
187
+ * const info = getFileInfo({ filePath: './document.pdf' });
188
+ * if (info) {
189
+ * console.log(`File size: ${info.sizeFormatted}`);
190
+ * }
191
+ * ```
192
+ */
193
+ function getFileInfo({ filePath }) {
194
+ const normalizedPath = normalizeFilePath({ filePath });
195
+ if (!validateFileExists({ filePath: normalizedPath })) {
196
+ return null;
197
+ }
198
+ const stats = fs.statSync(normalizedPath);
199
+ return {
200
+ name: path.basename(normalizedPath),
201
+ extension: path.extname(normalizedPath).slice(1),
202
+ directory: path.dirname(normalizedPath),
203
+ size: stats.size,
204
+ sizeFormatted: formatBytes({ bytes: stats.size }),
205
+ };
206
+ }
207
+ /**
208
+ * Parses a folder path string into an array of folder names.
209
+ *
210
+ * @param folderPath - The folder path to parse (e.g., "folder1/folder2/folder3")
211
+ * @returns An array of folder names
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * parseFolderPath({ folderPath: 'a/b/c' }); // ['a', 'b', 'c']
216
+ * parseFolderPath({ folderPath: '/' }); // []
217
+ * parseFolderPath({ folderPath: '' }); // []
218
+ * ```
219
+ */
220
+ function parseFolderPath({ folderPath }) {
221
+ if (!folderPath || folderPath === '/' || folderPath === '') {
222
+ return [];
223
+ }
224
+ return folderPath.split('/').filter((folder) => folder.trim() !== '');
225
+ }
226
+ /**
227
+ * Creates a readable stream for a local file.
228
+ *
229
+ * @param filePath - The absolute path to the file
230
+ * @returns A readable stream for the file
231
+ * @throws Error if the file doesn't exist
232
+ *
233
+ * @example
234
+ * ```typescript
235
+ * const stream = createFileReadStream({ filePath: './document.pdf' });
236
+ * // Use stream for upload
237
+ * ```
238
+ */
239
+ function createFileReadStream({ filePath }) {
240
+ const normalizedPath = normalizeFilePath({ filePath });
241
+ if (!validateFileExists({ filePath: normalizedPath })) {
242
+ throw new Error(`File does not exist: ${normalizedPath}`);
243
+ }
244
+ return fs.createReadStream(normalizedPath);
245
+ }
246
+
247
+ /**
248
+ * Google Drive Feature Library - Main Client Class
249
+ * @description A framework-agnostic client for interacting with Google Drive API.
250
+ * Provides methods for file upload, download, sharing, and storage management.
251
+ * @module gg-drive/google-drive
252
+ */
253
+ /**
254
+ * Google Drive Client for managing files and folders in Google Drive.
255
+ *
256
+ * @example
257
+ * ```typescript
258
+ * import { GoogleDriveClient } from 'bodevops-features/gg-drive';
259
+ *
260
+ * const client = new GoogleDriveClient({
261
+ * keyFilePath: './service-account.json'
262
+ * });
263
+ *
264
+ * // Get storage information
265
+ * const storage = await client.getStorageInfo();
266
+ * console.log(`Used: ${storage.formattedUsed} of ${storage.formattedTotal}`);
267
+ *
268
+ * // Upload a file
269
+ * const result = await client.uploadFile({
270
+ * localFilePath: './document.pdf',
271
+ * driveFolder: 'MyFolder/Documents',
272
+ * fileName: 'my-document.pdf'
273
+ * });
274
+ * console.log(`Uploaded: ${result.webViewLink}`);
275
+ * ```
276
+ */
277
+ class GoogleDriveClient {
278
+ /**
279
+ * Creates a new GoogleDriveClient instance.
280
+ *
281
+ * @param configOptions - Configuration options for the Google Drive client
282
+ */
283
+ constructor(configOptions) {
284
+ this.config = new GoogleDriveConfig(configOptions);
285
+ }
286
+ /**
287
+ * Creates and returns an authenticated Google Drive API client.
288
+ *
289
+ * @returns A Promise that resolves to an authenticated Drive API client
290
+ */
291
+ async getDriveClient() {
292
+ const authOptions = this.config.getAuthOptions();
293
+ const auth = new google.auth.GoogleAuth({
294
+ keyFile: authOptions.keyFile,
295
+ credentials: authOptions.credentials,
296
+ scopes: authOptions.scopes,
297
+ });
298
+ const authClient = await auth.getClient();
299
+ return google.drive({
300
+ version: 'v3',
301
+ auth: authClient,
302
+ });
303
+ }
304
+ /**
305
+ * Retrieves storage quota information for the Google Drive account.
306
+ *
307
+ * @returns A Promise that resolves to storage information including used/total space
308
+ * @throws Error if unable to retrieve storage information
309
+ *
310
+ * @example
311
+ * ```typescript
312
+ * const storage = await client.getStorageInfo();
313
+ * console.log(`Storage: ${storage.formattedUsed} / ${storage.formattedTotal} (${storage.percentage}%)`);
314
+ * ```
315
+ */
316
+ async getStorageInfo() {
317
+ const driveInstance = await this.getDriveClient();
318
+ const response = await driveInstance.about.get({
319
+ fields: 'storageQuota',
320
+ });
321
+ const quota = response.data.storageQuota;
322
+ if (!quota) {
323
+ throw new Error('Unable to retrieve storage quota information');
324
+ }
325
+ const used = parseInt(quota.usage || '0', 10);
326
+ const total = parseInt(quota.limit || '0', 10);
327
+ const usedInDrive = parseInt(quota.usageInDrive || '0', 10);
328
+ const percentage = total > 0 ? (used / total) * 100 : 0;
329
+ return {
330
+ used,
331
+ total,
332
+ usedInDrive,
333
+ percentage: parseFloat(percentage.toFixed(2)),
334
+ formattedUsed: formatBytes({ bytes: used }),
335
+ formattedTotal: formatBytes({ bytes: total }),
336
+ formattedUsedInDrive: formatBytes({ bytes: usedInDrive }),
337
+ };
338
+ }
339
+ /**
340
+ * Gets an existing folder by name or creates it if it doesn't exist.
341
+ *
342
+ * @param folderName - The name of the folder to find or create
343
+ * @param parentId - The ID of the parent folder (default: 'root')
344
+ * @returns A Promise that resolves to the folder ID
345
+ */
346
+ async getOrCreateFolder({ folderName, parentId = 'root', }) {
347
+ const driveInstance = await this.getDriveClient();
348
+ // Search for existing folder
349
+ const query = `name='${folderName}' and mimeType='application/vnd.google-apps.folder' and '${parentId}' in parents and trashed=false`;
350
+ const response = await driveInstance.files.list({
351
+ q: query,
352
+ fields: 'files(id, name)',
353
+ });
354
+ const folders = response.data.files || [];
355
+ if (folders.length > 0 && folders[0].id) {
356
+ // Folder exists, return its ID
357
+ return folders[0].id;
358
+ }
359
+ // Folder doesn't exist, create it
360
+ const folderMetadata = {
361
+ name: folderName,
362
+ mimeType: 'application/vnd.google-apps.folder',
363
+ parents: [parentId],
364
+ };
365
+ const folder = await driveInstance.files.create({
366
+ requestBody: folderMetadata,
367
+ fields: 'id',
368
+ });
369
+ if (!folder.data.id) {
370
+ throw new Error(`Failed to create folder: ${folderName}`);
371
+ }
372
+ return folder.data.id;
373
+ }
374
+ /**
375
+ * Creates a folder hierarchy from a path string (e.g., "a/b/c") and returns the ID of the final folder.
376
+ *
377
+ * @param folderPath - The folder path to create (e.g., "folder1/folder2/folder3")
378
+ * @returns A Promise that resolves to the ID of the innermost folder
379
+ */
380
+ async createFolderHierarchy({ folderPath }) {
381
+ const folders = parseFolderPath({ folderPath });
382
+ if (folders.length === 0) {
383
+ return 'root';
384
+ }
385
+ let currentParentId = 'root';
386
+ for (const folderName of folders) {
387
+ currentParentId = await this.getOrCreateFolder({
388
+ folderName,
389
+ parentId: currentParentId,
390
+ });
391
+ }
392
+ return currentParentId;
393
+ }
394
+ /**
395
+ * Uploads a file to Google Drive and automatically makes it public.
396
+ *
397
+ * @param params - Upload parameters including local file path and destination folder
398
+ * @returns A Promise that resolves to the upload result containing file ID and links
399
+ * @throws Error if the local file doesn't exist or upload fails
400
+ *
401
+ * @example
402
+ * ```typescript
403
+ * const result = await client.uploadFile({
404
+ * localFilePath: './document.pdf',
405
+ * driveFolder: 'MyFolder/Documents',
406
+ * fileName: 'my-document.pdf' // Optional
407
+ * });
408
+ * console.log(`View at: ${result.webViewLink}`);
409
+ * ```
410
+ */
411
+ async uploadFile({ localFilePath, driveFolder, fileName, }) {
412
+ // Normalize and validate the local file path
413
+ const normalizedPath = normalizeFilePath({ filePath: localFilePath });
414
+ if (!validateFileExists({ filePath: normalizedPath })) {
415
+ throw new Error(`File does not exist: ${normalizedPath}. Please check the file path and ensure the file exists.`);
416
+ }
417
+ // Use the local filename if fileName is not provided
418
+ const finalFileName = fileName || normalizedPath.split(/[\\/]/).pop() || 'unnamed';
419
+ const driveInstance = await this.getDriveClient();
420
+ // Create the folder hierarchy and get the ID of the innermost folder
421
+ const folderId = await this.createFolderHierarchy({ folderPath: driveFolder });
422
+ // File metadata
423
+ const fileMetadata = {
424
+ name: finalFileName,
425
+ parents: [folderId],
426
+ };
427
+ // Create media upload
428
+ const media = {
429
+ mimeType: 'application/octet-stream',
430
+ body: createFileReadStream({ filePath: normalizedPath }),
431
+ };
432
+ const file = await driveInstance.files.create({
433
+ requestBody: fileMetadata,
434
+ media: media,
435
+ fields: 'id, name, webViewLink, webContentLink',
436
+ });
437
+ const result = {
438
+ id: file.data.id || '',
439
+ name: file.data.name || '',
440
+ webViewLink: file.data.webViewLink || undefined,
441
+ webContentLink: file.data.webContentLink || undefined,
442
+ };
443
+ // Always make file public
444
+ await this.makeFilePublic({ fileId: result.id });
445
+ return result;
446
+ }
447
+ /**
448
+ * Uploads a file and shares it with a specified email address.
449
+ *
450
+ * @param params - Upload and share parameters
451
+ * @returns A Promise that resolves to the upload result
452
+ *
453
+ * @example
454
+ * ```typescript
455
+ * const result = await client.uploadFileAndShare({
456
+ * localFilePath: './report.pdf',
457
+ * driveFolder: 'SharedReports',
458
+ * shareWithEmail: 'colleague@example.com',
459
+ * role: 'writer'
460
+ * });
461
+ * ```
462
+ */
463
+ async uploadFileAndShare({ localFilePath, driveFolder, fileName, shareWithEmail, role = 'reader', }) {
464
+ // Upload file normally
465
+ const result = await this.uploadFile({
466
+ localFilePath,
467
+ driveFolder,
468
+ fileName,
469
+ });
470
+ // Get folder ID and share it with specified role
471
+ const folderId = await this.getFolderIdByPath({ folderPath: driveFolder });
472
+ await this.shareFolderWithEmail({
473
+ folderId,
474
+ emailAddress: shareWithEmail,
475
+ role,
476
+ });
477
+ return result;
478
+ }
479
+ /**
480
+ * Deletes a file from Google Drive.
481
+ * Note: Only the file owner can delete the file.
482
+ *
483
+ * @param params - Delete parameters including file ID
484
+ * @returns A Promise that resolves to true if deletion was successful
485
+ * @throws Error if deletion fails (e.g., permission denied)
486
+ *
487
+ * @example
488
+ * ```typescript
489
+ * await client.deleteFile({ fileId: '1abc123def456' });
490
+ * ```
491
+ */
492
+ async deleteFile({ fileId }) {
493
+ const driveInstance = await this.getDriveClient();
494
+ await driveInstance.files.delete({
495
+ fileId: fileId,
496
+ });
497
+ return true;
498
+ }
499
+ /**
500
+ * Lists all files and folders in a specified folder.
501
+ *
502
+ * @param params - List parameters including folder ID
503
+ * @returns A Promise that resolves to an array of file information objects
504
+ *
505
+ * @example
506
+ * ```typescript
507
+ * const files = await client.listFilesInFolder({ folderId: 'root' });
508
+ * for (const file of files) {
509
+ * console.log(`${file.name} (${file.mimeType})`);
510
+ * }
511
+ * ```
512
+ */
513
+ async listFilesInFolder({ folderId = 'root' } = {}) {
514
+ const driveInstance = await this.getDriveClient();
515
+ const query = `'${folderId}' in parents and trashed=false`;
516
+ const response = await driveInstance.files.list({
517
+ q: query,
518
+ fields: 'files(id, name, mimeType, webViewLink, parents)',
519
+ orderBy: 'name',
520
+ });
521
+ const files = response.data.files || [];
522
+ return files.map((file) => ({
523
+ id: file.id || '',
524
+ name: file.name || '',
525
+ mimeType: file.mimeType || '',
526
+ webViewLink: file.webViewLink || undefined,
527
+ parents: file.parents || undefined,
528
+ }));
529
+ }
530
+ /**
531
+ * Makes a file publicly accessible to anyone with the link.
532
+ *
533
+ * @param params - Parameters including file ID
534
+ * @returns A Promise that resolves to true if successful
535
+ *
536
+ * @example
537
+ * ```typescript
538
+ * await client.makeFilePublic({ fileId: '1abc123def456' });
539
+ * // File is now accessible via its webViewLink
540
+ * ```
541
+ */
542
+ async makeFilePublic({ fileId }) {
543
+ const driveInstance = await this.getDriveClient();
544
+ await driveInstance.permissions.create({
545
+ fileId: fileId,
546
+ requestBody: {
547
+ role: 'reader',
548
+ type: 'anyone',
549
+ },
550
+ });
551
+ return true;
552
+ }
553
+ /**
554
+ * Transfers ownership of a file to another user.
555
+ *
556
+ * @param params - Transfer parameters including file ID and new owner email
557
+ * @returns A Promise that resolves to true if successful
558
+ *
559
+ * @example
560
+ * ```typescript
561
+ * await client.transferFileOwnership({
562
+ * fileId: '1abc123def456',
563
+ * newOwnerEmail: 'newowner@example.com'
564
+ * });
565
+ * ```
566
+ */
567
+ async transferFileOwnership({ fileId, newOwnerEmail, role = 'reader', }) {
568
+ const driveInstance = await this.getDriveClient();
569
+ await driveInstance.permissions.create({
570
+ fileId: fileId,
571
+ requestBody: {
572
+ role: role,
573
+ type: 'user',
574
+ emailAddress: newOwnerEmail,
575
+ },
576
+ transferOwnership: true,
577
+ sendNotificationEmail: true,
578
+ });
579
+ return true;
580
+ }
581
+ /**
582
+ * Shares a folder with a specified email address.
583
+ *
584
+ * @param params - Share parameters including folder ID, email, and role
585
+ * @returns A Promise that resolves to true if successful
586
+ *
587
+ * @example
588
+ * ```typescript
589
+ * await client.shareFolderWithEmail({
590
+ * folderId: '1abc123def456',
591
+ * emailAddress: 'user@example.com',
592
+ * role: 'writer'
593
+ * });
594
+ * ```
595
+ */
596
+ async shareFolderWithEmail({ folderId, emailAddress, role = 'writer', }) {
597
+ const driveInstance = await this.getDriveClient();
598
+ const requestBody = {
599
+ role: role,
600
+ type: 'user',
601
+ emailAddress: emailAddress,
602
+ };
603
+ const requestOptions = {
604
+ fileId: folderId,
605
+ requestBody: requestBody,
606
+ };
607
+ // Configure notification email based on role
608
+ if (role === 'owner') {
609
+ requestOptions.transferOwnership = true;
610
+ requestOptions.sendNotificationEmail = true;
611
+ }
612
+ else {
613
+ requestOptions.sendNotificationEmail = false;
614
+ }
615
+ await driveInstance.permissions.create(requestOptions);
616
+ return true;
617
+ }
618
+ /**
619
+ * Gets a folder ID by its path, creating the folder hierarchy if it doesn't exist.
620
+ *
621
+ * @param params - Parameters including folder path
622
+ * @returns A Promise that resolves to the folder ID
623
+ *
624
+ * @example
625
+ * ```typescript
626
+ * const folderId = await client.getFolderIdByPath({
627
+ * folderPath: 'folder1/folder2/folder3'
628
+ * });
629
+ * ```
630
+ */
631
+ async getFolderIdByPath({ folderPath }) {
632
+ return this.createFolderHierarchy({ folderPath });
633
+ }
634
+ /**
635
+ * Checks if a file with the specified name exists in a folder.
636
+ *
637
+ * @param params - Parameters including file name and folder ID
638
+ * @returns A Promise that resolves to the file ID if found, or null if not found
639
+ *
640
+ * @example
641
+ * ```typescript
642
+ * const fileId = await client.fileExistsInFolder({
643
+ * fileName: 'document.pdf',
644
+ * folderId: 'root'
645
+ * });
646
+ * if (fileId) {
647
+ * console.log(`File exists with ID: ${fileId}`);
648
+ * }
649
+ * ```
650
+ */
651
+ async fileExistsInFolder({ fileName, folderId = 'root', }) {
652
+ const driveInstance = await this.getDriveClient();
653
+ const query = `name='${fileName}' and '${folderId}' in parents and trashed=false`;
654
+ const response = await driveInstance.files.list({
655
+ q: query,
656
+ fields: 'files(id, name)',
657
+ });
658
+ const files = response.data.files || [];
659
+ if (files.length > 0 && files[0].id) {
660
+ return files[0].id;
661
+ }
662
+ return null;
663
+ }
664
+ }
665
+
666
+ /**
667
+ * Google Drive Feature Library
668
+ * @description A framework-agnostic library for interacting with Google Drive API.
669
+ * Provides easy-to-use methods for file upload, sharing, and storage management.
670
+ * @module gg-drive
671
+ *
672
+ * @example
673
+ * ```typescript
674
+ * import { GoogleDriveClient } from 'bodevops-features/gg-drive';
675
+ *
676
+ * const client = new GoogleDriveClient({
677
+ * keyFilePath: './service-account.json'
678
+ * });
679
+ *
680
+ * // Upload a file
681
+ * const result = await client.uploadFile({
682
+ * localFilePath: './document.pdf',
683
+ * driveFolder: 'MyFolder/Documents'
684
+ * });
685
+ *
686
+ * // Get storage info
687
+ * const storage = await client.getStorageInfo();
688
+ * console.log(`Used: ${storage.formattedUsed}`);
689
+ * ```
690
+ */
691
+ // Export main client class
692
+
693
+ var index$1 = /*#__PURE__*/Object.freeze({
694
+ __proto__: null,
695
+ DEFAULT_DRIVE_SCOPES: DEFAULT_DRIVE_SCOPES,
696
+ GoogleDriveClient: GoogleDriveClient,
697
+ GoogleDriveConfig: GoogleDriveConfig,
698
+ createFileReadStream: createFileReadStream,
699
+ formatBytes: formatBytes,
700
+ getFileInfo: getFileInfo,
701
+ normalizeFilePath: normalizeFilePath,
702
+ parseFolderPath: parseFolderPath,
703
+ validateFileExists: validateFileExists
704
+ });
705
+
706
+ /**
707
+ * Google Sheet Feature Library - Configuration Module
708
+ * @description Configuration management for Google Sheet client initialization and authentication.
709
+ * @module gg-sheet/config
710
+ */
711
+ /**
712
+ * Default OAuth scope required for Google Sheets operations.
713
+ */
714
+ const DEFAULT_SHEET_SCOPES = ['https://www.googleapis.com/auth/spreadsheets'];
715
+ /**
716
+ * Google Sheet Configuration Manager.
717
+ * Handles validation and normalization of configuration options for the Google Sheet client.
718
+ *
719
+ * @example
720
+ * ```typescript
721
+ * // Using key file path
722
+ * const config = new GoogleSheetConfig({
723
+ * keyFilePath: './service-account.json'
724
+ * });
725
+ *
726
+ * // Using credentials object
727
+ * const config = new GoogleSheetConfig({
728
+ * credentials: {
729
+ * type: 'service_account',
730
+ * project_id: 'my-project',
731
+ * private_key: '-----BEGIN PRIVATE KEY-----\n...',
732
+ * client_email: 'service-account@my-project.iam.gserviceaccount.com',
733
+ * // ... other fields
734
+ * }
735
+ * });
736
+ * ```
737
+ */
738
+ class GoogleSheetConfig {
739
+ /**
740
+ * Creates a new GoogleSheetConfig instance.
741
+ *
742
+ * @param config - Configuration options for Google Sheet client
743
+ * @throws Error if neither keyFilePath nor credentials is provided
744
+ */
745
+ constructor({ keyFilePath, credentials, scopes }) {
746
+ // Validate that at least one authentication method is provided
747
+ if (!keyFilePath && !credentials) {
748
+ throw new Error('GoogleSheetConfig: Either keyFilePath or credentials must be provided');
749
+ }
750
+ this.keyFilePath = keyFilePath;
751
+ this.credentials = credentials;
752
+ this.scopes = scopes || DEFAULT_SHEET_SCOPES;
753
+ // Validate credentials structure if provided
754
+ if (credentials) {
755
+ this.validateCredentials(credentials);
756
+ }
757
+ }
758
+ /**
759
+ * Validates that the credentials object contains all required fields.
760
+ *
761
+ * @param credentials - The credentials object to validate
762
+ * @throws Error if required fields are missing
763
+ */
764
+ validateCredentials(credentials) {
765
+ const requiredFields = [
766
+ 'type',
767
+ 'project_id',
768
+ 'private_key',
769
+ 'client_email',
770
+ ];
771
+ for (const field of requiredFields) {
772
+ if (!credentials[field]) {
773
+ throw new Error(`GoogleSheetConfig: Missing required credential field: ${field}`);
774
+ }
775
+ }
776
+ if (credentials.type !== 'service_account') {
777
+ throw new Error(`GoogleSheetConfig: Invalid credential type. Expected 'service_account', got '${credentials.type}'`);
778
+ }
779
+ }
780
+ /**
781
+ * Returns the authentication configuration object suitable for googleapis.
782
+ *
783
+ * @returns Authentication options for Google Auth
784
+ */
785
+ getAuthOptions() {
786
+ if (this.keyFilePath) {
787
+ return {
788
+ keyFile: this.keyFilePath,
789
+ scopes: this.scopes,
790
+ };
791
+ }
792
+ return {
793
+ credentials: this.credentials,
794
+ scopes: this.scopes,
795
+ };
796
+ }
797
+ }
798
+
799
+ /**
800
+ * Google Sheet Feature Library - Type Definitions
801
+ * @description Type definitions for Google Sheet operations including reading, writing, and exporting data.
802
+ * @module gg-sheet/types
803
+ */
804
+ /**
805
+ * Export type enumeration for sheet export operations.
806
+ */
807
+ var ETypeExport;
808
+ (function (ETypeExport) {
809
+ /** Append data to the end of existing data */
810
+ ETypeExport["Append"] = "Append";
811
+ /** Overwrite all existing data starting from row 1 */
812
+ ETypeExport["Overwrite"] = "Overwrite";
813
+ })(ETypeExport || (ETypeExport = {}));
814
+
815
+ /**
816
+ * Google Sheet Feature Library - Utility Functions
817
+ * @description Helper functions for sheet operations, column conversion, and data transformation.
818
+ * @module gg-sheet/utils
819
+ */
820
+ /**
821
+ * Regular expression pattern to extract spreadsheet ID from a Google Sheets URL.
822
+ */
823
+ const SPREADSHEET_ID_PATTERN = /\/spreadsheets\/d\/([a-zA-Z0-9-_]+)/;
824
+ /**
825
+ * Extracts the spreadsheet ID from a Google Sheets URL.
826
+ *
827
+ * @param sheetUrl - The full URL of the Google Spreadsheet
828
+ * @returns The spreadsheet ID or null if not found
829
+ *
830
+ * @example
831
+ * ```typescript
832
+ * const id = getSheetIdFromUrl({
833
+ * sheetUrl: 'https://docs.google.com/spreadsheets/d/1abc123def/edit'
834
+ * });
835
+ * // Returns: '1abc123def'
836
+ * ```
837
+ */
838
+ function getSheetIdFromUrl({ sheetUrl }) {
839
+ const match = sheetUrl.match(SPREADSHEET_ID_PATTERN);
840
+ if (match && match[1]) {
841
+ return match[1];
842
+ }
843
+ return null;
844
+ }
845
+ /**
846
+ * Converts a 0-based column index to a column letter (e.g., 0 → A, 25 → Z, 26 → AA).
847
+ *
848
+ * @param columnIndex - The 0-based column index
849
+ * @returns The column letter(s) (e.g., "A", "B", "AA", "AB")
850
+ *
851
+ * @example
852
+ * ```typescript
853
+ * convertIndexToColumnName({ columnIndex: 0 }); // "A"
854
+ * convertIndexToColumnName({ columnIndex: 25 }); // "Z"
855
+ * convertIndexToColumnName({ columnIndex: 26 }); // "AA"
856
+ * convertIndexToColumnName({ columnIndex: 701 }); // "ZZ"
857
+ * ```
858
+ */
859
+ function convertIndexToColumnName({ columnIndex }) {
860
+ let columnName = '';
861
+ let index = columnIndex;
862
+ while (index >= 0) {
863
+ columnName = String.fromCharCode((index % 26) + 'A'.charCodeAt(0)) + columnName;
864
+ index = Math.floor(index / 26) - 1;
865
+ }
866
+ return columnName;
867
+ }
868
+ /**
869
+ * Converts a column letter to a 0-based column index (e.g., A → 0, Z → 25, AA → 26).
870
+ *
871
+ * @param columnName - The column letter(s) (e.g., "A", "B", "AA")
872
+ * @returns The 0-based column index
873
+ * @throws Error if the column name contains invalid characters
874
+ *
875
+ * @example
876
+ * ```typescript
877
+ * convertColumnNameToIndex({ columnName: 'A' }); // 0
878
+ * convertColumnNameToIndex({ columnName: 'Z' }); // 25
879
+ * convertColumnNameToIndex({ columnName: 'AA' }); // 26
880
+ * convertColumnNameToIndex({ columnName: 'ZZ' }); // 701
881
+ * ```
882
+ */
883
+ function convertColumnNameToIndex({ columnName }) {
884
+ // Convert to uppercase to handle both lower and upper case
885
+ const upperColumnName = columnName.toUpperCase().trim();
886
+ // Validate input - only accept A-Z characters
887
+ if (!/^[A-Z]+$/.test(upperColumnName)) {
888
+ throw new Error(`Invalid column name: '${columnName}'. Only letters A-Z are allowed.`);
889
+ }
890
+ let result = 0;
891
+ for (let i = 0; i < upperColumnName.length; i++) {
892
+ const charCode = upperColumnName.charCodeAt(i) - 'A'.charCodeAt(0);
893
+ result = result * 26 + (charCode + 1);
894
+ }
895
+ // Convert to 0-based index
896
+ return result - 1;
897
+ }
898
+ /**
899
+ * Converts raw sheet values (2D array) into an array of typed objects.
900
+ * Uses the first row (or row at rowOffset) as keys for the objects.
901
+ *
902
+ * @param values - Raw 2D array from sheet
903
+ * @param rowOffset - Number of rows to skip before the header row (default: 0)
904
+ * @returns Array of typed objects, or null if values is null/undefined
905
+ *
906
+ * @example
907
+ * ```typescript
908
+ * const rawData = [
909
+ * ['name', 'age', 'email'],
910
+ * ['John', '30', 'john@example.com'],
911
+ * ['Jane', '25', 'jane@example.com']
912
+ * ];
913
+ *
914
+ * interface Person { name: string; age: string; email: string; }
915
+ *
916
+ * const people = convertValueSheet<Person>({ values: rawData });
917
+ * // Returns: [
918
+ * // { name: 'John', age: '30', email: 'john@example.com' },
919
+ * // { name: 'Jane', age: '25', email: 'jane@example.com' }
920
+ * // ]
921
+ * ```
922
+ */
923
+ function convertValueSheet({ values, rowOffset = 0, }) {
924
+ if (!values || values.length === 0) {
925
+ return null;
926
+ }
927
+ // Get header row (keys for the objects)
928
+ const keys = values[rowOffset];
929
+ if (!keys || keys.length === 0) {
930
+ return null;
931
+ }
932
+ // Map remaining rows to objects
933
+ return values.slice(rowOffset + 1).map((row) => {
934
+ return keys.reduce((acc, key, index) => {
935
+ acc[key] = row[index] || '';
936
+ return acc;
937
+ }, {});
938
+ });
939
+ }
940
+ /**
941
+ * Gets the index of a column key in the list of keys.
942
+ *
943
+ * @param key - The key to find
944
+ * @param listKeys - Array of all keys
945
+ * @returns The index of the key, or -1 if not found
946
+ *
947
+ * @example
948
+ * ```typescript
949
+ * interface Person { id: string; name: string; email: string; }
950
+ * const keys: (keyof Person)[] = ['id', 'name', 'email'];
951
+ *
952
+ * getIndexCol({ key: 'name', listKeys: keys }); // 1
953
+ * getIndexCol({ key: 'email', listKeys: keys }); // 2
954
+ * ```
955
+ */
956
+ function getIndexCol({ key, listKeys }) {
957
+ return listKeys.indexOf(key);
958
+ }
959
+ /**
960
+ * Extracts column headers and data values from a result set for export.
961
+ * Takes an object mapping field keys to column names and an array of items.
962
+ *
963
+ * @param colsForSheet - Object mapping field keys to column header names
964
+ * @param resultItems - Array of items to export
965
+ * @returns Object containing listCols (headers) and valsExport (data matrix)
966
+ *
967
+ * @example
968
+ * ```typescript
969
+ * const colsMapping = { id: 'ID', name: 'Full Name', email: 'Email Address' };
970
+ * const items = [
971
+ * { id: '1', name: 'John Doe', email: 'john@example.com' },
972
+ * { id: '2', name: 'Jane Doe', email: 'jane@example.com' }
973
+ * ];
974
+ *
975
+ * const { listCols, valsExport } = getListColsAndValsExport({
976
+ * colsForSheet: colsMapping,
977
+ * resultItems: items
978
+ * });
979
+ * // listCols: ['ID', 'Full Name', 'Email Address']
980
+ * // valsExport: [['1', 'John Doe', 'john@example.com'], ['2', 'Jane Doe', 'jane@example.com']]
981
+ * ```
982
+ */
983
+ function getListColsAndValsExport({ colsForSheet, resultItems, }) {
984
+ // Extract column headers from the mapping values
985
+ const listCols = Object.values(colsForSheet);
986
+ // Extract data rows
987
+ const valsExport = [];
988
+ for (const item of resultItems) {
989
+ const row = [];
990
+ // Iterate through colsForSheet keys to ensure correct order
991
+ for (const fieldKey of Object.keys(colsForSheet)) {
992
+ const fieldValue = item[fieldKey];
993
+ // Convert value to string, handling different data types
994
+ let cellValue = '';
995
+ if (fieldValue === null || fieldValue === undefined) {
996
+ cellValue = '';
997
+ }
998
+ else if (fieldValue instanceof Date) {
999
+ cellValue = fieldValue.toISOString();
1000
+ }
1001
+ else if (typeof fieldValue === 'object') {
1002
+ // Handle nested objects (like relations)
1003
+ cellValue = JSON.stringify(fieldValue);
1004
+ }
1005
+ else {
1006
+ cellValue = String(fieldValue);
1007
+ }
1008
+ row.push(cellValue);
1009
+ }
1010
+ valsExport.push(row);
1011
+ }
1012
+ return { listCols, valsExport };
1013
+ }
1014
+ /**
1015
+ * Validates that a sheet URL is in the correct format.
1016
+ *
1017
+ * @param sheetUrl - The URL to validate
1018
+ * @returns True if the URL is valid, false otherwise
1019
+ *
1020
+ * @example
1021
+ * ```typescript
1022
+ * isValidSheetUrl({ sheetUrl: 'https://docs.google.com/spreadsheets/d/1abc123/edit' }); // true
1023
+ * isValidSheetUrl({ sheetUrl: 'https://example.com/sheet' }); // false
1024
+ * ```
1025
+ */
1026
+ function isValidSheetUrl({ sheetUrl }) {
1027
+ return SPREADSHEET_ID_PATTERN.test(sheetUrl);
1028
+ }
1029
+ /**
1030
+ * Calculates the actual row index in the sheet based on the data row index and offset.
1031
+ * This accounts for header row(s) and any additional offset rows.
1032
+ *
1033
+ * @param dataRowIndex - The 0-based index in the data (not counting headers)
1034
+ * @param rowOffset - Additional rows to skip after the header (default: 0)
1035
+ * @returns The 1-based row number in the actual sheet
1036
+ *
1037
+ * @example
1038
+ * ```typescript
1039
+ * // With rowOffset=0: header at row 1, data starts at row 2
1040
+ * calculateActualRow({ dataRowIndex: 0, rowOffset: 0 }); // 2 (first data row)
1041
+ * calculateActualRow({ dataRowIndex: 5, rowOffset: 0 }); // 7 (sixth data row)
1042
+ *
1043
+ * // With rowOffset=1: header at row 1, skip row 2, data starts at row 3
1044
+ * calculateActualRow({ dataRowIndex: 0, rowOffset: 1 }); // 3 (first data row)
1045
+ * ```
1046
+ */
1047
+ function calculateActualRow({ dataRowIndex, rowOffset = 0, }) {
1048
+ // Row 1 is header, so data starts at row 2
1049
+ // Add 2 for: 0-based to 1-based conversion + header row
1050
+ return dataRowIndex + 2 + rowOffset;
1051
+ }
1052
+
1053
+ /**
1054
+ * Google Sheet Feature Library - Main Client Class
1055
+ * @description A framework-agnostic client for interacting with Google Sheets API.
1056
+ * Provides methods for reading, writing, and managing spreadsheet data.
1057
+ * @module gg-sheet/google-sheet
1058
+ */
1059
+ /**
1060
+ * Google Sheet Client for managing spreadsheet data.
1061
+ *
1062
+ * @example
1063
+ * ```typescript
1064
+ * import { GoogleSheetClient } from 'bodevops-features/gg-sheet';
1065
+ *
1066
+ * const client = new GoogleSheetClient({
1067
+ * keyFilePath: './service-account.json'
1068
+ * });
1069
+ *
1070
+ * // Read data from a sheet
1071
+ * const data = await client.getValues({
1072
+ * sheetUrl: 'https://docs.google.com/spreadsheets/d/...',
1073
+ * sheetName: 'Sheet1'
1074
+ * });
1075
+ *
1076
+ * // Update specific cells
1077
+ * await client.updateValuesMultiCells({
1078
+ * sheetUrl: 'https://docs.google.com/spreadsheets/d/...',
1079
+ * sheetName: 'Sheet1',
1080
+ * cells: [
1081
+ * { row: 0, col: 0, content: 'Hello' },
1082
+ * { row: 0, col: 1, content: 'World' }
1083
+ * ]
1084
+ * });
1085
+ * ```
1086
+ */
1087
+ class GoogleSheetClient {
1088
+ /**
1089
+ * Creates a new GoogleSheetClient instance.
1090
+ *
1091
+ * @param configOptions - Configuration options for the Google Sheet client
1092
+ */
1093
+ constructor(configOptions) {
1094
+ this.config = new GoogleSheetConfig(configOptions);
1095
+ }
1096
+ /**
1097
+ * Creates and returns an authenticated Google Sheets API client.
1098
+ *
1099
+ * @returns A Promise that resolves to an authenticated Sheets API client
1100
+ */
1101
+ async getSheetsClient() {
1102
+ const authOptions = this.config.getAuthOptions();
1103
+ const auth = new google.auth.GoogleAuth({
1104
+ keyFile: authOptions.keyFile,
1105
+ credentials: authOptions.credentials,
1106
+ scopes: authOptions.scopes,
1107
+ });
1108
+ const authClient = await auth.getClient();
1109
+ return google.sheets({
1110
+ version: 'v4',
1111
+ auth: authClient,
1112
+ });
1113
+ }
1114
+ /**
1115
+ * Extracts the spreadsheet ID from a URL and validates it.
1116
+ *
1117
+ * @param sheetUrl - The Google Sheets URL
1118
+ * @returns The spreadsheet ID
1119
+ * @throws Error if the URL is invalid
1120
+ */
1121
+ extractSheetId({ sheetUrl }) {
1122
+ const sheetId = getSheetIdFromUrl({ sheetUrl });
1123
+ if (!sheetId) {
1124
+ throw new Error(`Invalid Google Sheet URL: ${sheetUrl}`);
1125
+ }
1126
+ return sheetId;
1127
+ }
1128
+ /**
1129
+ * Retrieves information about a Google Spreadsheet, including all sheet tabs.
1130
+ *
1131
+ * @param params - Parameters including the sheet URL
1132
+ * @returns A Promise that resolves to spreadsheet information
1133
+ *
1134
+ * @example
1135
+ * ```typescript
1136
+ * const info = await client.getSheetInfo({
1137
+ * sheetUrl: 'https://docs.google.com/spreadsheets/d/...'
1138
+ * });
1139
+ * console.log(`Spreadsheet: ${info.spreadsheetTitle}`);
1140
+ * console.log(`Sheets: ${info.sheets.map(s => s.title).join(', ')}`);
1141
+ * ```
1142
+ */
1143
+ async getSheetInfo({ sheetUrl }) {
1144
+ const sheetsInstance = await this.getSheetsClient();
1145
+ const spreadsheetId = this.extractSheetId({ sheetUrl });
1146
+ const response = await sheetsInstance.spreadsheets.get({
1147
+ spreadsheetId,
1148
+ includeGridData: false,
1149
+ });
1150
+ const spreadsheetTitle = response.data.properties?.title || '';
1151
+ const sheets = response.data.sheets?.map((sheet) => ({
1152
+ title: sheet.properties?.title || '',
1153
+ sheetId: sheet.properties?.sheetId || 0,
1154
+ rowCount: sheet.properties?.gridProperties?.rowCount || 0,
1155
+ columnCount: sheet.properties?.gridProperties?.columnCount || 0,
1156
+ })) || [];
1157
+ return {
1158
+ spreadsheetTitle,
1159
+ sheets,
1160
+ };
1161
+ }
1162
+ /**
1163
+ * Reads all values from a specific sheet tab.
1164
+ *
1165
+ * @param params - Parameters including sheet URL, sheet name, and optional row limit
1166
+ * @returns A Promise that resolves to a 2D array of string values
1167
+ *
1168
+ * @example
1169
+ * ```typescript
1170
+ * const data = await client.getValues({
1171
+ * sheetUrl: 'https://docs.google.com/spreadsheets/d/...',
1172
+ * sheetName: 'Sheet1',
1173
+ * endRow: 100 // Optional: limit to first 100 rows
1174
+ * });
1175
+ *
1176
+ * for (const row of data) {
1177
+ * console.log(row.join(', '));
1178
+ * }
1179
+ * ```
1180
+ */
1181
+ async getValues({ sheetUrl, sheetName, endRow }) {
1182
+ const sheetsInstance = await this.getSheetsClient();
1183
+ const spreadsheetId = this.extractSheetId({ sheetUrl });
1184
+ // Get sheet info to determine column count
1185
+ const sheetInfo = await this.getSheetInfo({ sheetUrl });
1186
+ const sheet = sheetInfo.sheets.find((s) => s.title === sheetName);
1187
+ if (!sheet) {
1188
+ throw new Error(`Sheet not found: ${sheetName}`);
1189
+ }
1190
+ // Build the range
1191
+ let range = sheetName;
1192
+ if (endRow) {
1193
+ const endCol = convertIndexToColumnName({
1194
+ columnIndex: sheet.columnCount - 1,
1195
+ });
1196
+ range = `${sheetName}!A1:${endCol}${endRow}`;
1197
+ }
1198
+ const result = await sheetsInstance.spreadsheets.values.get({
1199
+ spreadsheetId,
1200
+ range,
1201
+ });
1202
+ return result.data.values || [];
1203
+ }
1204
+ /**
1205
+ * Finds the row index (0-based) where a specific value appears in a column.
1206
+ *
1207
+ * @param params - Parameters including sheet URL, sheet name, column name, and value to find
1208
+ * @returns A Promise that resolves to the row index (0-based), or -1 if not found
1209
+ *
1210
+ * @example
1211
+ * ```typescript
1212
+ * const rowIndex = await client.getIdxRow({
1213
+ * sheetUrl: 'https://docs.google.com/spreadsheets/d/...',
1214
+ * sheetName: 'Sheet1',
1215
+ * colName: 'A',
1216
+ * value: 'John Doe'
1217
+ * });
1218
+ *
1219
+ * if (rowIndex >= 0) {
1220
+ * console.log(`Found at row index: ${rowIndex}`);
1221
+ * }
1222
+ * ```
1223
+ */
1224
+ async getIdxRow({ sheetUrl, sheetName, colName, value, }) {
1225
+ const sheetsInstance = await this.getSheetsClient();
1226
+ const spreadsheetId = this.extractSheetId({ sheetUrl });
1227
+ // Get sheet info to determine row count
1228
+ const sheetInfo = await this.getSheetInfo({ sheetUrl });
1229
+ const sheet = sheetInfo.sheets.find((s) => s.title === sheetName);
1230
+ if (!sheet) {
1231
+ throw new Error(`Sheet not found: ${sheetName}`);
1232
+ }
1233
+ const range = `${sheetName}!${colName}1:${colName}${sheet.rowCount}`;
1234
+ const result = await sheetsInstance.spreadsheets.values.get({
1235
+ spreadsheetId,
1236
+ range,
1237
+ });
1238
+ const values = result.data.values || [];
1239
+ // Find the index (0-based)
1240
+ const index = values.findIndex((row) => {
1241
+ if (!row || row.length === 0)
1242
+ return false;
1243
+ return row[0] === value;
1244
+ });
1245
+ return index;
1246
+ }
1247
+ /**
1248
+ * Exports data to a Google Sheet with either Append or Overwrite mode.
1249
+ *
1250
+ * @param params - Export parameters including data and export type
1251
+ * @returns A Promise that resolves to true if successful
1252
+ *
1253
+ * @example
1254
+ * ```typescript
1255
+ * // Overwrite existing data
1256
+ * await client.export({
1257
+ * sheetUrl: 'https://docs.google.com/spreadsheets/d/...',
1258
+ * sheetName: 'Sheet1',
1259
+ * listCols: ['Name', 'Email', 'Age'],
1260
+ * valsExport: [
1261
+ * ['John', 'john@example.com', '30'],
1262
+ * ['Jane', 'jane@example.com', '25']
1263
+ * ],
1264
+ * typeExport: ETypeExport.Overwrite
1265
+ * });
1266
+ *
1267
+ * // Append to existing data
1268
+ * await client.export({
1269
+ * sheetUrl: 'https://docs.google.com/spreadsheets/d/...',
1270
+ * sheetName: 'Sheet1',
1271
+ * listCols: ['Name', 'Email', 'Age'],
1272
+ * valsExport: [['New User', 'new@example.com', '28']],
1273
+ * typeExport: ETypeExport.Append
1274
+ * });
1275
+ * ```
1276
+ */
1277
+ async export({ sheetUrl, sheetName, listCols, valsExport, typeExport, }) {
1278
+ const sheetsInstance = await this.getSheetsClient();
1279
+ const spreadsheetId = this.extractSheetId({ sheetUrl });
1280
+ if (typeExport === ETypeExport.Overwrite) {
1281
+ return this.executeOverwriteExport({
1282
+ sheetsInstance,
1283
+ spreadsheetId,
1284
+ sheetName,
1285
+ listCols,
1286
+ valsExport,
1287
+ });
1288
+ }
1289
+ else if (typeExport === ETypeExport.Append) {
1290
+ return this.executeAppendExport({
1291
+ sheetsInstance,
1292
+ spreadsheetId,
1293
+ sheetName,
1294
+ listCols,
1295
+ valsExport,
1296
+ });
1297
+ }
1298
+ else {
1299
+ throw new Error(`Invalid export type: ${typeExport}`);
1300
+ }
1301
+ }
1302
+ /**
1303
+ * Executes an overwrite export - writes headers and data from row 1.
1304
+ */
1305
+ async executeOverwriteExport({ sheetsInstance, spreadsheetId, sheetName, listCols, valsExport, }) {
1306
+ // Prepare data matrix: headers + data rows
1307
+ const exportData = [listCols, ...valsExport];
1308
+ const numCols = listCols.length;
1309
+ const numRows = exportData.length;
1310
+ // Calculate range: A1 to [LastCol][LastRow]
1311
+ const endCol = convertIndexToColumnName({ columnIndex: numCols - 1 });
1312
+ const range = `${sheetName}!A1:${endCol}${numRows}`;
1313
+ await sheetsInstance.spreadsheets.values.batchUpdate({
1314
+ spreadsheetId,
1315
+ requestBody: {
1316
+ valueInputOption: 'RAW',
1317
+ data: [
1318
+ {
1319
+ range,
1320
+ values: exportData,
1321
+ },
1322
+ ],
1323
+ },
1324
+ });
1325
+ return true;
1326
+ }
1327
+ /**
1328
+ * Executes an append export - finds empty rows and appends data.
1329
+ */
1330
+ async executeAppendExport({ sheetsInstance, spreadsheetId, sheetName, listCols, valsExport, }) {
1331
+ // Get sheet info to determine max rows
1332
+ const sheetInfo = await sheetsInstance.spreadsheets.get({
1333
+ spreadsheetId,
1334
+ ranges: [sheetName],
1335
+ includeGridData: false,
1336
+ });
1337
+ const sheet = sheetInfo.data.sheets?.find((s) => s.properties?.title === sheetName);
1338
+ const maxRows = sheet?.properties?.gridProperties?.rowCount || 1000;
1339
+ // Read current data to find the last used row
1340
+ const readRange = `${sheetName}!A1:A${maxRows}`;
1341
+ const readResponse = await sheetsInstance.spreadsheets.values.get({
1342
+ spreadsheetId,
1343
+ range: readRange,
1344
+ });
1345
+ const currentData = readResponse.data.values || [];
1346
+ // Find the first empty row (last row with data + 1)
1347
+ let startRow = 1;
1348
+ if (currentData.length > 0) {
1349
+ let lastUsedRow = 0;
1350
+ for (let i = currentData.length - 1; i >= 0; i--) {
1351
+ if (currentData[i] &&
1352
+ currentData[i].length > 0 &&
1353
+ currentData[i][0] &&
1354
+ String(currentData[i][0]).trim() !== '') {
1355
+ lastUsedRow = i + 1;
1356
+ break;
1357
+ }
1358
+ }
1359
+ startRow = lastUsedRow + 1;
1360
+ }
1361
+ // Check if we need to write headers
1362
+ let dataToWrite = valsExport;
1363
+ const writeStartRow = startRow;
1364
+ // If sheet is empty, include headers
1365
+ if (startRow === 1) {
1366
+ dataToWrite = [listCols, ...valsExport];
1367
+ }
1368
+ // Calculate range for writing
1369
+ const numCols = listCols.length;
1370
+ const numRows = dataToWrite.length;
1371
+ const endCol = convertIndexToColumnName({ columnIndex: numCols - 1 });
1372
+ const endRow = writeStartRow + numRows - 1;
1373
+ const writeRange = `${sheetName}!A${writeStartRow}:${endCol}${endRow}`;
1374
+ await sheetsInstance.spreadsheets.values.batchUpdate({
1375
+ spreadsheetId,
1376
+ requestBody: {
1377
+ valueInputOption: 'RAW',
1378
+ data: [
1379
+ {
1380
+ range: writeRange,
1381
+ values: dataToWrite,
1382
+ },
1383
+ ],
1384
+ },
1385
+ });
1386
+ return true;
1387
+ }
1388
+ /**
1389
+ * Updates multiple cells at specific row and column positions.
1390
+ *
1391
+ * @param params - Update parameters including cells to update
1392
+ * @returns A Promise that resolves to true if successful
1393
+ *
1394
+ * @example
1395
+ * ```typescript
1396
+ * await client.updateValuesMultiCells({
1397
+ * sheetUrl: 'https://docs.google.com/spreadsheets/d/...',
1398
+ * sheetName: 'Sheet1',
1399
+ * cells: [
1400
+ * { row: 0, col: 0, content: 'Updated A2' },
1401
+ * { row: 1, col: 1, content: 'Updated B3' },
1402
+ * { row: 2, col: 2, content: 'Updated C4' }
1403
+ * ],
1404
+ * rowOffset: 0 // Header at row 1, data starts at row 2
1405
+ * });
1406
+ * ```
1407
+ */
1408
+ async updateValuesMultiCells({ sheetUrl, sheetName, cells, rowOffset = 0, }) {
1409
+ if (!cells || cells.length === 0) {
1410
+ throw new Error('No cells provided for update');
1411
+ }
1412
+ const sheetsInstance = await this.getSheetsClient();
1413
+ const spreadsheetId = this.extractSheetId({ sheetUrl });
1414
+ // Create update requests for each cell
1415
+ const requests = cells.map((cell) => {
1416
+ const colName = convertIndexToColumnName({ columnIndex: cell.col });
1417
+ const actualRow = calculateActualRow({
1418
+ dataRowIndex: cell.row,
1419
+ rowOffset,
1420
+ });
1421
+ const sheetRange = `${sheetName}!${colName}${actualRow}`;
1422
+ return {
1423
+ range: sheetRange,
1424
+ values: [[cell.content]],
1425
+ };
1426
+ });
1427
+ await sheetsInstance.spreadsheets.values.batchUpdate({
1428
+ spreadsheetId,
1429
+ requestBody: {
1430
+ valueInputOption: 'RAW',
1431
+ data: requests,
1432
+ },
1433
+ });
1434
+ return true;
1435
+ }
1436
+ /**
1437
+ * Updates multiple columns in a single row.
1438
+ *
1439
+ * @param params - Update parameters including row and column values
1440
+ * @returns A Promise that resolves to true if successful
1441
+ *
1442
+ * @example
1443
+ * ```typescript
1444
+ * await client.updateValuesMultiColsByRow({
1445
+ * sheetUrl: 'https://docs.google.com/spreadsheets/d/...',
1446
+ * sheetName: 'Sheet1',
1447
+ * row: 5,
1448
+ * values: [
1449
+ * { col: 0, content: 'Value for A7' },
1450
+ * { col: 1, content: 'Value for B7' },
1451
+ * { col: 2, content: 'Value for C7' }
1452
+ * ]
1453
+ * });
1454
+ * ```
1455
+ */
1456
+ async updateValuesMultiColsByRow({ sheetUrl, sheetName, row, values, rowOffset = 0, }) {
1457
+ if (!values || values.length === 0) {
1458
+ throw new Error('No values provided for update');
1459
+ }
1460
+ const sheetsInstance = await this.getSheetsClient();
1461
+ const spreadsheetId = this.extractSheetId({ sheetUrl });
1462
+ const actualRow = calculateActualRow({ dataRowIndex: row, rowOffset });
1463
+ const requests = values.map((valPair) => {
1464
+ const colName = convertIndexToColumnName({ columnIndex: valPair.col });
1465
+ const sheetRange = `${sheetName}!${colName}${actualRow}`;
1466
+ return {
1467
+ range: sheetRange,
1468
+ values: [[valPair.content]],
1469
+ };
1470
+ });
1471
+ await sheetsInstance.spreadsheets.values.batchUpdate({
1472
+ spreadsheetId,
1473
+ requestBody: {
1474
+ valueInputOption: 'RAW',
1475
+ data: requests,
1476
+ },
1477
+ });
1478
+ return true;
1479
+ }
1480
+ /**
1481
+ * Updates multiple rows in a single column.
1482
+ *
1483
+ * @param params - Update parameters including column and row values
1484
+ * @returns A Promise that resolves to true if successful
1485
+ *
1486
+ * @example
1487
+ * ```typescript
1488
+ * await client.updateValuesMultiRowsByCol({
1489
+ * sheetUrl: 'https://docs.google.com/spreadsheets/d/...',
1490
+ * sheetName: 'Sheet1',
1491
+ * col: 2,
1492
+ * values: [
1493
+ * { row: 0, content: 'Row 1 Col C' },
1494
+ * { row: 1, content: 'Row 2 Col C' },
1495
+ * { row: 2, content: 'Row 3 Col C' }
1496
+ * ]
1497
+ * });
1498
+ * ```
1499
+ */
1500
+ async updateValuesMultiRowsByCol({ sheetUrl, sheetName, col, values, rowOffset = 0, }) {
1501
+ if (!values || values.length === 0) {
1502
+ throw new Error('No values provided for update');
1503
+ }
1504
+ const sheetsInstance = await this.getSheetsClient();
1505
+ const spreadsheetId = this.extractSheetId({ sheetUrl });
1506
+ const colName = convertIndexToColumnName({ columnIndex: col });
1507
+ const requests = values.map((valPair) => {
1508
+ const actualRow = calculateActualRow({
1509
+ dataRowIndex: valPair.row,
1510
+ rowOffset,
1511
+ });
1512
+ const sheetRange = `${sheetName}!${colName}${actualRow}`;
1513
+ return {
1514
+ range: sheetRange,
1515
+ values: [[valPair.content]],
1516
+ };
1517
+ });
1518
+ await sheetsInstance.spreadsheets.values.batchUpdate({
1519
+ spreadsheetId,
1520
+ requestBody: {
1521
+ valueInputOption: 'RAW',
1522
+ data: requests,
1523
+ },
1524
+ });
1525
+ return true;
1526
+ }
1527
+ /**
1528
+ * Updates a range of multiple rows and columns at once.
1529
+ *
1530
+ * @param params - Update parameters including value matrix
1531
+ * @returns A Promise that resolves to true if successful
1532
+ *
1533
+ * @example
1534
+ * ```typescript
1535
+ * await client.updateValuesMultiRowsMultiCols({
1536
+ * sheetUrl: 'https://docs.google.com/spreadsheets/d/...',
1537
+ * sheetName: 'Sheet1',
1538
+ * values: [
1539
+ * ['A1', 'B1', 'C1'],
1540
+ * ['A2', 'B2', 'C2'],
1541
+ * ['A3', 'B3', 'C3']
1542
+ * ],
1543
+ * startRow: 0,
1544
+ * startCol: 0
1545
+ * });
1546
+ * ```
1547
+ */
1548
+ async updateValuesMultiRowsMultiCols({ sheetUrl, sheetName, values, startRow = 0, endRow, startCol = 0, rowOffset = 0, }) {
1549
+ if (!values || values.length === 0 || values[0].length === 0) {
1550
+ throw new Error('Invalid values matrix: no data to update');
1551
+ }
1552
+ const sheetsInstance = await this.getSheetsClient();
1553
+ const spreadsheetId = this.extractSheetId({ sheetUrl });
1554
+ const numRows = values.length;
1555
+ const numCols = values[0].length;
1556
+ const startColName = convertIndexToColumnName({ columnIndex: startCol });
1557
+ const endColName = convertIndexToColumnName({
1558
+ columnIndex: startCol + numCols - 1,
1559
+ });
1560
+ const startRowIndex = calculateActualRow({
1561
+ dataRowIndex: startRow,
1562
+ rowOffset,
1563
+ });
1564
+ let endRowIndex;
1565
+ if (endRow !== undefined) {
1566
+ endRowIndex = calculateActualRow({ dataRowIndex: endRow, rowOffset });
1567
+ }
1568
+ else {
1569
+ endRowIndex = startRowIndex + numRows - 1;
1570
+ }
1571
+ const sheetRange = `${sheetName}!${startColName}${startRowIndex}:${endColName}${endRowIndex}`;
1572
+ await sheetsInstance.spreadsheets.values.batchUpdate({
1573
+ spreadsheetId,
1574
+ requestBody: {
1575
+ valueInputOption: 'RAW',
1576
+ data: [
1577
+ {
1578
+ range: sheetRange,
1579
+ values,
1580
+ },
1581
+ ],
1582
+ },
1583
+ });
1584
+ return true;
1585
+ }
1586
+ /**
1587
+ * Deletes a row from a sheet.
1588
+ *
1589
+ * @param params - Delete parameters including row index
1590
+ * @returns A Promise that resolves to true if successful
1591
+ *
1592
+ * @example
1593
+ * ```typescript
1594
+ * await client.deleteRowSheet({
1595
+ * sheetUrl: 'https://docs.google.com/spreadsheets/d/...',
1596
+ * sheetName: 'Sheet1',
1597
+ * row: 5 // Delete data row at index 5
1598
+ * });
1599
+ * ```
1600
+ */
1601
+ async deleteRowSheet({ sheetUrl, sheetName, row, rowOffset = 0, }) {
1602
+ const sheetsInstance = await this.getSheetsClient();
1603
+ const spreadsheetId = this.extractSheetId({ sheetUrl });
1604
+ // Get sheet ID
1605
+ const sheetInfo = await sheetsInstance.spreadsheets.get({
1606
+ spreadsheetId,
1607
+ ranges: [sheetName],
1608
+ includeGridData: false,
1609
+ });
1610
+ const sheet = sheetInfo.data.sheets?.find((s) => s.properties?.title === sheetName);
1611
+ const sheetId = sheet?.properties?.sheetId;
1612
+ if (sheetId === undefined || sheetId === null) {
1613
+ throw new Error(`Sheet not found: ${sheetName}`);
1614
+ }
1615
+ // Calculate actual row index
1616
+ // +1 for header row, +rowOffset
1617
+ const actualRowIndex = row + 1 + rowOffset;
1618
+ const request = {
1619
+ deleteDimension: {
1620
+ range: {
1621
+ sheetId,
1622
+ dimension: 'ROWS',
1623
+ startIndex: actualRowIndex,
1624
+ endIndex: actualRowIndex + 1,
1625
+ },
1626
+ },
1627
+ };
1628
+ await sheetsInstance.spreadsheets.batchUpdate({
1629
+ spreadsheetId,
1630
+ requestBody: {
1631
+ requests: [request],
1632
+ },
1633
+ });
1634
+ return true;
1635
+ }
1636
+ }
1637
+
1638
+ /**
1639
+ * Google Sheet Feature Library
1640
+ * @description A framework-agnostic library for interacting with Google Sheets API.
1641
+ * Provides easy-to-use methods for reading, writing, and managing spreadsheet data.
1642
+ * @module gg-sheet
1643
+ *
1644
+ * @example
1645
+ * ```typescript
1646
+ * import { GoogleSheetClient, ETypeExport } from 'bodevops-features/gg-sheet';
1647
+ *
1648
+ * const client = new GoogleSheetClient({
1649
+ * keyFilePath: './service-account.json'
1650
+ * });
1651
+ *
1652
+ * // Read data from a sheet
1653
+ * const data = await client.getValues({
1654
+ * sheetUrl: 'https://docs.google.com/spreadsheets/d/...',
1655
+ * sheetName: 'Sheet1'
1656
+ * });
1657
+ *
1658
+ * // Export data
1659
+ * await client.export({
1660
+ * sheetUrl: 'https://docs.google.com/spreadsheets/d/...',
1661
+ * sheetName: 'Sheet1',
1662
+ * listCols: ['Name', 'Email'],
1663
+ * valsExport: [['John', 'john@example.com']],
1664
+ * typeExport: ETypeExport.Append
1665
+ * });
1666
+ * ```
1667
+ */
1668
+ // Export main client class
4
1669
 
5
1670
  var index = /*#__PURE__*/Object.freeze({
6
1671
  __proto__: null,
7
- test: test
1672
+ DEFAULT_SHEET_SCOPES: DEFAULT_SHEET_SCOPES,
1673
+ get ETypeExport () { return ETypeExport; },
1674
+ GoogleSheetClient: GoogleSheetClient,
1675
+ GoogleSheetConfig: GoogleSheetConfig,
1676
+ calculateActualRow: calculateActualRow,
1677
+ convertColumnNameToIndex: convertColumnNameToIndex,
1678
+ convertIndexToColumnName: convertIndexToColumnName,
1679
+ convertValueSheet: convertValueSheet,
1680
+ getIndexCol: getIndexCol,
1681
+ getListColsAndValsExport: getListColsAndValsExport,
1682
+ getSheetIdFromUrl: getSheetIdFromUrl,
1683
+ isValidSheetUrl: isValidSheetUrl
8
1684
  });
9
1685
 
10
- export { index as GGDrive };
1686
+ export { index$1 as GGDrive, index as GGSheet };
11
1687
  //# sourceMappingURL=index.js.map