or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced-utilities.mdcore-abilities.mderror-handling.mdindex.mdpermission-checking.mdrule-building.md
tile.json

advanced-utilities.mddocs/

Advanced Utilities

Advanced features for rule serialization, database integration, field-level access analysis, and MongoDB query generation from @casl/ability/extra.

Capabilities

Rule Serialization

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); // true

Field-Level Access Analysis

Analyze 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 }

Database Query Generation

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')

Rule Analysis and Debugging

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);