@opensaas/stack-core 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,632 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { defineFragment, runQuery, runQueryOne, buildInclude, pickFields, isFragment, } from './index.js';
3
+ // ─────────────────────────────────────────────────────────────
4
+ // Helpers for building a fake context.db delegate
5
+ // ─────────────────────────────────────────────────────────────
6
+ function makeDelegate(rows, findFirstRow) {
7
+ return {
8
+ findMany: vi.fn(async (_args) => rows),
9
+ findFirst: vi.fn(async (_args) => findFirstRow ?? rows[0] ?? null),
10
+ };
11
+ }
12
+ function makeContext(delegates) {
13
+ return { db: delegates };
14
+ }
15
+ // ─────────────────────────────────────────────────────────────
16
+ // defineFragment — construction
17
+ // ─────────────────────────────────────────────────────────────
18
+ describe('defineFragment', () => {
19
+ it('returns an object with _type: "fragment" and the field selection', () => {
20
+ const frag = defineFragment()({ id: true, name: true });
21
+ expect(frag._type).toBe('fragment');
22
+ expect(frag._fields).toEqual({ id: true, name: true });
23
+ });
24
+ it('accepts true for all field types', () => {
25
+ const frag = defineFragment()({
26
+ id: true,
27
+ title: true,
28
+ content: true,
29
+ published: true,
30
+ createdAt: true,
31
+ authorId: true,
32
+ });
33
+ expect(Object.keys(frag._fields)).toEqual([
34
+ 'id',
35
+ 'title',
36
+ 'content',
37
+ 'published',
38
+ 'createdAt',
39
+ 'authorId',
40
+ ]);
41
+ });
42
+ it('accepts a nested fragment for a relationship field', () => {
43
+ const userFrag = defineFragment()({ id: true, name: true });
44
+ const postFrag = defineFragment()({ id: true, author: userFrag });
45
+ expect(postFrag._fields.author).toBe(userFrag);
46
+ });
47
+ it('accepts a nested fragment for a many relationship', () => {
48
+ const tagFrag = defineFragment()({ id: true, name: true });
49
+ const postFrag = defineFragment()({ id: true, tags: tagFrag });
50
+ expect(postFrag._fields.tags).toBe(tagFrag);
51
+ });
52
+ });
53
+ // ─────────────────────────────────────────────────────────────
54
+ // ResultOf — static type tests (compile-time checks)
55
+ // ─────────────────────────────────────────────────────────────
56
+ describe('ResultOf (type-level checks)', () => {
57
+ it('scalar fragment produces a plain picked type', () => {
58
+ const frag = defineFragment()({ id: true, name: true, email: true });
59
+ const r = { id: '1', name: 'Alice', email: 'a@test.com' };
60
+ expect(r).toBeTruthy();
61
+ expect(frag._type).toBe('fragment');
62
+ });
63
+ it('nullable scalar fields are preserved', () => {
64
+ const frag = defineFragment()({ id: true, content: true });
65
+ // content should be string | null
66
+ const r = { id: '1', content: null };
67
+ expect(r.content).toBeNull();
68
+ expect(frag._type).toBe('fragment');
69
+ });
70
+ it('nested fragment on a nullable relationship preserves null', () => {
71
+ const userFrag = defineFragment()({ id: true, name: true });
72
+ const postFrag = defineFragment()({ id: true, author: userFrag });
73
+ // author should be { id: string; name: string } | null
74
+ const r = { id: '1', author: null };
75
+ expect(r.author).toBeNull();
76
+ expect(postFrag._type).toBe('fragment');
77
+ });
78
+ it('nested fragment on a many relationship produces an array', () => {
79
+ const tagFrag = defineFragment()({ id: true, name: true });
80
+ const postFrag = defineFragment()({ id: true, tags: tagFrag });
81
+ const r = { id: '1', tags: [{ id: 't1', name: 'ts' }] };
82
+ expect(r.tags).toHaveLength(1);
83
+ expect(postFrag._type).toBe('fragment');
84
+ });
85
+ it('deep nesting (comment → post → author) resolves correctly', () => {
86
+ const userFrag = defineFragment()({ id: true, name: true });
87
+ const postFrag = defineFragment()({ id: true, title: true, author: userFrag });
88
+ const commentFrag = defineFragment()({
89
+ id: true,
90
+ body: true,
91
+ post: postFrag,
92
+ });
93
+ const r = {
94
+ id: 'c1',
95
+ body: 'hi',
96
+ post: {
97
+ id: 'p1',
98
+ title: 'Hello',
99
+ author: { id: 'u1', name: 'Alice' },
100
+ },
101
+ };
102
+ expect(r.post?.author?.name).toBe('Alice');
103
+ expect(commentFrag._type).toBe('fragment');
104
+ });
105
+ });
106
+ // ─────────────────────────────────────────────────────────────
107
+ // runQuery — runtime behaviour
108
+ // ─────────────────────────────────────────────────────────────
109
+ describe('runQuery', () => {
110
+ const userFrag = defineFragment()({ id: true, name: true, email: true });
111
+ const rawUsers = [
112
+ { id: 'u1', name: 'Alice', email: 'alice@test.com', role: 'admin', bio: null },
113
+ { id: 'u2', name: 'Bob', email: 'bob@test.com', role: 'user', bio: 'Hey there' },
114
+ ];
115
+ it('calls context.db[dbKey].findMany with no args when none supplied', async () => {
116
+ const delegate = makeDelegate(rawUsers);
117
+ const ctx = makeContext({ user: delegate });
118
+ await runQuery(ctx, 'User', userFrag);
119
+ expect(delegate.findMany).toHaveBeenCalledWith(undefined);
120
+ });
121
+ it('returns only the fields specified in the fragment', async () => {
122
+ const ctx = makeContext({ user: makeDelegate(rawUsers) });
123
+ const results = await runQuery(ctx, 'User', userFrag);
124
+ expect(results).toHaveLength(2);
125
+ // Only id, name, email — not role or bio
126
+ expect(results[0]).toEqual({ id: 'u1', name: 'Alice', email: 'alice@test.com' });
127
+ expect(results[1]).toEqual({ id: 'u2', name: 'Bob', email: 'bob@test.com' });
128
+ expect(results[0]).not.toHaveProperty('role');
129
+ expect(results[0]).not.toHaveProperty('bio');
130
+ });
131
+ it('passes where, orderBy, take, skip to findMany', async () => {
132
+ const delegate = makeDelegate(rawUsers);
133
+ const ctx = makeContext({ user: delegate });
134
+ await runQuery(ctx, 'User', userFrag, {
135
+ where: { role: 'admin' },
136
+ orderBy: { name: 'asc' },
137
+ take: 5,
138
+ skip: 2,
139
+ });
140
+ expect(delegate.findMany).toHaveBeenCalledWith({
141
+ where: { role: 'admin' },
142
+ orderBy: { name: 'asc' },
143
+ take: 5,
144
+ skip: 2,
145
+ });
146
+ });
147
+ it('converts PascalCase listKey to camelCase for db access', async () => {
148
+ const delegate = makeDelegate([]);
149
+ const ctx = makeContext({ blogPost: delegate });
150
+ await runQuery(ctx, 'BlogPost', defineFragment()({ id: true }));
151
+ expect(delegate.findMany).toHaveBeenCalled();
152
+ });
153
+ it('returns an empty array when findMany returns nothing', async () => {
154
+ const ctx = makeContext({ user: makeDelegate([]) });
155
+ const results = await runQuery(ctx, 'User', userFrag);
156
+ expect(results).toEqual([]);
157
+ });
158
+ describe('with nested fragment (relationship)', () => {
159
+ const tagFrag = defineFragment()({ id: true, name: true });
160
+ const postFrag = defineFragment()({
161
+ id: true,
162
+ title: true,
163
+ author: userFrag,
164
+ tags: tagFrag,
165
+ });
166
+ const rawPosts = [
167
+ {
168
+ id: 'p1',
169
+ title: 'Hello World',
170
+ content: 'body',
171
+ published: true,
172
+ createdAt: new Date('2024-01-01'),
173
+ authorId: 'u1',
174
+ author: {
175
+ id: 'u1',
176
+ name: 'Alice',
177
+ email: 'alice@test.com',
178
+ role: 'admin',
179
+ bio: null,
180
+ },
181
+ tags: [
182
+ { id: 't1', name: 'TypeScript', slug: 'typescript' },
183
+ { id: 't2', name: 'Node', slug: 'node' },
184
+ ],
185
+ },
186
+ ];
187
+ it('passes include for relationship fields to findMany', async () => {
188
+ const delegate = makeDelegate(rawPosts);
189
+ const ctx = makeContext({ post: delegate });
190
+ await runQuery(ctx, 'Post', postFrag);
191
+ expect(delegate.findMany).toHaveBeenCalledWith(expect.objectContaining({
192
+ include: { author: true, tags: true },
193
+ }));
194
+ });
195
+ it('picks only the selected nested fields', async () => {
196
+ const ctx = makeContext({ post: makeDelegate(rawPosts) });
197
+ const results = await runQuery(ctx, 'Post', postFrag);
198
+ expect(results[0].author).toEqual({ id: 'u1', name: 'Alice', email: 'alice@test.com' });
199
+ // role and bio are NOT selected in userFrag
200
+ expect(results[0].author).not.toHaveProperty('role');
201
+ expect(results[0].author).not.toHaveProperty('bio');
202
+ });
203
+ it('maps tag arrays and strips unselected fields', async () => {
204
+ const ctx = makeContext({ post: makeDelegate(rawPosts) });
205
+ const results = await runQuery(ctx, 'Post', postFrag);
206
+ // slug is not in tagFrag, so it must be absent
207
+ expect(results[0].tags[0]).not.toHaveProperty('slug');
208
+ expect(results[0].tags[0]).toEqual({ id: 't1', name: 'TypeScript' });
209
+ expect(results[0].tags[1]).toEqual({ id: 't2', name: 'Node' });
210
+ });
211
+ });
212
+ describe('with null relationship', () => {
213
+ it('preserves null for an unset relationship', async () => {
214
+ const authorFrag = defineFragment()({ id: true, name: true });
215
+ const postFrag = defineFragment()({ id: true, author: authorFrag });
216
+ const rawPost = {
217
+ id: 'p2',
218
+ title: 'Draft',
219
+ content: null,
220
+ published: false,
221
+ createdAt: new Date(),
222
+ authorId: null,
223
+ author: null,
224
+ tags: [],
225
+ };
226
+ const ctx = makeContext({ post: makeDelegate([rawPost]) });
227
+ const results = await runQuery(ctx, 'Post', postFrag);
228
+ expect(results[0].author).toBeNull();
229
+ });
230
+ });
231
+ describe('deeply nested (three levels)', () => {
232
+ it('recursively builds include and picks fields', async () => {
233
+ const authorFrag = defineFragment()({ id: true, name: true });
234
+ const postFrag = defineFragment()({
235
+ id: true,
236
+ title: true,
237
+ author: authorFrag,
238
+ });
239
+ const commentFrag = defineFragment()({
240
+ id: true,
241
+ body: true,
242
+ post: postFrag,
243
+ });
244
+ const raw = [
245
+ {
246
+ id: 'c1',
247
+ body: 'Nice post!',
248
+ post: {
249
+ id: 'p1',
250
+ title: 'Hello',
251
+ content: 'body',
252
+ published: true,
253
+ createdAt: new Date(),
254
+ authorId: 'u1',
255
+ author: { id: 'u1', name: 'Alice', email: 'a@t.com', role: 'user', bio: null },
256
+ tags: [],
257
+ },
258
+ author: null,
259
+ },
260
+ ];
261
+ const delegate = makeDelegate(raw);
262
+ const ctx = makeContext({ comment: delegate });
263
+ const results = await runQuery(ctx, 'Comment', commentFrag);
264
+ // include passed to findMany
265
+ expect(delegate.findMany).toHaveBeenCalledWith(expect.objectContaining({
266
+ include: {
267
+ post: { include: { author: true } },
268
+ },
269
+ }));
270
+ // result shape
271
+ expect(results[0]).toEqual({
272
+ id: 'c1',
273
+ body: 'Nice post!',
274
+ post: {
275
+ id: 'p1',
276
+ title: 'Hello',
277
+ author: { id: 'u1', name: 'Alice' },
278
+ },
279
+ });
280
+ });
281
+ });
282
+ });
283
+ // ─────────────────────────────────────────────────────────────
284
+ // runQueryOne — runtime behaviour
285
+ // ─────────────────────────────────────────────────────────────
286
+ describe('runQueryOne', () => {
287
+ const userFrag = defineFragment()({ id: true, name: true, email: true });
288
+ const rawUser = {
289
+ id: 'u1',
290
+ name: 'Alice',
291
+ email: 'alice@test.com',
292
+ role: 'admin',
293
+ bio: null,
294
+ };
295
+ it('calls context.db[dbKey].findFirst with the where clause', async () => {
296
+ const delegate = makeDelegate([rawUser], rawUser);
297
+ const ctx = makeContext({ user: delegate });
298
+ await runQueryOne(ctx, 'User', userFrag, { id: 'u1' });
299
+ expect(delegate.findFirst).toHaveBeenCalledWith({ where: { id: 'u1' } });
300
+ });
301
+ it('returns only the fields specified in the fragment', async () => {
302
+ const ctx = makeContext({ user: makeDelegate([rawUser], rawUser) });
303
+ const result = await runQueryOne(ctx, 'User', userFrag, { id: 'u1' });
304
+ expect(result).toEqual({ id: 'u1', name: 'Alice', email: 'alice@test.com' });
305
+ expect(result).not.toHaveProperty('role');
306
+ expect(result).not.toHaveProperty('bio');
307
+ });
308
+ it('returns null when findFirst returns null', async () => {
309
+ const delegate = makeDelegate([], null);
310
+ const ctx = makeContext({ user: delegate });
311
+ const result = await runQueryOne(ctx, 'User', userFrag, { id: 'nope' });
312
+ expect(result).toBeNull();
313
+ });
314
+ it('returns null when findFirst returns undefined', async () => {
315
+ const delegate = {
316
+ findMany: vi.fn(async () => []),
317
+ findFirst: vi.fn(async () => null),
318
+ };
319
+ const ctx = makeContext({ user: delegate });
320
+ const result = await runQueryOne(ctx, 'User', userFrag, { id: 'nope' });
321
+ expect(result).toBeNull();
322
+ });
323
+ it('passes include for relationship fields to findFirst', async () => {
324
+ const authorFrag = defineFragment()({ id: true, name: true });
325
+ const postFrag = defineFragment()({ id: true, title: true, author: authorFrag });
326
+ const rawPost = {
327
+ id: 'p1',
328
+ title: 'Hello',
329
+ content: null,
330
+ published: true,
331
+ createdAt: new Date(),
332
+ authorId: 'u1',
333
+ author: { id: 'u1', name: 'Alice', email: 'a@t.com', role: 'user', bio: null },
334
+ tags: [],
335
+ };
336
+ const delegate = makeDelegate([rawPost], rawPost);
337
+ const ctx = makeContext({ post: delegate });
338
+ const result = await runQueryOne(ctx, 'Post', postFrag, { id: 'p1' });
339
+ expect(delegate.findFirst).toHaveBeenCalledWith({
340
+ where: { id: 'p1' },
341
+ include: { author: true },
342
+ });
343
+ expect(result).toEqual({
344
+ id: 'p1',
345
+ title: 'Hello',
346
+ author: { id: 'u1', name: 'Alice' },
347
+ });
348
+ });
349
+ it('converts PascalCase listKey to camelCase', async () => {
350
+ const delegate = makeDelegate([], null);
351
+ const ctx = makeContext({ blogPost: delegate });
352
+ await runQueryOne(ctx, 'BlogPost', defineFragment()({ id: true }), {
353
+ id: 'p1',
354
+ });
355
+ expect(delegate.findFirst).toHaveBeenCalled();
356
+ });
357
+ });
358
+ // ─────────────────────────────────────────────────────────────
359
+ // Fragment composition — reusability
360
+ // ─────────────────────────────────────────────────────────────
361
+ describe('fragment composition', () => {
362
+ it('the same fragment can be reused in multiple parent fragments', async () => {
363
+ const userFrag = defineFragment()({ id: true, name: true });
364
+ const postFrag = defineFragment()({ id: true, author: userFrag });
365
+ const commentFrag = defineFragment()({ id: true, author: userFrag });
366
+ // Both refer to the exact same userFrag instance
367
+ expect(postFrag._fields.author).toBe(userFrag);
368
+ expect(commentFrag._fields.author).toBe(userFrag);
369
+ });
370
+ it('composes three levels deep without mutation', async () => {
371
+ const userFrag = defineFragment()({ id: true, name: true });
372
+ const postFrag = defineFragment()({
373
+ id: true,
374
+ title: true,
375
+ author: userFrag,
376
+ });
377
+ const commentFrag = defineFragment()({
378
+ id: true,
379
+ body: true,
380
+ post: postFrag,
381
+ });
382
+ // Verify structure without executing any query
383
+ expect(commentFrag._type).toBe('fragment');
384
+ const postFieldInComment = commentFrag._fields.post;
385
+ expect(postFieldInComment._type).toBe('fragment');
386
+ const authorFieldInPost = postFieldInComment._fields.author;
387
+ expect(authorFieldInPost._fields).toEqual({ id: true, name: true });
388
+ });
389
+ });
390
+ // ─────────────────────────────────────────────────────────────
391
+ // isFragment — runtime guard
392
+ // ─────────────────────────────────────────────────────────────
393
+ describe('isFragment', () => {
394
+ it('returns true for a fragment created by defineFragment', () => {
395
+ const frag = defineFragment()({ id: true });
396
+ expect(isFragment(frag)).toBe(true);
397
+ });
398
+ it('returns false for a RelationSelector object', () => {
399
+ const frag = defineFragment()({ id: true });
400
+ const selector = { query: frag, where: { active: true } };
401
+ expect(isFragment(selector)).toBe(false);
402
+ });
403
+ it('returns false for primitives and null', () => {
404
+ expect(isFragment(true)).toBe(false);
405
+ expect(isFragment(null)).toBe(false);
406
+ expect(isFragment(undefined)).toBe(false);
407
+ expect(isFragment('fragment')).toBe(false);
408
+ expect(isFragment(42)).toBe(false);
409
+ });
410
+ it('returns false for a plain object without _type', () => {
411
+ expect(isFragment({ id: true })).toBe(false);
412
+ });
413
+ });
414
+ describe('buildInclude with RelationSelector', () => {
415
+ it('generates a simple include for a shorthand fragment', () => {
416
+ const userFrag = defineFragment()({ id: true, name: true });
417
+ const postFrag = defineFragment()({ id: true, author: userFrag });
418
+ const result = buildInclude(postFrag._fields);
419
+ expect(result).toEqual({ author: true });
420
+ });
421
+ it('generates a where-filtered include for a RelationSelector', () => {
422
+ const commentFrag = defineFragment()({ id: true, body: true });
423
+ const postFrag = defineFragment()({
424
+ id: true,
425
+ comments: {
426
+ query: commentFrag,
427
+ where: { approved: true },
428
+ },
429
+ });
430
+ const result = buildInclude(postFrag._fields);
431
+ expect(result).toEqual({
432
+ comments: { where: { approved: true } },
433
+ });
434
+ });
435
+ it('includes orderBy, take and skip in the nested include', () => {
436
+ const commentFrag = defineFragment()({ id: true });
437
+ const postFrag = defineFragment()({
438
+ id: true,
439
+ comments: {
440
+ query: commentFrag,
441
+ where: { approved: true },
442
+ orderBy: { id: 'asc' },
443
+ take: 5,
444
+ skip: 10,
445
+ },
446
+ });
447
+ const result = buildInclude(postFrag._fields);
448
+ expect(result).toEqual({
449
+ comments: {
450
+ where: { approved: true },
451
+ orderBy: { id: 'asc' },
452
+ take: 5,
453
+ skip: 10,
454
+ },
455
+ });
456
+ });
457
+ it('combines RelationSelector args with a nested include from the inner fragment', () => {
458
+ const userFrag = defineFragment()({ id: true, name: true });
459
+ const commentWithAuthorFrag = defineFragment()({
460
+ id: true,
461
+ body: true,
462
+ author: userFrag,
463
+ });
464
+ const postFrag = defineFragment()({
465
+ id: true,
466
+ // Using RelationSelector with nested include (Comment has author)
467
+ author: {
468
+ query: defineFragment()({ id: true }),
469
+ where: { role: 'admin' },
470
+ },
471
+ });
472
+ const result = buildInclude(postFrag._fields);
473
+ // author has where clause (no nested include needed since User scalar fields)
474
+ expect(result).toEqual({
475
+ author: { where: { role: 'admin' } },
476
+ });
477
+ // Separate test: RelationSelector where inner fragment has nested relationships
478
+ const commentSelector = defineFragment()({
479
+ id: true,
480
+ author: userFrag,
481
+ });
482
+ const result2 = buildInclude(commentWithAuthorFrag._fields);
483
+ expect(result2).toEqual({ author: true });
484
+ // Suppress unused variable warning
485
+ expect(commentSelector._type).toBe('fragment');
486
+ });
487
+ it('returns undefined when there are no relationship fields', () => {
488
+ const frag = defineFragment()({ id: true, name: true, email: true });
489
+ const result = buildInclude(frag._fields);
490
+ expect(result).toBeUndefined();
491
+ });
492
+ });
493
+ // ─────────────────────────────────────────────────────────────
494
+ // pickFields — RelationSelector branch
495
+ // ─────────────────────────────────────────────────────────────
496
+ describe('pickFields with RelationSelector', () => {
497
+ it('picks fields from a nested array using RelationSelector', () => {
498
+ const commentFrag = defineFragment()({ id: true, body: true });
499
+ const postFrag = defineFragment()({
500
+ id: true,
501
+ comments: { query: commentFrag, where: { approved: true } },
502
+ });
503
+ const raw = {
504
+ id: 'p1',
505
+ title: 'Hello',
506
+ comments: [
507
+ { id: 'c1', body: 'Great!', approved: true },
508
+ { id: 'c2', body: 'Nice', approved: false },
509
+ ],
510
+ };
511
+ const result = pickFields(raw, postFrag._fields);
512
+ expect(result).toEqual({
513
+ id: 'p1',
514
+ comments: [
515
+ { id: 'c1', body: 'Great!' },
516
+ { id: 'c2', body: 'Nice' },
517
+ ],
518
+ });
519
+ // title is not selected
520
+ expect(result).not.toHaveProperty('title');
521
+ // approved is not in commentFrag
522
+ expect(result.comments[0]).not.toHaveProperty('approved');
523
+ });
524
+ it('handles null relationship in RelationSelector', () => {
525
+ const userFrag = defineFragment()({ id: true, name: true });
526
+ const postFrag = defineFragment()({
527
+ id: true,
528
+ author: { query: userFrag, where: { role: 'admin' } },
529
+ });
530
+ const raw = { id: 'p1', author: null };
531
+ const result = pickFields(raw, postFrag._fields);
532
+ expect(result).toEqual({ id: 'p1', author: null });
533
+ });
534
+ });
535
+ // ─────────────────────────────────────────────────────────────
536
+ // runQuery — RelationSelector with filter args
537
+ // ─────────────────────────────────────────────────────────────
538
+ describe('runQuery with RelationSelector', () => {
539
+ it('passes nested where/orderBy/take/skip to include entry', async () => {
540
+ const commentFrag = defineFragment()({ id: true, body: true });
541
+ const postFrag = defineFragment()({
542
+ id: true,
543
+ title: true,
544
+ comments: {
545
+ query: commentFrag,
546
+ where: { approved: true },
547
+ orderBy: { id: 'asc' },
548
+ take: 3,
549
+ },
550
+ });
551
+ const rawPosts = [
552
+ {
553
+ id: 'p1',
554
+ title: 'Hello',
555
+ comments: [{ id: 'c1', body: 'First!', approved: true }],
556
+ },
557
+ ];
558
+ const delegate = makeDelegate(rawPosts);
559
+ const ctx = makeContext({ postWithComments: delegate });
560
+ await runQuery(ctx, 'PostWithComments', postFrag);
561
+ expect(delegate.findMany).toHaveBeenCalledWith(expect.objectContaining({
562
+ include: {
563
+ comments: { where: { approved: true }, orderBy: { id: 'asc' }, take: 3 },
564
+ },
565
+ }));
566
+ });
567
+ it('picks only fragment fields from nested items in RelationSelector results', async () => {
568
+ const commentFrag = defineFragment()({ id: true, body: true });
569
+ const postFrag = defineFragment()({
570
+ id: true,
571
+ comments: { query: commentFrag, where: { approved: true } },
572
+ });
573
+ const rawPosts = [
574
+ {
575
+ id: 'p1',
576
+ title: 'Hello',
577
+ comments: [
578
+ { id: 'c1', body: 'Yes!', approved: true },
579
+ { id: 'c2', body: 'No', approved: false },
580
+ ],
581
+ },
582
+ ];
583
+ const ctx = makeContext({ postWithComments: makeDelegate(rawPosts) });
584
+ const results = await runQuery(ctx, 'PostWithComments', postFrag);
585
+ expect(results[0]).toEqual({
586
+ id: 'p1',
587
+ comments: [
588
+ { id: 'c1', body: 'Yes!' },
589
+ { id: 'c2', body: 'No' },
590
+ ],
591
+ });
592
+ expect(results[0]).not.toHaveProperty('title');
593
+ });
594
+ });
595
+ // ─────────────────────────────────────────────────────────────
596
+ // Variables pattern — factory function
597
+ // ─────────────────────────────────────────────────────────────
598
+ describe('factory function (variables) pattern', () => {
599
+ it('creates different fragments with different runtime values', () => {
600
+ const commentFrag = defineFragment()({ id: true, body: true });
601
+ function makePostFrag(approvedOnly) {
602
+ return defineFragment()({
603
+ id: true,
604
+ comments: {
605
+ query: commentFrag,
606
+ where: { approved: approvedOnly },
607
+ },
608
+ });
609
+ }
610
+ const approvedFrag = makePostFrag(true);
611
+ const allFrag = makePostFrag(false);
612
+ const approvedInclude = buildInclude(approvedFrag._fields);
613
+ const allInclude = buildInclude(allFrag._fields);
614
+ expect(approvedInclude).toEqual({ comments: { where: { approved: true } } });
615
+ expect(allInclude).toEqual({ comments: { where: { approved: false } } });
616
+ });
617
+ it('ResultOf is the same shape regardless of runtime where values', () => {
618
+ const commentFrag = defineFragment()({ id: true, body: true });
619
+ const makePostFrag = (status) => defineFragment()({
620
+ id: true,
621
+ comments: { query: commentFrag, where: { approved: status } },
622
+ });
623
+ // Runtime usage (ensures the function is used, not just as a type)
624
+ const frag = makePostFrag(true);
625
+ expect(frag._type).toBe('fragment');
626
+ // Compile-time check: PostData should have id and comments
627
+ const r = { id: 'p1', comments: [{ id: 'c1', body: 'hi' }] };
628
+ expect(r.id).toBe('p1');
629
+ expect(r.comments).toHaveLength(1);
630
+ });
631
+ });
632
+ //# sourceMappingURL=index.test.js.map