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