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,1935 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import
3
+ {
4
+ AfterInsert,
5
+ AfterLoad,
6
+ AfterRemove,
7
+ AfterUpdate,
8
+ BeforeInsert,
9
+ BeforeRemove,
10
+ BeforeUpdate,
11
+ Column,
12
+ DefaultNamingStrategy,
13
+ Entity,
14
+ EventSubscriber,
15
+ ManyToOne,
16
+ PrimaryColumn,
17
+ PrimaryGeneratedColumn,
18
+ type EntitySubscriberInterface,
19
+ type InsertEvent,
20
+ } from 'typeorm';
21
+ import type { GoogleSheetsDriver } from '../../src/core/driver';
22
+ import { Memory } from '../../src/core/memory';
23
+ import { GoogleSheetsConsume, createGoogleSheetsDataSource, type GoogleSheetsDataSourceOptions } from '../../src';
24
+
25
+ describe(
26
+ 'GoogleSheetsDataSource',
27
+ () =>
28
+ {
29
+ class TestNamingStrategy
30
+ extends DefaultNamingStrategy
31
+ {
32
+ override tableName(
33
+ className: string,
34
+ customName: string | undefined,
35
+ ): string
36
+ {
37
+ return `sheet_${customName ?? className}`.toLowerCase();
38
+ }
39
+
40
+ override columnName(
41
+ propertyName: string,
42
+ customName: string | undefined,
43
+ embeddedPrefixes: string[],
44
+ ): string
45
+ {
46
+ return `col_${propertyName}`.toLowerCase();
47
+ }
48
+ }
49
+
50
+ @EventSubscriber()
51
+ class LifecycleUserSubscriber
52
+ implements EntitySubscriberInterface<LifecycleUser>
53
+ {
54
+ beforeInsert(event: InsertEvent<LifecycleUser>)
55
+ {
56
+ event.entity.name =
57
+ `${event.entity.name}-subscriber-before`;
58
+ }
59
+
60
+ afterInsert(event: InsertEvent<LifecycleUser>)
61
+ {
62
+ event.entity.name =
63
+ `${event.entity.name}-subscriber-after`;
64
+ }
65
+ }
66
+
67
+ @Entity()
68
+ class NamingStrategyUser
69
+ {
70
+ @PrimaryGeneratedColumn()
71
+ id!: number;
72
+
73
+ @Column()
74
+ displayName!: string;
75
+ }
76
+
77
+ @Entity('custom_users')
78
+ class CustomUser
79
+ {
80
+ @PrimaryGeneratedColumn()
81
+ id!: number;
82
+
83
+ @Column()
84
+ name!: string;
85
+ }
86
+
87
+ @Entity('mapped_users')
88
+ class MappedUser
89
+ {
90
+ @PrimaryGeneratedColumn()
91
+ id!: number;
92
+
93
+ @Column({
94
+ name: 'user_name',
95
+ })
96
+ name!: string;
97
+ }
98
+
99
+ @Entity('typed_users')
100
+ class TypedUser
101
+ {
102
+ @PrimaryGeneratedColumn()
103
+ id!: number;
104
+
105
+ @Column({
106
+ type: 'number',
107
+ })
108
+ age!: number;
109
+
110
+ @Column({
111
+ type: 'boolean',
112
+ })
113
+ active!: boolean;
114
+ }
115
+
116
+ @Entity('nullable_users')
117
+ class NullableUser
118
+ {
119
+ @PrimaryGeneratedColumn()
120
+ id!: number;
121
+
122
+ @Column({
123
+ nullable: true,
124
+ })
125
+ nickname!: string | null;
126
+ }
127
+
128
+ @Entity('generated_users')
129
+ class GeneratedUser
130
+ {
131
+ @PrimaryGeneratedColumn()
132
+ id!: number;
133
+
134
+ @Column()
135
+ name!: string;
136
+ }
137
+
138
+ @Entity('default_users')
139
+ class DefaultUser
140
+ {
141
+ @PrimaryGeneratedColumn()
142
+ id!: number;
143
+
144
+ @Column({
145
+ default: 'active',
146
+ })
147
+ status!: string;
148
+ }
149
+
150
+ @Entity('uuid_users')
151
+ class UuidUser
152
+ {
153
+ @PrimaryGeneratedColumn('uuid')
154
+ id!: string;
155
+
156
+ @Column()
157
+ name!: string;
158
+ }
159
+
160
+ @Entity('sync_users')
161
+ class SyncUser
162
+ {
163
+ @PrimaryGeneratedColumn()
164
+ id!: number;
165
+
166
+ @Column()
167
+ name!: string;
168
+ }
169
+
170
+ @Entity('sync_missing_headers')
171
+ class SyncMissingHeaderUser
172
+ {
173
+ @PrimaryGeneratedColumn()
174
+ id!: number;
175
+
176
+ @Column()
177
+ name!: string;
178
+
179
+ @Column()
180
+ email!: string;
181
+ }
182
+
183
+ @Entity('required_users')
184
+ class RequiredUser
185
+ {
186
+ @PrimaryColumn()
187
+ id!: number;
188
+
189
+ @Column()
190
+ name!: string;
191
+ }
192
+
193
+ @Entity('lifecycle_users')
194
+ class LifecycleUser
195
+ {
196
+ @PrimaryGeneratedColumn()
197
+ id!: number;
198
+
199
+ @Column()
200
+ name!: string;
201
+
202
+ @BeforeInsert()
203
+ beforeInsert()
204
+ {
205
+ this.name =
206
+ this.name.toUpperCase();
207
+ }
208
+
209
+ @BeforeUpdate()
210
+ beforeUpdate()
211
+ {
212
+ this.name =
213
+ this.name.toUpperCase();
214
+ }
215
+
216
+ @BeforeRemove()
217
+ beforeRemove()
218
+ {
219
+ this.name =
220
+ `${this.name}-before`;
221
+ }
222
+
223
+ @AfterLoad()
224
+ afterLoad()
225
+ {
226
+ this.name =
227
+ `${this.name}-loaded`;
228
+ }
229
+ }
230
+
231
+ @Entity('lifecycle_users2')
232
+ class LifecycleUser2
233
+ {
234
+ @PrimaryGeneratedColumn()
235
+ id!: number;
236
+
237
+ @Column()
238
+ name!: string;
239
+
240
+ @AfterInsert()
241
+ afterInsert()
242
+ {
243
+ this.name =
244
+ `${this.name}-after`;
245
+ }
246
+
247
+ @AfterUpdate()
248
+ afterUpdate()
249
+ {
250
+ this.name =
251
+ `${this.name}-after`;
252
+ }
253
+
254
+ @AfterRemove()
255
+ afterRemove()
256
+ {
257
+ this.name =
258
+ `${this.name}-after`;
259
+ }
260
+ }
261
+
262
+ @Entity('relation_users')
263
+ class RelationUser
264
+ {
265
+ @PrimaryGeneratedColumn()
266
+ id!: number;
267
+
268
+ @Column()
269
+ name!: string;
270
+ }
271
+
272
+ @Entity('relation_posts')
273
+ class RelationPost
274
+ {
275
+ @PrimaryGeneratedColumn()
276
+ id!: number;
277
+
278
+ @Column()
279
+ title!: string;
280
+
281
+ @ManyToOne(
282
+ () => RelationUser,
283
+ {
284
+ nullable: false,
285
+ },
286
+ )
287
+ author!: RelationUser;
288
+ }
289
+
290
+ test(
291
+ 'should use default GoogleSheetsConsume when client is not provided',
292
+ () =>
293
+ {
294
+ const dataSource = createGoogleSheetsDataSource({
295
+ type: 'google-sheets',
296
+
297
+ spreadsheetId:
298
+ 'test-spreadsheet-id',
299
+
300
+ credentials: {
301
+ clientEmail:
302
+ 'test@example.com',
303
+
304
+ privateKey:
305
+ 'test-private-key',
306
+ },
307
+ });
308
+
309
+ const driver =
310
+ dataSource.driver as GoogleSheetsDriver;
311
+
312
+ expect(driver.client)
313
+ .toBeInstanceOf(
314
+ GoogleSheetsConsume,
315
+ );
316
+ },
317
+ );
318
+
319
+ test(
320
+ 'should use custom client when provided',
321
+ () =>
322
+ {
323
+ const client =
324
+ new Memory();
325
+
326
+ const dataSource =
327
+ createGoogleSheetsDataSource({
328
+ type: 'google-sheets',
329
+
330
+ spreadsheetId:
331
+ 'test-spreadsheet-id',
332
+
333
+ credentials: {
334
+ clientEmail:
335
+ 'test@example.com',
336
+
337
+ privateKey:
338
+ 'test-private-key',
339
+ },
340
+
341
+ client,
342
+ });
343
+
344
+ const driver =
345
+ dataSource.driver as GoogleSheetsDriver;
346
+
347
+ expect(driver.client)
348
+ .toBe(client);
349
+ },
350
+ );
351
+
352
+ test(
353
+ 'should resolve custom entity table name',
354
+ async () =>
355
+ {
356
+ const dataSource =
357
+ createGoogleSheetsDataSource({
358
+ type: 'google-sheets',
359
+
360
+ client:
361
+ new Memory(),
362
+
363
+ entities: [
364
+ CustomUser,
365
+ ],
366
+ });
367
+
368
+ await dataSource.initialize();
369
+
370
+ try
371
+ {
372
+ const metadata =
373
+ dataSource.entityMetadatas.find(
374
+ (item) =>
375
+ item.target === CustomUser,
376
+ );
377
+
378
+ expect(metadata)
379
+ .toBeDefined();
380
+
381
+ expect(metadata?.tableName)
382
+ .toBe('custom_users');
383
+ }
384
+ finally
385
+ {
386
+ await dataSource.destroy();
387
+ }
388
+ },
389
+ );
390
+
391
+ test(
392
+ 'should use resolved entity table name as worksheet',
393
+ async () =>
394
+ {
395
+ const client =
396
+ new Memory({
397
+ custom_users: [
398
+ {
399
+ id: 1,
400
+ name: 'Basuni',
401
+ },
402
+ ],
403
+ });
404
+
405
+ const dataSource =
406
+ createGoogleSheetsDataSource({
407
+ type: 'google-sheets',
408
+
409
+ client,
410
+
411
+ entities: [
412
+ CustomUser,
413
+ ],
414
+ });
415
+
416
+ await dataSource.initialize();
417
+
418
+ try
419
+ {
420
+ const repository =
421
+ dataSource.getRepository(CustomUser);
422
+
423
+ const users =
424
+ await repository.find();
425
+
426
+ expect(users)
427
+ .toHaveLength(1);
428
+
429
+ expect(users[0]?.id)
430
+ .toBe(1);
431
+
432
+ expect(users[0]?.name)
433
+ .toBe('Basuni');
434
+ }
435
+ finally
436
+ {
437
+ await dataSource.destroy();
438
+ }
439
+ },
440
+ );
441
+
442
+ test(
443
+ 'should resolve custom column name',
444
+ async () =>
445
+ {
446
+ const dataSource =
447
+ createGoogleSheetsDataSource({
448
+ type: 'google-sheets',
449
+
450
+ client:
451
+ new Memory(),
452
+
453
+ entities: [
454
+ MappedUser,
455
+ ],
456
+ });
457
+
458
+ await dataSource.initialize();
459
+
460
+ try
461
+ {
462
+ const metadata =
463
+ dataSource.entityMetadatas.find(
464
+ (item) =>
465
+ item.target === MappedUser,
466
+ );
467
+
468
+ const column =
469
+ metadata?.findColumnWithPropertyName(
470
+ 'name',
471
+ );
472
+
473
+ expect(column)
474
+ .toBeDefined();
475
+
476
+ expect(column?.propertyName)
477
+ .toBe('name');
478
+
479
+ expect(column?.databaseName)
480
+ .toBe('user_name');
481
+ }
482
+ finally
483
+ {
484
+ await dataSource.destroy();
485
+ }
486
+ },
487
+ );
488
+
489
+ test(
490
+ 'should resolve column type',
491
+ async () =>
492
+ {
493
+ const dataSource =
494
+ createGoogleSheetsDataSource({
495
+ type: 'google-sheets',
496
+
497
+ client:
498
+ new Memory(),
499
+
500
+ entities: [
501
+ TypedUser,
502
+ ],
503
+ });
504
+
505
+ await dataSource.initialize();
506
+
507
+ try
508
+ {
509
+ const metadata =
510
+ dataSource.entityMetadatas.find(
511
+ (item) =>
512
+ item.target === TypedUser,
513
+ );
514
+
515
+ const ageColumn =
516
+ metadata?.findColumnWithPropertyName(
517
+ 'age',
518
+ );
519
+
520
+ const activeColumn =
521
+ metadata?.findColumnWithPropertyName(
522
+ 'active',
523
+ );
524
+
525
+ expect(ageColumn?.type)
526
+ .toBe('number');
527
+
528
+ expect(activeColumn?.type)
529
+ .toBe('boolean');
530
+ }
531
+ finally
532
+ {
533
+ await dataSource.destroy();
534
+ }
535
+ },
536
+ );
537
+
538
+ test(
539
+ 'should resolve nullable column',
540
+ async () =>
541
+ {
542
+ const dataSource =
543
+ createGoogleSheetsDataSource({
544
+ type: 'google-sheets',
545
+
546
+ client:
547
+ new Memory(),
548
+
549
+ entities: [
550
+ NullableUser,
551
+ ],
552
+ });
553
+
554
+ await dataSource.initialize();
555
+
556
+ try
557
+ {
558
+ const metadata =
559
+ dataSource.entityMetadatas.find(
560
+ (item) =>
561
+ item.target === NullableUser,
562
+ );
563
+
564
+ const column =
565
+ metadata?.findColumnWithPropertyName(
566
+ 'nickname',
567
+ );
568
+
569
+ expect(column)
570
+ .toBeDefined();
571
+
572
+ expect(column?.isNullable)
573
+ .toBe(true);
574
+ }
575
+ finally
576
+ {
577
+ await dataSource.destroy();
578
+ }
579
+ },
580
+ );
581
+
582
+ test(
583
+ 'should resolve generated column',
584
+ async () =>
585
+ {
586
+ const dataSource =
587
+ createGoogleSheetsDataSource({
588
+ type: 'google-sheets',
589
+
590
+ client:
591
+ new Memory(),
592
+
593
+ entities: [
594
+ GeneratedUser,
595
+ ],
596
+ });
597
+
598
+ await dataSource.initialize();
599
+
600
+ try
601
+ {
602
+ const metadata =
603
+ dataSource.entityMetadatas.find(
604
+ (item) =>
605
+ item.target === GeneratedUser,
606
+ );
607
+
608
+ const column =
609
+ metadata?.findColumnWithPropertyName(
610
+ 'id',
611
+ );
612
+
613
+ expect(column)
614
+ .toBeDefined();
615
+
616
+ expect(column?.isGenerated)
617
+ .toBe(true);
618
+
619
+ expect(column?.isPrimary)
620
+ .toBe(true);
621
+ }
622
+ finally
623
+ {
624
+ await dataSource.destroy();
625
+ }
626
+ },
627
+ );
628
+
629
+ test(
630
+ 'should resolve increment generation strategy',
631
+ async () =>
632
+ {
633
+ const dataSource =
634
+ createGoogleSheetsDataSource({
635
+ type: 'google-sheets',
636
+
637
+ client:
638
+ new Memory(),
639
+
640
+ entities: [
641
+ GeneratedUser,
642
+ ],
643
+ });
644
+
645
+ await dataSource.initialize();
646
+
647
+ try
648
+ {
649
+ const metadata =
650
+ dataSource.entityMetadatas.find(
651
+ (item) =>
652
+ item.target === GeneratedUser,
653
+ );
654
+
655
+ const column =
656
+ metadata?.findColumnWithPropertyName(
657
+ 'id',
658
+ );
659
+
660
+ expect(column?.generationStrategy)
661
+ .toBe('increment');
662
+ }
663
+ finally
664
+ {
665
+ await dataSource.destroy();
666
+ }
667
+ },
668
+ );
669
+
670
+ test(
671
+ 'should resolve default value',
672
+ async () =>
673
+ {
674
+ const dataSource =
675
+ createGoogleSheetsDataSource({
676
+ type: 'google-sheets',
677
+
678
+ client:
679
+ new Memory(),
680
+
681
+ entities: [
682
+ DefaultUser,
683
+ ],
684
+ });
685
+
686
+ await dataSource.initialize();
687
+
688
+ try
689
+ {
690
+ const metadata =
691
+ dataSource.entityMetadatas.find(
692
+ (item) =>
693
+ item.target === DefaultUser,
694
+ );
695
+
696
+ const column =
697
+ metadata?.findColumnWithPropertyName(
698
+ 'status',
699
+ );
700
+
701
+ expect(column)
702
+ .toBeDefined();
703
+
704
+ expect(column?.default)
705
+ .toBe('active');
706
+ }
707
+ finally
708
+ {
709
+ await dataSource.destroy();
710
+ }
711
+ },
712
+ );
713
+
714
+ test(
715
+ 'should resolve uuid generation strategy',
716
+ async () =>
717
+ {
718
+ const dataSource =
719
+ createGoogleSheetsDataSource({
720
+ type: 'google-sheets',
721
+
722
+ client:
723
+ new Memory(),
724
+
725
+ entities: [
726
+ UuidUser,
727
+ ],
728
+ });
729
+
730
+ await dataSource.initialize();
731
+
732
+ try
733
+ {
734
+ const metadata =
735
+ dataSource.entityMetadatas.find(
736
+ (item) =>
737
+ item.target === UuidUser,
738
+ );
739
+
740
+ const column =
741
+ metadata?.findColumnWithPropertyName(
742
+ 'id',
743
+ );
744
+
745
+ expect(column)
746
+ .toBeDefined();
747
+
748
+ expect(column?.isPrimary)
749
+ .toBe(true);
750
+
751
+ expect(column?.isGenerated)
752
+ .toBe(true);
753
+
754
+ expect(column?.generationStrategy)
755
+ .toBe('uuid');
756
+ }
757
+ finally
758
+ {
759
+ await dataSource.destroy();
760
+ }
761
+ },
762
+ );
763
+
764
+ test(
765
+ 'should initialize with synchronize enabled',
766
+ async () =>
767
+ {
768
+ const dataSource =
769
+ createGoogleSheetsDataSource({
770
+ type: 'google-sheets',
771
+
772
+ client:
773
+ new Memory(),
774
+
775
+ entities: [
776
+ SyncUser,
777
+ ],
778
+
779
+ synchronize: true,
780
+ });
781
+
782
+ try
783
+ {
784
+ await dataSource.initialize();
785
+
786
+ expect(dataSource.isInitialized)
787
+ .toBe(true);
788
+ }
789
+ finally
790
+ {
791
+ if (dataSource.isInitialized)
792
+ {
793
+ await dataSource.destroy();
794
+ }
795
+ }
796
+ },
797
+ );
798
+
799
+ test(
800
+ 'should detect existing worksheet',
801
+ async () =>
802
+ {
803
+ const client =
804
+ new Memory({
805
+ sync_users: [],
806
+ });
807
+
808
+ const dataSource =
809
+ createGoogleSheetsDataSource({
810
+ type: 'google-sheets',
811
+
812
+ client,
813
+
814
+ entities: [
815
+ SyncUser,
816
+ ],
817
+
818
+ synchronize: true,
819
+ });
820
+
821
+ try
822
+ {
823
+ await dataSource.initialize();
824
+
825
+ expect(
826
+ await client.hasSheet('sync_users'),
827
+ ).toBe(true);
828
+ }
829
+ finally
830
+ {
831
+ if (dataSource.isInitialized)
832
+ {
833
+ await dataSource.destroy();
834
+ }
835
+ }
836
+ },
837
+ );
838
+
839
+ test(
840
+ 'should create worksheet with headers',
841
+ async () =>
842
+ {
843
+ const client =
844
+ new Memory();
845
+
846
+ const dataSource =
847
+ createGoogleSheetsDataSource({
848
+ type: 'google-sheets',
849
+
850
+ client,
851
+
852
+ entities: [
853
+ SyncUser,
854
+ ],
855
+
856
+ synchronize: true,
857
+ });
858
+
859
+ try
860
+ {
861
+ await dataSource.initialize();
862
+
863
+ expect(
864
+ await client.hasSheet('sync_users'),
865
+ ).toBe(true);
866
+
867
+ expect(
868
+ await client.getHeaders('sync_users'),
869
+ ).toEqual([
870
+ 'id',
871
+ 'name',
872
+ ]);
873
+ }
874
+ finally
875
+ {
876
+ if (dataSource.isInitialized)
877
+ {
878
+ await dataSource.destroy();
879
+ }
880
+ }
881
+ },
882
+ );
883
+
884
+ test(
885
+ 'should add missing headers to existing worksheet',
886
+ async () =>
887
+ {
888
+ const client =
889
+ new Memory();
890
+
891
+ await client.createSheet(
892
+ 'sync_missing_headers',
893
+ );
894
+
895
+ await client.insertHeaders(
896
+ 'sync_missing_headers',
897
+ [
898
+ 'id',
899
+ 'name',
900
+ ],
901
+ );
902
+
903
+ const dataSource =
904
+ createGoogleSheetsDataSource({
905
+ type: 'google-sheets',
906
+
907
+ client,
908
+
909
+ entities: [
910
+ SyncMissingHeaderUser,
911
+ ],
912
+
913
+ synchronize: true,
914
+ });
915
+
916
+ try
917
+ {
918
+ await dataSource.initialize();
919
+
920
+ expect(
921
+ await client.getHeaders(
922
+ 'sync_missing_headers',
923
+ ),
924
+ ).toEqual([
925
+ 'id',
926
+ 'name',
927
+ 'email',
928
+ ]);
929
+ }
930
+ finally
931
+ {
932
+ if (dataSource.isInitialized)
933
+ {
934
+ await dataSource.destroy();
935
+ }
936
+ }
937
+ },
938
+ );
939
+
940
+ test(
941
+ 'should add headers to empty worksheet',
942
+ async () =>
943
+ {
944
+ const client =
945
+ new Memory();
946
+
947
+ await client.createSheet(
948
+ 'sync_missing_headers',
949
+ );
950
+
951
+ const dataSource =
952
+ createGoogleSheetsDataSource({
953
+ type: 'google-sheets',
954
+
955
+ client,
956
+
957
+ entities: [
958
+ SyncMissingHeaderUser,
959
+ ],
960
+
961
+ synchronize: true,
962
+ });
963
+
964
+ try
965
+ {
966
+ await dataSource.initialize();
967
+
968
+ expect(
969
+ await client.getHeaders(
970
+ 'sync_missing_headers',
971
+ ),
972
+ ).toEqual([
973
+ 'id',
974
+ 'name',
975
+ 'email',
976
+ ]);
977
+ }
978
+ finally
979
+ {
980
+ if (dataSource.isInitialized)
981
+ {
982
+ await dataSource.destroy();
983
+ }
984
+ }
985
+ },
986
+ );
987
+
988
+ test(
989
+ 'should preserve existing data in worksheet',
990
+ async () =>
991
+ {
992
+ const client =
993
+ new Memory({
994
+ sync_missing_headers: [
995
+ {
996
+ id: 1,
997
+ name: 'John',
998
+ email: 'john@example.com',
999
+ },
1000
+ {
1001
+ id: 2,
1002
+ name: 'Jane',
1003
+ email: 'jane@example.com',
1004
+ },
1005
+ ],
1006
+ });
1007
+
1008
+ await client.insertHeaders(
1009
+ 'sync_missing_headers',
1010
+ [
1011
+ 'id',
1012
+ 'name',
1013
+ 'email',
1014
+ ],
1015
+ );
1016
+
1017
+ const dataSource =
1018
+ createGoogleSheetsDataSource({
1019
+ type: 'google-sheets',
1020
+
1021
+ client,
1022
+
1023
+ entities: [
1024
+ SyncMissingHeaderUser,
1025
+ ],
1026
+
1027
+ synchronize: true,
1028
+ });
1029
+
1030
+ try
1031
+ {
1032
+ await dataSource.initialize();
1033
+
1034
+ expect(
1035
+ await client.getRows(
1036
+ 'sync_missing_headers',
1037
+ ),
1038
+ ).toEqual([
1039
+ {
1040
+ id: 1,
1041
+ name: 'John',
1042
+ email: 'john@example.com',
1043
+ },
1044
+ {
1045
+ id: 2,
1046
+ name: 'Jane',
1047
+ email: 'jane@example.com',
1048
+ },
1049
+ ]);
1050
+ }
1051
+ finally
1052
+ {
1053
+ if (dataSource.isInitialized)
1054
+ {
1055
+ await dataSource.destroy();
1056
+ }
1057
+ }
1058
+ },
1059
+ );
1060
+
1061
+ test(
1062
+ 'should apply non-destructive schema changes',
1063
+ async () =>
1064
+ {
1065
+ const client =
1066
+ new Memory({
1067
+ sync_missing_headers: [
1068
+ {
1069
+ id: 1,
1070
+ name: 'John',
1071
+ },
1072
+ ],
1073
+ });
1074
+
1075
+ await client.insertHeaders(
1076
+ 'sync_missing_headers',
1077
+ [
1078
+ 'id',
1079
+ 'name',
1080
+ ],
1081
+ );
1082
+
1083
+ const dataSource =
1084
+ createGoogleSheetsDataSource({
1085
+ type: 'google-sheets',
1086
+
1087
+ client,
1088
+
1089
+ entities: [
1090
+ SyncMissingHeaderUser,
1091
+ ],
1092
+
1093
+ synchronize: true,
1094
+ });
1095
+
1096
+ try
1097
+ {
1098
+ await dataSource.initialize();
1099
+
1100
+ expect(
1101
+ await client.getHeaders(
1102
+ 'sync_missing_headers',
1103
+ ),
1104
+ ).toEqual([
1105
+ 'id',
1106
+ 'name',
1107
+ 'email',
1108
+ ]);
1109
+
1110
+ expect(
1111
+ await client.getRows(
1112
+ 'sync_missing_headers',
1113
+ ),
1114
+ ).toEqual([
1115
+ {
1116
+ id: 1,
1117
+ name: 'John',
1118
+ },
1119
+ ]);
1120
+ }
1121
+ finally
1122
+ {
1123
+ if (dataSource.isInitialized)
1124
+ {
1125
+ await dataSource.destroy();
1126
+ }
1127
+ }
1128
+ },
1129
+ );
1130
+
1131
+ test(
1132
+ 'should synchronize worksheet schema end-to-end',
1133
+ async () =>
1134
+ {
1135
+ const client =
1136
+ new Memory();
1137
+
1138
+ const dataSource =
1139
+ createGoogleSheetsDataSource({
1140
+ type: 'google-sheets',
1141
+
1142
+ client,
1143
+
1144
+ entities: [
1145
+ SyncMissingHeaderUser,
1146
+ ],
1147
+
1148
+ synchronize: true,
1149
+ });
1150
+
1151
+ try
1152
+ {
1153
+ await dataSource.initialize();
1154
+
1155
+ expect(
1156
+ await client.hasSheet(
1157
+ 'sync_missing_headers',
1158
+ ),
1159
+ ).toBe(true);
1160
+
1161
+ expect(
1162
+ await client.getHeaders(
1163
+ 'sync_missing_headers',
1164
+ ),
1165
+ ).toEqual([
1166
+ 'id',
1167
+ 'name',
1168
+ 'email',
1169
+ ]);
1170
+
1171
+ expect(
1172
+ await client.getRows(
1173
+ 'sync_missing_headers',
1174
+ ),
1175
+ ).toEqual([]);
1176
+ }
1177
+ finally
1178
+ {
1179
+ if (dataSource.isInitialized)
1180
+ {
1181
+ await dataSource.destroy();
1182
+ }
1183
+ }
1184
+ },
1185
+ );
1186
+
1187
+ test(
1188
+ 'rejects duplicate primary key',
1189
+ async () =>
1190
+ {
1191
+ const client =
1192
+ new Memory({
1193
+ generated_users: [
1194
+ {
1195
+ id: 1,
1196
+ name: 'Basuni',
1197
+ },
1198
+ ],
1199
+ });
1200
+
1201
+ const dataSource =
1202
+ createGoogleSheetsDataSource({
1203
+ type: 'google-sheets',
1204
+
1205
+ client,
1206
+
1207
+ entities: [
1208
+ GeneratedUser,
1209
+ ],
1210
+ });
1211
+
1212
+ await dataSource.initialize();
1213
+
1214
+ try
1215
+ {
1216
+ const queryRunner =
1217
+ dataSource.createQueryRunner();
1218
+
1219
+ await queryRunner.connect();
1220
+
1221
+ try
1222
+ {
1223
+ await expect(
1224
+ queryRunner.query(
1225
+ 'INSERT INTO "generated_users"("id","name") VALUES (:orm_param_0,:orm_param_1)',
1226
+ [
1227
+ 1,
1228
+ 'Budi',
1229
+ ],
1230
+ ),
1231
+ ).rejects.toThrow(
1232
+ 'Duplicate primary key',
1233
+ );
1234
+ }
1235
+ finally
1236
+ {
1237
+ await queryRunner.release();
1238
+ }
1239
+ }
1240
+ finally
1241
+ {
1242
+ await dataSource.destroy();
1243
+ }
1244
+ },
1245
+ );
1246
+
1247
+ test(
1248
+ 'rejects missing primary key',
1249
+ async () =>
1250
+ {
1251
+ const client =
1252
+ new Memory({
1253
+ required_users: [],
1254
+ });
1255
+
1256
+ const dataSource =
1257
+ createGoogleSheetsDataSource({
1258
+ type: 'google-sheets',
1259
+ client,
1260
+ entities: [
1261
+ RequiredUser,
1262
+ ],
1263
+ });
1264
+
1265
+ await dataSource.initialize();
1266
+
1267
+ try
1268
+ {
1269
+ const queryRunner =
1270
+ dataSource.createQueryRunner();
1271
+
1272
+ await queryRunner.connect();
1273
+
1274
+ try
1275
+ {
1276
+ await expect(
1277
+ queryRunner.query(
1278
+ 'INSERT INTO "required_users"("name") VALUES (:orm_param_0)',
1279
+ [
1280
+ 'Budi',
1281
+ ],
1282
+ ),
1283
+ ).rejects.toThrow(
1284
+ 'Missing primary key',
1285
+ );
1286
+ }
1287
+ finally
1288
+ {
1289
+ await queryRunner.release();
1290
+ }
1291
+ }
1292
+ finally
1293
+ {
1294
+ await dataSource.destroy();
1295
+ }
1296
+ },
1297
+ );
1298
+
1299
+ test(
1300
+ 'rejects unsupported operation',
1301
+ async () =>
1302
+ {
1303
+ const client =
1304
+ new Memory({
1305
+ generated_users: [],
1306
+ });
1307
+
1308
+ const dataSource =
1309
+ createGoogleSheetsDataSource({
1310
+ type: 'google-sheets',
1311
+ client,
1312
+ entities: [
1313
+ GeneratedUser,
1314
+ ],
1315
+ });
1316
+
1317
+ await dataSource.initialize();
1318
+
1319
+ try
1320
+ {
1321
+ const queryRunner =
1322
+ dataSource.createQueryRunner();
1323
+
1324
+ await queryRunner.connect();
1325
+
1326
+ try
1327
+ {
1328
+ await expect(
1329
+ queryRunner.query(
1330
+ 'TRUNCATE TABLE "generated_users"',
1331
+ ),
1332
+ ).rejects.toThrow();
1333
+ }
1334
+ finally
1335
+ {
1336
+ await queryRunner.release();
1337
+ }
1338
+ }
1339
+ finally
1340
+ {
1341
+ await dataSource.destroy();
1342
+ }
1343
+ },
1344
+ );
1345
+
1346
+ test(
1347
+ 'should execute BeforeInsert lifecycle hook',
1348
+ async () =>
1349
+ {
1350
+ const client =
1351
+ new Memory({
1352
+ lifecycle_users: [],
1353
+ });
1354
+
1355
+ const dataSource =
1356
+ createGoogleSheetsDataSource({
1357
+ type: 'google-sheets',
1358
+ client,
1359
+ entities: [
1360
+ LifecycleUser,
1361
+ ],
1362
+ });
1363
+
1364
+ await dataSource.initialize();
1365
+
1366
+ try
1367
+ {
1368
+ const repository =
1369
+ dataSource.getRepository(
1370
+ LifecycleUser,
1371
+ );
1372
+
1373
+ const entity =
1374
+ repository.create({
1375
+ name: 'Budi',
1376
+ });
1377
+
1378
+ await repository.save(entity);
1379
+
1380
+ expect(
1381
+ entity.name,
1382
+ ).toBe('BUDI');
1383
+
1384
+ expect(
1385
+ await repository.find(),
1386
+ ).toEqual([
1387
+ expect.objectContaining({
1388
+ name: 'BUDI-loaded',
1389
+ }),
1390
+ ]);
1391
+ }
1392
+ finally
1393
+ {
1394
+ await dataSource.destroy();
1395
+ }
1396
+ },
1397
+ );
1398
+
1399
+ test(
1400
+ 'should execute AfterInsert lifecycle hook',
1401
+ async () =>
1402
+ {
1403
+ const client =
1404
+ new Memory({
1405
+ lifecycle_users: [],
1406
+ });
1407
+
1408
+ const dataSource =
1409
+ createGoogleSheetsDataSource({
1410
+ type: 'google-sheets',
1411
+ client,
1412
+ entities: [
1413
+ LifecycleUser2,
1414
+ ],
1415
+ });
1416
+
1417
+ await dataSource.initialize();
1418
+
1419
+ try
1420
+ {
1421
+ const repository =
1422
+ dataSource.getRepository(
1423
+ LifecycleUser2,
1424
+ );
1425
+
1426
+ const entity =
1427
+ repository.create({
1428
+ name: 'Budi',
1429
+ });
1430
+
1431
+ await repository.save(entity);
1432
+
1433
+ expect(
1434
+ entity.name,
1435
+ ).toBe('Budi-after');
1436
+ }
1437
+ finally
1438
+ {
1439
+ await dataSource.destroy();
1440
+ }
1441
+ },
1442
+ );
1443
+
1444
+ test(
1445
+ 'should execute BeforeUpdate lifecycle hook',
1446
+ async () =>
1447
+ {
1448
+ const client =
1449
+ new Memory({
1450
+ lifecycle_users: [
1451
+ {
1452
+ id: 1,
1453
+ name: 'Budi',
1454
+ },
1455
+ ],
1456
+ });
1457
+
1458
+ const dataSource =
1459
+ createGoogleSheetsDataSource({
1460
+ type: 'google-sheets',
1461
+ client,
1462
+ entities: [
1463
+ LifecycleUser,
1464
+ ],
1465
+ });
1466
+
1467
+ await dataSource.initialize();
1468
+
1469
+ try
1470
+ {
1471
+ const repository =
1472
+ dataSource.getRepository(
1473
+ LifecycleUser,
1474
+ );
1475
+
1476
+ const entity =
1477
+ await repository.findOneBy({
1478
+ id: 1,
1479
+ });
1480
+
1481
+ expect(entity).toBeDefined();
1482
+
1483
+ entity!.name = 'andi';
1484
+
1485
+ await repository.save(entity!);
1486
+
1487
+ expect(entity!.name).toBe('ANDI');
1488
+
1489
+ expect(
1490
+ await repository.find(),
1491
+ ).toEqual([
1492
+ expect.objectContaining({
1493
+ name: 'ANDI-loaded',
1494
+ }),
1495
+ ]);
1496
+ }
1497
+ finally
1498
+ {
1499
+ await dataSource.destroy();
1500
+ }
1501
+ },
1502
+ );
1503
+
1504
+ test(
1505
+ 'should execute AfterUpdate lifecycle hook',
1506
+ async () =>
1507
+ {
1508
+ const client =
1509
+ new Memory({
1510
+ lifecycle_users: [
1511
+ {
1512
+ id: 1,
1513
+ name: 'Budi',
1514
+ },
1515
+ ],
1516
+ });
1517
+
1518
+ const dataSource =
1519
+ createGoogleSheetsDataSource({
1520
+ type: 'google-sheets',
1521
+ client,
1522
+ entities: [
1523
+ LifecycleUser2,
1524
+ ],
1525
+ });
1526
+
1527
+ await dataSource.initialize();
1528
+
1529
+ try
1530
+ {
1531
+ const repository =
1532
+ dataSource.getRepository(
1533
+ LifecycleUser2,
1534
+ );
1535
+
1536
+ const entity =
1537
+ repository.create({
1538
+ id: 1,
1539
+ name: 'andi',
1540
+ });
1541
+
1542
+ await repository.save(entity);
1543
+
1544
+ expect(entity.name).toBe('andi-after');
1545
+ }
1546
+ finally
1547
+ {
1548
+ await dataSource.destroy();
1549
+ }
1550
+ },
1551
+ );
1552
+
1553
+ test(
1554
+ 'should execute BeforeRemove lifecycle hook',
1555
+ async () =>
1556
+ {
1557
+ const client =
1558
+ new Memory({
1559
+ lifecycle_users: [
1560
+ {
1561
+ id: 1,
1562
+ name: 'Budi',
1563
+ },
1564
+ ],
1565
+ });
1566
+
1567
+ const dataSource =
1568
+ createGoogleSheetsDataSource({
1569
+ type: 'google-sheets',
1570
+ client,
1571
+ entities: [
1572
+ LifecycleUser,
1573
+ ],
1574
+ });
1575
+
1576
+ await dataSource.initialize();
1577
+
1578
+ try
1579
+ {
1580
+ const repository =
1581
+ dataSource.getRepository(
1582
+ LifecycleUser,
1583
+ );
1584
+
1585
+ const entity =
1586
+ repository.create({
1587
+ id: 1,
1588
+ name: 'Budi',
1589
+ });
1590
+
1591
+ await repository.remove(entity);
1592
+
1593
+ expect(
1594
+ entity.name,
1595
+ ).toBe('Budi-before');
1596
+ }
1597
+ finally
1598
+ {
1599
+ await dataSource.destroy();
1600
+ }
1601
+ },
1602
+ );
1603
+
1604
+ test(
1605
+ 'should execute AfterRemove lifecycle hook',
1606
+ async () =>
1607
+ {
1608
+ const client =
1609
+ new Memory({
1610
+ lifecycle_users: [
1611
+ {
1612
+ id: 1,
1613
+ name: 'Budi',
1614
+ },
1615
+ ],
1616
+ });
1617
+
1618
+ const dataSource =
1619
+ createGoogleSheetsDataSource({
1620
+ type: 'google-sheets',
1621
+ client,
1622
+ entities: [
1623
+ LifecycleUser2,
1624
+ ],
1625
+ });
1626
+
1627
+ await dataSource.initialize();
1628
+
1629
+ try
1630
+ {
1631
+ const repository =
1632
+ dataSource.getRepository(
1633
+ LifecycleUser2,
1634
+ );
1635
+
1636
+ const entity =
1637
+ repository.create({
1638
+ id: 1,
1639
+ name: 'Budi',
1640
+ });
1641
+
1642
+ await repository.remove(entity);
1643
+
1644
+ expect(
1645
+ entity.name,
1646
+ ).toBe('Budi-after');
1647
+ }
1648
+ finally
1649
+ {
1650
+ await dataSource.destroy();
1651
+ }
1652
+ },
1653
+ );
1654
+
1655
+ test(
1656
+ 'should execute subscriber beforeInsert hook',
1657
+ async () =>
1658
+ {
1659
+ const client =
1660
+ new Memory({
1661
+ lifecycle_users: [],
1662
+ });
1663
+
1664
+ const dataSource =
1665
+ createGoogleSheetsDataSource({
1666
+ type: 'google-sheets',
1667
+ client,
1668
+ entities: [
1669
+ LifecycleUser,
1670
+ ],
1671
+ subscribers: [
1672
+ LifecycleUserSubscriber,
1673
+ ],
1674
+ });
1675
+
1676
+ await dataSource.initialize();
1677
+
1678
+ try
1679
+ {
1680
+ const repository =
1681
+ dataSource.getRepository(
1682
+ LifecycleUser,
1683
+ );
1684
+
1685
+ const entity =
1686
+ repository.create({
1687
+ name: 'Budi',
1688
+ });
1689
+
1690
+ await repository.save(entity);
1691
+
1692
+ expect(entity.name).toBe(
1693
+ 'BUDI-subscriber-before-subscriber-after',
1694
+ );
1695
+ }
1696
+ finally
1697
+ {
1698
+ await dataSource.destroy();
1699
+ }
1700
+ },
1701
+ );
1702
+
1703
+ test(
1704
+ 'should execute subscriber afterInsert hook',
1705
+ async () =>
1706
+ {
1707
+ const client =
1708
+ new Memory({
1709
+ lifecycle_users: [],
1710
+ });
1711
+
1712
+ const dataSource =
1713
+ createGoogleSheetsDataSource({
1714
+ type: 'google-sheets',
1715
+ client,
1716
+ entities: [
1717
+ LifecycleUser,
1718
+ ],
1719
+ subscribers: [
1720
+ LifecycleUserSubscriber,
1721
+ ],
1722
+ });
1723
+
1724
+ await dataSource.initialize();
1725
+
1726
+ try
1727
+ {
1728
+ const repository =
1729
+ dataSource.getRepository(
1730
+ LifecycleUser,
1731
+ );
1732
+
1733
+ const entity =
1734
+ repository.create({
1735
+ name: 'Budi',
1736
+ });
1737
+
1738
+ await repository.save(entity);
1739
+
1740
+ expect(entity.name).toBe(
1741
+ 'BUDI-subscriber-before-subscriber-after',
1742
+ );
1743
+ }
1744
+ finally
1745
+ {
1746
+ await dataSource.destroy();
1747
+ }
1748
+ },
1749
+ );
1750
+
1751
+ test(
1752
+ 'should apply naming strategy to worksheet and columns',
1753
+ async () =>
1754
+ {
1755
+ const client =
1756
+ new Memory();
1757
+
1758
+ const dataSource =
1759
+ createGoogleSheetsDataSource({
1760
+ type: 'google-sheets',
1761
+ client,
1762
+ entities: [
1763
+ NamingStrategyUser,
1764
+ ],
1765
+ synchronize: true,
1766
+ namingStrategy:
1767
+ new TestNamingStrategy(),
1768
+ } as GoogleSheetsDataSourceOptions & {
1769
+ namingStrategy: TestNamingStrategy;
1770
+ });
1771
+
1772
+ await dataSource.initialize();
1773
+
1774
+ try
1775
+ {
1776
+ const metadata =
1777
+ dataSource.getMetadata(
1778
+ NamingStrategyUser,
1779
+ );
1780
+
1781
+ expect(metadata.tableName).toBe(
1782
+ 'sheet_namingstrategyuser',
1783
+ );
1784
+
1785
+ expect(
1786
+ metadata.findColumnWithPropertyName(
1787
+ 'displayName',
1788
+ )?.databaseName,
1789
+ ).toBe(
1790
+ 'col_displayname',
1791
+ );
1792
+
1793
+ expect(
1794
+ await client.hasSheet(
1795
+ 'sheet_namingstrategyuser',
1796
+ ),
1797
+ ).toBe(true);
1798
+
1799
+ expect(
1800
+ await client.getHeaders(
1801
+ 'sheet_namingstrategyuser',
1802
+ ),
1803
+ ).toEqual([
1804
+ 'col_id',
1805
+ 'col_displayname',
1806
+ ]);
1807
+ }
1808
+ finally
1809
+ {
1810
+ await dataSource.destroy();
1811
+ }
1812
+ },
1813
+ );
1814
+
1815
+ test(
1816
+ 'should resolve relation metadata',
1817
+ async () =>
1818
+ {
1819
+ const client =
1820
+ new Memory({
1821
+ relation_users: [],
1822
+ relation_posts: [],
1823
+ });
1824
+
1825
+ const dataSource =
1826
+ createGoogleSheetsDataSource({
1827
+ type: 'google-sheets',
1828
+ client,
1829
+ entities: [
1830
+ RelationUser,
1831
+ RelationPost,
1832
+ ],
1833
+ synchronize: true,
1834
+ });
1835
+
1836
+ await dataSource.initialize();
1837
+
1838
+ try
1839
+ {
1840
+ const metadata =
1841
+ dataSource.getMetadata(
1842
+ RelationPost,
1843
+ );
1844
+
1845
+ const relation =
1846
+ metadata.relations.find(
1847
+ (item) =>
1848
+ item.propertyName === 'author',
1849
+ );
1850
+
1851
+ expect(relation).toBeDefined();
1852
+
1853
+ expect(
1854
+ relation!.isManyToOne,
1855
+ ).toBe(true);
1856
+
1857
+ expect(
1858
+ relation!.inverseEntityMetadata.target,
1859
+ ).toBe(RelationUser);
1860
+
1861
+ expect(
1862
+ relation!.inverseRelation,
1863
+ ).toBeUndefined();
1864
+ }
1865
+ finally
1866
+ {
1867
+ await dataSource.destroy();
1868
+ }
1869
+ },
1870
+ );
1871
+
1872
+ test(
1873
+ 'should load many-to-one relation',
1874
+ async () =>
1875
+ {
1876
+ const client =
1877
+ new Memory({
1878
+ relation_users: [
1879
+ {
1880
+ id: 1,
1881
+ name: 'Budi',
1882
+ },
1883
+ ],
1884
+ relation_posts: [
1885
+ {
1886
+ id: 1,
1887
+ title: 'Post Budi',
1888
+ authorId: 1,
1889
+ },
1890
+ ],
1891
+ });
1892
+
1893
+ const dataSource =
1894
+ createGoogleSheetsDataSource({
1895
+ type: 'google-sheets',
1896
+ client,
1897
+ entities: [
1898
+ RelationUser,
1899
+ RelationPost,
1900
+ ],
1901
+ });
1902
+
1903
+ await dataSource.initialize();
1904
+
1905
+ try
1906
+ {
1907
+ const repository =
1908
+ dataSource.getRepository(
1909
+ RelationPost,
1910
+ );
1911
+
1912
+ const post =
1913
+ await repository.findOne({
1914
+ where: {
1915
+ id: 1,
1916
+ },
1917
+ relations: {
1918
+ author: true,
1919
+ },
1920
+ });
1921
+
1922
+ expect(post).toBeDefined();
1923
+ expect(post!.title).toBe('Post Budi');
1924
+ expect(post!.author).toBeDefined();
1925
+ expect(post!.author.id).toBe(1);
1926
+ expect(post!.author.name).toBe('Budi');
1927
+ }
1928
+ finally
1929
+ {
1930
+ await dataSource.destroy();
1931
+ }
1932
+ },
1933
+ );
1934
+ },
1935
+ );