Claude-skill-registry cube-definition
Define semantic layer cubes with Drizzle ORM tables, including dimensions, measures, time dimensions, and security context. Use when creating analytics cubes, defining data models, setting up multi-tenant filtering, or working with drizzle-cube semantic layers.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/cube-definition" ~/.claude/skills/majiayu000-claude-skill-registry-cube-definition && rm -rf "$T"
manifest:
skills/data/cube-definition/SKILL.mdsource content
Drizzle Cube Definition
This skill helps you create semantic layer cubes using Drizzle Cube's
defineCube function. Cubes provide a business-friendly abstraction over database tables with type-safe dimensions, measures, and built-in security.
Core Concept
A cube in Drizzle Cube is:
- A semantic layer over one or more database tables
- Defined using Drizzle ORM table references
- Always filtered by security context (mandatory for multi-tenant isolation)
- Type-safe with full TypeScript support
Basic Cube Structure
import { defineCube } from 'drizzle-cube' import { eq } from 'drizzle-orm' import { employees } from './schema' // Your Drizzle schema export const employeesCube = defineCube('Employees', { title: 'Employee Analytics', // Optional human-readable title description: 'Analytics for employee data', // Optional description // MANDATORY: Security context filtering for multi-tenant isolation sql: (ctx) => ({ from: employees, where: eq(employees.organisationId, ctx.securityContext.organisationId) }), // Define dimensions (categorical/time fields for grouping/filtering) dimensions: { id: { title: 'Employee ID', type: 'number', sql: () => employees.id, primaryKey: true // Mark the primary key }, name: { title: 'Employee Name', type: 'string', sql: () => employees.name }, email: { title: 'Email Address', type: 'string', sql: () => employees.email }, departmentId: { title: 'Department', type: 'number', sql: () => employees.departmentId }, isActive: { title: 'Active Status', type: 'boolean', sql: () => employees.isActive }, createdAt: { title: 'Created Date', type: 'time', sql: () => employees.createdAt } }, // Define measures (aggregated numeric values) measures: { count: { title: 'Total Employees', type: 'count', sql: () => employees.id }, totalSalary: { title: 'Total Salary', type: 'sum', sql: () => employees.salary }, avgSalary: { title: 'Average Salary', type: 'avg', sql: () => employees.salary }, minSalary: { title: 'Minimum Salary', type: 'min', sql: () => employees.salary }, maxSalary: { title: 'Maximum Salary', type: 'max', sql: () => employees.salary } } })
Important: The
defineCube function takes two parameters:
- name (string) - The cube name (e.g., 'Employees')
- definition (object) - The cube configuration (sql, dimensions, measures, etc.)
Dimension Types
Drizzle Cube supports four dimension types:
1. String Dimensions
dimensions: { name: { title: 'Full Name', type: 'string', sql: () => employees.name }, email: { type: 'string', sql: () => employees.email } }
2. Number Dimensions
dimensions: { id: { type: 'number', sql: () => employees.id, primaryKey: true }, departmentId: { type: 'number', sql: () => employees.departmentId } }
3. Time Dimensions
dimensions: { createdAt: { title: 'Created Date', type: 'time', sql: () => employees.createdAt }, updatedAt: { type: 'time', sql: () => employees.updatedAt } }
4. Boolean Dimensions
dimensions: { isActive: { title: 'Active', type: 'boolean', sql: () => employees.isActive }, isRemote: { title: 'Remote Worker', type: 'boolean', sql: () => employees.isRemote } }
Measure Types
Drizzle Cube supports several aggregation types:
1. Count Measures
measures: { count: { title: 'Total Count', type: 'count', sql: () => employees.id // Column to count }, activeCount: { title: 'Active Employees', type: 'count', sql: () => employees.id, filters: [(ctx) => eq(employees.isActive, true)] // Filtered count } }
2. Count Distinct Measures
measures: { uniqueDepartments: { title: 'Unique Departments', type: 'countDistinct', sql: () => employees.departmentId } }
3. Sum Measures
measures: { totalSalary: { title: 'Total Salary', type: 'sum', sql: () => employees.salary } }
4. Average Measures
measures: { avgSalary: { title: 'Average Salary', type: 'avg', sql: () => employees.salary } }
5. Min/Max Measures
measures: { minSalary: { title: 'Minimum Salary', type: 'min', sql: () => employees.salary }, maxSalary: { title: 'Maximum Salary', type: 'max', sql: () => employees.salary } }
6. Calculated Measures
measures: { salaryPercentage: { title: 'Salary as Percentage', type: 'calculated', calculatedSql: '{totalSalary} / NULLIF({departmentBudget}, 0) * 100' } }
SQL Property Patterns
The
sql property in dimensions and measures can be defined in two ways:
1. Direct Column Reference (Recommended)
dimensions: { name: { type: 'string', sql: () => employees.name // Function returning column } }
2. Direct Column (Also Valid)
dimensions: { name: { type: 'string', sql: employees.name // Direct column reference } }
Best Practice: Use the function form
() => employees.column for consistency and to access the QueryContext if needed.
Advanced Patterns
Filtered Measures
Add filters to measures for conditional aggregation:
measures: { activeEmployees: { title: 'Active Employees', type: 'count', sql: () => employees.id, filters: [ (ctx) => eq(employees.isActive, true) ] }, seniorEmployees: { title: 'Senior Employees', type: 'count', sql: () => employees.id, filters: [ (ctx) => { const { gte } = ctx.imports return gte(employees.yearsOfService, 5) } ] }, highEarners: { title: 'High Earners', type: 'count', sql: () => employees.id, filters: [ (ctx) => { const { gt } = ctx.imports return gt(employees.salary, 100000) } ] } }
Computed Dimensions
Use SQL expressions for computed values:
import { sql } from 'drizzle-orm' dimensions: { fullName: { title: 'Full Name', type: 'string', sql: () => sql`${employees.firstName} || ' ' || ${employees.lastName}` }, seniorityLevel: { title: 'Seniority', type: 'string', sql: () => sql`CASE WHEN ${employees.yearsOfService} < 2 THEN 'Junior' WHEN ${employees.yearsOfService} < 5 THEN 'Mid-level' ELSE 'Senior' END` } }
Security Context (MANDATORY)
Every cube MUST filter by security context to ensure multi-tenant data isolation:
// ✅ CORRECT - Security context filtering sql: (ctx) => ({ from: employees, where: eq(employees.organisationId, ctx.securityContext.organisationId) }) // ✅ CORRECT - Multiple security conditions sql: (ctx) => ({ from: employees, where: and( eq(employees.organisationId, ctx.securityContext.organisationId), eq(employees.tenantId, ctx.securityContext.tenantId) ) }) // ❌ WRONG - No security filtering (data leak!) sql: (ctx) => ({ from: employees // Missing where clause - SECURITY VIOLATION })
Note: The
sql function receives a QueryContext object (abbreviated as ctx), which contains:
- The security context with tenant/organization informationctx.securityContext
- Drizzle ORM operators and functionsctx.imports
Complete Example
import { defineCube } from 'drizzle-cube' import { eq, sql, and, gte } from 'drizzle-orm' import { employees } from './schema' export const employeesCube = defineCube('Employees', { title: 'Employee Analytics', description: 'Comprehensive employee data and metrics', // Security context filtering (MANDATORY) sql: (ctx) => ({ from: employees, where: eq(employees.organisationId, ctx.securityContext.organisationId) }), dimensions: { id: { title: 'Employee ID', type: 'number', sql: () => employees.id, primaryKey: true }, name: { title: 'Name', type: 'string', sql: () => employees.name }, email: { title: 'Email', type: 'string', sql: () => employees.email }, department: { title: 'Department', type: 'string', sql: () => employees.departmentName }, isActive: { title: 'Active', type: 'boolean', sql: () => employees.isActive }, createdAt: { title: 'Hire Date', type: 'time', sql: () => employees.createdAt }, // Computed dimension seniorityLevel: { title: 'Seniority Level', type: 'string', sql: () => sql`CASE WHEN ${employees.yearsOfService} < 2 THEN 'Junior' WHEN ${employees.yearsOfService} < 5 THEN 'Mid-level' ELSE 'Senior' END` } }, measures: { count: { title: 'Total Employees', type: 'count', sql: () => employees.id }, activeCount: { title: 'Active Employees', type: 'count', sql: () => employees.id, filters: [(ctx) => eq(employees.isActive, true)] }, totalSalary: { title: 'Total Salary', type: 'sum', sql: () => employees.salary }, avgSalary: { title: 'Average Salary', type: 'avg', sql: () => employees.salary }, minSalary: { title: 'Minimum Salary', type: 'min', sql: () => employees.salary }, maxSalary: { title: 'Maximum Salary', type: 'max', sql: () => employees.salary }, uniqueDepartments: { title: 'Unique Departments', type: 'countDistinct', sql: () => employees.departmentId }, // Filtered measure seniorEmployees: { title: 'Senior Employees', type: 'count', sql: () => employees.id, filters: [(ctx) => gte(employees.yearsOfService, 5)] } } })
Registering Cubes
Once defined, register cubes with the semantic layer compiler:
import { SemanticLayerCompiler } from 'drizzle-cube' import { drizzle } from 'drizzle-orm/postgres-js' import { employeesCube } from './cubes/employees' const db = drizzle(process.env.DATABASE_URL) const compiler = new SemanticLayerCompiler({ drizzle: db, schema: schema }) // Register your cube compiler.registerCube(employeesCube)
Best Practices
- Always include security context filtering - This is mandatory for multi-tenant isolation
- Use meaningful names - Cube names and dimension/measure keys should be clear and descriptive
- Add titles - Provide human-readable titles for UI display
- Mark primary keys - Set
on ID dimensionsprimaryKey: true - Type safety - Use Drizzle ORM table references for compile-time validation
- Filtered measures - Use filters for conditional aggregations instead of creating separate cubes
- Use function form for sql - Prefer
over directsql: () => column
for consistencysql: column
Common Pitfalls
- Wrong defineCube signature - Remember: name is first parameter, NOT inside the object
// ❌ WRONG defineCube({ name: 'Employees', sql: ... }) // ✅ CORRECT defineCube('Employees', { sql: ... }) - Missing security context - Every cube must filter by security context
- Wrong SQL syntax - Use Drizzle ORM operators (eq, and, or), not raw SQL strings
- Incorrect types - Ensure dimension/measure types match the actual data types
- Missing imports - Import necessary operators from drizzle-orm
- Redundant name fields - Don't add
property to dimensions/measures (the key IS the name)name:
Next Steps
- Learn about cube joins with the
skillcube-joins - Build queries using your cubes with the
skillqueries - Set up server APIs with the
skillserver-setup