Dependency Management & Required Fields
Relevant source files
The following files were used as context for generating this wiki page:
Dependency management in @joindeed/prisma-auth ensures that the data required by authorization matchers is always fetched from the database, even if the user did not explicitly request those fields in their GraphQL query. This system prevents runtime errors where a matcher function attempts to access a property on a record that Prisma did not return.
The core mechanism involves extracting field requirements from role definitions and merging them into the final Prisma select object. This process is handled primarily by the PrismaSelect class and the getMatcherDependenciesSelect utility.
Sources: README.md:63-65, src/getMatcherDependenciesSelect.ts:7-11
Role Data Requirements
Each role defined in the system can specify its own data dependencies through the matcherDependenciesSelect property. This property defines exactly which fields the matcher function needs to evaluate its boolean logic against a specific record.
Configuration Methods
A role's dependencies can be defined in two ways:
- Static Object: A standard Prisma
selectobject where keys are field names and values aretrue. - Dynamic Function: A function that receives
RoleArgs(parsed from the@Authannotation) and returns a Prismaselectobject. This allows dependencies to change based on annotation parameters, such as a field name passed into the role.
Sources: src/index.ts:9-13, src/getMatcherDependenciesSelect.ts:25-30
Dependency Resolution Flow
The system resolves dependencies by parsing the @Auth annotations found in the Prisma schema documentation.
This diagram illustrates how role-specific field requirements are identified and merged into the Prisma query.
Sources: src/getMatcherDependenciesSelect.ts:21-33, src/select.ts:192-225
The PrismaSelect Integration
The PrismaSelect class serves as the central integration point. It traverses the GraphQL info object and the Prisma DMMF to build a valid Prisma selection set. During this traversal, it calls getMatcherDependenciesSelect for every model and field encountered to ensure security dependencies are included.
Deep Merging
Because multiple roles might be applied to a single type or field, the system uses a mergeDeep utility to combine all required fields into a single select object without losing data or causing prototype pollution.
Sources: src/select.ts:98-129, src/select.ts:182-205
Data Structures
| Component | Description | Source |
|---|---|---|
RoleArgs | A record of arguments parsed from @Auth annotations, e.g., (userField: 'ownerId'). | src/descriptionToRoles.ts:3 |
matcherDependenciesSelect | A property in Role interface defining required fields for a matcher. | src/index.ts:11 |
RoleDescription | Object containing the name of a role and its parsed arguments. | src/descriptionToRoles.ts:5-8 |
Configuration | The global config object containing rolesPerType and globalRoles. | src/index.ts:20-31 |
Execution in Middleware
Dependency management is executed within the makeListConstraintMiddleware. This middleware provides the withAuth helper to the GraphQL context. When withAuth is called, it invokes the selection logic to generate the enriched select clause.
Field-Level vs. Type-Level Dependencies
The system distinguishes between dependencies at two levels:
- Type-Level: Fields required to evaluate roles defined on the
modelitself (e.g.,/// @Auth(read: [Admin])). - Field-Level: Fields required to evaluate roles defined on specific properties (e.g., an
emailfield restricted toOwner).
If a field is restricted by a role, the system automatically selects the fields necessary to verify that role, even if they aren't in the original GraphQL request.
Sources: src/select.ts:208-212, src/select.ts:228-233, src/makeListConstraintMiddleware.ts:40-60
Implementation Example
The following logic demonstrates how the system retrieves dependencies for a specific field type based on its description.
// src/getMatcherDependenciesSelect.ts:21-33
return readRoles.reduce((acc, role) => {
const matcherDependenciesSelect =
rolesPerType?.[fieldType as keyof RolesPerType]?.[role.name]?.matcherDependenciesSelect ||
globalRoles?.[role.name]?.matcherDependenciesSelect
if (typeof matcherDependenciesSelect === 'function') {
PrismaSelect.mergeDeep(acc, matcherDependenciesSelect(role.args))
} else if (typeof matcherDependenciesSelect === 'object') {
PrismaSelect.mergeDeep(acc, matcherDependenciesSelect)
}
return acc
}, {})
Sources: src/getMatcherDependenciesSelect.ts:21-33
Summary
Dependency management ensures the integrity of the authorization layer by guaranteeing that matcher functions always operate on complete data. By automating the selection of required fields through PrismaSelect and matcherDependenciesSelect, the project removes the manual burden of ensuring data availability from the developer and reduces the risk of authorization bypass due to missing record properties.
Sources: README.md:63-65, src/makeListConstraintMiddleware.ts:11-20