@opensaas/stack-core 0.19.1 → 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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +78 -0
- package/dist/access/index.d.ts +1 -1
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js.map +1 -1
- package/dist/access/types.d.ts +69 -2
- package/dist/access/types.d.ts.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +40 -14
- package/dist/context/index.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/query/index.d.ts +299 -0
- package/dist/query/index.d.ts.map +1 -0
- package/dist/query/index.js +255 -0
- package/dist/query/index.js.map +1 -0
- package/dist/query/index.test.d.ts +2 -0
- package/dist/query/index.test.d.ts.map +1 -0
- package/dist/query/index.test.js +632 -0
- package/dist/query/index.test.js.map +1 -0
- package/package.json +6 -6
- package/src/access/index.ts +3 -0
- package/src/access/types.ts +83 -2
- package/src/context/index.ts +57 -24
- package/src/index.ts +14 -0
- package/src/query/index.test.ts +830 -0
- package/src/query/index.ts +505 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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
|