@apisr/drizzle-model 2.0.0 → 2.0.2

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,593 @@
1
+ import { beforeAll, describe, expect, test } from "bun:test";
2
+ import { model } from "tests/base";
3
+ import { db } from "tests/db";
4
+ import { esc } from "@/model";
5
+
6
+ const userModel = model("user", {});
7
+ const postModel = model("userPosts", {});
8
+ const commentModel = model("postComments", {});
9
+ const ideaModel = model("userIdeas", {});
10
+
11
+ function uid(): string {
12
+ return `${Date.now()}-${Math.random()}`;
13
+ }
14
+
15
+ describe("relations", () => {
16
+ let testUserId: number;
17
+ let testPostId: number;
18
+ let testCommentId: number;
19
+ let inviteeUserId: number;
20
+
21
+ beforeAll(async () => {
22
+ const user = await userModel
23
+ .insert({
24
+ name: "Relation Test User",
25
+ email: `${uid()}@relations.com`,
26
+ age: 25,
27
+ })
28
+ .returnFirst();
29
+ testUserId = user.id;
30
+
31
+ const invitee = await userModel
32
+ .insert({
33
+ name: "Invitee User",
34
+ email: `${uid()}@relations.com`,
35
+ age: 22,
36
+ invitedBy: testUserId,
37
+ })
38
+ .returnFirst();
39
+ inviteeUserId = invitee.id;
40
+
41
+ const post = await postModel
42
+ .insert({
43
+ title: "Test Post for Relations",
44
+ description: "Testing relations",
45
+ userId: testUserId,
46
+ })
47
+ .returnFirst();
48
+ testPostId = post.id;
49
+
50
+ const comment = await commentModel
51
+ .insert({
52
+ content: "Test comment",
53
+ authorId: testUserId,
54
+ postId: testPostId,
55
+ })
56
+ .returnFirst();
57
+ testCommentId = comment.id;
58
+
59
+ await ideaModel.insert({
60
+ content: "Test idea",
61
+ userId: testUserId,
62
+ });
63
+ });
64
+
65
+ // -----------------------------------------------------------------------
66
+ // Basic .with() - loading relations
67
+ // -----------------------------------------------------------------------
68
+
69
+ describe(".with() - basic relation loading", () => {
70
+ test("loads one-to-many relation (posts)", async () => {
71
+ const users = await userModel
72
+ .where({ id: esc(testUserId) })
73
+ .findMany()
74
+ .with({ posts: true });
75
+
76
+ expect(users).toBeArray();
77
+ const user = users[0]!;
78
+ expect(user).toBeDefined();
79
+ expect(user.posts).toBeArray();
80
+ expect(user.posts.length).toBeGreaterThan(0);
81
+ expect(user.posts[0]?.title).toBeDefined();
82
+ });
83
+
84
+ test("loads many-to-one relation (user from post)", async () => {
85
+ const posts = await postModel
86
+ .where({ id: esc(testPostId) })
87
+ .findMany()
88
+ .with({ user: true });
89
+
90
+ expect(posts).toBeArray();
91
+ const post = posts[0]!;
92
+ expect(post).toBeDefined();
93
+ expect(post.user).toBeDefined();
94
+ expect(post.user.name).toBe("Relation Test User");
95
+ });
96
+
97
+ test("loads self-referencing relation (invitee)", async () => {
98
+ const users = await userModel
99
+ .where({ id: esc(inviteeUserId) })
100
+ .findMany()
101
+ .with({ invitee: true });
102
+
103
+ expect(users).toBeArray();
104
+ const user = users[0]!;
105
+ expect(user).toBeDefined();
106
+ expect(user.invitee).toBeDefined();
107
+ expect(user.invitee?.id).toBe(testUserId);
108
+ });
109
+ });
110
+
111
+ // -----------------------------------------------------------------------
112
+ // Nested relations
113
+ // -----------------------------------------------------------------------
114
+
115
+ describe(".with() - nested relations", () => {
116
+ test("loads nested relations (user -> posts -> comments)", async () => {
117
+ const users = await userModel
118
+ .where({ id: esc(testUserId) })
119
+ .findMany()
120
+ .with({
121
+ posts: {
122
+ comments: true,
123
+ },
124
+ });
125
+
126
+ expect(users).toBeArray();
127
+ const user = users[0]!;
128
+ expect(user).toBeDefined();
129
+ expect(user.posts).toBeArray();
130
+ const post = user.posts[0]!;
131
+ expect(post).toBeDefined();
132
+ expect(post.comments).toBeArray();
133
+ expect(post.comments.length).toBeGreaterThan(0);
134
+ });
135
+
136
+ test("loads deeply nested relations (post -> comments -> author)", async () => {
137
+ const posts = await postModel
138
+ .where({ id: esc(testPostId) })
139
+ .findMany()
140
+ .with({
141
+ comments: {
142
+ author: true,
143
+ },
144
+ });
145
+
146
+ expect(posts).toBeArray();
147
+ const post = posts[0]!;
148
+ expect(post).toBeDefined();
149
+ expect(post.comments).toBeArray();
150
+ const comment = post.comments[0]!;
151
+ expect(comment).toBeDefined();
152
+ expect(comment.author).toBeDefined();
153
+ expect(comment.author.name).toBe("Relation Test User");
154
+ });
155
+ });
156
+
157
+ // -----------------------------------------------------------------------
158
+ // Multiple relations
159
+ // -----------------------------------------------------------------------
160
+
161
+ describe(".with() - multiple relations", () => {
162
+ test("loads multiple relations at once", async () => {
163
+ const users = await userModel
164
+ .where({ id: esc(testUserId) })
165
+ .findMany()
166
+ .with({
167
+ posts: true,
168
+ invitee: true,
169
+ });
170
+
171
+ expect(users).toBeArray();
172
+ const user = users[0]!;
173
+ expect(user).toBeDefined();
174
+ expect(user.posts).toBeArray();
175
+ expect(user.invitee).toBeDefined();
176
+ });
177
+
178
+ test("loads multiple relations with nested relations", async () => {
179
+ const posts = await postModel
180
+ .where({ id: esc(testPostId) })
181
+ .findMany()
182
+ .with({
183
+ user: true,
184
+ comments: {
185
+ author: true,
186
+ },
187
+ });
188
+
189
+ expect(posts).toBeArray();
190
+ const post = posts[0]!;
191
+ expect(post).toBeDefined();
192
+ expect(post.user).toBeDefined();
193
+ expect(post.comments).toBeArray();
194
+ const comment = post.comments[0]!;
195
+ expect(comment).toBeDefined();
196
+ expect(comment.author).toBeDefined();
197
+ });
198
+ });
199
+
200
+ // -----------------------------------------------------------------------
201
+ // Query where relations
202
+ // -----------------------------------------------------------------------
203
+
204
+ describe(".with() - filtered relations", () => {
205
+ test("filters relation with where clause", async () => {
206
+ await postModel.insert({
207
+ title: "New Post Title",
208
+ description: "Another post",
209
+ userId: testUserId,
210
+ });
211
+
212
+ const users = await userModel
213
+ .where({ id: esc(testUserId) })
214
+ .findMany()
215
+ .with({
216
+ posts: postModel.where({
217
+ title: {
218
+ like: "New%",
219
+ },
220
+ }),
221
+ });
222
+
223
+ expect(users).toBeArray();
224
+ const user = users[0]!;
225
+ expect(user).toBeDefined();
226
+ expect(user.posts).toBeArray();
227
+ for (const post of user.posts) {
228
+ expect(post.title.startsWith("New")).toBe(true);
229
+ }
230
+ });
231
+
232
+ test("filters nested relations", async () => {
233
+ const users = await userModel
234
+ .where({ id: esc(testUserId) })
235
+ .findMany()
236
+ .with({
237
+ posts: postModel.where({ likes: { gte: esc(0) } }).include({
238
+ comments: true,
239
+ }),
240
+ });
241
+
242
+ expect(users).toBeArray();
243
+ const user = users[0]!;
244
+ expect(user).toBeDefined();
245
+ expect(user.posts).toBeArray();
246
+ });
247
+ });
248
+
249
+ // -----------------------------------------------------------------------
250
+ // .include() for type-safe relation values
251
+ // -----------------------------------------------------------------------
252
+
253
+ describe(".include() - type-safe relation selection", () => {
254
+ test("uses .include() to pass nested relations to .with()", async () => {
255
+ const users = await userModel
256
+ .where({ id: esc(testUserId) })
257
+ .findMany()
258
+ .with({
259
+ posts: postModel
260
+ .where({
261
+ title: {
262
+ like: "%",
263
+ },
264
+ })
265
+ .include({
266
+ comments: true,
267
+ }),
268
+ });
269
+
270
+ expect(users).toBeArray();
271
+ const user = users[0]!;
272
+ expect(user).toBeDefined();
273
+ expect(user.posts).toBeArray();
274
+ if (user.posts.length > 0) {
275
+ const post = user.posts[0]!;
276
+ expect(post).toBeDefined();
277
+ expect(post.comments).toBeDefined();
278
+ }
279
+ });
280
+
281
+ test("chains .include() with filtered relations", async () => {
282
+ const posts = await postModel
283
+ .where({ id: esc(testPostId) })
284
+ .findMany()
285
+ .with({
286
+ comments: commentModel
287
+ .where({ content: { like: "%test%" } })
288
+ .include({
289
+ author: true,
290
+ }),
291
+ });
292
+
293
+ expect(posts).toBeArray();
294
+ const post = posts[0]!;
295
+ expect(post).toBeDefined();
296
+ expect(post.comments).toBeArray();
297
+ });
298
+ });
299
+
300
+ // -----------------------------------------------------------------------
301
+ // Combining relations with select/exclude
302
+ // -----------------------------------------------------------------------
303
+
304
+ describe("relations with .select() and .exclude()", () => {
305
+ test("combines .with() and .select()", async () => {
306
+ const users = await userModel
307
+ .where({ id: esc(testUserId) })
308
+ .findMany()
309
+ .with({ posts: true })
310
+ .select({ id: true, name: true });
311
+
312
+ expect(users).toBeArray();
313
+ const user = users[0] as (typeof users)[0] & { posts: unknown[] };
314
+ expect(user).toBeDefined();
315
+ expect(user.id).toBeDefined();
316
+ expect(user.name).toBeDefined();
317
+ // @ts-expect-error
318
+ expect(user.email).toBeUndefined();
319
+ expect(user.posts).toBeArray();
320
+ });
321
+
322
+ test("combines .with() and .exclude()", async () => {
323
+ const users = await userModel
324
+ .where({ id: esc(testUserId) })
325
+ .findMany()
326
+ .with({ posts: true })
327
+ .exclude({ email: true, secretField: true });
328
+
329
+ expect(users).toBeArray();
330
+ const user = users[0]!;
331
+ expect(user).toBeDefined();
332
+ expect(user.id).toBeDefined();
333
+ expect(user.name).toBeDefined();
334
+ // @ts-expect-error
335
+ expect(user.email).toBeUndefined();
336
+ // @ts-expect-error
337
+ expect(user.secretField).toBeUndefined();
338
+ expect(user.posts).toBeArray();
339
+ });
340
+
341
+ test("combines .with() and .exclude() removes specific columns", async () => {
342
+ const users = await userModel
343
+ .where({ id: esc(testUserId) })
344
+ .findMany()
345
+ .with({ posts: true })
346
+ .exclude({ email: true, age: true });
347
+
348
+ expect(users).toBeArray();
349
+ const user = users[0]!;
350
+ expect(user).toBeDefined();
351
+ expect(user.id).toBeDefined();
352
+ expect(user.name).toBeDefined();
353
+ // @ts-expect-error
354
+ expect(user.email).toBeUndefined();
355
+ // @ts-expect-error
356
+ expect(user.age).toBeUndefined();
357
+ expect(user.posts).toBeArray();
358
+ });
359
+ });
360
+
361
+ // -----------------------------------------------------------------------
362
+ // Relations with findFirst
363
+ // -----------------------------------------------------------------------
364
+
365
+ describe("relations with .findFirst()", () => {
366
+ test("loads relations with findFirst", async () => {
367
+ const user = await userModel
368
+ .where({ id: esc(testUserId) })
369
+ .findFirst()
370
+ .with({ posts: true });
371
+
372
+ expect(user).toBeDefined();
373
+ if (user) {
374
+ expect(user.posts).toBeArray();
375
+ }
376
+ });
377
+
378
+ test("loads nested relations with findFirst", async () => {
379
+ const user = await userModel
380
+ .where({ id: esc(testUserId) })
381
+ .findFirst()
382
+ .with({
383
+ posts: {
384
+ comments: true,
385
+ },
386
+ });
387
+
388
+ expect(user).toBeDefined();
389
+ if (user) {
390
+ expect(user.posts).toBeArray();
391
+ if (user.posts.length > 0) {
392
+ const post = user.posts[0]!;
393
+ expect(post).toBeDefined();
394
+ expect(post.comments).toBeDefined();
395
+ }
396
+ }
397
+ });
398
+ });
399
+
400
+ // -----------------------------------------------------------------------
401
+ // Relations with .safe()
402
+ // -----------------------------------------------------------------------
403
+
404
+ describe("relations with .safe()", () => {
405
+ test("wraps relation query in safe result", async () => {
406
+ const result = await userModel
407
+ .where({ id: esc(testUserId) })
408
+ .findMany()
409
+ .with({ posts: true })
410
+ .safe();
411
+
412
+ expect(result.error).toBeUndefined();
413
+ expect(result.data).toBeDefined();
414
+ if (result.data) {
415
+ expect(result.data).toBeArray();
416
+ const user = result.data[0]!;
417
+ expect(user).toBeDefined();
418
+ expect(user.posts).toBeArray();
419
+ }
420
+ });
421
+
422
+ test("safe with findFirst and relations", async () => {
423
+ const result = await userModel
424
+ .where({ id: esc(testUserId) })
425
+ .findFirst()
426
+ .with({ posts: true })
427
+ .safe();
428
+
429
+ expect(result.error).toBeUndefined();
430
+ expect(result.data).toBeDefined();
431
+ if (result.data) {
432
+ expect(result.data.posts).toBeArray();
433
+ }
434
+ });
435
+ });
436
+
437
+ // -----------------------------------------------------------------------
438
+ // Relations with .raw()
439
+ // -----------------------------------------------------------------------
440
+
441
+ describe("relations with .raw()", () => {
442
+ test("skips format function with relations", async () => {
443
+ const userModelWithFormat = model("user", {
444
+ format({ secretField, ...rest }) {
445
+ return rest;
446
+ },
447
+ });
448
+
449
+ const users = await userModelWithFormat
450
+ .where({ id: esc(testUserId) })
451
+ .findMany()
452
+ .with({ posts: true })
453
+ .raw();
454
+
455
+ expect(users).toBeArray();
456
+ const user = users[0]!;
457
+ expect(user).toBeDefined();
458
+ expect(user.secretField).toBeDefined();
459
+ expect(user.posts).toBeArray();
460
+ });
461
+ });
462
+
463
+ // -----------------------------------------------------------------------
464
+ // Edge cases
465
+ // -----------------------------------------------------------------------
466
+
467
+ describe("edge cases", () => {
468
+ test("handles empty relation arrays", async () => {
469
+ const newUser = await userModel
470
+ .insert({
471
+ name: "No Posts User",
472
+ email: `${uid()}@relations.com`,
473
+ age: 30,
474
+ })
475
+ .returnFirst();
476
+
477
+ const users = await userModel
478
+ .where({ id: esc(newUser.id) })
479
+ .findMany()
480
+ .with({ posts: true });
481
+
482
+ expect(users).toBeArray();
483
+ const user = users[0]!;
484
+ expect(user).toBeDefined();
485
+ expect(user.posts).toBeArray();
486
+ expect(user.posts).toHaveLength(0);
487
+ });
488
+
489
+ test("handles null one-to-one relations", async () => {
490
+ const newUser = await userModel
491
+ .insert({
492
+ name: "No Inviter User",
493
+ email: `${uid()}@relations.com`,
494
+ age: 28,
495
+ })
496
+ .returnFirst();
497
+
498
+ const users = await userModel
499
+ .where({ id: esc(newUser.id) })
500
+ .findMany()
501
+ .with({ invitee: true });
502
+
503
+ expect(users).toBeArray();
504
+ const user = users[0]!;
505
+ expect(user).toBeDefined();
506
+ expect(user.invitee).toBeNull();
507
+ });
508
+
509
+ test("loads multiple levels of nested relations", async () => {
510
+ const users = await userModel
511
+ .where({ id: esc(testUserId) })
512
+ .findMany()
513
+ .with({
514
+ posts: {
515
+ comments: {
516
+ author: true,
517
+ },
518
+ },
519
+ });
520
+
521
+ expect(users).toBeArray();
522
+ const user = users[0]!;
523
+ expect(user).toBeDefined();
524
+ expect(user.posts).toBeArray();
525
+ if (user.posts.length > 0) {
526
+ const post = user.posts[0]!;
527
+ if (post && post.comments.length > 0) {
528
+ const comment = post.comments[0]!;
529
+ expect(comment).toBeDefined();
530
+ expect(comment.author).toBeDefined();
531
+ }
532
+ }
533
+ });
534
+ });
535
+
536
+ // -----------------------------------------------------------------------
537
+ // Comparison with raw Drizzle queries
538
+ // -----------------------------------------------------------------------
539
+
540
+ describe("comparison with raw drizzle", () => {
541
+ test("matches drizzle query.findFirst with relations", async () => {
542
+ const modelResult = await userModel
543
+ .where({ id: esc(testUserId) })
544
+ .findFirst()
545
+ .with({ posts: true });
546
+
547
+ const drizzleAll = await db.query.user.findMany({
548
+ with: {
549
+ posts: true,
550
+ },
551
+ });
552
+ const drizzleResult = drizzleAll.find((u) => u.id === testUserId);
553
+
554
+ expect(modelResult).toBeDefined();
555
+ expect(drizzleResult).toBeDefined();
556
+ if (modelResult && drizzleResult) {
557
+ expect(modelResult.id).toBe(drizzleResult.id);
558
+ expect(modelResult.posts).toBeArray();
559
+ expect(drizzleResult.posts).toBeArray();
560
+ }
561
+ });
562
+
563
+ test("matches drizzle query.findMany with nested relations", async () => {
564
+ const modelResult = await userModel
565
+ .where({ id: esc(testUserId) })
566
+ .findMany()
567
+ .with({
568
+ posts: {
569
+ comments: true,
570
+ },
571
+ });
572
+
573
+ const drizzleAll = await db.query.user.findMany({
574
+ with: {
575
+ posts: {
576
+ with: {
577
+ comments: true,
578
+ },
579
+ },
580
+ },
581
+ });
582
+ const drizzleResult = drizzleAll.find((u) => u.id === testUserId);
583
+
584
+ expect(modelResult.length).toBeGreaterThan(0);
585
+ expect(drizzleResult).toBeDefined();
586
+ if (modelResult.length > 0 && drizzleResult) {
587
+ const modelUser = modelResult[0]!;
588
+ expect(modelUser).toBeDefined();
589
+ expect(modelUser.posts.length).toBe(drizzleResult.posts.length);
590
+ }
591
+ });
592
+ });
593
+ });
@@ -18,7 +18,7 @@ describe("upsert", () => {
18
18
  .upsert({
19
19
  insert: { name: "Upsert new", email, age: 25 },
20
20
  update: { name: "Should not apply" },
21
- target: schema.user.email as any,
21
+ target: schema.user.email,
22
22
  })
23
23
  .return()) as any[];
24
24
 
@@ -88,14 +88,14 @@ describe("upsert", () => {
88
88
  test("return > omit — removes specified fields", async () => {
89
89
  const email = `upsert-omit-${uid()}@test.com`;
90
90
 
91
- const [row] = (await userModel
91
+ const [row] = await userModel
92
92
  .upsert({
93
93
  insert: { name: "Omit upsert", email, age: 40, secretField: 99 },
94
94
  update: { name: "Omit updated" },
95
95
  target: schema.user.email as any,
96
96
  })
97
97
  .return()
98
- .omit({ age: true, secretField: true })) as any[];
98
+ .omit({ age: true, secretField: true });
99
99
 
100
100
  expect(row.age).toBeUndefined();
101
101
  expect(row.secretField).toBeUndefined();
@@ -0,0 +1,28 @@
1
+ import { esc, modelBuilder } from "src/model";
2
+ import { db } from "../db";
3
+ import { relations } from "../relations";
4
+ import * as schema from "../schema";
5
+
6
+ const model = modelBuilder({
7
+ schema,
8
+ db,
9
+ relations,
10
+ dialect: "PostgreSQL",
11
+ });
12
+
13
+ // create model
14
+ const userModel = model("user", {});
15
+
16
+ await userModel
17
+ .where({
18
+ name: {
19
+ like: "A%",
20
+ },
21
+ })
22
+ .findFirst();
23
+
24
+ await userModel
25
+ .where({
26
+ name: esc.like("A%"),
27
+ })
28
+ .findFirst();