Global vs. Per-Type Role Configuration
Relevant source files
The following files were used as context for generating this wiki page:
Role configuration in @joindeed/prisma-auth defines how the system authorizes access to Prisma models and fields. The system provides two scopes for role definitions: Global Roles and Per-Type Roles. Global roles provide a baseline of reusable logic across the entire schema, while per-type roles allow for specific implementation overrides for individual models.
This dual-layered approach ensures that common authorization patterns (like "Admin") can be defined once, while context-specific logic (like "Owner" for a Purchase vs. "Owner" for a Profile) remains distinct. The library uses these configurations to generate Prisma where clauses, validate record-level access, and ensure required fields are fetched for the matching logic.
Configuration Architecture
The configuration is centralized in the Configuration object passed to the middleware generator. This object maps role names to their technical implementations.
Role Definition Components
Every role, whether global or per-type, consists of three primary components:
| Component | Description | Signature |
|---|---|---|
matcher | Validates access to a specific record. Returns true if access is granted. | (context, record, roleArgs) => boolean |
queryConstraint | Generates a Prisma where clause to filter lists and relations. | (context, roleArgs) => W | boolean |
matcherDependenciesSelect | (Optional) Defines fields that must be selected from the database for the matcher to function. | Record | ((roleArgs) => Record) |
Sources: src/index.ts:9-13, README.md:55-75
Configuration Structure
The Configuration interface organizes these roles into two distinct blocks.
export interface Configuration {
rolesPerType?: RolesPerType // Model-specific overrides
globalRoles?: { // Application-wide defaults
[roleName: string]: Role<any, any, any>
}
// ... other properties
}
Sources: src/index.ts:18-28
Role Resolution Priority
The system follows a specific priority when resolving a role requested via an @Auth annotation in the Prisma schema. It always searches for a model-specific definition before falling back to the global definition.
Resolution Logic Flow
- Identify the requested role name (e.g.,
Owner) from the@Authannotation. - Search in
rolesPerType[ModelName][RoleName]. - If not found, search in
globalRoles[RoleName]. - If neither exists, the system throws an error.
The diagram shows the hierarchical lookup sequence performed by the middleware.
Implementation in Middleware
The resolution logic is consistently implemented across different constraint middlewares:
- Field Constraints: The system checks the
rolesPerTypefor the specific parent type before checkingglobalRoles.
Sources: src/makeFieldConstraintMiddleware.ts:42-43 - Type/Relation Constraints: When resolving relations, the system extracts the type name and performs the same hierarchical lookup.
Sources: src/makeTypeConstraintMiddleware.ts:56-58 - List Constraints: The generator for Prisma
whereclauses utilizes the same lookup pattern to determine whichqueryConstraintfunction to execute.
Sources: src/getListWhereConstrains.ts:44-46
Practical Configuration Example
A common use case involves defining a generic Admin role globally while defining specific Owner logic for different models.
const config = {
globalRoles: {
Admin: {
matcher: (ctx) => ctx.currentUser?.isAdmin,
queryConstraint: () => true, // Admins see everything
},
},
rolesPerType: {
Purchases: {
Owner: {
matcher: (ctx, record) => ctx.currentUser?.id === record.userId,
queryConstraint: (ctx) => ({ userId: ctx.currentUser?.id }),
},
},
},
}
Sources: README.md:58-75
Data Dependency Management
Both global and per-type roles can define matcherDependenciesSelect. This ensures that if a matcher logic requires specific fields (e.g., record.userId), those fields are automatically included in the Prisma select statement.
Selection Merging
The getMatcherDependenciesSelect function iterates through all roles assigned to a field or type. It merges the required fields from both global and per-type definitions using PrismaSelect.mergeDeep.
- If
matcherDependenciesSelectis a function, it executes it withroleArgs. - If it is a static object, it merges it directly.
Sources: src/getMatcherDependenciesSelect.ts:19-31
Summary
The separation between global and per-type roles allows @joindeed/prisma-auth to remain flexible and DRY (Don't Repeat Yourself). You should define universal roles like Admin or Superuser in globalRoles. You should use rolesPerType to implement relationship-based logic like Owner, Member, or Editor, where the definition of those roles changes based on the specific Prisma model being accessed.