@opensaas/stack-core 0.39.2 → 0.40.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 +150 -0
- package/CLAUDE.md +51 -0
- package/dist/access/access-filter.d.ts +83 -10
- package/dist/access/access-filter.d.ts.map +1 -1
- package/dist/access/access-filter.js +139 -18
- package/dist/access/access-filter.js.map +1 -1
- package/dist/access/access-filter.test.js +224 -12
- package/dist/access/access-filter.test.js.map +1 -1
- package/dist/access/engine.d.ts +43 -1
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +58 -0
- package/dist/access/engine.js.map +1 -1
- package/dist/access/engine.test.d.ts +2 -0
- package/dist/access/engine.test.d.ts.map +1 -0
- package/dist/access/engine.test.js +56 -0
- package/dist/access/engine.test.js.map +1 -0
- package/dist/access/errors.d.ts +28 -0
- package/dist/access/errors.d.ts.map +1 -1
- package/dist/access/errors.js +38 -2
- package/dist/access/errors.js.map +1 -1
- package/dist/access/field-access.d.ts +12 -0
- package/dist/access/field-access.d.ts.map +1 -1
- package/dist/access/field-access.js +22 -2
- package/dist/access/field-access.js.map +1 -1
- package/dist/access/field-access.test.js +66 -0
- package/dist/access/field-access.test.js.map +1 -1
- package/dist/access/field-visibility.d.ts +2 -1
- package/dist/access/field-visibility.d.ts.map +1 -1
- package/dist/access/field-visibility.js +45 -3
- package/dist/access/field-visibility.js.map +1 -1
- package/dist/access/index.d.ts +5 -2
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +4 -2
- package/dist/access/index.js.map +1 -1
- package/dist/config/types.d.ts +76 -10
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/index.d.ts +19 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +61 -17
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +54 -13
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/context/write-pipeline.d.ts +3 -2
- package/dist/context/write-pipeline.d.ts.map +1 -1
- package/dist/context/write-pipeline.js +8 -5
- package/dist/context/write-pipeline.js.map +1 -1
- package/dist/fields/index.d.ts +13 -1
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +50 -5
- package/dist/fields/index.js.map +1 -1
- package/dist/fields/relationship-foreign-key.test.d.ts +2 -0
- package/dist/fields/relationship-foreign-key.test.d.ts.map +1 -0
- package/dist/fields/relationship-foreign-key.test.js +18 -0
- package/dist/fields/relationship-foreign-key.test.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/case-utils.d.ts +15 -0
- package/dist/lib/case-utils.d.ts.map +1 -1
- package/dist/lib/case-utils.js +21 -0
- package/dist/lib/case-utils.js.map +1 -1
- package/dist/lib/case-utils.test.js +23 -1
- package/dist/lib/case-utils.test.js.map +1 -1
- package/dist/lib/prisma-errors.d.ts +38 -0
- package/dist/lib/prisma-errors.d.ts.map +1 -0
- package/dist/lib/prisma-errors.js +53 -0
- package/dist/lib/prisma-errors.js.map +1 -0
- package/dist/mcp/constants.d.ts +23 -0
- package/dist/mcp/constants.d.ts.map +1 -0
- package/dist/mcp/constants.js +23 -0
- package/dist/mcp/constants.js.map +1 -0
- package/dist/mcp/field-schema.d.ts +8 -0
- package/dist/mcp/field-schema.d.ts.map +1 -0
- package/dist/mcp/field-schema.js +69 -0
- package/dist/mcp/field-schema.js.map +1 -0
- package/dist/mcp/handler.d.ts.map +1 -1
- package/dist/mcp/handler.js +76 -90
- package/dist/mcp/handler.js.map +1 -1
- package/dist/mcp/projection.d.ts +62 -0
- package/dist/mcp/projection.d.ts.map +1 -0
- package/dist/mcp/projection.js +390 -0
- package/dist/mcp/projection.js.map +1 -0
- package/dist/utils/password.d.ts +3 -1
- package/dist/utils/password.d.ts.map +1 -1
- package/dist/utils/password.js +4 -2
- package/dist/utils/password.js.map +1 -1
- package/package.json +1 -1
- package/src/access/access-filter.test.ts +278 -12
- package/src/access/access-filter.ts +207 -19
- package/src/access/engine.test.ts +67 -0
- package/src/access/engine.ts +81 -1
- package/src/access/errors.ts +42 -2
- package/src/access/field-access.test.ts +78 -0
- package/src/access/field-access.ts +34 -2
- package/src/access/field-visibility.ts +75 -10
- package/src/access/index.ts +14 -0
- package/src/config/types.ts +76 -10
- package/src/context/index.ts +95 -15
- package/src/context/nested-operations.ts +61 -16
- package/src/context/write-pipeline.ts +8 -4
- package/src/fields/index.ts +55 -5
- package/src/fields/relationship-foreign-key.test.ts +29 -0
- package/src/index.ts +17 -1
- package/src/lib/case-utils.test.ts +32 -0
- package/src/lib/case-utils.ts +26 -0
- package/src/lib/prisma-errors.ts +86 -0
- package/src/mcp/constants.ts +24 -0
- package/src/mcp/field-schema.ts +84 -0
- package/src/mcp/handler.ts +109 -107
- package/src/mcp/projection.ts +497 -0
- package/src/utils/password.ts +5 -3
- package/tests/access-relationships.test.ts +3 -3
- package/tests/access.test.ts +83 -0
- package/tests/context.test.ts +259 -12
- package/tests/field-types.test.ts +113 -0
- package/tests/mcp-fields-projection-access.test.ts +250 -0
- package/tests/mcp-handler.test.ts +695 -4
- package/tests/nested-access-and-hooks.test.ts +65 -0
- package/tests/nested-write-synthetic-relation.test.ts +437 -0
- package/tests/password-types.test.ts +28 -0
- package/tests/password.test.ts +16 -2
- package/tests/prisma-errors.test.ts +141 -0
- package/tests/singleton.test.ts +9 -1
- package/tests/to-one-include-access-filter.test.ts +172 -0
- package/tests/with-session.test.ts +243 -0
- package/tests/write-pipeline.test.ts +29 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Session, AccessContext } from './types.js'
|
|
2
2
|
import type { FieldAccess, FieldAccessControl } from './types.js'
|
|
3
|
+
import type { OpenSaasConfig } from '../config/types.js'
|
|
3
4
|
// `ValidationError` is referenced only inside function bodies (call-time), never
|
|
4
5
|
// at module-evaluation time, so the field-access ⇄ hooks import cycle is safe
|
|
5
6
|
// under ESM live bindings.
|
|
6
7
|
import { ValidationError } from '../hooks/index.js'
|
|
7
8
|
import { InvalidFieldAccessResultError } from './errors.js'
|
|
9
|
+
import { resolveSyntheticReverseRelation } from './engine.js'
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Marks a throw caused by touching {@link createPoisonedItem}'s `item`, as
|
|
@@ -196,6 +198,17 @@ export async function filterWritableFields<T extends Record<string, unknown>>(
|
|
|
196
198
|
item?: Record<string, unknown>
|
|
197
199
|
context: AccessContext & { _isSudo?: boolean }
|
|
198
200
|
inputData?: Record<string, unknown>
|
|
201
|
+
/**
|
|
202
|
+
* The list being written and the full config — used ONLY to recognise a
|
|
203
|
+
* synthetic reverse-relation key (`from_<List>_<field>`, #978) among the
|
|
204
|
+
* undeclared keys sudo would otherwise pass through unchecked. Both
|
|
205
|
+
* production call sites (the write pipeline, nested-operations) supply
|
|
206
|
+
* these; a direct unit test that omits them keeps the pre-#978 sudo
|
|
207
|
+
* behaviour of passing any undeclared key through, since it has no config
|
|
208
|
+
* to resolve a synthetic key against.
|
|
209
|
+
*/
|
|
210
|
+
listName?: string
|
|
211
|
+
config?: OpenSaasConfig
|
|
199
212
|
},
|
|
200
213
|
): Promise<Partial<T>> {
|
|
201
214
|
const filtered: Record<string, unknown> = {}
|
|
@@ -286,10 +299,29 @@ export async function filterWritableFields<T extends Record<string, unknown>>(
|
|
|
286
299
|
// declares (e.g. back-relations like `from_Enrolment_student`), so allowing
|
|
287
300
|
// an undeclared key to pass through lets a non-sudo caller drive ungated
|
|
288
301
|
// nested writes on undeclared back-relations. Mirror Keystone's
|
|
289
|
-
// GraphQL-schema behaviour and reject it.
|
|
290
|
-
// bypass, so undeclared keys still pass through under sudo.
|
|
302
|
+
// GraphQL-schema behaviour and reject it.
|
|
291
303
|
if (!fieldConfig) {
|
|
292
304
|
if (isSudo) {
|
|
305
|
+
// #978 — sudo bypasses ACCESS CONTROL, not the hooks/validation a
|
|
306
|
+
// recognised relation is entitled to. A synthetic reverse-relation key
|
|
307
|
+
// (a list-only ref's back-relation) is handed to the caller unchanged
|
|
308
|
+
// so processNestedOperations can run its target list's full pipeline,
|
|
309
|
+
// exactly as it would for a declared relationship field. Any other
|
|
310
|
+
// undeclared key has no such route to hooks — passing it straight to
|
|
311
|
+
// Prisma is the same silent-bypass shape this issue closed for
|
|
312
|
+
// relations, so it is refused even under sudo. `listName`/`config`
|
|
313
|
+
// are omitted only by direct unit tests of this function, which keep
|
|
314
|
+
// the pre-#978 blanket sudo passthrough since they have no config to
|
|
315
|
+
// resolve a synthetic key against.
|
|
316
|
+
if (args.listName && args.config) {
|
|
317
|
+
const synthetic = resolveSyntheticReverseRelation(fieldName, args.listName, args.config)
|
|
318
|
+
if (!synthetic) {
|
|
319
|
+
throw new ValidationError([
|
|
320
|
+
`Cannot ${operation} "${fieldName}": it is not a field of this list. ` +
|
|
321
|
+
`Undeclared data keys are rejected, even under sudo.`,
|
|
322
|
+
])
|
|
323
|
+
}
|
|
324
|
+
}
|
|
293
325
|
filtered[fieldName] = value
|
|
294
326
|
continue
|
|
295
327
|
}
|
|
@@ -7,6 +7,8 @@ import { ResolveOutputCycleError } from './errors.js'
|
|
|
7
7
|
import type { DeclaredOnlyTree } from './declared-dependencies.js'
|
|
8
8
|
import { emptyDeclaredOnlyTree } from './declared-dependencies.js'
|
|
9
9
|
import type { FieldSelectionScope } from '../query/index.js'
|
|
10
|
+
import type { ToOneAccessVisibilityTree } from './access-filter.js'
|
|
11
|
+
import { emptyToOneAccessVisibilityTree } from './access-filter.js'
|
|
10
12
|
// NOTE: `context/index.ts` imports `filterReadableFields` from this module
|
|
11
13
|
// (via the `access/index.ts` barrel) — this is an intentional cyclic
|
|
12
14
|
// dependency, the same shape and for the same reason as the one documented in
|
|
@@ -36,6 +38,22 @@ import { buildDbDelegate } from '../context/index.js'
|
|
|
36
38
|
* Phase 1 (pre-query row/relation scoping) lives in `access-filter.ts`. See
|
|
37
39
|
* `docs/adr/0001-access-control-is-a-two-phase-read.md` and the access-control
|
|
38
40
|
* glossary in `CONTEXT.md`.
|
|
41
|
+
*
|
|
42
|
+
* **To-one relation nulling (issue #974).** A to-one relation whose related
|
|
43
|
+
* list's `query` access resolves to a filter cannot be scoped by Prisma's own
|
|
44
|
+
* `where` (it only accepts one on a to-many include — see the "To-one
|
|
45
|
+
* relations" section of `access-filter.ts`'s module doc), so
|
|
46
|
+
* `buildAccessScopedInclude` fetches it unscoped and hands this module a
|
|
47
|
+
* `ToOneAccessVisibilityTree` — already resolved, via one batched existence
|
|
48
|
+
* check per relation across the whole read, by
|
|
49
|
+
* `resolveToOneAccessVisibility`. This module is where that resolution
|
|
50
|
+
* actually becomes the caller-visible `null`: a `kind: 'denied'` key is
|
|
51
|
+
* forced to `null` even though it was never fetched at all (the key is
|
|
52
|
+
* absent from `workingItem`), and a `kind: 'visible'` key's fetched row is
|
|
53
|
+
* nulled out unless its id survived the existence check — in both branches,
|
|
54
|
+
* before any field-level access check or `resolveOutput` hook runs, so the
|
|
55
|
+
* rest of the pipeline sees exactly what a denied to-one read has always
|
|
56
|
+
* meant elsewhere: `null`, never a thrown error.
|
|
39
57
|
*/
|
|
40
58
|
|
|
41
59
|
type ResolveOutputHookRuntime = (args: {
|
|
@@ -193,6 +211,11 @@ export async function filterReadableFields<T extends Record<string, unknown>>(
|
|
|
193
211
|
// The fragment scope this level was reached under (ADR-0027, see module doc
|
|
194
212
|
// above), and the same tree one level down for each nested relation.
|
|
195
213
|
selection?: FieldSelectionScope,
|
|
214
|
+
// Resolved to-one existence checks at THIS level (issue #974, see module
|
|
215
|
+
// doc above), and the same tree one level down for each nested relation —
|
|
216
|
+
// regardless of that relation's own arity, since a filtered to-one can sit
|
|
217
|
+
// beneath a to-many hop.
|
|
218
|
+
toOneVisibility: ToOneAccessVisibilityTree = emptyToOneAccessVisibilityTree(),
|
|
196
219
|
): Promise<Partial<T>> {
|
|
197
220
|
const filtered: Record<string, unknown> = {}
|
|
198
221
|
|
|
@@ -286,6 +309,15 @@ export async function filterReadableFields<T extends Record<string, unknown>>(
|
|
|
286
309
|
// unrestricted — matching what naming a relation without narrowing it
|
|
287
310
|
// further has always meant.
|
|
288
311
|
const nestedSelection = selection?.nested[fieldName]
|
|
312
|
+
// This relation's own resolved to-one visibility, if
|
|
313
|
+
// `buildAccessScopedInclude` flagged anything beneath it (issue #974).
|
|
314
|
+
// Falls back to empty — the common case for a relation with no
|
|
315
|
+
// filtered to-one anywhere in its own nested include.
|
|
316
|
+
const nestedToOneVisibility =
|
|
317
|
+
toOneVisibility.nested[fieldName] ?? emptyToOneAccessVisibilityTree()
|
|
318
|
+
// This key's OWN to-one existence check, if `fieldName` itself is a
|
|
319
|
+
// filtered to-one relation (as opposed to one further down its tree).
|
|
320
|
+
const toOneEntry = toOneVisibility.filters[fieldName]
|
|
289
321
|
|
|
290
322
|
if (relatedConfig) {
|
|
291
323
|
if (Array.isArray(value)) {
|
|
@@ -300,20 +332,28 @@ export async function filterReadableFields<T extends Record<string, unknown>>(
|
|
|
300
332
|
relatedConfig.listName,
|
|
301
333
|
nestedDeclaredOnly,
|
|
302
334
|
nestedSelection,
|
|
335
|
+
nestedToOneVisibility,
|
|
303
336
|
),
|
|
304
337
|
),
|
|
305
338
|
)
|
|
306
339
|
} else if (typeof value === 'object') {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
340
|
+
const relatedId = (value as Record<string, unknown>).id
|
|
341
|
+
const isVisible =
|
|
342
|
+
!toOneEntry || toOneEntry.kind !== 'visible' || toOneEntry.ids.has(String(relatedId))
|
|
343
|
+
|
|
344
|
+
filtered[fieldName] = isVisible
|
|
345
|
+
? await filterReadableFields(
|
|
346
|
+
value as Record<string, unknown>,
|
|
347
|
+
relatedConfig.listConfig.fields,
|
|
348
|
+
args,
|
|
349
|
+
config,
|
|
350
|
+
depth + 1,
|
|
351
|
+
relatedConfig.listName,
|
|
352
|
+
nestedDeclaredOnly,
|
|
353
|
+
nestedSelection,
|
|
354
|
+
nestedToOneVisibility,
|
|
355
|
+
)
|
|
356
|
+
: null
|
|
317
357
|
}
|
|
318
358
|
} else {
|
|
319
359
|
filtered[fieldName] = value
|
|
@@ -341,6 +381,31 @@ export async function filterReadableFields<T extends Record<string, unknown>>(
|
|
|
341
381
|
}
|
|
342
382
|
}
|
|
343
383
|
|
|
384
|
+
// To-one relations `buildAccessScopedInclude` denied outright (issue #974)
|
|
385
|
+
// were never asked of Prisma at all, so `fieldName` has no entry in
|
|
386
|
+
// `workingItem` and the loop above never visits it. Force it present as an
|
|
387
|
+
// explicit `null` here — matching what a denied single-record read means
|
|
388
|
+
// everywhere else in the context — rather than leaving the key silently
|
|
389
|
+
// absent.
|
|
390
|
+
for (const [fieldName, entry] of Object.entries(toOneVisibility.filters)) {
|
|
391
|
+
if (entry.kind !== 'denied') continue
|
|
392
|
+
if (fieldName in filtered || fieldName in workingItem) continue
|
|
393
|
+
if (selection?.fields && !selection.fields.has(fieldName)) continue
|
|
394
|
+
|
|
395
|
+
const fieldConfig = fieldConfigs[fieldName]
|
|
396
|
+
const canRead = await checkFieldAccess(fieldConfig?.access, 'read', {
|
|
397
|
+
...args,
|
|
398
|
+
item: workingItem,
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
if (!canRead) {
|
|
402
|
+
accessDeniedKeys.add(fieldName)
|
|
403
|
+
continue
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
filtered[fieldName] = null
|
|
407
|
+
}
|
|
408
|
+
|
|
344
409
|
// The item a virtual field's hook sees: stored columns and fetched
|
|
345
410
|
// relations (from `workingItem`, never a resolved value — no hook's output
|
|
346
411
|
// is ever written back into `workingItem`). A key is visible here if it
|
package/src/access/index.ts
CHANGED
|
@@ -14,11 +14,14 @@ export type {
|
|
|
14
14
|
// Operation-level access primitives and shared ref-parsing helper.
|
|
15
15
|
export {
|
|
16
16
|
checkAccess,
|
|
17
|
+
checkCreateAccess,
|
|
17
18
|
mergeFilters,
|
|
18
19
|
isBoolean,
|
|
19
20
|
isPrismaFilter,
|
|
20
21
|
getRelatedListConfig,
|
|
22
|
+
resolveSyntheticReverseRelation,
|
|
21
23
|
} from './engine.js'
|
|
24
|
+
export type { SyntheticReverseRelation } from './engine.js'
|
|
22
25
|
// Canonical field-level access evaluation (shared by read and write paths).
|
|
23
26
|
export {
|
|
24
27
|
checkFieldAccess,
|
|
@@ -36,6 +39,15 @@ export {
|
|
|
36
39
|
buildAccessScopedInclude,
|
|
37
40
|
buildAccessScopedWhere,
|
|
38
41
|
stripVirtualFieldsFromInclude,
|
|
42
|
+
resolveToOneAccessVisibility,
|
|
43
|
+
emptyToOneAccessFilterTree,
|
|
44
|
+
emptyToOneAccessVisibilityTree,
|
|
45
|
+
} from './access-filter.js'
|
|
46
|
+
export type {
|
|
47
|
+
ToOneAccessFilterTree,
|
|
48
|
+
ToOneAccessFilterEntry,
|
|
49
|
+
ToOneAccessVisibilityTree,
|
|
50
|
+
ToOneVisibility,
|
|
39
51
|
} from './access-filter.js'
|
|
40
52
|
// Phase 2 — Field Visibility (post-query field stripping + resolveOutput).
|
|
41
53
|
export { filterReadableFields } from './field-visibility.js'
|
|
@@ -53,5 +65,7 @@ export { AccessScopeDepthExceededError } from './errors.js'
|
|
|
53
65
|
export { ResolveOutputCycleError } from './errors.js'
|
|
54
66
|
// Thrown when a field-level access control function returns a non-boolean result.
|
|
55
67
|
export { InvalidFieldAccessResultError } from './errors.js'
|
|
68
|
+
// Thrown when operation-level `create` access control returns a non-boolean result (#1009).
|
|
69
|
+
export { InvalidCreateAccessResultError } from './errors.js'
|
|
56
70
|
// Thrown when a relation filter's related list denies query access outright (#916).
|
|
57
71
|
export { RelationFilterAccessDeniedError } from './errors.js'
|
package/src/config/types.ts
CHANGED
|
@@ -591,6 +591,32 @@ export type BaseFieldConfig<TTypeInfo extends TypeInfo> = {
|
|
|
591
591
|
* ```
|
|
592
592
|
*/
|
|
593
593
|
description?: string
|
|
594
|
+
/**
|
|
595
|
+
* Whether this field belongs in a list/related-list table's DEFAULT
|
|
596
|
+
* column set (issue #1018) — the columns shown when nothing explicitly
|
|
597
|
+
* names them (`ui.listView.initialColumns` on the list, or a
|
|
598
|
+
* relationship's own `ui.itemView.columns`). Naming the field explicitly
|
|
599
|
+
* in either of those always shows it regardless of this flag; it governs
|
|
600
|
+
* only what appears absent an explicit column list.
|
|
601
|
+
*
|
|
602
|
+
* This is a PRESENTATION default, not an access control — a field can be
|
|
603
|
+
* read-denied and still default to `true` here (it simply renders empty
|
|
604
|
+
* for a viewer who can't read it), and setting this to `false` hides a
|
|
605
|
+
* column without restricting who can read the underlying value. The
|
|
606
|
+
* field-level `access.read` deny remains the only real boundary.
|
|
607
|
+
*
|
|
608
|
+
* @default true
|
|
609
|
+
*
|
|
610
|
+
* @example Hide an internal field from default table views without denying read access
|
|
611
|
+
* ```typescript
|
|
612
|
+
* fields: {
|
|
613
|
+
* internalScore: integer({ ui: { listView: { defaultColumn: false } } }),
|
|
614
|
+
* }
|
|
615
|
+
* ```
|
|
616
|
+
*/
|
|
617
|
+
listView?: {
|
|
618
|
+
defaultColumn?: boolean
|
|
619
|
+
}
|
|
594
620
|
/**
|
|
595
621
|
* Transform field value before sending to client (browser)
|
|
596
622
|
* Useful for sensitive fields (e.g., passwords) or complex data structures
|
|
@@ -844,6 +870,7 @@ export type IntegerField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfi
|
|
|
844
870
|
min?: number
|
|
845
871
|
max?: number
|
|
846
872
|
}
|
|
873
|
+
isIndexed?: boolean | 'unique'
|
|
847
874
|
}
|
|
848
875
|
|
|
849
876
|
export type DecimalField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig<TTypeInfo> & {
|
|
@@ -885,6 +912,7 @@ export type CheckboxField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConf
|
|
|
885
912
|
export type TimestampField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig<TTypeInfo> & {
|
|
886
913
|
type: 'timestamp'
|
|
887
914
|
defaultValue?: { kind: 'now' } | Date
|
|
915
|
+
isIndexed?: boolean | 'unique'
|
|
888
916
|
}
|
|
889
917
|
|
|
890
918
|
export type CalendarDayField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig<TTypeInfo> & {
|
|
@@ -989,6 +1017,7 @@ export type SelectField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig
|
|
|
989
1017
|
validation?: {
|
|
990
1018
|
isRequired?: boolean
|
|
991
1019
|
}
|
|
1020
|
+
isIndexed?: boolean | 'unique'
|
|
992
1021
|
ui?: {
|
|
993
1022
|
displayMode?: 'select' | 'segmented-control' | 'radio'
|
|
994
1023
|
}
|
|
@@ -1015,8 +1044,10 @@ export type RelationshipItemViewConfig = {
|
|
|
1015
1044
|
* The related list's fields to show as Relationship-table columns, in order.
|
|
1016
1045
|
*
|
|
1017
1046
|
* When omitted, the columns default to the related list's own column
|
|
1018
|
-
* curation (`ui.listView.initialColumns`, else
|
|
1019
|
-
*
|
|
1047
|
+
* curation (`ui.listView.initialColumns`, else every field whose own
|
|
1048
|
+
* `ui.listView.defaultColumn` declaration holds — see {@link
|
|
1049
|
+
* BaseFieldConfig.ui}) minus the back-reference field that points at the
|
|
1050
|
+
* parent record.
|
|
1020
1051
|
*
|
|
1021
1052
|
* @example
|
|
1022
1053
|
* ```typescript
|
|
@@ -1133,13 +1164,20 @@ export type RelationshipField<TTypeInfo extends TypeInfo = TypeInfo> =
|
|
|
1133
1164
|
*/
|
|
1134
1165
|
isNullable?: boolean
|
|
1135
1166
|
/**
|
|
1136
|
-
* Controls foreign key placement and column name
|
|
1137
|
-
* Can be a boolean or an object with a map property
|
|
1138
|
-
* Only valid on single (non-many) relationships
|
|
1139
|
-
* Cannot be true on both sides of a one-to-one relationship
|
|
1167
|
+
* Controls foreign key placement and column name.
|
|
1168
|
+
* Can be a boolean or an object with a map property.
|
|
1169
|
+
* Only valid on single (non-many) relationships.
|
|
1170
|
+
* Cannot be true on both sides of a one-to-one relationship.
|
|
1140
1171
|
*
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
1172
|
+
* The boolean form (the "which side owns the foreign key" sense) is only
|
|
1173
|
+
* meaningful on a bidirectional ref (`ref: 'ListName.fieldName'`) — a
|
|
1174
|
+
* list-only ref (`ref: 'ListName'`) always owns the foreign key, so a
|
|
1175
|
+
* boolean here is rejected. The `{ map }` form (the column-name sense)
|
|
1176
|
+
* works on both: it renames the foreign key column without changing
|
|
1177
|
+
* ownership.
|
|
1178
|
+
*
|
|
1179
|
+
* When a boolean, defaults the foreign key column name to the field name.
|
|
1180
|
+
* When an object with map, uses the provided column name.
|
|
1143
1181
|
*
|
|
1144
1182
|
* @example
|
|
1145
1183
|
* ```typescript
|
|
@@ -1164,6 +1202,14 @@ export type RelationshipField<TTypeInfo extends TypeInfo = TypeInfo> =
|
|
|
1164
1202
|
* user: relationship({ ref: 'User.account' }) // No foreign key on this side
|
|
1165
1203
|
* }
|
|
1166
1204
|
* })
|
|
1205
|
+
*
|
|
1206
|
+
* // List-only ref: rename the foreign key column (ownership is implicit)
|
|
1207
|
+
* Post: list({
|
|
1208
|
+
* fields: {
|
|
1209
|
+
* category: relationship({ ref: 'Category', db: { foreignKey: { map: 'category_id' } } })
|
|
1210
|
+
* // Generates: categoryId String? @map("category_id")
|
|
1211
|
+
* }
|
|
1212
|
+
* })
|
|
1167
1213
|
* ```
|
|
1168
1214
|
*/
|
|
1169
1215
|
foreignKey?: boolean | { map?: string }
|
|
@@ -1488,6 +1534,18 @@ export interface TypeInfo<
|
|
|
1488
1534
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1489
1535
|
export type OperationAccess<T = any> = {
|
|
1490
1536
|
query?: AccessControl<T>
|
|
1537
|
+
/**
|
|
1538
|
+
* Shares `AccessControl`'s signature (so a filter still type-checks here),
|
|
1539
|
+
* but at runtime `create` accepts a `boolean` result only. There is no
|
|
1540
|
+
* existing row to scope with a filter, and — unlike `update`/`delete`,
|
|
1541
|
+
* which re-check a returned filter against the target row via
|
|
1542
|
+
* `findFirst` — no equivalent re-check exists for a row that doesn't exist
|
|
1543
|
+
* in the database yet. A rule that returns a filter (or any other
|
|
1544
|
+
* non-boolean) throws `InvalidCreateAccessResultError` rather than being
|
|
1545
|
+
* treated as an allow (see #1009, ADR-0022, ADR-0030). To scope create by
|
|
1546
|
+
* ownership, evaluate the condition in a `resolveInput`/`validate` hook,
|
|
1547
|
+
* where the input data is in scope.
|
|
1548
|
+
*/
|
|
1491
1549
|
create?: AccessControl<T>
|
|
1492
1550
|
update?: AccessControl<T>
|
|
1493
1551
|
delete?: AccessControl<T>
|
|
@@ -1975,6 +2033,12 @@ export type ListConfig<TTypeInfo extends TypeInfo> = {
|
|
|
1975
2033
|
* the bad field too — no entry is ever silently dropped or emitted as
|
|
1976
2034
|
* invalid Prisma.
|
|
1977
2035
|
*
|
|
2036
|
+
* `createdAt`/`updatedAt` are a valid entry even when the list has no
|
|
2037
|
+
* matching declared field — an entry may name either as long as the
|
|
2038
|
+
* list's auto-timestamps (`db.timestamps`, global or per-list) are
|
|
2039
|
+
* enabled for that column, since the auto-injected column has no `@map`
|
|
2040
|
+
* of its own and its Prisma field name is exactly `createdAt`/`updatedAt`.
|
|
2041
|
+
*
|
|
1978
2042
|
* @example One audition per student per production (composite unique)
|
|
1979
2043
|
* ```typescript
|
|
1980
2044
|
* Audition: list({
|
|
@@ -2241,14 +2305,16 @@ export interface BulkAction {
|
|
|
2241
2305
|
* `ui.listView`.
|
|
2242
2306
|
*
|
|
2243
2307
|
* When omitted, the admin UI falls back to its existing defaults: every
|
|
2244
|
-
*
|
|
2308
|
+
* field whose own `ui.listView.defaultColumn` declaration holds is shown as
|
|
2309
|
+
* a column (see {@link BaseFieldConfig.ui}) and no default sort is applied.
|
|
2245
2310
|
*/
|
|
2246
2311
|
export type ListViewUIConfig = {
|
|
2247
2312
|
/**
|
|
2248
2313
|
* The fields to show as columns in the list table, in order.
|
|
2249
2314
|
*
|
|
2250
2315
|
* Drives both the column **selection** and their **order**. When omitted,
|
|
2251
|
-
*
|
|
2316
|
+
* every field whose own `ui.listView.defaultColumn` declaration holds is
|
|
2317
|
+
* shown (current default behaviour).
|
|
2252
2318
|
*
|
|
2253
2319
|
* @example
|
|
2254
2320
|
* ```typescript
|
package/src/context/index.ts
CHANGED
|
@@ -10,10 +10,13 @@ import {
|
|
|
10
10
|
foldDeclaredDependencies,
|
|
11
11
|
validateQueryKeys,
|
|
12
12
|
validateQueryFieldReadAccess,
|
|
13
|
+
resolveToOneAccessVisibility,
|
|
14
|
+
emptyToOneAccessFilterTree,
|
|
13
15
|
} from '../access/index.js'
|
|
14
|
-
import type { DeclaredOnlyTree } from '../access/index.js'
|
|
16
|
+
import type { DeclaredOnlyTree, ToOneAccessFilterTree } from '../access/index.js'
|
|
15
17
|
import { ValidationError, DatabaseError } from '../hooks/index.js'
|
|
16
18
|
import { getDbKey } from '../lib/case-utils.js'
|
|
19
|
+
import { uniqueConstraintOf } from '../lib/prisma-errors.js'
|
|
17
20
|
import type { PrismaClientLike } from '../access/types.js'
|
|
18
21
|
import { buildInclude, pickFields, isFragment, buildFieldSelectionScope } from '../query/index.js'
|
|
19
22
|
import type { FieldSelection, FieldSelectionScope } from '../query/index.js'
|
|
@@ -202,6 +205,14 @@ function getDefaultData(listConfig: ListConfig<any>): Record<string, unknown> {
|
|
|
202
205
|
return data
|
|
203
206
|
}
|
|
204
207
|
|
|
208
|
+
// A camelCase field name (e.g. a relationship's `tenantId` foreign key) needs
|
|
209
|
+
// its word boundary split before title-casing, or it reads as one run-together
|
|
210
|
+
// word ("Tenantid") in a user-facing unique-constraint message.
|
|
211
|
+
function humanizeFieldName(fieldName: string): string {
|
|
212
|
+
const spaced = fieldName.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
213
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1)
|
|
214
|
+
}
|
|
215
|
+
|
|
205
216
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
206
217
|
function parsePrismaError(error: unknown, listConfig: ListConfig<any>): Error {
|
|
207
218
|
if (
|
|
@@ -215,22 +226,22 @@ function parsePrismaError(error: unknown, listConfig: ListConfig<any>): Error {
|
|
|
215
226
|
|
|
216
227
|
// P2002 is Prisma's unique constraint violation code.
|
|
217
228
|
if (prismaError.code === 'P2002') {
|
|
218
|
-
const target = prismaError
|
|
229
|
+
const target = uniqueConstraintOf(prismaError)?.fields
|
|
219
230
|
const fieldErrors: Record<string, string> = {}
|
|
220
231
|
|
|
221
|
-
if (target &&
|
|
232
|
+
if (target && target.length > 0) {
|
|
222
233
|
for (const fieldName of target) {
|
|
223
234
|
const fieldConfig = listConfig.fields[fieldName]
|
|
224
|
-
const label = fieldName.charAt(0).toUpperCase() + fieldName.slice(1)
|
|
225
235
|
|
|
226
236
|
if (fieldConfig) {
|
|
227
|
-
fieldErrors[fieldName] =
|
|
237
|
+
fieldErrors[fieldName] =
|
|
238
|
+
`This ${humanizeFieldName(fieldName).toLowerCase()} is already in use`
|
|
228
239
|
} else {
|
|
229
240
|
fieldErrors[fieldName] = `This value is already in use`
|
|
230
241
|
}
|
|
231
242
|
}
|
|
232
243
|
|
|
233
|
-
const fieldLabels = target.map(
|
|
244
|
+
const fieldLabels = target.map(humanizeFieldName).join(', ')
|
|
234
245
|
return new DatabaseError(
|
|
235
246
|
`${fieldLabels} must be unique. The value you entered is already in use.`,
|
|
236
247
|
fieldErrors,
|
|
@@ -293,7 +304,8 @@ interface TransactionCapable<TPrisma> {
|
|
|
293
304
|
*
|
|
294
305
|
* Exposes the secured `db` delegate plus the session, raw `prisma`, storage,
|
|
295
306
|
* plugin services, the generic `serverAction` handler, `sudo()` (bypasses access
|
|
296
|
-
* control but still runs hooks),
|
|
307
|
+
* control but still runs hooks), `withSession()` (substitutes the session but
|
|
308
|
+
* still runs access control), and `transaction()` (interactive, hook-firing
|
|
297
309
|
* transaction). All access-checked operations run their list/field hooks.
|
|
298
310
|
*/
|
|
299
311
|
export interface StackContext<TPrisma extends PrismaClientLike = PrismaClientLike> {
|
|
@@ -329,6 +341,23 @@ export interface StackContext<TPrisma extends PrismaClientLike = PrismaClientLik
|
|
|
329
341
|
options?: TransactionOptions,
|
|
330
342
|
) => Promise<T>
|
|
331
343
|
sudo: () => StackContext<TPrisma>
|
|
344
|
+
/**
|
|
345
|
+
* Derive a context identical to this one except for its session, reusing
|
|
346
|
+
* the already-resolved config and this context's own client (including a
|
|
347
|
+
* transaction client — a call inside `context.transaction()` stays in that
|
|
348
|
+
* transaction) and storage.
|
|
349
|
+
*
|
|
350
|
+
* **This is not an authorisation.** It substitutes who hooks and access
|
|
351
|
+
* control see; it does not change what they decide. The derived context
|
|
352
|
+
* can do exactly what any context built with `session` directly could
|
|
353
|
+
* do — access rules still evaluate against the new session. The caller is
|
|
354
|
+
* responsible for deciding who may invoke this.
|
|
355
|
+
*
|
|
356
|
+
* Orthogonal to `sudo()`: `withSession(s)` preserves the receiver's sudo
|
|
357
|
+
* state (elevated stays elevated), so `context.withSession(s).sudo()` and
|
|
358
|
+
* `context.sudo().withSession(s)` are equivalent.
|
|
359
|
+
*/
|
|
360
|
+
withSession: (session: Session | null) => StackContext<TPrisma>
|
|
332
361
|
_isSudo: boolean
|
|
333
362
|
}
|
|
334
363
|
|
|
@@ -765,6 +794,22 @@ export function getContext<
|
|
|
765
794
|
)
|
|
766
795
|
}
|
|
767
796
|
|
|
797
|
+
// Substitutes the session; access control and hooks still run against it
|
|
798
|
+
// (orthogonal to `sudo`, so the receiver's sudo state is preserved).
|
|
799
|
+
function withSession(newSession: Session | null): StackContext<TPrisma> {
|
|
800
|
+
return getContext(
|
|
801
|
+
config,
|
|
802
|
+
prisma,
|
|
803
|
+
newSession,
|
|
804
|
+
context.storage,
|
|
805
|
+
_isSudo,
|
|
806
|
+
undefined,
|
|
807
|
+
// ADR-0028: a write issued from inside an owned transaction (e.g.
|
|
808
|
+
// `tx.withSession(s).db.x.create()`) must still defer to that owner.
|
|
809
|
+
context._transactionOwner,
|
|
810
|
+
)
|
|
811
|
+
}
|
|
812
|
+
|
|
768
813
|
// Interactive, hook-firing transaction (#614). See the `transaction` doc on
|
|
769
814
|
// `StackContext` above for the atomicity/isolation/retry contract.
|
|
770
815
|
//
|
|
@@ -831,6 +876,7 @@ export function getContext<
|
|
|
831
876
|
plugins: context.plugins,
|
|
832
877
|
serverAction,
|
|
833
878
|
sudo,
|
|
879
|
+
withSession,
|
|
834
880
|
transaction,
|
|
835
881
|
_isSudo,
|
|
836
882
|
}
|
|
@@ -924,6 +970,12 @@ export function buildDbDelegate<TPrisma extends PrismaClientLike>(
|
|
|
924
970
|
* `undefined` for every non-fragment path: a caller `include` (sudo or not)
|
|
925
971
|
* and a bare read both mean "compute every field," matching what they
|
|
926
972
|
* already fetch.
|
|
973
|
+
*
|
|
974
|
+
* Also returns `toOneAccessFilters` — the to-one relations `buildAccessScopedInclude`
|
|
975
|
+
* flagged as needing a post-query existence check rather than a Prisma-side
|
|
976
|
+
* `where` (issue #974). Only the non-sudo caller-include path can produce a
|
|
977
|
+
* non-empty tree: it's the only path that evaluates a related list's `query`
|
|
978
|
+
* access at all. The fragment and sudo paths always return an empty tree.
|
|
927
979
|
*/
|
|
928
980
|
async function resolveReadInclude(
|
|
929
981
|
callerInclude: Record<string, unknown> | undefined,
|
|
@@ -937,6 +989,7 @@ async function resolveReadInclude(
|
|
|
937
989
|
include: Record<string, unknown> | undefined
|
|
938
990
|
declaredOnly: DeclaredOnlyTree
|
|
939
991
|
selection: FieldSelectionScope | undefined
|
|
992
|
+
toOneAccessFilters: ToOneAccessFilterTree
|
|
940
993
|
}> {
|
|
941
994
|
if (fragmentFields !== undefined) {
|
|
942
995
|
const fragmentInclude = buildInclude(fragmentFields) ?? undefined
|
|
@@ -949,27 +1002,27 @@ async function resolveReadInclude(
|
|
|
949
1002
|
[listName],
|
|
950
1003
|
selection,
|
|
951
1004
|
)
|
|
952
|
-
return { ...folded, selection }
|
|
1005
|
+
return { ...folded, selection, toOneAccessFilters: emptyToOneAccessFilterTree() }
|
|
953
1006
|
}
|
|
954
1007
|
|
|
955
1008
|
if (context._isSudo) {
|
|
956
1009
|
const folded = foldDeclaredDependencies(callerInclude, listConfig.fields, config, listName)
|
|
957
|
-
return { ...folded, selection: undefined }
|
|
1010
|
+
return { ...folded, selection: undefined, toOneAccessFilters: emptyToOneAccessFilterTree() }
|
|
958
1011
|
}
|
|
959
1012
|
|
|
960
1013
|
const folded = foldDeclaredDependencies(callerInclude, listConfig.fields, config, listName)
|
|
961
1014
|
if (!folded.include) {
|
|
962
|
-
return { ...folded, selection: undefined }
|
|
1015
|
+
return { ...folded, selection: undefined, toOneAccessFilters: emptyToOneAccessFilterTree() }
|
|
963
1016
|
}
|
|
964
1017
|
|
|
965
|
-
const include = await buildAccessScopedInclude(
|
|
1018
|
+
const { include, toOneAccessFilters } = await buildAccessScopedInclude(
|
|
966
1019
|
folded.include,
|
|
967
1020
|
listConfig.fields,
|
|
968
1021
|
{ session: context.session, context },
|
|
969
1022
|
config,
|
|
970
1023
|
listName,
|
|
971
1024
|
)
|
|
972
|
-
return { include, declaredOnly: folded.declaredOnly, selection: undefined }
|
|
1025
|
+
return { include, declaredOnly: folded.declaredOnly, selection: undefined, toOneAccessFilters }
|
|
973
1026
|
}
|
|
974
1027
|
|
|
975
1028
|
function createFindUnique<TPrisma extends PrismaClientLike>(
|
|
@@ -1026,7 +1079,7 @@ function createFindUnique<TPrisma extends PrismaClientLike>(
|
|
|
1026
1079
|
// Resolve `include`, folding any declared dependencies (`needs`,
|
|
1027
1080
|
// ADR-0025) in alongside whatever the fragment/caller/sudo/bare path
|
|
1028
1081
|
// already produces — see `resolveReadInclude`'s doc comment.
|
|
1029
|
-
let { include, declaredOnly, selection } = await resolveReadInclude(
|
|
1082
|
+
let { include, declaredOnly, selection, toOneAccessFilters } = await resolveReadInclude(
|
|
1030
1083
|
args.include,
|
|
1031
1084
|
fragment ? fragment._fields : undefined,
|
|
1032
1085
|
listName,
|
|
@@ -1056,6 +1109,14 @@ function createFindUnique<TPrisma extends PrismaClientLike>(
|
|
|
1056
1109
|
return null
|
|
1057
1110
|
}
|
|
1058
1111
|
|
|
1112
|
+
// Resolve which of the to-one relations flagged by `toOneAccessFilters`
|
|
1113
|
+
// actually survive their related list's `query` access (issue #974) —
|
|
1114
|
+
// one batched existence check per relation, before field visibility runs.
|
|
1115
|
+
const toOneVisibility = await resolveToOneAccessVisibility([item], toOneAccessFilters, {
|
|
1116
|
+
session: context.session,
|
|
1117
|
+
context,
|
|
1118
|
+
})
|
|
1119
|
+
|
|
1059
1120
|
// Pass sudo flag through context to skip field-level access checks
|
|
1060
1121
|
const filtered = await filterReadableFields(
|
|
1061
1122
|
item,
|
|
@@ -1069,6 +1130,7 @@ function createFindUnique<TPrisma extends PrismaClientLike>(
|
|
|
1069
1130
|
listName,
|
|
1070
1131
|
declaredOnly,
|
|
1071
1132
|
selection,
|
|
1133
|
+
toOneVisibility,
|
|
1072
1134
|
)
|
|
1073
1135
|
|
|
1074
1136
|
if (fragment) {
|
|
@@ -1178,7 +1240,7 @@ function createFindMany<TPrisma extends PrismaClientLike>(
|
|
|
1178
1240
|
// Resolve `include`, folding any declared dependencies (`needs`,
|
|
1179
1241
|
// ADR-0025) in alongside whatever the fragment/caller/sudo/bare path
|
|
1180
1242
|
// already produces — see `resolveReadInclude`'s doc comment.
|
|
1181
|
-
let { include, declaredOnly, selection } = await resolveReadInclude(
|
|
1243
|
+
let { include, declaredOnly, selection, toOneAccessFilters } = await resolveReadInclude(
|
|
1182
1244
|
args?.include,
|
|
1183
1245
|
fragment ? fragment._fields : undefined,
|
|
1184
1246
|
listName,
|
|
@@ -1202,6 +1264,15 @@ function createFindMany<TPrisma extends PrismaClientLike>(
|
|
|
1202
1264
|
include,
|
|
1203
1265
|
})
|
|
1204
1266
|
|
|
1267
|
+
// Resolve which of the to-one relations flagged by `toOneAccessFilters`
|
|
1268
|
+
// actually survive their related list's `query` access (issue #974) —
|
|
1269
|
+
// ONE batched existence check per relation across every row in `items`,
|
|
1270
|
+
// before field visibility runs on any of them.
|
|
1271
|
+
const toOneVisibility = await resolveToOneAccessVisibility(items, toOneAccessFilters, {
|
|
1272
|
+
session: context.session,
|
|
1273
|
+
context,
|
|
1274
|
+
})
|
|
1275
|
+
|
|
1205
1276
|
// Pass sudo flag through context to skip field-level access checks
|
|
1206
1277
|
const filtered = await Promise.all(
|
|
1207
1278
|
items.map((item: Record<string, unknown>) =>
|
|
@@ -1217,6 +1288,7 @@ function createFindMany<TPrisma extends PrismaClientLike>(
|
|
|
1217
1288
|
listName,
|
|
1218
1289
|
declaredOnly,
|
|
1219
1290
|
selection,
|
|
1291
|
+
toOneVisibility,
|
|
1220
1292
|
),
|
|
1221
1293
|
),
|
|
1222
1294
|
)
|
|
@@ -1501,7 +1573,7 @@ function createGet<TPrisma extends PrismaClientLike>(
|
|
|
1501
1573
|
// Resolve `include`, folding any declared dependencies (`needs`,
|
|
1502
1574
|
// ADR-0025) in alongside whatever the fragment/caller/sudo/bare path
|
|
1503
1575
|
// already produces — see `resolveReadInclude`'s doc comment.
|
|
1504
|
-
let { include, declaredOnly, selection } = await resolveReadInclude(
|
|
1576
|
+
let { include, declaredOnly, selection, toOneAccessFilters } = await resolveReadInclude(
|
|
1505
1577
|
args?.include,
|
|
1506
1578
|
fragment ? fragment._fields : undefined,
|
|
1507
1579
|
listName,
|
|
@@ -1519,6 +1591,13 @@ function createGet<TPrisma extends PrismaClientLike>(
|
|
|
1519
1591
|
})
|
|
1520
1592
|
|
|
1521
1593
|
if (item) {
|
|
1594
|
+
// Resolve which of the to-one relations flagged by `toOneAccessFilters`
|
|
1595
|
+
// actually survive their related list's `query` access (issue #974).
|
|
1596
|
+
const toOneVisibility = await resolveToOneAccessVisibility([item], toOneAccessFilters, {
|
|
1597
|
+
session: context.session,
|
|
1598
|
+
context,
|
|
1599
|
+
})
|
|
1600
|
+
|
|
1522
1601
|
const filtered = await filterReadableFields(
|
|
1523
1602
|
item,
|
|
1524
1603
|
listConfig.fields,
|
|
@@ -1531,6 +1610,7 @@ function createGet<TPrisma extends PrismaClientLike>(
|
|
|
1531
1610
|
listName,
|
|
1532
1611
|
declaredOnly,
|
|
1533
1612
|
selection,
|
|
1613
|
+
toOneVisibility,
|
|
1534
1614
|
)
|
|
1535
1615
|
if (fragment) {
|
|
1536
1616
|
return pickFields(filtered, fragment._fields)
|