Advanced features for rule serialization, database integration, field-level access analysis, and MongoDB query generation from @casl/ability/extra.
Serialize and deserialize ability rules for storage, transmission, or caching.
/**
* Serialize rules for storage or transmission
* @param rules - Array of rules to serialize
* @param packSubject - Optional function to transform subjects during packing
* @returns Array of packed/serialized rules
*/
function packRules<T extends RawRule<any, any>>(
rules: T[],
packSubject?: (subject: any) => any
): PackRule<T>[];
/**
* Deserialize packed rules back to usable rule objects
* @param rules - Array of packed rules to deserialize
* @param unpackSubject - Optional function to restore subjects during unpacking
* @returns Array of restored rule objects
*/
function unpackRules<T extends RawRule<any, any>>(
rules: PackRule<T>[],
unpackSubject?: (subject: any) => any
): T[];
/**
* Serialized rule format optimized for storage/transmission
*/
type PackRule<T> =
[string, string] |
[string, string, T['conditions']] |
[string, string, T['conditions'] | 0, 1] |
[string, string, T['conditions'] | 0, 1 | 0, string] |
[string, string, T['conditions'] | 0, 1 | 0, string | 0, string];Usage Examples:
import { packRules, unpackRules } from "@casl/ability/extra";
import { createMongoAbility } from "@casl/ability";
const originalRules = [
{ action: 'read', subject: 'Post' },
{ action: 'update', subject: 'Post', conditions: { authorId: 'user123' } },
{ action: 'delete', subject: 'Post', inverted: true, conditions: { published: true }, reason: 'Cannot delete published posts' },
{ action: 'read', subject: 'User', fields: ['name', 'email'] }
];
// Pack rules for storage
const packedRules = packRules(originalRules);
console.log(packedRules);
// [
// ['read', 'Post'],
// ['update', 'Post', { authorId: 'user123' }],
// ['delete', 'Post', { published: true }, true, undefined, 'Cannot delete published posts'],
// ['read', 'User', undefined, false, ['name', 'email']]
// ]
// Store packed rules (e.g., in database, localStorage, etc.)
const serializedRules = JSON.stringify(packedRules);
localStorage.setItem('userRules', serializedRules);
// Later: retrieve and unpack rules
const retrievedRules = JSON.parse(localStorage.getItem('userRules')!);
const unpackedRules = unpackRules(retrievedRules);
// Create ability from unpacked rules
const ability = createMongoAbility(unpackedRules);
console.log(ability.can('read', 'Post')); // true
// Custom subject packing (for class-based subjects)
class Article {
static modelName = 'Article';
constructor(public id: string, public title: string) {}
}
const rulesWithClasses = [
{ action: 'read', subject: Article },
{ action: 'create', subject: 'Comment' }
];
const packedWithClasses = packRules(rulesWithClasses, (subject) => {
return typeof subject === 'function' ? subject.modelName || subject.name : subject;
});
const unpackedWithClasses = unpackRules(packedWithClasses, (subjectName) => {
// Restore class references from string names
const classMap: Record<string, any> = { Article };
return classMap[subjectName] || subjectName;
});
console.log(unpackedWithClasses[0].subject === Article); // trueAnalyze and determine permitted fields for subjects based on ability rules.
/**
* Get all permitted fields for a specific action and subject
* @param ability - Ability instance to analyze
* @param action - Action to check fields for
* @param subject - Subject to check fields for
* @param options - Additional options for field analysis
* @returns Array of permitted field names
*/
function permittedFieldsOf<T extends AnyAbility>(
ability: T,
action: Parameters<T['can']>[0],
subject: Parameters<T['can']>[1],
options: PermittedFieldsOptions<T>
): string[];
/**
* Options for permittedFieldsOf function
*/
interface PermittedFieldsOptions<T extends AnyAbility> {
fieldsFrom: GetRuleFields<RuleOf<T>>;
}
/**
* Function type to extract fields from rules
*/
type GetRuleFields<R extends Rule<any, any>> = (rule: R) => string[];
/**
* Class for analyzing field accessibility with fluent API
*/
class AccessibleFields<T extends AnyAbility> {
constructor(ability: T);
/**
* Get accessible fields for a subject type
* @param action - Action to check
* @param subjectType - Subject type to analyze
* @returns Array of accessible field names
*/
ofType(action: Parameters<T['can']>[0], subjectType: string): string[];
/**
* Get accessible fields for a specific subject instance
* @param action - Action to check
* @param subject - Subject instance to analyze
* @returns Array of accessible field names
*/
of(action: Parameters<T['can']>[0], subject: any): string[];
}Usage Examples:
import { permittedFieldsOf, AccessibleFields } from "@casl/ability/extra";
import { createMongoAbility } from "@casl/ability";
// Create ability with field-level rules
const ability = createMongoAbility([
{ action: 'read', subject: 'User', fields: ['name', 'email', 'avatar'] },
{ action: 'read', subject: 'User', conditions: { role: 'admin' }, fields: ['name', 'email', 'role', 'permissions'] },
{ action: 'update', subject: 'User', conditions: { id: 'user123' }, fields: ['name', 'email'] },
{ action: 'read', subject: 'Post' }, // No field restrictions - all fields allowed
]);
// Define field extraction function
const fieldsFrom = (rule: Rule<any, any>): string[] => {
// Extract fields from rule, return all known fields if no restriction
return rule.fields || ['name', 'email', 'role', 'permissions', 'id', 'avatar'];
};
// Get permitted fields using function
const userFields = permittedFieldsOf(ability, 'read', 'User', { fieldsFrom });
console.log(userFields); // ['name', 'email', 'avatar']
const adminUser = { __type: 'User', role: 'admin', id: 'admin1' };
const adminFields = permittedFieldsOf(ability, 'read', adminUser, { fieldsFrom });
console.log(adminFields); // ['name', 'email', 'role', 'permissions'] (admin can see more)
const updateFields = permittedFieldsOf(ability, 'update', { __type: 'User', id: 'user123' }, { fieldsFrom });
console.log(updateFields); // ['name', 'email']
// No field restrictions means all fields allowed
const postFields = permittedFieldsOf(ability, 'read', 'Post', { fieldsFrom });
console.log(postFields); // [] (empty array means all fields allowed)
// Using fieldsFrom option to get field names from an object
const sampleUser = {
id: 'user1',
name: 'John Doe',
email: 'john@example.com',
password: 'secret',
socialSecurityNumber: '123-45-6789',
avatar: 'avatar.jpg'
};
const permittedFromSample = permittedFieldsOf(ability, 'read', 'User', {
fieldsFrom: (rule: Rule<any, any>) => Object.keys(sampleUser)
});
console.log(permittedFromSample); // ['name', 'email', 'avatar'] (only these are permitted)
// Using AccessibleFields class
const fieldAnalyzer = new AccessibleFields(ability);
// Type-based analysis
const userTypeFields = fieldAnalyzer.ofType('read', 'User');
console.log(userTypeFields); // ['name', 'email', 'avatar']
// Instance-based analysis
const specificUserFields = fieldAnalyzer.of('read', adminUser);
console.log(specificUserFields); // ['name', 'email', 'role', 'permissions']
// Practical usage: Filter API response
function filterApiResponse(data: any, ability: any, action: string) {
const allowedFields = permittedFieldsOf(ability, action, data, {
fieldsFrom: (rule: Rule<any, any>) => Object.keys(data)
});
if (allowedFields.length === 0) {
// No restrictions - return all fields
return data;
}
// Filter object to only include allowed fields
return allowedFields.reduce((filtered, field) => {
if (data.hasOwnProperty(field)) {
filtered[field] = data[field];
}
return filtered;
}, {} as any);
}
// Usage in API endpoint
const userData = {
id: 'user1',
name: 'John Doe',
email: 'john@example.com',
password: 'secret123',
role: 'user'
};
const filteredUser = filterApiResponse(userData, ability, 'read');
console.log(filteredUser); // { name: 'John Doe', email: 'john@example.com', avatar: undefined }Convert ability rules into database queries for efficient permission-aware data retrieval.
/**
* Convert ability rules to database query conditions
* @param ability - Ability instance to analyze
* @param action - Action to generate query for
* @param subjectType - Subject type to generate query for
* @param convert - Function to convert rule conditions to database query format
* @returns Database query object or null if no permissions
*/
function rulesToQuery<T extends AnyAbility, Q>(
ability: T,
action: Parameters<T['can']>[0],
subjectType: string,
convert: (rule: Rule<any, any>) => Q
): Q | null;
/**
* Convert ability rules to field restrictions object
* @param ability - Ability instance to analyze
* @param action - Action to generate field restrictions for
* @param subjectType - Subject type to analyze
* @returns Object with field access information
*/
function rulesToFields<T extends AnyAbility>(
ability: T,
action: Parameters<T['can']>[0],
subjectType: string
): AnyObject;
/**
* Convert ability rules to AST (Abstract Syntax Tree) representation
* Uses MongoDB conditions to generate abstract syntax tree
* @param ability - Ability instance to analyze
* @param action - Action to generate AST for
* @param subjectType - Subject type to analyze
* @returns Condition AST or null if no permissions
*/
function rulesToAST<T extends AnyAbility>(
ability: T,
action: Parameters<T['rulesFor']>[0],
subjectType: ExtractSubjectType<Parameters<T['rulesFor']>[1]>
): Condition | null;
/**
* Result type for database query generation
*/
interface AbilityQuery<Q> {
query: Q;
fields?: string[];
}
/**
* AST node for conditions
*/
interface Condition {
operator: string;
field?: string;
value?: any;
conditions?: Condition[];
}Usage Examples:
import { rulesToQuery, rulesToFields, rulesToAST } from "@casl/ability/extra";
import { createMongoAbility } from "@casl/ability";
// Create ability with various rules
const ability = createMongoAbility([
{ action: 'read', subject: 'Post', conditions: { published: true } },
{ action: 'read', subject: 'Post', conditions: { authorId: 'user123' } },
{ action: 'update', subject: 'Post', conditions: { authorId: 'user123', status: 'draft' } },
{ action: 'read', subject: 'Post', fields: ['title', 'content', 'publishedAt'] }
]);
// Generate MongoDB query
const mongoQuery = rulesToQuery(ability, 'read', 'Post', (rule) => rule.conditions || {});
console.log(mongoQuery);
// { $or: [{ published: true }, { authorId: 'user123' }] }
// Generate SQL WHERE conditions
const sqlQuery = rulesToQuery(ability, 'read', 'Post', (rule) => {
const conditions = rule.conditions || {};
return Object.entries(conditions)
.map(([field, value]) => `${field} = '${value}'`)
.join(' AND ');
});
console.log(sqlQuery);
// { $or: ['published = true', 'authorId = user123'] }
// Generate Prisma where conditions
const prismaQuery = rulesToQuery(ability, 'read', 'Post', (rule) => {
return rule.conditions ? { ...rule.conditions } : {};
});
console.log(prismaQuery);
// { OR: [{ published: true }, { authorId: 'user123' }] }
// Get field restrictions
const fieldRestrictions = rulesToFields(ability, 'read', 'Post');
console.log(fieldRestrictions);
// { fields: ['title', 'content', 'publishedAt'] }
// Generate AST representation
const ast = rulesToAST(ability, 'read', 'Post');
console.log(JSON.stringify(ast, null, 2));
// {
// "operator": "$or",
// "conditions": [
// { "operator": "$eq", "field": "published", "value": true },
// { "operator": "$eq", "field": "authorId", "value": "user123" }
// ]
// }
// Practical database integration examples
// MongoDB with Mongoose
async function findPostsWithPermissions(userId: string) {
const userAbility = createUserAbility(userId);
const query = rulesToQuery(userAbility, 'read', 'Post', (rule) => rule.conditions);
if (!query) {
return []; // No read permissions
}
const allowedFields = rulesToFields(userAbility, 'read', 'Post');
const projection = allowedFields.fields ?
allowedFields.fields.reduce((proj, field) => ({ ...proj, [field]: 1 }), {}) :
{};
return await PostModel.find(query, projection);
}
// Prisma integration
async function findPostsWithPrisma(userId: string) {
const userAbility = createUserAbility(userId);
const whereCondition = rulesToQuery(userAbility, 'read', 'Post', (rule) => rule.conditions);
if (!whereCondition) {
return [];
}
const fieldRestrictions = rulesToFields(userAbility, 'read', 'Post');
const select = fieldRestrictions.fields ?
fieldRestrictions.fields.reduce((sel, field) => ({ ...sel, [field]: true }), {}) :
undefined;
return await prisma.post.findMany({
where: whereCondition,
select
});
}
// SQL query building
function buildSQLQueryWithPermissions(userId: string, tableName: string) {
const userAbility = createUserAbility(userId);
const conditions = rulesToQuery(userAbility, 'read', 'Post', (rule) => {
if (!rule.conditions) return '1=1'; // Always true
return Object.entries(rule.conditions)
.map(([field, value]) => {
if (typeof value === 'string') {
return `${field} = '${value}'`;
}
return `${field} = ${value}`;
})
.join(' AND ');
});
if (!conditions) {
return null; // No permissions
}
const fieldRestrictions = rulesToFields(userAbility, 'read', 'Post');
const selectFields = fieldRestrictions.fields ?
fieldRestrictions.fields.join(', ') :
'*';
const whereClause = Array.isArray(conditions) ?
`(${conditions.join(' OR ')})` :
conditions;
return `SELECT ${selectFields} FROM ${tableName} WHERE ${whereClause}`;
}
// Helper function for creating user-specific ability
function createUserAbility(userId: string) {
return createMongoAbility([
{ action: 'read', subject: 'Post', conditions: { published: true } },
{ action: 'read', subject: 'Post', conditions: { authorId: userId } },
{ action: 'update', subject: 'Post', conditions: { authorId: userId } }
]);
}
// Usage
console.log(buildSQLQueryWithPermissions('user123', 'posts'));
// SELECT * FROM posts WHERE (published = true OR authorId = 'user123')Utilities for analyzing and debugging complex rule configurations.
/**
* Analyze rule coverage and conflicts for debugging
*/
interface RuleAnalysis {
/** Total number of rules */
totalRules: number;
/** Rules by action */
rulesByAction: Record<string, number>;
/** Rules by subject */
rulesBySubject: Record<string, number>;
/** Conflicting rules (allow vs deny for same action/subject) */
conflicts: Array<{
action: string;
subject: string;
allowRules: number;
denyRules: number;
}>;
/** Rules with field restrictions */
fieldRestrictedRules: number;
/** Rules with conditions */
conditionalRules: number;
}
/**
* Analyze ability rules for debugging and optimization
* @param ability - Ability instance to analyze
* @returns Analysis report
*/
function analyzeRules<T extends AnyAbility>(ability: T): RuleAnalysis;Usage Examples:
import { createMongoAbility } from "@casl/ability";
// Create complex ability for analysis
const complexAbility = createMongoAbility([
{ action: 'read', subject: 'Post' },
{ action: 'read', subject: 'Post', conditions: { authorId: 'user123' } },
{ action: 'create', subject: 'Post' },
{ action: 'update', subject: 'Post', conditions: { authorId: 'user123' } },
{ action: 'delete', subject: 'Post', inverted: true, conditions: { published: true } },
{ action: 'read', subject: 'User', fields: ['name', 'email'] },
{ action: 'update', subject: 'User', conditions: { id: 'user123' }, fields: ['name'] },
{ action: 'read', subject: 'Comment' },
{ action: 'moderate', subject: 'Comment', conditions: { reported: true } }
]);
// Mock analysis function (would be implemented in actual library)
function analyzeRules(ability: any): RuleAnalysis {
const rules = ability.rules;
const analysis: RuleAnalysis = {
totalRules: rules.length,
rulesByAction: {},
rulesBySubject: {},
conflicts: [],
fieldRestrictedRules: 0,
conditionalRules: 0
};
rules.forEach((rule: any) => {
// Count by action
const action = Array.isArray(rule.action) ? rule.action[0] : rule.action;
analysis.rulesByAction[action] = (analysis.rulesByAction[action] || 0) + 1;
// Count by subject
if (rule.subject) {
const subject = Array.isArray(rule.subject) ? rule.subject[0] : rule.subject;
analysis.rulesBySubject[subject] = (analysis.rulesBySubject[subject] || 0) + 1;
}
// Count field restrictions
if (rule.fields) {
analysis.fieldRestrictedRules++;
}
// Count conditional rules
if (rule.conditions) {
analysis.conditionalRules++;
}
});
return analysis;
}
const analysis = analyzeRules(complexAbility);
console.log('Rule Analysis:', analysis);
// {
// totalRules: 9,
// rulesByAction: { read: 4, create: 1, update: 2, delete: 1, moderate: 1 },
// rulesBySubject: { Post: 5, User: 2, Comment: 2 },
// conflicts: [],
// fieldRestrictedRules: 2,
// conditionalRules: 5
// }
// Rule optimization suggestions
function suggestOptimizations(analysis: RuleAnalysis) {
const suggestions = [];
if (analysis.fieldRestrictedRules > analysis.totalRules * 0.5) {
suggestions.push('Consider using separate abilities for different permission levels');
}
if (analysis.conditionalRules > analysis.totalRules * 0.7) {
suggestions.push('High number of conditional rules may impact performance');
}
if (analysis.conflicts.length > 0) {
suggestions.push('Conflicting rules detected - review rule order and specificity');
}
return suggestions;
}
const optimizations = suggestOptimizations(analysis);
console.log('Optimization suggestions:', optimizations);