Optimizing Prisma Selects
Relevant source files
The following files were used as context for generating this wiki page:
Optimizing Prisma Selects within @joindeed/prisma-auth involves a declarative approach to fetching only necessary data fields while enforcing security constraints. The system automatically converts GraphQL ResolveInfo into Prisma-compatible select and where objects, ensuring that data requirements for authorization matchers are met without over-fetching.
This module addresses the security risks of automatically exposing all relations in GraphQL by moving authorization from the resolver level to the Prisma model level. It allows developers to define roles and access rules directly in the Prisma schema using @Auth annotations.
Sources: README.md:3-33, src/select.ts:1-35
Core Architecture and Data Flow
The optimization logic centers on the PrismaSelect class. This class parses the GraphQL query structure and overlays security requirements defined in the schema.
The PrismaSelect Class
The PrismaSelect class handles the transformation of GraphQL fields into Prisma selection sets. It recursively traverses the GraphQL requested fields and applies filters based on the Prisma Data Model Meta Format (DMMF).
The diagram above illustrates how GraphQL query information is transformed into a secure Prisma selection object.
Sources: src/select.ts:47-90, src/select.ts:193-270
Security Injection Logic
When generating a selection set, the system performs the following operations:
- Field Extraction: Extracts fields requested by the GraphQL client.
- Default Fields: Injects fields defined in the global configuration (e.g., fetching a relation when a count is requested).
- Role Dependencies: Adds fields required by role matchers via
getMatcherDependenciesSelect. - Query Constraints: Injects
whereclauses for list relations viagetListWhereConstrains.
Sources: src/select.ts:223-268, src/getMatcherDependenciesSelect.ts:10-31
Key Components and Functions
Configuration Structure
The optimization behavior is governed by the Configuration interface, which defines how roles and fields are handled.
| Property | Type | Description |
|---|---|---|
rolesPerType | RolesPerType | Specific role definitions (matcher, queryConstraint) for models. |
globalRoles | Record<string, Role> | Role definitions applicable to all models. |
defaultFields | Record<string, object | function> | Fields to always select for specific types or based on current selection. |
Sources: src/index.ts:16-32
Dependency Resolution
getMatcherDependenciesSelect ensures that if a role matcher needs specific fields (like ownerId to check ownership), those fields are included in the Prisma select clause even if the client didn't request them.
This flow shows how authorization data requirements are reconciled with the final database query.
Sources: src/getMatcherDependenciesSelect.ts:10-31, src/select.ts:258-265
Secure Merging and Deep Merges
The system uses PrismaSelect.mergeDeep to combine selection sets from various sources (GraphQL request, default fields, and security dependencies). This utility includes explicit protections against prototype pollution.
Prototype Pollution Protection
The implementation blocks dangerous keys during the merge process to prevent security vulnerabilities.
- Blocked Keys:
__proto__,constructor,prototype. - Validation: Only processes own properties of the source object.
Sources: src/select.ts:133-167, src/select.test.ts:4-72
List and Relation Optimization
For list fields and nested relations, the makeListConstraintMiddleware utilizes PrismaSelect to overlay where constraints directly into the query.
withAuth Integration
The withAuth function is the primary API for applying these optimizations. It merges the user's query parameters with the automatically generated select and where constraints.
const withAuth = <T>(query: T, path?: string, type?: string): T => {
let selectValue
if (path && type) {
selectValue = select.valueOf(path, type)
} else if (!path && type) {
const where = getListWhereConstrains(type, info.schema.getType(type)?.description || '', options, context)
selectValue = where ? { where } : {}
} else {
selectValue = select.value
}
return PrismaSelect.mergeDeep({}, query || {}, selectValue)
}
Sources: src/makeListConstraintMiddleware.ts:41-59
Handling List Constraints
When a field is identified as a list (field.isList), the system calls getListWhereConstrains to generate the appropriate filter. It supports alwaysTrueCondition and alwaysFalseCondition (short-circuiting) to ensure data is never leaked if no roles match.
Sources: src/getListWhereConstrains.ts:9-62, src/select.ts:246-255
Conclusion
Optimizing Prisma selects in this architecture is inherently linked to security. By centralizing field selection and dependency management in the PrismaSelect class, the system ensures that performance (fetching only what is needed) and security (ensuring required fields for auth are present and constraints are applied) are handled in a single pass before the database query is executed.