orchid-orm 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1219 @@
1
+ import { db, pgConfig } from '../test-utils/test-db';
2
+ import {
3
+ assertType,
4
+ chatData,
5
+ expectSql,
6
+ profileData,
7
+ userData,
8
+ useTestDatabase,
9
+ } from '../test-utils/test-utils';
10
+ import { User, Profile, Model } from '../test-utils/test-models';
11
+ import { RelationQuery } from 'pqb';
12
+ import { orchidORM } from '../orm';
13
+
14
+ describe('hasOne', () => {
15
+ useTestDatabase();
16
+
17
+ describe('querying', () => {
18
+ it('should have method to query related data', async () => {
19
+ const profileQuery = db.profile.take();
20
+
21
+ assertType<
22
+ typeof db.user.profile,
23
+ RelationQuery<
24
+ 'profile',
25
+ { id: number },
26
+ 'userId',
27
+ typeof profileQuery,
28
+ true,
29
+ true,
30
+ true
31
+ >
32
+ >();
33
+
34
+ const userId = await db.user.get('id').create(userData);
35
+
36
+ await db.profile.create({ ...profileData, userId });
37
+
38
+ const user = await db.user.find(userId);
39
+ const query = db.user.profile(user);
40
+
41
+ expectSql(
42
+ query.toSql(),
43
+ `
44
+ SELECT * FROM "profile"
45
+ WHERE "profile"."userId" = $1
46
+ LIMIT $2
47
+ `,
48
+ [userId, 1],
49
+ );
50
+
51
+ const profile = await query;
52
+
53
+ expect(profile).toMatchObject(profileData);
54
+ });
55
+
56
+ it('should handle chained query', () => {
57
+ const query = db.user
58
+ .where({ name: 'name' })
59
+ .profile.where({ bio: 'bio' });
60
+
61
+ expectSql(
62
+ query.toSql(),
63
+ `
64
+ SELECT * FROM "profile"
65
+ WHERE EXISTS (
66
+ SELECT 1 FROM "user"
67
+ WHERE "user"."name" = $1
68
+ AND "user"."id" = "profile"."userId"
69
+ LIMIT 1
70
+ )
71
+ AND "profile"."bio" = $2
72
+ LIMIT $3
73
+ `,
74
+ ['name', 'bio', 1],
75
+ );
76
+ });
77
+
78
+ it('should have create with defaults of provided id', () => {
79
+ const user = { id: 1 };
80
+ const now = new Date();
81
+
82
+ const query = db.user.profile(user).count().create({
83
+ bio: 'bio',
84
+ updatedAt: now,
85
+ createdAt: now,
86
+ });
87
+
88
+ expectSql(
89
+ query.toSql(),
90
+ `
91
+ INSERT INTO "profile"("userId", "bio", "updatedAt", "createdAt")
92
+ VALUES ($1, $2, $3, $4)
93
+ `,
94
+ [1, 'bio', now, now],
95
+ );
96
+ });
97
+
98
+ it('can create after calling method', async () => {
99
+ const id = await db.user.get('id').create(userData);
100
+ const now = new Date();
101
+ await db.user.profile({ id }).create({
102
+ userId: id,
103
+ bio: 'bio',
104
+ updatedAt: now,
105
+ createdAt: now,
106
+ });
107
+ });
108
+
109
+ describe('chained create', () => {
110
+ it('should have create based on find query', () => {
111
+ const query = db.user.find(1).profile.create({
112
+ bio: 'bio',
113
+ });
114
+
115
+ expectSql(
116
+ query.toSql(),
117
+ `
118
+ INSERT INTO "profile"("userId", "bio")
119
+ SELECT "user"."id" AS "userId", $1
120
+ FROM "user"
121
+ WHERE "user"."id" = $2
122
+ LIMIT $3
123
+ RETURNING *
124
+ `,
125
+ ['bio', 1, 1],
126
+ );
127
+ });
128
+
129
+ it('should throw when the main query returns many records', async () => {
130
+ await expect(
131
+ async () =>
132
+ await db.user.profile.create({
133
+ bio: 'bio',
134
+ }),
135
+ ).rejects.toThrow(
136
+ 'Cannot create based on a query which returns multiple records',
137
+ );
138
+ });
139
+
140
+ it('should throw when main record is not found', async () => {
141
+ await expect(
142
+ async () =>
143
+ await db.user.find(1).profile.create({
144
+ bio: 'bio',
145
+ }),
146
+ ).rejects.toThrow('Record is not found');
147
+ });
148
+
149
+ it('should not throw when searching with findOptional', async () => {
150
+ await db.user.findOptional(1).profile.takeOptional().create({
151
+ bio: 'bio',
152
+ });
153
+ });
154
+ });
155
+
156
+ describe('chained delete', () => {
157
+ it('should delete relation records', () => {
158
+ const query = db.user
159
+ .where({ name: 'name' })
160
+ .profile.where({ bio: 'bio' })
161
+ .delete();
162
+
163
+ expectSql(
164
+ query.toSql(),
165
+ `
166
+ DELETE FROM "profile"
167
+ WHERE EXISTS (
168
+ SELECT 1 FROM "user"
169
+ WHERE "user"."name" = $1
170
+ AND "user"."id" = "profile"."userId"
171
+ LIMIT 1
172
+ )
173
+ AND "profile"."bio" = $2
174
+ `,
175
+ ['name', 'bio'],
176
+ );
177
+ });
178
+ });
179
+
180
+ it('should have proper joinQuery', () => {
181
+ expectSql(
182
+ db.user.relations.profile
183
+ .joinQuery(db.user.as('u'), db.profile.as('p'))
184
+ .toSql(),
185
+ `
186
+ SELECT * FROM "profile" AS "p"
187
+ WHERE "p"."userId" = "u"."id"
188
+ `,
189
+ );
190
+ });
191
+
192
+ it('should be supported in whereExists', () => {
193
+ expectSql(
194
+ db.user.as('u').whereExists('profile').toSql(),
195
+ `
196
+ SELECT * FROM "user" AS "u"
197
+ WHERE EXISTS (
198
+ SELECT 1 FROM "profile"
199
+ WHERE "profile"."userId" = "u"."id"
200
+ LIMIT 1
201
+ )
202
+ `,
203
+ );
204
+
205
+ expectSql(
206
+ db.user
207
+ .as('u')
208
+ .whereExists('profile', (q) => q.where({ bio: 'bio' }))
209
+ .toSql(),
210
+ `
211
+ SELECT * FROM "user" AS "u"
212
+ WHERE EXISTS (
213
+ SELECT 1 FROM "profile"
214
+ WHERE "profile"."userId" = "u"."id"
215
+ AND "profile"."bio" = $1
216
+ LIMIT 1
217
+ )
218
+ `,
219
+ ['bio'],
220
+ );
221
+ });
222
+
223
+ it('should be supported in join', () => {
224
+ const query = db.user
225
+ .as('u')
226
+ .join('profile', (q) => q.where({ bio: 'bio' }))
227
+ .select('name', 'profile.bio');
228
+
229
+ assertType<
230
+ Awaited<typeof query>,
231
+ { name: string; bio: string | null }[]
232
+ >();
233
+
234
+ expectSql(
235
+ query.toSql(),
236
+ `
237
+ SELECT "u"."name", "profile"."bio" FROM "user" AS "u"
238
+ JOIN "profile"
239
+ ON "profile"."userId" = "u"."id"
240
+ AND "profile"."bio" = $1
241
+ `,
242
+ ['bio'],
243
+ );
244
+ });
245
+
246
+ describe('select', () => {
247
+ it('should be selectable', () => {
248
+ const query = db.user.as('u').select('id', {
249
+ profile: (q) => q.profile.where({ bio: 'bio' }),
250
+ });
251
+
252
+ assertType<Awaited<typeof query>, { id: number; profile: Profile }[]>();
253
+
254
+ expectSql(
255
+ query.toSql(),
256
+ `
257
+ SELECT
258
+ "u"."id",
259
+ (
260
+ SELECT row_to_json("t".*)
261
+ FROM (
262
+ SELECT * FROM "profile"
263
+ WHERE "profile"."bio" = $1
264
+ AND "profile"."userId" = "u"."id"
265
+ LIMIT $2
266
+ ) AS "t"
267
+ ) AS "profile"
268
+ FROM "user" AS "u"
269
+ `,
270
+ ['bio', 1],
271
+ );
272
+ });
273
+
274
+ it('should be selectable by relation name', () => {
275
+ const query = db.user.select('id', 'profile');
276
+
277
+ assertType<Awaited<typeof query>, { id: number; profile: Profile }[]>();
278
+
279
+ expectSql(
280
+ query.toSql(),
281
+ `
282
+ SELECT
283
+ "user"."id",
284
+ (
285
+ SELECT row_to_json("t".*)
286
+ FROM (
287
+ SELECT * FROM "profile"
288
+ WHERE "profile"."userId" = "user"."id"
289
+ LIMIT $1
290
+ ) AS "t"
291
+ ) AS "profile"
292
+ FROM "user"
293
+ `,
294
+ [1],
295
+ );
296
+ });
297
+
298
+ it('should handle exists sub query', () => {
299
+ const query = db.user.as('u').select('id', {
300
+ hasProfile: (q) => q.profile.exists(),
301
+ });
302
+
303
+ assertType<
304
+ Awaited<typeof query>,
305
+ { id: number; hasProfile: boolean }[]
306
+ >();
307
+
308
+ expectSql(
309
+ query.toSql(),
310
+ `
311
+ SELECT
312
+ "u"."id",
313
+ COALESCE((
314
+ SELECT true
315
+ FROM "profile"
316
+ WHERE "profile"."userId" = "u"."id"
317
+ ), false) AS "hasProfile"
318
+ FROM "user" AS "u"
319
+ `,
320
+ );
321
+ });
322
+ });
323
+
324
+ describe('create', () => {
325
+ const checkUserAndProfile = ({
326
+ user,
327
+ profile,
328
+ name,
329
+ bio,
330
+ }: {
331
+ user: User;
332
+ profile: Profile;
333
+ name: string;
334
+ bio: string;
335
+ }) => {
336
+ expect(user).toEqual({
337
+ ...userData,
338
+ id: user.id,
339
+ name,
340
+ active: null,
341
+ age: null,
342
+ data: null,
343
+ picture: null,
344
+ });
345
+
346
+ expect(profile).toMatchObject({
347
+ id: profile.id,
348
+ bio,
349
+ userId: user.id,
350
+ });
351
+ };
352
+
353
+ describe('nested create', () => {
354
+ it('should support create', async () => {
355
+ const query = db.user.create({
356
+ ...userData,
357
+ name: 'user',
358
+ profile: {
359
+ create: {
360
+ ...profileData,
361
+ bio: 'profile',
362
+ },
363
+ },
364
+ });
365
+
366
+ const user = await query;
367
+ const profile = await db.profile.findBy({ userId: user.id });
368
+
369
+ checkUserAndProfile({ user, profile, name: 'user', bio: 'profile' });
370
+ });
371
+
372
+ it('should support create many', async () => {
373
+ const query = db.user.createMany([
374
+ {
375
+ ...userData,
376
+ name: 'user 1',
377
+ profile: {
378
+ create: {
379
+ ...profileData,
380
+ bio: 'profile 1',
381
+ },
382
+ },
383
+ },
384
+ {
385
+ ...userData,
386
+ name: 'user 2',
387
+ profile: {
388
+ create: {
389
+ ...profileData,
390
+ bio: 'profile 2',
391
+ },
392
+ },
393
+ },
394
+ ]);
395
+
396
+ const users = await query;
397
+ const profiles = await db.profile
398
+ .where({
399
+ userId: { in: users.map((user) => user.id) },
400
+ })
401
+ .order('id');
402
+
403
+ checkUserAndProfile({
404
+ user: users[0],
405
+ profile: profiles[0],
406
+ name: 'user 1',
407
+ bio: 'profile 1',
408
+ });
409
+
410
+ checkUserAndProfile({
411
+ user: users[1],
412
+ profile: profiles[1],
413
+ name: 'user 2',
414
+ bio: 'profile 2',
415
+ });
416
+ });
417
+ });
418
+
419
+ describe('nested connect', () => {
420
+ it('should support connect', async () => {
421
+ await db.profile.create({
422
+ ...profileData,
423
+ bio: 'profile',
424
+ user: {
425
+ create: {
426
+ ...userData,
427
+ name: 'tmp',
428
+ },
429
+ },
430
+ });
431
+
432
+ const query = db.user.create({
433
+ ...userData,
434
+ name: 'user',
435
+ profile: {
436
+ connect: { bio: 'profile' },
437
+ },
438
+ });
439
+
440
+ const user = await query;
441
+ const profile = await db.user.profile(user);
442
+
443
+ checkUserAndProfile({ user, profile, name: 'user', bio: 'profile' });
444
+ });
445
+
446
+ it('should support connect many', async () => {
447
+ await db.profile.createMany([
448
+ {
449
+ ...profileData,
450
+ bio: 'profile 1',
451
+ user: {
452
+ create: {
453
+ ...userData,
454
+ name: 'tmp',
455
+ },
456
+ },
457
+ },
458
+ {
459
+ ...profileData,
460
+ bio: 'profile 2',
461
+ user: {
462
+ connect: { name: 'tmp' },
463
+ },
464
+ },
465
+ ]);
466
+
467
+ const query = db.user.createMany([
468
+ {
469
+ ...userData,
470
+ name: 'user 1',
471
+ profile: {
472
+ connect: { bio: 'profile 1' },
473
+ },
474
+ },
475
+ {
476
+ ...userData,
477
+ name: 'user 2',
478
+ profile: {
479
+ connect: { bio: 'profile 2' },
480
+ },
481
+ },
482
+ ]);
483
+
484
+ const users = await query;
485
+ const profiles = await db.profile
486
+ .where({
487
+ userId: { in: users.map((user) => user.id) },
488
+ })
489
+ .order('id');
490
+
491
+ checkUserAndProfile({
492
+ user: users[0],
493
+ profile: profiles[0],
494
+ name: 'user 1',
495
+ bio: 'profile 1',
496
+ });
497
+
498
+ checkUserAndProfile({
499
+ user: users[1],
500
+ profile: profiles[1],
501
+ name: 'user 2',
502
+ bio: 'profile 2',
503
+ });
504
+ });
505
+ });
506
+
507
+ describe('connect or create', () => {
508
+ it('should support connect or create', async () => {
509
+ const profileId = await db.profile.get('id').create({
510
+ ...profileData,
511
+ bio: 'profile 1',
512
+ user: {
513
+ create: {
514
+ ...userData,
515
+ name: 'tmp',
516
+ },
517
+ },
518
+ });
519
+
520
+ const user1 = await db.user.create({
521
+ ...userData,
522
+ name: 'user 1',
523
+ profile: {
524
+ connectOrCreate: {
525
+ where: { bio: 'profile 1' },
526
+ create: { ...profileData, bio: 'profile 1' },
527
+ },
528
+ },
529
+ });
530
+
531
+ const user2 = await db.user.create({
532
+ ...userData,
533
+ name: 'user 2',
534
+ profile: {
535
+ connectOrCreate: {
536
+ where: { bio: 'profile 2' },
537
+ create: { ...profileData, bio: 'profile 2' },
538
+ },
539
+ },
540
+ });
541
+
542
+ const profile1 = await db.user.profile(user1);
543
+ expect(profile1.id).toBe(profileId);
544
+ checkUserAndProfile({
545
+ user: user1,
546
+ profile: profile1,
547
+ name: 'user 1',
548
+ bio: 'profile 1',
549
+ });
550
+
551
+ const profile2 = await db.user.profile(user2);
552
+ checkUserAndProfile({
553
+ user: user2,
554
+ profile: profile2,
555
+ name: 'user 2',
556
+ bio: 'profile 2',
557
+ });
558
+ });
559
+
560
+ it('should support connect or create many', async () => {
561
+ const profileId = await db.profile.get('id').create({
562
+ ...profileData,
563
+ bio: 'profile 1',
564
+ user: {
565
+ create: {
566
+ ...userData,
567
+ name: 'tmp',
568
+ },
569
+ },
570
+ });
571
+
572
+ const [user1, user2] = await db.user.createMany([
573
+ {
574
+ ...userData,
575
+ name: 'user 1',
576
+ profile: {
577
+ connectOrCreate: {
578
+ where: { bio: 'profile 1' },
579
+ create: { ...profileData, bio: 'profile 1' },
580
+ },
581
+ },
582
+ },
583
+ {
584
+ ...userData,
585
+ name: 'user 2',
586
+ profile: {
587
+ connectOrCreate: {
588
+ where: { bio: 'profile 2' },
589
+ create: { ...profileData, bio: 'profile 2' },
590
+ },
591
+ },
592
+ },
593
+ ]);
594
+
595
+ const profile1 = await db.user.profile(user1);
596
+ expect(profile1.id).toBe(profileId);
597
+ checkUserAndProfile({
598
+ user: user1,
599
+ profile: profile1,
600
+ name: 'user 1',
601
+ bio: 'profile 1',
602
+ });
603
+
604
+ const profile2 = await db.user.profile(user2);
605
+ checkUserAndProfile({
606
+ user: user2,
607
+ profile: profile2,
608
+ name: 'user 2',
609
+ bio: 'profile 2',
610
+ });
611
+ });
612
+ });
613
+ });
614
+
615
+ describe('update', () => {
616
+ describe('disconnect', () => {
617
+ it('should nullify foreignKey', async () => {
618
+ const user = await db.user
619
+ .selectAll()
620
+ .create({ ...userData, profile: { create: profileData } });
621
+ const { id: profileId } = await db.user.profile(user);
622
+
623
+ const id = await db.user
624
+ .get('id')
625
+ .where(user)
626
+ .update({
627
+ profile: {
628
+ disconnect: true,
629
+ },
630
+ });
631
+
632
+ expect(id).toBe(user.id);
633
+
634
+ const profile = await db.profile.find(profileId);
635
+ expect(profile.userId).toBe(null);
636
+ });
637
+
638
+ it('should nullify foreignKey in batch update', async () => {
639
+ const userIds = await db.user.pluck('id').createMany([
640
+ { ...userData, profile: { create: profileData } },
641
+ { ...userData, profile: { create: profileData } },
642
+ ]);
643
+
644
+ const profileIds = await db.profile.pluck('id').where({
645
+ userId: { in: userIds },
646
+ });
647
+
648
+ await db.user.where({ id: { in: userIds } }).update({
649
+ profile: {
650
+ disconnect: true,
651
+ },
652
+ });
653
+
654
+ const updatedUserIds = await db.profile
655
+ .pluck('userId')
656
+ .where({ id: { in: profileIds } });
657
+ expect(updatedUserIds).toEqual([null, null]);
658
+ });
659
+ });
660
+
661
+ describe('set', () => {
662
+ it('should nullify foreignKey of previous related record and set foreignKey to new related record', async () => {
663
+ const id = await db.user.get('id').create(userData);
664
+
665
+ const [{ id: profile1Id }, { id: profile2Id }] = await db.profile
666
+ .select('id')
667
+ .createMany([{ ...profileData, userId: id }, { ...profileData }]);
668
+
669
+ await db.user.find(id).update({
670
+ profile: {
671
+ set: { id: profile2Id },
672
+ },
673
+ });
674
+
675
+ const profile1 = await db.profile.find(profile1Id);
676
+ expect(profile1.userId).toBe(null);
677
+
678
+ const profile2 = await db.profile.find(profile2Id);
679
+ expect(profile2.userId).toBe(id);
680
+ });
681
+
682
+ it('should throw in batch update', async () => {
683
+ const query = db.user.where({ id: { in: [1, 2, 3] } }).update({
684
+ profile: {
685
+ // @ts-expect-error not allows in batch update
686
+ set: { id: 1 },
687
+ },
688
+ });
689
+
690
+ await expect(query).rejects.toThrow();
691
+ });
692
+ });
693
+
694
+ describe('delete', () => {
695
+ it('should delete related record', async () => {
696
+ const id = await db.user
697
+ .get('id')
698
+ .create({ ...userData, profile: { create: profileData } });
699
+
700
+ const { id: profileId } = await db.user
701
+ .profile({ id })
702
+ .select('id')
703
+ .take();
704
+
705
+ await db.user.find(id).update({
706
+ profile: {
707
+ delete: true,
708
+ },
709
+ });
710
+
711
+ const profile = await db.profile.findByOptional({ id: profileId });
712
+ expect(profile).toBe(undefined);
713
+ });
714
+
715
+ it('should delete related record in batch update', async () => {
716
+ const userIds = await db.user.pluck('id').createMany([
717
+ { ...userData, profile: { create: profileData } },
718
+ { ...userData, profile: { create: profileData } },
719
+ ]);
720
+
721
+ await db.user.where({ id: { in: userIds } }).update({
722
+ profile: {
723
+ delete: true,
724
+ },
725
+ });
726
+
727
+ const count = await db.profile.count();
728
+ expect(count).toBe(0);
729
+ });
730
+ });
731
+
732
+ describe('nested update', () => {
733
+ it('should update related record', async () => {
734
+ const id = await db.user
735
+ .get('id')
736
+ .create({ ...userData, profile: { create: profileData } });
737
+
738
+ await db.user.find(id).update({
739
+ profile: {
740
+ update: {
741
+ bio: 'updated',
742
+ },
743
+ },
744
+ });
745
+
746
+ const profile = await db.user.profile({ id }).take();
747
+ expect(profile.bio).toBe('updated');
748
+ });
749
+
750
+ it('should update related record in batch update', async () => {
751
+ const userIds = await db.user.pluck('id').createMany([
752
+ { ...userData, profile: { create: profileData } },
753
+ { ...userData, profile: { create: profileData } },
754
+ ]);
755
+
756
+ await db.user.where({ id: { in: userIds } }).update({
757
+ profile: {
758
+ update: {
759
+ bio: 'updated',
760
+ },
761
+ },
762
+ });
763
+
764
+ const bios = await db.profile.pluck('bio');
765
+ expect(bios).toEqual(['updated', 'updated']);
766
+ });
767
+ });
768
+
769
+ describe('nested upsert', () => {
770
+ it('should update related record if it exists', async () => {
771
+ const user = await db.user.create({
772
+ ...userData,
773
+ profile: { create: profileData },
774
+ });
775
+
776
+ await db.user.find(user.id).update({
777
+ profile: {
778
+ upsert: {
779
+ update: {
780
+ bio: 'updated',
781
+ },
782
+ create: profileData,
783
+ },
784
+ },
785
+ });
786
+
787
+ const profile = await db.user.profile(user);
788
+ expect(profile.bio).toBe('updated');
789
+ });
790
+
791
+ it('should create related record if it does not exists', async () => {
792
+ const user = await db.user.create(userData);
793
+
794
+ await db.user.find(user.id).update({
795
+ profile: {
796
+ upsert: {
797
+ update: {
798
+ bio: 'updated',
799
+ },
800
+ create: {
801
+ ...profileData,
802
+ bio: 'created',
803
+ },
804
+ },
805
+ },
806
+ });
807
+
808
+ const profile = await db.user.profile(user);
809
+ expect(profile.bio).toBe('created');
810
+ });
811
+
812
+ it('should throw in batch update', async () => {
813
+ const query = db.user.where({ id: { in: [1, 2, 3] } }).update({
814
+ profile: {
815
+ // @ts-expect-error not allows in batch update
816
+ upsert: {
817
+ update: {
818
+ bio: 'updated',
819
+ },
820
+ create: {
821
+ ...profileData,
822
+ bio: 'created',
823
+ },
824
+ },
825
+ },
826
+ });
827
+
828
+ await expect(query).rejects.toThrow();
829
+ });
830
+ });
831
+
832
+ describe('nested create', () => {
833
+ it('should create new related record', async () => {
834
+ const userId = await db.user
835
+ .get('id')
836
+ .create({ ...userData, profile: { create: profileData } });
837
+
838
+ const previousProfileId = await db.user
839
+ .profile({ id: userId })
840
+ .get('id');
841
+
842
+ const updated = await db.user
843
+ .selectAll()
844
+ .find(userId)
845
+ .update({
846
+ profile: {
847
+ create: { ...profileData, bio: 'created' },
848
+ },
849
+ });
850
+
851
+ const previousProfile = await db.profile.find(previousProfileId);
852
+ expect(previousProfile.userId).toBe(null);
853
+
854
+ const profile = await db.user.profile(updated);
855
+ expect(profile.bio).toBe('created');
856
+ });
857
+
858
+ it('should throw in batch update', async () => {
859
+ const query = db.user.where({ id: { in: [1, 2, 3] } }).update({
860
+ profile: {
861
+ // @ts-expect-error not allows in batch update
862
+ create: {
863
+ ...profileData,
864
+ bio: 'created',
865
+ },
866
+ },
867
+ });
868
+
869
+ await expect(query).rejects.toThrow();
870
+ });
871
+ });
872
+ });
873
+ });
874
+ });
875
+
876
+ describe('hasOne through', () => {
877
+ it('should resolve recursive situation when both models depends on each other', () => {
878
+ class Post extends Model {
879
+ table = 'post';
880
+ columns = this.setColumns((t) => ({
881
+ id: t.serial().primaryKey(),
882
+ }));
883
+
884
+ relations = {
885
+ postTag: this.hasOne(() => PostTag, {
886
+ primaryKey: 'id',
887
+ foreignKey: 'postId',
888
+ }),
889
+
890
+ tag: this.hasOne(() => Tag, {
891
+ through: 'postTag',
892
+ source: 'tag',
893
+ }),
894
+ };
895
+ }
896
+
897
+ class Tag extends Model {
898
+ table = 'tag';
899
+ columns = this.setColumns((t) => ({
900
+ id: t.serial().primaryKey(),
901
+ }));
902
+
903
+ relations = {
904
+ postTag: this.hasOne(() => PostTag, {
905
+ primaryKey: 'id',
906
+ foreignKey: 'postId',
907
+ }),
908
+
909
+ post: this.hasOne(() => Post, {
910
+ through: 'postTag',
911
+ source: 'post',
912
+ }),
913
+ };
914
+ }
915
+
916
+ class PostTag extends Model {
917
+ table = 'postTag';
918
+ columns = this.setColumns((t) => ({
919
+ postId: t.integer().foreignKey(() => Post, 'id'),
920
+ tagId: t.integer().foreignKey(() => Tag, 'id'),
921
+ }));
922
+
923
+ relations = {
924
+ post: this.belongsTo(() => Post, {
925
+ primaryKey: 'id',
926
+ foreignKey: 'postId',
927
+ }),
928
+
929
+ tag: this.belongsTo(() => Tag, {
930
+ primaryKey: 'id',
931
+ foreignKey: 'tagId',
932
+ }),
933
+ };
934
+ }
935
+
936
+ const db = orchidORM(
937
+ {
938
+ ...pgConfig,
939
+ log: false,
940
+ },
941
+ {
942
+ post: Post,
943
+ tag: Tag,
944
+ postTag: PostTag,
945
+ },
946
+ );
947
+
948
+ expect(Object.keys(db.post.relations)).toEqual(['postTag', 'tag']);
949
+ expect(Object.keys(db.tag.relations)).toEqual(['postTag', 'post']);
950
+ });
951
+
952
+ it('should have method to query related data', async () => {
953
+ const profileQuery = db.profile.take();
954
+
955
+ assertType<
956
+ typeof db.message.profile,
957
+ RelationQuery<
958
+ 'profile',
959
+ { authorId: number | null },
960
+ never,
961
+ typeof profileQuery,
962
+ true,
963
+ false,
964
+ true
965
+ >
966
+ >();
967
+
968
+ const query = db.message.profile({ authorId: 1 });
969
+ expectSql(
970
+ query.toSql(),
971
+ `
972
+ SELECT * FROM "profile"
973
+ WHERE EXISTS (
974
+ SELECT 1 FROM "user"
975
+ WHERE "profile"."userId" = "user"."id"
976
+ AND "user"."id" = $1
977
+ LIMIT 1
978
+ )
979
+ LIMIT $2
980
+ `,
981
+ [1, 1],
982
+ );
983
+ });
984
+
985
+ it('should handle chained query', () => {
986
+ const query = db.message
987
+ .where({ text: 'text' })
988
+ .profile.where({ bio: 'bio' });
989
+
990
+ expectSql(
991
+ query.toSql(),
992
+ `
993
+ SELECT * FROM "profile"
994
+ WHERE EXISTS (
995
+ SELECT 1 FROM "message"
996
+ WHERE "message"."text" = $1
997
+ AND EXISTS (
998
+ SELECT 1 FROM "user"
999
+ WHERE "profile"."userId" = "user"."id"
1000
+ AND "user"."id" = "message"."authorId"
1001
+ LIMIT 1
1002
+ )
1003
+ LIMIT 1
1004
+ )
1005
+ AND "profile"."bio" = $2
1006
+ LIMIT $3
1007
+ `,
1008
+ ['text', 'bio', 1],
1009
+ );
1010
+ });
1011
+
1012
+ it('should have disabled create method', () => {
1013
+ // @ts-expect-error hasOne with through option should not have chained create
1014
+ db.message.profile.create(chatData);
1015
+ });
1016
+
1017
+ it('should have chained delete method', () => {
1018
+ const query = db.message
1019
+ .where({ text: 'text' })
1020
+ .profile.where({ bio: 'bio' })
1021
+ .delete();
1022
+
1023
+ expectSql(
1024
+ query.toSql(),
1025
+ `
1026
+ DELETE FROM "profile"
1027
+ WHERE EXISTS (
1028
+ SELECT 1 FROM "message"
1029
+ WHERE "message"."text" = $1
1030
+ AND EXISTS (
1031
+ SELECT 1 FROM "user"
1032
+ WHERE "profile"."userId" = "user"."id"
1033
+ AND "user"."id" = "message"."authorId"
1034
+ LIMIT 1
1035
+ )
1036
+ LIMIT 1
1037
+ )
1038
+ AND "profile"."bio" = $2
1039
+ `,
1040
+ ['text', 'bio'],
1041
+ );
1042
+ });
1043
+
1044
+ it('should have proper joinQuery', () => {
1045
+ expectSql(
1046
+ db.message.relations.profile
1047
+ .joinQuery(db.message.as('m'), db.profile.as('p'))
1048
+ .toSql(),
1049
+ `
1050
+ SELECT * FROM "profile" AS "p"
1051
+ WHERE EXISTS (
1052
+ SELECT 1 FROM "user"
1053
+ WHERE "p"."userId" = "user"."id"
1054
+ AND "user"."id" = "m"."authorId"
1055
+ LIMIT 1
1056
+ )
1057
+ `,
1058
+ );
1059
+ });
1060
+
1061
+ it('should be supported in whereExists', () => {
1062
+ expectSql(
1063
+ db.message.whereExists('profile').toSql(),
1064
+ `
1065
+ SELECT * FROM "message"
1066
+ WHERE EXISTS (
1067
+ SELECT 1 FROM "profile"
1068
+ WHERE EXISTS (
1069
+ SELECT 1 FROM "user"
1070
+ WHERE "profile"."userId" = "user"."id"
1071
+ AND "user"."id" = "message"."authorId"
1072
+ LIMIT 1
1073
+ )
1074
+ LIMIT 1
1075
+ )
1076
+ `,
1077
+ );
1078
+
1079
+ expectSql(
1080
+ db.message
1081
+ .as('m')
1082
+ .whereExists('profile', (q) => q.where({ bio: 'bio' }))
1083
+ .toSql(),
1084
+ `
1085
+ SELECT * FROM "message" AS "m"
1086
+ WHERE EXISTS (
1087
+ SELECT 1 FROM "profile"
1088
+ WHERE EXISTS (
1089
+ SELECT 1 FROM "user"
1090
+ WHERE "profile"."userId" = "user"."id"
1091
+ AND "user"."id" = "m"."authorId"
1092
+ LIMIT 1
1093
+ )
1094
+ AND "profile"."bio" = $1
1095
+ LIMIT 1
1096
+ )
1097
+ `,
1098
+ ['bio'],
1099
+ );
1100
+ });
1101
+
1102
+ it('should be supported in join', () => {
1103
+ const query = db.message
1104
+ .as('m')
1105
+ .join('profile', (q) => q.where({ bio: 'bio' }))
1106
+ .select('text', 'profile.bio');
1107
+
1108
+ assertType<Awaited<typeof query>, { text: string; bio: string | null }[]>();
1109
+
1110
+ expectSql(
1111
+ query.toSql(),
1112
+ `
1113
+ SELECT "m"."text", "profile"."bio" FROM "message" AS "m"
1114
+ JOIN "profile"
1115
+ ON EXISTS (
1116
+ SELECT 1 FROM "user"
1117
+ WHERE "profile"."userId" = "user"."id"
1118
+ AND "user"."id" = "m"."authorId"
1119
+ LIMIT 1
1120
+ )
1121
+ AND "profile"."bio" = $1
1122
+ `,
1123
+ ['bio'],
1124
+ );
1125
+ });
1126
+
1127
+ describe('select', () => {
1128
+ it('should be selectable', () => {
1129
+ const query = db.message.as('m').select('id', {
1130
+ profile: (q) => q.profile.where({ bio: 'bio' }),
1131
+ });
1132
+
1133
+ assertType<Awaited<typeof query>, { id: number; profile: Profile }[]>();
1134
+
1135
+ expectSql(
1136
+ query.toSql(),
1137
+ `
1138
+ SELECT
1139
+ "m"."id",
1140
+ (
1141
+ SELECT row_to_json("t".*)
1142
+ FROM (
1143
+ SELECT * FROM "profile"
1144
+ WHERE "profile"."bio" = $1
1145
+ AND EXISTS (
1146
+ SELECT 1 FROM "user"
1147
+ WHERE "profile"."userId" = "user"."id"
1148
+ AND "user"."id" = "m"."authorId"
1149
+ LIMIT 1
1150
+ )
1151
+ LIMIT $2
1152
+ ) AS "t"
1153
+ ) AS "profile"
1154
+ FROM "message" AS "m"
1155
+ `,
1156
+ ['bio', 1],
1157
+ );
1158
+ });
1159
+
1160
+ it('should be selectable by relation name', () => {
1161
+ const query = db.message.select('id', 'profile');
1162
+
1163
+ assertType<Awaited<typeof query>, { id: number; profile: Profile }[]>();
1164
+
1165
+ expectSql(
1166
+ query.toSql(),
1167
+ `
1168
+ SELECT
1169
+ "message"."id",
1170
+ (
1171
+ SELECT row_to_json("t".*)
1172
+ FROM (
1173
+ SELECT * FROM "profile"
1174
+ WHERE EXISTS (
1175
+ SELECT 1 FROM "user"
1176
+ WHERE "profile"."userId" = "user"."id"
1177
+ AND "user"."id" = "message"."authorId"
1178
+ LIMIT 1
1179
+ )
1180
+ LIMIT $1
1181
+ ) AS "t"
1182
+ ) AS "profile"
1183
+ FROM "message"
1184
+ `,
1185
+ [1],
1186
+ );
1187
+ });
1188
+
1189
+ it('should handle exists sub query', () => {
1190
+ const query = db.message.as('m').select('id', {
1191
+ hasProfile: (q) => q.profile.exists(),
1192
+ });
1193
+
1194
+ assertType<
1195
+ Awaited<typeof query>,
1196
+ { id: number; hasProfile: boolean }[]
1197
+ >();
1198
+
1199
+ expectSql(
1200
+ query.toSql(),
1201
+ `
1202
+ SELECT
1203
+ "m"."id",
1204
+ COALESCE((
1205
+ SELECT true
1206
+ FROM "profile"
1207
+ WHERE EXISTS (
1208
+ SELECT 1 FROM "user"
1209
+ WHERE "profile"."userId" = "user"."id"
1210
+ AND "user"."id" = "m"."authorId"
1211
+ LIMIT 1
1212
+ )
1213
+ ), false) AS "hasProfile"
1214
+ FROM "message" AS "m"
1215
+ `,
1216
+ );
1217
+ });
1218
+ });
1219
+ });