tantan-typeorm-gs 1.0.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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/bun.lock +300 -0
  4. package/docs/authentication.md +118 -0
  5. package/docs/configuration.md +680 -0
  6. package/docs/contributing.md +177 -0
  7. package/docs/crud.md +351 -0
  8. package/docs/custom-client.md +165 -0
  9. package/docs/development.md +243 -0
  10. package/docs/entities.md +502 -0
  11. package/docs/find-options.md +296 -0
  12. package/docs/google-cloud.md +101 -0
  13. package/docs/installation.md +51 -0
  14. package/docs/integration-testing.md +178 -0
  15. package/docs/limitations.md +223 -0
  16. package/docs/pagination.md +384 -0
  17. package/docs/performance.md +171 -0
  18. package/docs/security.md +153 -0
  19. package/docs/sorting.md +0 -0
  20. package/docs/supported-features.md +310 -0
  21. package/docs/synchronization.md +203 -0
  22. package/package.json +43 -0
  23. package/src/client/index.ts +589 -0
  24. package/src/core/data-source.ts +47 -0
  25. package/src/core/driver.ts +290 -0
  26. package/src/core/error.ts +144 -0
  27. package/src/core/memory.ts +152 -0
  28. package/src/core/query/interpreter.ts +949 -0
  29. package/src/core/query/runner.ts +1297 -0
  30. package/src/core/query/types.ts +155 -0
  31. package/src/core/schema-builder.ts +64 -0
  32. package/src/core/types.ts +113 -0
  33. package/src/core/utils.ts +13 -0
  34. package/src/index.ts +3 -0
  35. package/tantan-typeorm-gs.code-workspace +8 -0
  36. package/test/base/0001-data-source.test.ts +252 -0
  37. package/test/base/0002-operator.test.ts +616 -0
  38. package/test/base/0003-select.test.ts +281 -0
  39. package/test/base/0004-aggregate.test.ts +213 -0
  40. package/test/base/0005-transcation.test.ts +1566 -0
  41. package/test/base/0006-relation.test.ts +1611 -0
  42. package/test/base/0007-logging.test.ts +182 -0
  43. package/test/base/0008-soft-delete.test.ts +649 -0
  44. package/test/google-sheets/0001-client.test.ts +1705 -0
  45. package/test/google-sheets/0002-worksheet-management.test.ts +408 -0
  46. package/test/google-sheets/0003-data-source.test.ts +1935 -0
  47. package/test/google-sheets/0004-transactions.test.ts +952 -0
  48. package/test/google-sheets/0005-operator.test.ts +1124 -0
  49. package/test/google-sheets/0006-object-criteria.test.ts +283 -0
  50. package/test/google-sheets/0007-performance.test.ts +532 -0
  51. package/test/public-api.test.ts +34 -0
  52. package/tsconfig.build.json +21 -0
  53. package/tsconfig.json +34 -0
@@ -0,0 +1,1705 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from 'bun:test';
2
+
3
+ const getMock = mock();
4
+ const sheetsMock = mock();
5
+ const appendMock = mock();
6
+ const updateMock = mock();
7
+ const getSpreadsheetMock = mock();
8
+ const batchUpdateMock = mock();
9
+
10
+ mock.module(
11
+ 'googleapis',
12
+ () =>
13
+ ({
14
+ google: {
15
+ auth: {
16
+ GoogleAuth: mock(),
17
+ },
18
+
19
+ sheets: sheetsMock,
20
+ },
21
+ }),
22
+ );
23
+
24
+ const { GoogleSheetsConsume } = await import('../../src/index')
25
+
26
+ describe(
27
+ 'GoogleSheetsConsume',
28
+ () =>
29
+ {
30
+ beforeEach(() =>
31
+ {
32
+ getMock.mockReset();
33
+
34
+ appendMock.mockReset();
35
+
36
+ updateMock.mockReset();
37
+
38
+ getSpreadsheetMock.mockReset();
39
+
40
+ batchUpdateMock.mockReset();
41
+
42
+ sheetsMock.mockReset();
43
+
44
+ sheetsMock.mockReturnValue({
45
+ spreadsheets: {
46
+ values: {
47
+ get: getMock,
48
+ append: appendMock,
49
+ update: updateMock,
50
+ },
51
+ get: getSpreadsheetMock,
52
+
53
+ batchUpdate: batchUpdateMock,
54
+ },
55
+ });
56
+
57
+ });
58
+
59
+ afterEach(() =>
60
+ {
61
+ mock.restore();
62
+ });
63
+
64
+ test(
65
+ 'should read rows from Google Sheets',
66
+ async () =>
67
+ {
68
+ getMock.mockResolvedValue({
69
+ data: {
70
+ values: [
71
+ [
72
+ 'id',
73
+ 'name',
74
+ 'age',
75
+ ],
76
+ [
77
+ 1,
78
+ 'Budi',
79
+ 20,
80
+ ],
81
+ [
82
+ 2,
83
+ 'Andi',
84
+ 25,
85
+ ],
86
+ ],
87
+ },
88
+ });
89
+
90
+ const client =
91
+ new GoogleSheetsConsume({
92
+ spreadsheetId:
93
+ 'spreadsheet-123',
94
+
95
+ credentials: {
96
+ clientEmail:
97
+ 'test@example.com',
98
+
99
+ privateKey:
100
+ 'private-key',
101
+ },
102
+ });
103
+
104
+ await client.connect();
105
+
106
+ const rows =
107
+ await client.getRows(
108
+ 'users',
109
+ );
110
+
111
+ expect(
112
+ rows,
113
+ ).toEqual([
114
+ {
115
+ id: 1,
116
+ name: 'Budi',
117
+ age: 20,
118
+ },
119
+ {
120
+ id: 2,
121
+ name: 'Andi',
122
+ age: 25,
123
+ },
124
+ ]);
125
+
126
+ expect(
127
+ getMock,
128
+ ).toHaveBeenCalledWith({
129
+ spreadsheetId:
130
+ 'spreadsheet-123',
131
+
132
+ range:
133
+ 'users',
134
+ });
135
+ },
136
+ );
137
+
138
+ test(
139
+ 'should return empty array when sheet has no rows',
140
+ async () =>
141
+ {
142
+ getMock.mockResolvedValue({
143
+ data: {
144
+ values: [],
145
+ },
146
+ });
147
+
148
+ const client =
149
+ new GoogleSheetsConsume({
150
+ spreadsheetId:
151
+ 'spreadsheet-123',
152
+
153
+ credentials: {
154
+ clientEmail:
155
+ 'test@example.com',
156
+
157
+ privateKey:
158
+ 'private-key',
159
+ },
160
+ });
161
+
162
+ await client.connect();
163
+
164
+ const rows =
165
+ await client.getRows(
166
+ 'users',
167
+ );
168
+
169
+ expect(
170
+ rows,
171
+ ).toEqual([]);
172
+ },
173
+ );
174
+
175
+ test(
176
+ 'should insert rows into Google Sheets',
177
+ async () =>
178
+ {
179
+ getMock.mockResolvedValue({
180
+ data: {
181
+ values: [
182
+ ['id', 'name', 'age'],
183
+ ],
184
+ },
185
+ });
186
+
187
+ appendMock.mockResolvedValue({
188
+ data: {},
189
+ });
190
+
191
+ const client =
192
+ new GoogleSheetsConsume({
193
+ spreadsheetId:
194
+ 'spreadsheet-123',
195
+
196
+ credentials: {
197
+ clientEmail:
198
+ 'test@example.com',
199
+
200
+ privateKey:
201
+ 'private-key',
202
+ },
203
+ });
204
+
205
+ await client.connect();
206
+
207
+ await client.insertRows(
208
+ 'users',
209
+ [
210
+ {
211
+ id: 1,
212
+ name: 'Budi',
213
+ age: 20,
214
+ },
215
+ {
216
+ id: 2,
217
+ name: 'Andi',
218
+ age: 25,
219
+ },
220
+ ],
221
+ );
222
+
223
+ expect(
224
+ appendMock,
225
+ ).toHaveBeenCalledWith({
226
+ spreadsheetId:
227
+ 'spreadsheet-123',
228
+
229
+ range:
230
+ 'users',
231
+
232
+ valueInputOption:
233
+ 'RAW',
234
+
235
+ insertDataOption:
236
+ 'INSERT_ROWS',
237
+
238
+ requestBody: {
239
+ values: [
240
+ [
241
+ 1,
242
+ 'Budi',
243
+ 20,
244
+ ],
245
+ [
246
+ 2,
247
+ 'Andi',
248
+ 25,
249
+ ],
250
+ ],
251
+ },
252
+ });
253
+ },
254
+ );
255
+
256
+ test(
257
+ 'should do nothing when inserting empty rows',
258
+ async () =>
259
+ {
260
+ const client =
261
+ new GoogleSheetsConsume({
262
+ spreadsheetId:
263
+ 'spreadsheet-123',
264
+
265
+ credentials: {
266
+ clientEmail:
267
+ 'test@example.com',
268
+
269
+ privateKey:
270
+ 'private-key',
271
+ },
272
+ });
273
+
274
+ await client.connect();
275
+
276
+ await client.insertRows(
277
+ 'users',
278
+ [],
279
+ );
280
+
281
+ expect(
282
+ appendMock,
283
+ ).not.toHaveBeenCalled();
284
+ },
285
+ );
286
+
287
+ test(
288
+ 'should update matching rows in Google Sheets',
289
+ async () =>
290
+ {
291
+ getMock.mockResolvedValue({
292
+ data: {
293
+ values: [
294
+ [
295
+ 'id',
296
+ 'name',
297
+ 'age',
298
+ ],
299
+ [
300
+ 1,
301
+ 'Budi',
302
+ 20,
303
+ ],
304
+ [
305
+ 2,
306
+ 'Andi',
307
+ 25,
308
+ ],
309
+ ],
310
+ },
311
+ });
312
+
313
+ updateMock.mockResolvedValue({
314
+ data: {},
315
+ });
316
+
317
+ const client =
318
+ new GoogleSheetsConsume({
319
+ spreadsheetId:
320
+ 'spreadsheet-123',
321
+
322
+ credentials: {
323
+ clientEmail:
324
+ 'test@example.com',
325
+
326
+ privateKey:
327
+ 'private-key',
328
+ },
329
+ });
330
+
331
+ await client.connect();
332
+
333
+ const affected =
334
+ await client.updateRows(
335
+ 'users',
336
+ (row) =>
337
+ row.id === 2,
338
+ {
339
+ age: 26,
340
+ },
341
+ );
342
+
343
+ expect(
344
+ affected,
345
+ ).toBe(1);
346
+
347
+ expect(
348
+ updateMock,
349
+ ).toHaveBeenCalledWith({
350
+ spreadsheetId:
351
+ 'spreadsheet-123',
352
+
353
+ range:
354
+ "'users'!A3",
355
+
356
+ valueInputOption:
357
+ 'RAW',
358
+
359
+ requestBody: {
360
+ values: [
361
+ [
362
+ 2,
363
+ 'Andi',
364
+ 26,
365
+ ],
366
+ ],
367
+ },
368
+ });
369
+ },
370
+ );
371
+
372
+ test(
373
+ 'should return zero when no rows match update predicate',
374
+ async () =>
375
+ {
376
+ getMock.mockResolvedValue({
377
+ data: {
378
+ values: [
379
+ [
380
+ 'id',
381
+ 'name',
382
+ ],
383
+ [
384
+ 1,
385
+ 'Budi',
386
+ ],
387
+ ],
388
+ },
389
+ });
390
+
391
+ const client =
392
+ new GoogleSheetsConsume({
393
+ spreadsheetId:
394
+ 'spreadsheet-123',
395
+
396
+ credentials: {
397
+ clientEmail:
398
+ 'test@example.com',
399
+
400
+ privateKey:
401
+ 'private-key',
402
+ },
403
+ });
404
+
405
+ await client.connect();
406
+
407
+ const affected =
408
+ await client.updateRows(
409
+ 'users',
410
+ (row) =>
411
+ row.id === 999,
412
+ {
413
+ name: 'Nobody',
414
+ },
415
+ );
416
+
417
+ expect(
418
+ affected,
419
+ ).toBe(0);
420
+
421
+ expect(
422
+ updateMock,
423
+ ).not.toHaveBeenCalled();
424
+ },
425
+ );
426
+
427
+ test(
428
+ 'should delete matching rows from Google Sheets',
429
+ async () =>
430
+ {
431
+ getMock.mockResolvedValue({
432
+ data: {
433
+ values: [
434
+ [
435
+ 'id',
436
+ 'name',
437
+ 'age',
438
+ ],
439
+ [
440
+ 1,
441
+ 'Budi',
442
+ 20,
443
+ ],
444
+ [
445
+ 2,
446
+ 'Andi',
447
+ 25,
448
+ ],
449
+ [
450
+ 3,
451
+ 'Citra',
452
+ 30,
453
+ ],
454
+ ],
455
+ },
456
+ });
457
+
458
+ getSpreadsheetMock.mockResolvedValue({
459
+ data: {
460
+ sheets: [
461
+ {
462
+ properties: {
463
+ sheetId: 123,
464
+ title: 'users',
465
+ },
466
+ },
467
+ ],
468
+ },
469
+ });
470
+
471
+ batchUpdateMock.mockResolvedValue({
472
+ data: {},
473
+ });
474
+
475
+ const client =
476
+ new GoogleSheetsConsume({
477
+ spreadsheetId:
478
+ 'spreadsheet-123',
479
+
480
+ credentials: {
481
+ clientEmail:
482
+ 'test@example.com',
483
+
484
+ privateKey:
485
+ 'private-key',
486
+ },
487
+ });
488
+
489
+ await client.connect();
490
+
491
+ const affected =
492
+ await client.deleteRows(
493
+ 'users',
494
+ (row) =>
495
+ row.id === 2,
496
+ );
497
+
498
+ expect(
499
+ affected,
500
+ ).toBe(1);
501
+
502
+ expect(
503
+ getSpreadsheetMock,
504
+ ).toHaveBeenCalledWith({
505
+ spreadsheetId:
506
+ 'spreadsheet-123',
507
+
508
+ fields:
509
+ 'sheets.properties',
510
+ });
511
+
512
+ expect(
513
+ batchUpdateMock,
514
+ ).toHaveBeenCalledWith({
515
+ spreadsheetId:
516
+ 'spreadsheet-123',
517
+
518
+ requestBody: {
519
+ requests: [
520
+ {
521
+ deleteDimension: {
522
+ range: {
523
+ sheetId: 123,
524
+ dimension: 'ROWS',
525
+ startIndex: 2,
526
+ endIndex: 3,
527
+ },
528
+ },
529
+ },
530
+ ],
531
+ },
532
+ });
533
+ },
534
+ );
535
+
536
+ test(
537
+ 'should return zero when no rows match delete predicate',
538
+ async () =>
539
+ {
540
+ getMock.mockResolvedValue({
541
+ data: {
542
+ values: [
543
+ [
544
+ 'id',
545
+ 'name',
546
+ ],
547
+ [
548
+ 1,
549
+ 'Budi',
550
+ ],
551
+ ],
552
+ },
553
+ });
554
+
555
+ const client =
556
+ new GoogleSheetsConsume({
557
+ spreadsheetId:
558
+ 'spreadsheet-123',
559
+
560
+ credentials: {
561
+ clientEmail:
562
+ 'test@example.com',
563
+
564
+ privateKey:
565
+ 'private-key',
566
+ },
567
+ });
568
+
569
+ await client.connect();
570
+
571
+ const affected =
572
+ await client.deleteRows(
573
+ 'users',
574
+ (row) =>
575
+ row.id === 999,
576
+ );
577
+
578
+ expect(
579
+ affected,
580
+ ).toBe(0);
581
+
582
+ expect(
583
+ getSpreadsheetMock,
584
+ ).not.toHaveBeenCalled();
585
+
586
+ expect(
587
+ batchUpdateMock,
588
+ ).not.toHaveBeenCalled();
589
+ },
590
+ );
591
+
592
+ test(
593
+ 'should escape worksheet name with spaces when updating rows',
594
+ async () =>
595
+ {
596
+ getMock.mockResolvedValue({
597
+ data: {
598
+ values: [
599
+ [
600
+ 'id',
601
+ 'name',
602
+ ],
603
+ [
604
+ 1,
605
+ 'Budi',
606
+ ],
607
+ ],
608
+ },
609
+ });
610
+
611
+ updateMock.mockResolvedValue({
612
+ data: {},
613
+ });
614
+
615
+ const client =
616
+ new GoogleSheetsConsume({
617
+ spreadsheetId:
618
+ 'spreadsheet-123',
619
+
620
+ credentials: {
621
+ clientEmail:
622
+ 'test@example.com',
623
+
624
+ privateKey:
625
+ 'private-key',
626
+ },
627
+ });
628
+
629
+ await client.connect();
630
+
631
+ await client.updateRows(
632
+ 'User Data',
633
+ (row) =>
634
+ row.id === 1,
635
+ {
636
+ name: 'Andi',
637
+ },
638
+ );
639
+
640
+ expect(
641
+ updateMock,
642
+ ).toHaveBeenCalledWith({
643
+ spreadsheetId:
644
+ 'spreadsheet-123',
645
+
646
+ range:
647
+ "'User Data'!A2",
648
+
649
+ valueInputOption:
650
+ 'RAW',
651
+
652
+ requestBody: {
653
+ values: [
654
+ [
655
+ 1,
656
+ 'Andi',
657
+ ],
658
+ ],
659
+ },
660
+ });
661
+ },
662
+ );
663
+
664
+ test(
665
+ 'should escape apostrophe in worksheet name when updating rows',
666
+ async () =>
667
+ {
668
+ getMock.mockResolvedValue({
669
+ data: {
670
+ values: [
671
+ [
672
+ 'id',
673
+ 'name',
674
+ ],
675
+ [
676
+ 1,
677
+ 'Budi',
678
+ ],
679
+ ],
680
+ },
681
+ });
682
+
683
+ updateMock.mockResolvedValue({
684
+ data: {},
685
+ });
686
+
687
+ const client =
688
+ new GoogleSheetsConsume({
689
+ spreadsheetId:
690
+ 'spreadsheet-123',
691
+
692
+ credentials: {
693
+ clientEmail:
694
+ 'test@example.com',
695
+
696
+ privateKey:
697
+ 'private-key',
698
+ },
699
+ });
700
+
701
+ await client.connect();
702
+
703
+ await client.updateRows(
704
+ "User's Data",
705
+ (row) =>
706
+ row.id === 1,
707
+ {
708
+ name: 'Andi',
709
+ },
710
+ );
711
+
712
+ expect(
713
+ updateMock,
714
+ ).toHaveBeenCalledWith({
715
+ spreadsheetId:
716
+ 'spreadsheet-123',
717
+
718
+ range:
719
+ "'User''s Data'!A2",
720
+
721
+ valueInputOption:
722
+ 'RAW',
723
+
724
+ requestBody: {
725
+ values: [
726
+ [
727
+ 1,
728
+ 'Andi',
729
+ ],
730
+ ],
731
+ },
732
+ });
733
+ },
734
+ );
735
+
736
+ test(
737
+ 'should read multiple rows with one API request',
738
+ async () =>
739
+ {
740
+ getMock.mockResolvedValue({
741
+ data: {
742
+ values: [
743
+ ['id', 'name'],
744
+ [1, 'Budi'],
745
+ [2, 'Andi'],
746
+ [3, 'Citra'],
747
+ [4, 'Deni'],
748
+ ],
749
+ },
750
+ });
751
+
752
+ const client =
753
+ new GoogleSheetsConsume({
754
+ spreadsheetId:
755
+ 'spreadsheet-123',
756
+
757
+ credentials: {
758
+ clientEmail:
759
+ 'test@example.com',
760
+
761
+ privateKey:
762
+ 'private-key',
763
+ },
764
+ });
765
+
766
+ await client.connect();
767
+
768
+ const rows =
769
+ await client.getRows(
770
+ 'users',
771
+ );
772
+
773
+ expect(
774
+ rows,
775
+ ).toHaveLength(4);
776
+
777
+ expect(
778
+ getMock,
779
+ ).toHaveBeenCalledTimes(1);
780
+ },
781
+ );
782
+
783
+ test(
784
+ 'should append multiple rows with one API request',
785
+ async () =>
786
+ {
787
+ getMock.mockResolvedValue({
788
+ data: {
789
+ values: [
790
+ ['id', 'name'],
791
+ ],
792
+ },
793
+ });
794
+
795
+ appendMock.mockResolvedValue({
796
+ data: {},
797
+ });
798
+
799
+ const client =
800
+ new GoogleSheetsConsume({
801
+ spreadsheetId:
802
+ 'spreadsheet-123',
803
+
804
+ credentials: {
805
+ clientEmail:
806
+ 'test@example.com',
807
+
808
+ privateKey:
809
+ 'private-key',
810
+ },
811
+ });
812
+
813
+ await client.connect();
814
+
815
+ const rows = [
816
+ {
817
+ id: 1,
818
+ name: 'Budi',
819
+ },
820
+ {
821
+ id: 2,
822
+ name: 'Andi',
823
+ },
824
+ {
825
+ id: 3,
826
+ name: 'Citra',
827
+ },
828
+ ];
829
+
830
+ await client.insertRows(
831
+ 'users',
832
+ rows,
833
+ );
834
+
835
+ expect(
836
+ appendMock,
837
+ ).toHaveBeenCalledTimes(1);
838
+
839
+ expect(
840
+ appendMock,
841
+ ).toHaveBeenCalledWith({
842
+ spreadsheetId:
843
+ 'spreadsheet-123',
844
+
845
+ range:
846
+ 'users',
847
+
848
+ valueInputOption:
849
+ 'RAW',
850
+
851
+ insertDataOption:
852
+ 'INSERT_ROWS',
853
+
854
+ requestBody: {
855
+ values: [
856
+ [1, 'Budi'],
857
+ [2, 'Andi'],
858
+ [3, 'Citra'],
859
+ ],
860
+ },
861
+ });
862
+ },
863
+ );
864
+
865
+ test(
866
+ 'should update multiple rows with one API request',
867
+ async () =>
868
+ {
869
+ getMock.mockResolvedValue({
870
+ data: {
871
+ values: [
872
+ ['id', 'name'],
873
+ [1, 'Budi'],
874
+ [2, 'Andi'],
875
+ [3, 'Citra'],
876
+ ],
877
+ },
878
+ });
879
+
880
+ updateMock.mockResolvedValue({
881
+ data: {},
882
+ });
883
+
884
+ const client =
885
+ new GoogleSheetsConsume({
886
+ spreadsheetId:
887
+ 'spreadsheet-123',
888
+
889
+ credentials: {
890
+ clientEmail:
891
+ 'test@example.com',
892
+
893
+ privateKey:
894
+ 'private-key',
895
+ },
896
+ });
897
+
898
+ await client.connect();
899
+
900
+ const affected =
901
+ await client.updateRows(
902
+ 'users',
903
+ (row) =>
904
+ row.id === 1 ||
905
+ row.id === 2 ||
906
+ row.id === 3,
907
+ {
908
+ name: 'Updated',
909
+ },
910
+ );
911
+
912
+ expect(
913
+ affected,
914
+ ).toBe(3);
915
+
916
+ expect(
917
+ updateMock,
918
+ ).toHaveBeenCalledTimes(1);
919
+
920
+ expect(
921
+ updateMock,
922
+ ).toHaveBeenCalledWith({
923
+ spreadsheetId:
924
+ 'spreadsheet-123',
925
+
926
+ range:
927
+ "'users'!A2",
928
+
929
+ valueInputOption:
930
+ 'RAW',
931
+
932
+ requestBody: {
933
+ values: [
934
+ [1, 'Updated'],
935
+ [2, 'Updated'],
936
+ [3, 'Updated'],
937
+ ],
938
+ },
939
+ });
940
+ },
941
+ );
942
+
943
+ test(
944
+ 'should update non-contiguous rows with separate API requests',
945
+ async () =>
946
+ {
947
+ getMock.mockResolvedValue({
948
+ data: {
949
+ values: [
950
+ ['id', 'name'],
951
+ [1, 'Budi'],
952
+ [2, 'Andi'],
953
+ [3, 'Citra'],
954
+ [4, 'Deni'],
955
+ [5, 'Eko'],
956
+ ],
957
+ },
958
+ });
959
+
960
+ updateMock.mockResolvedValue({
961
+ data: {},
962
+ });
963
+
964
+ const client =
965
+ new GoogleSheetsConsume({
966
+ spreadsheetId:
967
+ 'spreadsheet-123',
968
+
969
+ credentials: {
970
+ clientEmail:
971
+ 'test@example.com',
972
+
973
+ privateKey:
974
+ 'private-key',
975
+ },
976
+ });
977
+
978
+ await client.connect();
979
+
980
+ const affected =
981
+ await client.updateRows(
982
+ 'users',
983
+ (row) =>
984
+ row.id === 1 ||
985
+ row.id === 2 ||
986
+ row.id === 4 ||
987
+ row.id === 5,
988
+ {
989
+ name: 'Updated',
990
+ },
991
+ );
992
+
993
+ expect(
994
+ affected,
995
+ ).toBe(4);
996
+
997
+ expect(
998
+ updateMock,
999
+ ).toHaveBeenCalledTimes(2);
1000
+
1001
+ expect(
1002
+ updateMock,
1003
+ ).toHaveBeenNthCalledWith(
1004
+ 1,
1005
+ {
1006
+ spreadsheetId:
1007
+ 'spreadsheet-123',
1008
+
1009
+ range:
1010
+ "'users'!A2",
1011
+
1012
+ valueInputOption:
1013
+ 'RAW',
1014
+
1015
+ requestBody: {
1016
+ values: [
1017
+ [1, 'Updated'],
1018
+ [2, 'Updated'],
1019
+ ],
1020
+ },
1021
+ },
1022
+ );
1023
+
1024
+ expect(
1025
+ updateMock,
1026
+ ).toHaveBeenNthCalledWith(
1027
+ 2,
1028
+ {
1029
+ spreadsheetId:
1030
+ 'spreadsheet-123',
1031
+
1032
+ range:
1033
+ "'users'!A5",
1034
+
1035
+ valueInputOption:
1036
+ 'RAW',
1037
+
1038
+ requestBody: {
1039
+ values: [
1040
+ [4, 'Updated'],
1041
+ [5, 'Updated'],
1042
+ ],
1043
+ },
1044
+ },
1045
+ );
1046
+ },
1047
+ );
1048
+
1049
+ test(
1050
+ 'should delete multiple rows with one API request',
1051
+ async () =>
1052
+ {
1053
+ getMock.mockResolvedValue({
1054
+ data: {
1055
+ values: [
1056
+ ['id', 'name'],
1057
+ [1, 'Budi'],
1058
+ [2, 'Andi'],
1059
+ [3, 'Citra'],
1060
+ [4, 'Deni'],
1061
+ ],
1062
+ },
1063
+ });
1064
+
1065
+ getSpreadsheetMock.mockResolvedValue({
1066
+ data: {
1067
+ sheets: [
1068
+ {
1069
+ properties: {
1070
+ sheetId: 123,
1071
+ title: 'users',
1072
+ },
1073
+ },
1074
+ ],
1075
+ },
1076
+ });
1077
+
1078
+ batchUpdateMock.mockResolvedValue({
1079
+ data: {},
1080
+ });
1081
+
1082
+ const client =
1083
+ new GoogleSheetsConsume({
1084
+ spreadsheetId:
1085
+ 'spreadsheet-123',
1086
+
1087
+ credentials: {
1088
+ clientEmail:
1089
+ 'test@example.com',
1090
+
1091
+ privateKey:
1092
+ 'private-key',
1093
+ },
1094
+ });
1095
+
1096
+ await client.connect();
1097
+
1098
+ const affected =
1099
+ await client.deleteRows(
1100
+ 'users',
1101
+ (row) =>
1102
+ row.id === 2 ||
1103
+ row.id === 3 ||
1104
+ row.id === 4,
1105
+ );
1106
+
1107
+ expect(
1108
+ affected,
1109
+ ).toBe(3);
1110
+
1111
+ expect(
1112
+ batchUpdateMock,
1113
+ ).toHaveBeenCalledTimes(1);
1114
+ },
1115
+ );
1116
+
1117
+ test(
1118
+ 'should cache worksheet metadata',
1119
+ async () =>
1120
+ {
1121
+ getSpreadsheetMock.mockResolvedValue({
1122
+ data: {
1123
+ sheets: [
1124
+ {
1125
+ properties: {
1126
+ sheetId: 123,
1127
+ title: 'users',
1128
+ },
1129
+ },
1130
+ ],
1131
+ },
1132
+ });
1133
+
1134
+ const client =
1135
+ new GoogleSheetsConsume({
1136
+ spreadsheetId:
1137
+ 'spreadsheet-123',
1138
+
1139
+ credentials: {
1140
+ clientEmail:
1141
+ 'test@example.com',
1142
+
1143
+ privateKey:
1144
+ 'private-key',
1145
+ },
1146
+ });
1147
+
1148
+ await client.connect();
1149
+
1150
+ const first =
1151
+ await client.getSheetMetadata(
1152
+ 'users',
1153
+ );
1154
+
1155
+ const second =
1156
+ await client.getSheetMetadata(
1157
+ 'users',
1158
+ );
1159
+
1160
+ expect(first).toEqual({
1161
+ sheetId: 123,
1162
+ title: 'users',
1163
+ });
1164
+
1165
+ expect(second).toEqual({
1166
+ sheetId: 123,
1167
+ title: 'users',
1168
+ });
1169
+
1170
+ expect(
1171
+ getSpreadsheetMock,
1172
+ ).toHaveBeenCalledTimes(1);
1173
+ },
1174
+ );
1175
+
1176
+ test(
1177
+ 'should cache worksheet headers',
1178
+ async () =>
1179
+ {
1180
+ getMock.mockResolvedValue({
1181
+ data: {
1182
+ values: [
1183
+ [
1184
+ 'id',
1185
+ 'name',
1186
+ 'email',
1187
+ ],
1188
+ ],
1189
+ },
1190
+ });
1191
+
1192
+ const client =
1193
+ new GoogleSheetsConsume({
1194
+ spreadsheetId:
1195
+ 'spreadsheet-123',
1196
+
1197
+ credentials: {
1198
+ clientEmail:
1199
+ 'test@example.com',
1200
+
1201
+ privateKey:
1202
+ 'private-key',
1203
+ },
1204
+ });
1205
+
1206
+ await client.connect();
1207
+
1208
+ const first =
1209
+ await client.getHeaders(
1210
+ 'users',
1211
+ );
1212
+
1213
+ const second =
1214
+ await client.getHeaders(
1215
+ 'users',
1216
+ );
1217
+
1218
+ expect(first).toEqual([
1219
+ 'id',
1220
+ 'name',
1221
+ 'email',
1222
+ ]);
1223
+
1224
+ expect(second).toEqual([
1225
+ 'id',
1226
+ 'name',
1227
+ 'email',
1228
+ ]);
1229
+
1230
+ expect(
1231
+ getMock,
1232
+ ).toHaveBeenCalledTimes(1);
1233
+ },
1234
+ );
1235
+
1236
+ test(
1237
+ 'should retry failed API request',
1238
+ async () =>
1239
+ {
1240
+ getMock
1241
+ .mockRejectedValueOnce(
1242
+ {
1243
+ response: {
1244
+ status: 500,
1245
+ },
1246
+ },
1247
+ )
1248
+ .mockResolvedValueOnce({
1249
+ data: {
1250
+ values: [
1251
+ ['id', 'name'],
1252
+ [1, 'John'],
1253
+ ],
1254
+ },
1255
+ });
1256
+
1257
+ const client =
1258
+ new GoogleSheetsConsume({
1259
+ spreadsheetId:
1260
+ 'spreadsheet-123',
1261
+
1262
+ credentials: {
1263
+ clientEmail:
1264
+ 'test@example.com',
1265
+
1266
+ privateKey:
1267
+ 'private-key',
1268
+ },
1269
+ });
1270
+
1271
+ await client.connect();
1272
+
1273
+ const rows =
1274
+ await client.getRows(
1275
+ 'users',
1276
+ );
1277
+
1278
+ expect(rows).toEqual([
1279
+ {
1280
+ id: 1,
1281
+ name: 'John',
1282
+ },
1283
+ ]);
1284
+
1285
+ expect(
1286
+ getMock,
1287
+ ).toHaveBeenCalledTimes(2);
1288
+ },
1289
+ );
1290
+
1291
+ test(
1292
+ 'should retry API request multiple times before failing',
1293
+ async () =>
1294
+ {
1295
+ const error =
1296
+ Object.assign(
1297
+ new Error(
1298
+ 'Temporary API error',
1299
+ ),
1300
+ {
1301
+ response: {
1302
+ status: 500,
1303
+ },
1304
+ },
1305
+ );
1306
+
1307
+ getMock
1308
+ .mockRejectedValueOnce(error)
1309
+ .mockRejectedValueOnce(error)
1310
+ .mockRejectedValueOnce(error)
1311
+ .mockRejectedValueOnce(error);
1312
+
1313
+ const client =
1314
+ new GoogleSheetsConsume({
1315
+ spreadsheetId:
1316
+ 'spreadsheet-id',
1317
+
1318
+ credentials: {
1319
+ clientEmail:
1320
+ 'client@example.com',
1321
+
1322
+ privateKey:
1323
+ 'private-key',
1324
+ },
1325
+ });
1326
+
1327
+ await client.connect();
1328
+
1329
+ await expect(
1330
+ client.getRows('users'),
1331
+ ).rejects.toBe(error);
1332
+
1333
+ expect(
1334
+ getMock,
1335
+ ).toHaveBeenCalledTimes(4);
1336
+ },
1337
+ );
1338
+
1339
+ test(
1340
+ 'should retry rate limit API request',
1341
+ async () =>
1342
+ {
1343
+ getMock
1344
+ .mockRejectedValueOnce({
1345
+ response: {
1346
+ status: 429,
1347
+ },
1348
+ })
1349
+ .mockResolvedValueOnce({
1350
+ data: {
1351
+ values: [
1352
+ ['id', 'name'],
1353
+ [1, 'John'],
1354
+ ],
1355
+ },
1356
+ });
1357
+
1358
+ const client =
1359
+ new GoogleSheetsConsume({
1360
+ spreadsheetId:
1361
+ 'spreadsheet-id',
1362
+
1363
+ credentials: {
1364
+ clientEmail:
1365
+ 'client@example.com',
1366
+
1367
+ privateKey:
1368
+ 'private-key',
1369
+ },
1370
+ });
1371
+
1372
+ await client.connect();
1373
+
1374
+ const rows =
1375
+ await client.getRows('users');
1376
+
1377
+ expect(rows).toEqual([
1378
+ {
1379
+ id: 1,
1380
+ name: 'John',
1381
+ },
1382
+ ]);
1383
+
1384
+ expect(
1385
+ getMock,
1386
+ ).toHaveBeenCalledTimes(2);
1387
+ },
1388
+ );
1389
+
1390
+ test(
1391
+ 'should use exponential backoff when retrying API request',
1392
+ async () =>
1393
+ {
1394
+ const sleepMock =
1395
+ spyOn(
1396
+ globalThis,
1397
+ 'setTimeout',
1398
+ );
1399
+
1400
+ getMock
1401
+ .mockRejectedValueOnce({
1402
+ response: {
1403
+ status: 500,
1404
+ },
1405
+ })
1406
+ .mockRejectedValueOnce({
1407
+ response: {
1408
+ status: 500,
1409
+ },
1410
+ })
1411
+ .mockResolvedValueOnce({
1412
+ data: {
1413
+ values: [
1414
+ ['id', 'name'],
1415
+ [1, 'John'],
1416
+ ],
1417
+ },
1418
+ });
1419
+
1420
+ const client =
1421
+ new GoogleSheetsConsume({
1422
+ spreadsheetId:
1423
+ 'spreadsheet-id',
1424
+
1425
+ credentials: {
1426
+ clientEmail:
1427
+ 'client@example.com',
1428
+
1429
+ privateKey:
1430
+ 'private-key',
1431
+ },
1432
+ });
1433
+
1434
+ await client.connect();
1435
+
1436
+ const rows =
1437
+ await client.getRows('users');
1438
+
1439
+ expect(rows).toEqual([
1440
+ {
1441
+ id: 1,
1442
+ name: 'John',
1443
+ },
1444
+ ]);
1445
+
1446
+ expect(
1447
+ getMock,
1448
+ ).toHaveBeenCalledTimes(3);
1449
+
1450
+ expect(
1451
+ sleepMock,
1452
+ ).toHaveBeenCalledTimes(2);
1453
+
1454
+ expect(
1455
+ sleepMock.mock.calls[0]?.[1],
1456
+ ).toBe(100);
1457
+
1458
+ expect(
1459
+ sleepMock.mock.calls[1]?.[1],
1460
+ ).toBe(200);
1461
+
1462
+ sleepMock.mockRestore();
1463
+ },
1464
+ );
1465
+
1466
+ test(
1467
+ 'should respect retry-after when rate limit API request fails',
1468
+ async () =>
1469
+ {
1470
+ const sleepMock =
1471
+ spyOn(
1472
+ globalThis,
1473
+ 'setTimeout',
1474
+ );
1475
+
1476
+ getMock
1477
+ .mockRejectedValueOnce({
1478
+ response: {
1479
+ status: 429,
1480
+ headers: {
1481
+ 'retry-after': '1',
1482
+ },
1483
+ },
1484
+ })
1485
+ .mockResolvedValueOnce({
1486
+ data: {
1487
+ values: [
1488
+ ['id', 'name'],
1489
+ [1, 'John'],
1490
+ ],
1491
+ },
1492
+ });
1493
+
1494
+ const client =
1495
+ new GoogleSheetsConsume({
1496
+ spreadsheetId:
1497
+ 'spreadsheet-id',
1498
+
1499
+ credentials: {
1500
+ clientEmail:
1501
+ 'client@example.com',
1502
+
1503
+ privateKey:
1504
+ 'private-key',
1505
+ },
1506
+ });
1507
+
1508
+ await client.connect();
1509
+
1510
+ const rows =
1511
+ await client.getRows('users');
1512
+
1513
+ expect(rows).toEqual([
1514
+ {
1515
+ id: 1,
1516
+ name: 'John',
1517
+ },
1518
+ ]);
1519
+
1520
+ expect(
1521
+ getMock,
1522
+ ).toHaveBeenCalledTimes(2);
1523
+
1524
+ expect(
1525
+ sleepMock,
1526
+ ).toHaveBeenCalledTimes(1);
1527
+
1528
+ expect(
1529
+ sleepMock.mock.calls[0]?.[1],
1530
+ ).toBe(1000);
1531
+
1532
+ sleepMock.mockRestore();
1533
+ },
1534
+ );
1535
+
1536
+ test(
1537
+ 'should throw authentication error when Google API rejects with 401',
1538
+ async () =>
1539
+ {
1540
+ getMock.mockRejectedValue({
1541
+ response: {
1542
+ status: 401,
1543
+ },
1544
+ });
1545
+
1546
+ const client =
1547
+ new GoogleSheetsConsume({
1548
+ spreadsheetId:
1549
+ 'spreadsheet-123',
1550
+
1551
+ credentials: {
1552
+ clientEmail:
1553
+ 'test@example.com',
1554
+
1555
+ privateKey:
1556
+ 'private-key',
1557
+ },
1558
+ });
1559
+
1560
+ await client.connect();
1561
+
1562
+ await expect(
1563
+ client.getRows('users'),
1564
+ ).rejects.toMatchObject({
1565
+ name:
1566
+ 'GoogleSheetsAuthenticationError',
1567
+ });
1568
+
1569
+ expect(
1570
+ getMock,
1571
+ ).toHaveBeenCalledTimes(1);
1572
+ },
1573
+ );
1574
+
1575
+ test(
1576
+ 'should throw spreadsheet not found error when Google API rejects with 404',
1577
+ async () =>
1578
+ {
1579
+ getMock.mockRejectedValue({
1580
+ response: {
1581
+ status: 404,
1582
+ },
1583
+ });
1584
+
1585
+ const client =
1586
+ new GoogleSheetsConsume({
1587
+ spreadsheetId:
1588
+ 'spreadsheet-123',
1589
+
1590
+ credentials: {
1591
+ clientEmail:
1592
+ 'test@example.com',
1593
+
1594
+ privateKey:
1595
+ 'private-key',
1596
+ },
1597
+ });
1598
+
1599
+ await client.connect();
1600
+
1601
+ await expect(
1602
+ client.getRows('users'),
1603
+ ).rejects.toMatchObject({
1604
+ name:
1605
+ 'GoogleSheetsSpreadsheetNotFoundError',
1606
+ });
1607
+
1608
+ expect(
1609
+ getMock,
1610
+ ).toHaveBeenCalledTimes(1);
1611
+ },
1612
+ );
1613
+
1614
+ test(
1615
+ 'should throw worksheet not found error when deleting from missing worksheet',
1616
+ async () =>
1617
+ {
1618
+ getMock.mockResolvedValue({
1619
+ data: {
1620
+ values: [
1621
+ ['id', 'name'],
1622
+ [1, 'John'],
1623
+ ],
1624
+ },
1625
+ });
1626
+
1627
+ getSpreadsheetMock.mockResolvedValue({
1628
+ data: {
1629
+ sheets: [],
1630
+ },
1631
+ });
1632
+
1633
+ const client =
1634
+ new GoogleSheetsConsume({
1635
+ spreadsheetId:
1636
+ 'spreadsheet-123',
1637
+
1638
+ credentials: {
1639
+ clientEmail:
1640
+ 'test@example.com',
1641
+
1642
+ privateKey:
1643
+ 'private-key',
1644
+ },
1645
+ });
1646
+
1647
+ await client.connect();
1648
+
1649
+ await expect(
1650
+ client.deleteRows(
1651
+ 'missing-sheet',
1652
+ () => true,
1653
+ ),
1654
+ ).rejects.toMatchObject({
1655
+ name:
1656
+ 'GoogleSheetsWorksheetNotFoundError',
1657
+
1658
+ message:
1659
+ 'Worksheet "missing-sheet" not found.',
1660
+ });
1661
+ },
1662
+ );
1663
+
1664
+ test(
1665
+ 'should throw invalid range error when Google API rejects with invalid range',
1666
+ async () =>
1667
+ {
1668
+ getMock.mockRejectedValue({
1669
+ response: {
1670
+ status: 400,
1671
+ },
1672
+ });
1673
+
1674
+ const client =
1675
+ new GoogleSheetsConsume({
1676
+ spreadsheetId:
1677
+ 'spreadsheet-123',
1678
+
1679
+ credentials: {
1680
+ clientEmail:
1681
+ 'test@example.com',
1682
+
1683
+ privateKey:
1684
+ 'private-key',
1685
+ },
1686
+ });
1687
+
1688
+ await client.connect();
1689
+
1690
+ await expect(
1691
+ client.getRows(
1692
+ 'invalid:range',
1693
+ ),
1694
+ ).rejects.toMatchObject({
1695
+ name:
1696
+ 'GoogleSheetsInvalidRangeError',
1697
+ });
1698
+
1699
+ expect(
1700
+ getMock,
1701
+ ).toHaveBeenCalledTimes(1);
1702
+ },
1703
+ );
1704
+ },
1705
+ );