@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +80 -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,505 @@
|
|
|
1
|
+
import { getDbKey } from '../lib/case-utils.js'
|
|
2
|
+
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
// Internal helpers
|
|
5
|
+
// ─────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Unwrap the item type from a field type, stripping null, undefined, and
|
|
9
|
+
* Array wrappers so we can constrain nested fragment shapes.
|
|
10
|
+
*
|
|
11
|
+
* Examples:
|
|
12
|
+
* User | null → User
|
|
13
|
+
* User[] → User
|
|
14
|
+
* (User | null)[] → User
|
|
15
|
+
*/
|
|
16
|
+
type UnwrapItem<T> = NonNullable<T> extends Array<infer U> ? NonNullable<U> : NonNullable<T>
|
|
17
|
+
|
|
18
|
+
// ─────────────────────────────────────────────────────────────
|
|
19
|
+
// Core types
|
|
20
|
+
// ─────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A selector for a relationship field.
|
|
24
|
+
*
|
|
25
|
+
* Two forms are accepted:
|
|
26
|
+
* 1. A `Fragment` directly (shorthand — no extra Prisma args on the nested query).
|
|
27
|
+
* 2. An object `{ query, where?, orderBy?, take?, skip? }` to combine a fragment
|
|
28
|
+
* with Prisma filter/ordering/pagination applied to the nested relationship.
|
|
29
|
+
*
|
|
30
|
+
* @example Shorthand (most common)
|
|
31
|
+
* ```ts
|
|
32
|
+
* const postFrag = defineFragment<Post>()({
|
|
33
|
+
* id: true,
|
|
34
|
+
* author: authorFragment, // shorthand
|
|
35
|
+
* } as const)
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @example With nested filtering
|
|
39
|
+
* ```ts
|
|
40
|
+
* const postFrag = defineFragment<Post>()({
|
|
41
|
+
* id: true,
|
|
42
|
+
* comments: {
|
|
43
|
+
* query: commentFragment,
|
|
44
|
+
* where: { approved: true },
|
|
45
|
+
* orderBy: { createdAt: 'desc' },
|
|
46
|
+
* take: 5,
|
|
47
|
+
* },
|
|
48
|
+
* } as const)
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @example Variables via factory function
|
|
52
|
+
* ```ts
|
|
53
|
+
* function makePostFragment(status: string) {
|
|
54
|
+
* return defineFragment<Post>()({
|
|
55
|
+
* id: true,
|
|
56
|
+
* comments: { query: commentFragment, where: { status } },
|
|
57
|
+
* } as const)
|
|
58
|
+
* }
|
|
59
|
+
* type PostData = ResultOf<ReturnType<typeof makePostFragment>>
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export type RelationSelector<TRelated extends Record<string, unknown>> =
|
|
63
|
+
| Fragment<TRelated, FieldSelection<TRelated>>
|
|
64
|
+
| {
|
|
65
|
+
readonly query: Fragment<TRelated, FieldSelection<TRelated>>
|
|
66
|
+
readonly where?: Record<string, unknown>
|
|
67
|
+
readonly orderBy?: Record<string, 'asc' | 'desc'> | Array<Record<string, 'asc' | 'desc'>>
|
|
68
|
+
readonly take?: number
|
|
69
|
+
readonly skip?: number
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A field selection for model type `TItem`.
|
|
74
|
+
*
|
|
75
|
+
* Each key maps to:
|
|
76
|
+
* - `true` — include the scalar/primitive field as-is
|
|
77
|
+
* - A `Fragment` — include a relationship and recurse (shorthand)
|
|
78
|
+
* - A `RelationSelector` — include a relationship with optional Prisma filter/ordering
|
|
79
|
+
*
|
|
80
|
+
* Only keys present in `TItem` are accepted. For relationship (object) fields
|
|
81
|
+
* you may pass a Fragment, a RelationSelector, or `true` (returns the raw Prisma
|
|
82
|
+
* value and loses type narrowing).
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* const sel: FieldSelection<Post> = {
|
|
87
|
+
* id: true,
|
|
88
|
+
* title: true,
|
|
89
|
+
* author: authorFragment,
|
|
90
|
+
* comments: { query: commentFragment, where: { approved: true } },
|
|
91
|
+
* }
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export type FieldSelection<T> = {
|
|
95
|
+
readonly [K in keyof T]?: UnwrapItem<T[K]> extends Record<string, unknown>
|
|
96
|
+
? RelationSelector<UnwrapItem<T[K]>> | true
|
|
97
|
+
: true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A reusable, composable field-selection descriptor for model type `TItem`.
|
|
102
|
+
*
|
|
103
|
+
* Create with {@link defineFragment}. Compose by referencing another Fragment
|
|
104
|
+
* (or a {@link RelationSelector}) as the value for a relationship key.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```ts
|
|
108
|
+
* const userFragment = defineFragment<User>()({ id: true, name: true } as const)
|
|
109
|
+
* const postFragment = defineFragment<Post>()({
|
|
110
|
+
* id: true,
|
|
111
|
+
* title: true,
|
|
112
|
+
* author: userFragment,
|
|
113
|
+
* } as const)
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export type Fragment<TItem, TFields extends FieldSelection<TItem> = FieldSelection<TItem>> = {
|
|
117
|
+
readonly _type: 'fragment'
|
|
118
|
+
readonly _fields: TFields
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─────────────────────────────────────────────────────────────
|
|
122
|
+
// Internal type helpers
|
|
123
|
+
// ─────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @internal
|
|
127
|
+
* Extract the Fragment from either a Fragment directly or a RelationSelector object.
|
|
128
|
+
* Returns `never` for scalar `true` selections (so they fall to the scalar branch).
|
|
129
|
+
*/
|
|
130
|
+
type ExtractFragment<TSelector> =
|
|
131
|
+
TSelector extends Fragment<infer TItem, infer TFields>
|
|
132
|
+
? Fragment<TItem, TFields>
|
|
133
|
+
: TSelector extends { readonly query: Fragment<infer TItem, infer TFields> }
|
|
134
|
+
? Fragment<TItem, TFields>
|
|
135
|
+
: never
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @internal
|
|
139
|
+
* Map a FieldSelection over a model type, computing the picked output type.
|
|
140
|
+
*/
|
|
141
|
+
type SelectedFields<TItem, TFields extends FieldSelection<TItem>> = {
|
|
142
|
+
[K in keyof TFields & keyof TItem]: [ExtractFragment<TFields[K]>] extends [never]
|
|
143
|
+
? // Scalar field (value is `true`) — tuple wrapping avoids the vacuous `never extends T` pitfall
|
|
144
|
+
TItem[K]
|
|
145
|
+
: // Relationship field — preserve array/null/undefined wrappers from the model
|
|
146
|
+
TItem[K] extends Array<unknown>
|
|
147
|
+
? ResultOf<ExtractFragment<TFields[K]>>[]
|
|
148
|
+
: null extends TItem[K]
|
|
149
|
+
? ResultOf<ExtractFragment<TFields[K]>> | null
|
|
150
|
+
: undefined extends TItem[K]
|
|
151
|
+
? ResultOf<ExtractFragment<TFields[K]>> | undefined
|
|
152
|
+
: ResultOf<ExtractFragment<TFields[K]>>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ─────────────────────────────────────────────────────────────
|
|
156
|
+
// Public type utilities
|
|
157
|
+
// ─────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Infer the TypeScript result type from a Fragment.
|
|
161
|
+
*
|
|
162
|
+
* Analogous to `gql.tada`'s `ResultOf` helper — given a fragment definition,
|
|
163
|
+
* `ResultOf` tells you exactly what shape you will receive at runtime.
|
|
164
|
+
*
|
|
165
|
+
* - Scalar fields selected with `true` retain their original Prisma type.
|
|
166
|
+
* - Relationship fields selected with a nested Fragment/RelationSelector are
|
|
167
|
+
* recursively narrowed.
|
|
168
|
+
* - Nullability and array wrappers from the original model type are preserved.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```ts
|
|
172
|
+
* type UserData = ResultOf<typeof userFragment>
|
|
173
|
+
* // → { id: string; name: string }
|
|
174
|
+
*
|
|
175
|
+
* type PostData = ResultOf<typeof postFragment>
|
|
176
|
+
* // → { id: string; title: string; author: { id: string; name: string } | null }
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
export type ResultOf<F> =
|
|
180
|
+
F extends Fragment<infer TItem, infer TFields> ? SelectedFields<TItem, TFields> : never
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Arguments accepted by {@link runQuery}.
|
|
184
|
+
*/
|
|
185
|
+
export type QueryArgs = {
|
|
186
|
+
/** Prisma where filter. The access control layer will additionally scope results. */
|
|
187
|
+
where?: Record<string, unknown>
|
|
188
|
+
/** Prisma orderBy clause. Pass a single object or an array for multi-column ordering. */
|
|
189
|
+
orderBy?: Record<string, 'asc' | 'desc'> | Array<Record<string, 'asc' | 'desc'>>
|
|
190
|
+
/** Maximum number of records to return. */
|
|
191
|
+
take?: number
|
|
192
|
+
/** Number of records to skip (for pagination). */
|
|
193
|
+
skip?: number
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Minimal context shape required by the query runners.
|
|
198
|
+
* Compatible with the full `AccessContext` produced by `getContext()`.
|
|
199
|
+
*/
|
|
200
|
+
export interface QueryRunnerContext {
|
|
201
|
+
db: {
|
|
202
|
+
[key: string]: {
|
|
203
|
+
findMany: (args?: unknown) => Promise<unknown[]>
|
|
204
|
+
findFirst: (args?: unknown) => Promise<unknown>
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ─────────────────────────────────────────────────────────────
|
|
210
|
+
// Fragment factory
|
|
211
|
+
// ─────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create a type-safe, reusable fragment for a given model type.
|
|
215
|
+
*
|
|
216
|
+
* The function is curried so that TypeScript can infer both the model type
|
|
217
|
+
* (from the explicit type parameter) and the field selection (from the
|
|
218
|
+
* argument), without requiring you to repeat yourself.
|
|
219
|
+
*
|
|
220
|
+
* @example Basic usage
|
|
221
|
+
* ```ts
|
|
222
|
+
* import type { User } from '.prisma/client'
|
|
223
|
+
* import { defineFragment } from '@opensaas/stack-core'
|
|
224
|
+
*
|
|
225
|
+
* export const userFragment = defineFragment<User>()({
|
|
226
|
+
* id: true,
|
|
227
|
+
* name: true,
|
|
228
|
+
* email: true,
|
|
229
|
+
* } as const)
|
|
230
|
+
* ```
|
|
231
|
+
*
|
|
232
|
+
* @example Compose fragments
|
|
233
|
+
* ```ts
|
|
234
|
+
* import type { Post } from '.prisma/client'
|
|
235
|
+
*
|
|
236
|
+
* export const postFragment = defineFragment<Post>()({
|
|
237
|
+
* id: true,
|
|
238
|
+
* title: true,
|
|
239
|
+
* author: userFragment,
|
|
240
|
+
* } as const)
|
|
241
|
+
* ```
|
|
242
|
+
*
|
|
243
|
+
* @example Nested filtering with RelationSelector
|
|
244
|
+
* ```ts
|
|
245
|
+
* export const postWithApprovedComments = defineFragment<Post>()({
|
|
246
|
+
* id: true,
|
|
247
|
+
* title: true,
|
|
248
|
+
* comments: {
|
|
249
|
+
* query: commentFragment,
|
|
250
|
+
* where: { approved: true },
|
|
251
|
+
* orderBy: { createdAt: 'desc' },
|
|
252
|
+
* take: 5,
|
|
253
|
+
* },
|
|
254
|
+
* } as const)
|
|
255
|
+
* ```
|
|
256
|
+
*
|
|
257
|
+
* @example Variables via factory function
|
|
258
|
+
* ```ts
|
|
259
|
+
* function makePostFragment(status: string) {
|
|
260
|
+
* return defineFragment<Post>()({
|
|
261
|
+
* id: true,
|
|
262
|
+
* comments: { query: commentFragment, where: { status } },
|
|
263
|
+
* } as const)
|
|
264
|
+
* }
|
|
265
|
+
* type PostData = ResultOf<ReturnType<typeof makePostFragment>>
|
|
266
|
+
*
|
|
267
|
+
* const posts = await context.db.post.findMany({
|
|
268
|
+
* query: makePostFragment('approved'),
|
|
269
|
+
* where: { published: true },
|
|
270
|
+
* })
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
export function defineFragment<TItem>() {
|
|
274
|
+
return function <TFields extends FieldSelection<TItem>>(
|
|
275
|
+
fields: TFields,
|
|
276
|
+
): Fragment<TItem, TFields> {
|
|
277
|
+
return { _type: 'fragment', _fields: fields }
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ─────────────────────────────────────────────────────────────
|
|
282
|
+
// Runtime helpers — exported for use in context/index.ts
|
|
283
|
+
// ─────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
/** @internal */
|
|
286
|
+
export function isFragment(value: unknown): value is Fragment<unknown, FieldSelection<unknown>> {
|
|
287
|
+
return (
|
|
288
|
+
value !== null &&
|
|
289
|
+
typeof value === 'object' &&
|
|
290
|
+
'_type' in value &&
|
|
291
|
+
(value as { _type: unknown })._type === 'fragment'
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Walk a field selection and build the Prisma `include` map needed to eagerly
|
|
297
|
+
* load all nested relationship fragments/selectors.
|
|
298
|
+
*
|
|
299
|
+
* Scalar fields (`true`) do not require an include entry — Prisma returns all
|
|
300
|
+
* scalar columns by default. Only relationship fields backed by a Fragment or
|
|
301
|
+
* RelationSelector generate include entries (recursively).
|
|
302
|
+
*
|
|
303
|
+
* Exported for use in `context/index.ts` when the `query` parameter is present.
|
|
304
|
+
* @internal
|
|
305
|
+
*/
|
|
306
|
+
export function buildInclude(fields: FieldSelection<unknown>): Record<string, unknown> | undefined {
|
|
307
|
+
const include: Record<string, unknown> = {}
|
|
308
|
+
let hasIncludes = false
|
|
309
|
+
|
|
310
|
+
for (const [key, value] of Object.entries(fields as Record<string, unknown>)) {
|
|
311
|
+
if (value === null || value === true || typeof value !== 'object') continue
|
|
312
|
+
|
|
313
|
+
const val = value as Record<string, unknown>
|
|
314
|
+
|
|
315
|
+
// ── Shorthand: Fragment directly ──────────────────────────
|
|
316
|
+
if (isFragment(val)) {
|
|
317
|
+
hasIncludes = true
|
|
318
|
+
const nestedInclude = buildInclude(val._fields as FieldSelection<unknown>)
|
|
319
|
+
include[key] = nestedInclude ? { include: nestedInclude } : true
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── RelationSelector: { query, where?, orderBy?, take?, skip? } ──
|
|
324
|
+
if ('query' in val && isFragment(val.query)) {
|
|
325
|
+
hasIncludes = true
|
|
326
|
+
const selector = val as {
|
|
327
|
+
query: Fragment<unknown, FieldSelection<unknown>>
|
|
328
|
+
where?: Record<string, unknown>
|
|
329
|
+
orderBy?: unknown
|
|
330
|
+
take?: number
|
|
331
|
+
skip?: number
|
|
332
|
+
}
|
|
333
|
+
const nestedInclude = buildInclude(selector.query._fields as FieldSelection<unknown>)
|
|
334
|
+
const includeEntry: Record<string, unknown> = {}
|
|
335
|
+
if (selector.where !== undefined) includeEntry.where = selector.where
|
|
336
|
+
if (selector.orderBy !== undefined) includeEntry.orderBy = selector.orderBy
|
|
337
|
+
if (selector.take !== undefined) includeEntry.take = selector.take
|
|
338
|
+
if (selector.skip !== undefined) includeEntry.skip = selector.skip
|
|
339
|
+
if (nestedInclude) includeEntry.include = nestedInclude
|
|
340
|
+
include[key] = Object.keys(includeEntry).length > 0 ? includeEntry : true
|
|
341
|
+
continue
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return hasIncludes ? include : undefined
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Recursively pick only the fields requested by a fragment from a raw Prisma
|
|
350
|
+
* result object. This ensures the runtime shape exactly matches the type
|
|
351
|
+
* produced by `ResultOf<F>`.
|
|
352
|
+
*
|
|
353
|
+
* Exported for use in `context/index.ts`.
|
|
354
|
+
* @internal
|
|
355
|
+
*/
|
|
356
|
+
export function pickFields<TItem, TFields extends FieldSelection<TItem>>(
|
|
357
|
+
item: TItem,
|
|
358
|
+
fields: TFields,
|
|
359
|
+
): SelectedFields<TItem, TFields> {
|
|
360
|
+
const result: Record<string, unknown> = {}
|
|
361
|
+
|
|
362
|
+
for (const [key, value] of Object.entries(fields as Record<string, unknown>)) {
|
|
363
|
+
const fieldValue = (item as Record<string, unknown>)[key]
|
|
364
|
+
|
|
365
|
+
if (value === true) {
|
|
366
|
+
result[key] = fieldValue
|
|
367
|
+
continue
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (value === null || typeof value !== 'object') continue
|
|
371
|
+
|
|
372
|
+
const val = value as Record<string, unknown>
|
|
373
|
+
|
|
374
|
+
// ── Shorthand: Fragment directly ──────────────────────────
|
|
375
|
+
if (isFragment(val)) {
|
|
376
|
+
if (Array.isArray(fieldValue)) {
|
|
377
|
+
result[key] = fieldValue.map((elem) =>
|
|
378
|
+
pickFields(elem as unknown, val._fields as FieldSelection<unknown>),
|
|
379
|
+
)
|
|
380
|
+
} else if (fieldValue === null || fieldValue === undefined) {
|
|
381
|
+
result[key] = fieldValue
|
|
382
|
+
} else {
|
|
383
|
+
result[key] = pickFields(fieldValue as unknown, val._fields as FieldSelection<unknown>)
|
|
384
|
+
}
|
|
385
|
+
continue
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ── RelationSelector: { query, where?, ... } ──────────────
|
|
389
|
+
if ('query' in val && isFragment(val.query)) {
|
|
390
|
+
const nestedFrag = val.query as Fragment<unknown, FieldSelection<unknown>>
|
|
391
|
+
if (Array.isArray(fieldValue)) {
|
|
392
|
+
result[key] = fieldValue.map((elem) =>
|
|
393
|
+
pickFields(elem as unknown, nestedFrag._fields as FieldSelection<unknown>),
|
|
394
|
+
)
|
|
395
|
+
} else if (fieldValue === null || fieldValue === undefined) {
|
|
396
|
+
result[key] = fieldValue
|
|
397
|
+
} else {
|
|
398
|
+
result[key] = pickFields(
|
|
399
|
+
fieldValue as unknown,
|
|
400
|
+
nestedFrag._fields as FieldSelection<unknown>,
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
continue
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return result as SelectedFields<TItem, TFields>
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ─────────────────────────────────────────────────────────────
|
|
411
|
+
// Standalone query runners
|
|
412
|
+
// ─────────────────────────────────────────────────────────────
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Execute a fragment-based query against a list, returning all matching
|
|
416
|
+
* records shaped to the fragment's field selection.
|
|
417
|
+
*
|
|
418
|
+
* Under the hood this calls `context.db[listKey].findMany()`, so all access
|
|
419
|
+
* control rules defined in your config are still enforced.
|
|
420
|
+
*
|
|
421
|
+
* **Tip:** You can also call `context.db.post.findMany({ query: fragment, ... })`
|
|
422
|
+
* directly — both forms produce the same result.
|
|
423
|
+
*
|
|
424
|
+
* @param context - An `AccessContext` (or any object with a compatible `db`).
|
|
425
|
+
* @param listKey - The PascalCase list name (e.g. `'Post'`, `'BlogPost'`).
|
|
426
|
+
* @param fragment - A fragment created with {@link defineFragment}.
|
|
427
|
+
* @param args - Optional query arguments (where, orderBy, take, skip).
|
|
428
|
+
* @returns An array typed to exactly the fragment's field selection.
|
|
429
|
+
*
|
|
430
|
+
* @example
|
|
431
|
+
* ```ts
|
|
432
|
+
* const posts = await runQuery(context, 'Post', postFragment, {
|
|
433
|
+
* where: { published: true },
|
|
434
|
+
* orderBy: { createdAt: 'desc' },
|
|
435
|
+
* take: 10,
|
|
436
|
+
* })
|
|
437
|
+
* // posts: Array<ResultOf<typeof postFragment>>
|
|
438
|
+
* ```
|
|
439
|
+
*/
|
|
440
|
+
export async function runQuery<TItem, TFields extends FieldSelection<TItem>>(
|
|
441
|
+
context: QueryRunnerContext,
|
|
442
|
+
listKey: string,
|
|
443
|
+
fragment: Fragment<TItem, TFields>,
|
|
444
|
+
args?: QueryArgs,
|
|
445
|
+
): Promise<SelectedFields<TItem, TFields>[]> {
|
|
446
|
+
const dbKey = getDbKey(listKey)
|
|
447
|
+
const include = buildInclude(fragment._fields as FieldSelection<unknown>)
|
|
448
|
+
|
|
449
|
+
const findManyArgs: Record<string, unknown> = {}
|
|
450
|
+
if (args?.where !== undefined) findManyArgs.where = args.where
|
|
451
|
+
if (args?.orderBy !== undefined) findManyArgs.orderBy = args.orderBy
|
|
452
|
+
if (args?.take !== undefined) findManyArgs.take = args.take
|
|
453
|
+
if (args?.skip !== undefined) findManyArgs.skip = args.skip
|
|
454
|
+
if (include) findManyArgs.include = include
|
|
455
|
+
|
|
456
|
+
const results = await context.db[dbKey].findMany(
|
|
457
|
+
Object.keys(findManyArgs).length > 0 ? findManyArgs : undefined,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
return results.map((item) => pickFields(item as TItem, fragment._fields)) as SelectedFields<
|
|
461
|
+
TItem,
|
|
462
|
+
TFields
|
|
463
|
+
>[]
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Execute a fragment-based query that returns a single record (or `null`).
|
|
468
|
+
*
|
|
469
|
+
* Under the hood this calls `context.db[listKey].findFirst()`, so all access
|
|
470
|
+
* control rules are still enforced.
|
|
471
|
+
*
|
|
472
|
+
* **Tip:** You can also call `context.db.post.findUnique({ where: { id }, query: fragment })`
|
|
473
|
+
* directly.
|
|
474
|
+
*
|
|
475
|
+
* @param context - An `AccessContext` (or any object with a compatible `db`).
|
|
476
|
+
* @param listKey - The PascalCase list name (e.g. `'Post'`).
|
|
477
|
+
* @param fragment - A fragment created with {@link defineFragment}.
|
|
478
|
+
* @param where - A Prisma where clause to identify the record.
|
|
479
|
+
* @returns The matched record shaped to the fragment, or `null`.
|
|
480
|
+
*
|
|
481
|
+
* @example
|
|
482
|
+
* ```ts
|
|
483
|
+
* const post = await runQueryOne(context, 'Post', postFragment, { id: postId })
|
|
484
|
+
* if (!post) return notFound()
|
|
485
|
+
* // post: ResultOf<typeof postFragment>
|
|
486
|
+
* ```
|
|
487
|
+
*/
|
|
488
|
+
export async function runQueryOne<TItem, TFields extends FieldSelection<TItem>>(
|
|
489
|
+
context: QueryRunnerContext,
|
|
490
|
+
listKey: string,
|
|
491
|
+
fragment: Fragment<TItem, TFields>,
|
|
492
|
+
where: Record<string, unknown>,
|
|
493
|
+
): Promise<SelectedFields<TItem, TFields> | null> {
|
|
494
|
+
const dbKey = getDbKey(listKey)
|
|
495
|
+
const include = buildInclude(fragment._fields as FieldSelection<unknown>)
|
|
496
|
+
|
|
497
|
+
const findFirstArgs: Record<string, unknown> = { where }
|
|
498
|
+
if (include) findFirstArgs.include = include
|
|
499
|
+
|
|
500
|
+
const item = await context.db[dbKey].findFirst(findFirstArgs)
|
|
501
|
+
|
|
502
|
+
if (item === null || item === undefined) return null
|
|
503
|
+
|
|
504
|
+
return pickFields(item as TItem, fragment._fields) as SelectedFields<TItem, TFields>
|
|
505
|
+
}
|