
dc-cube-definition
by cliftonc
Claude code marketplace / MCP for Drizzle Cube enabled applications.
SKILL.md
name: dc-cube-definition description: Create and configure Drizzle Cube semantic layer cube definitions with proper security context, measures, dimensions, and joins.
Cube Definition Skill
This skill helps you create properly structured cube definitions for Drizzle Cube's semantic layer.
Core Concepts
Drizzle Cube is Drizzle ORM-first. All cubes reference Drizzle schema columns directly for compile-time validation and SQL injection protection.
Basic Cube Structure
import { defineCube } from 'drizzle-cube/server'
import { eq } from 'drizzle-orm'
import { employees } from './schema'
export const employeesCube = defineCube({
name: 'Employees',
// REQUIRED: Security context filter for multi-tenant isolation
sql: (securityContext) => eq(employees.organisationId, securityContext.organisationId),
measures: {
count: {
type: 'count',
sql: () => employees.id
},
totalSalary: {
type: 'sum',
sql: () => employees.salary
},
averageSalary: {
type: 'avg',
sql: () => employees.salary
}
},
dimensions: {
id: {
type: 'number',
sql: () => employees.id,
primaryKey: true
},
name: {
type: 'string',
sql: () => employees.name
},
email: {
type: 'string',
sql: () => employees.email
},
createdAt: {
type: 'time',
sql: () => employees.createdAt
}
}
})
CRITICAL: Security Context
Every cube MUST implement security filtering. This is mandatory for multi-tenant data isolation.
// REQUIRED pattern - filter by security context
sql: (securityContext) => eq(table.organisationId, securityContext.organisationId)
// For multiple conditions
sql: (securityContext) => and(
eq(table.organisationId, securityContext.organisationId),
eq(table.isDeleted, false)
)
The security context is passed to every query execution:
const result = await semanticLayer.execute(query, {
organisationId: 'org-123',
userId: 'user-456'
})
Measure Types
| Type | Description | Example |
|---|---|---|
count | Count rows | { type: 'count', sql: () => table.id } |
countDistinct | Count unique values | { type: 'countDistinct', sql: () => table.userId } |
sum | Sum numeric values | { type: 'sum', sql: () => table.amount } |
avg | Average numeric values | { type: 'avg', sql: () => table.price } |
min | Minimum value | { type: 'min', sql: () => table.date } |
max | Maximum value | { type: 'max', sql: () => table.score } |
Filtered Measures
Apply filters within measures:
measures: {
activeCount: {
type: 'count',
sql: () => employees.id,
filters: [{ sql: () => eq(employees.isActive, true) }]
},
highValueOrders: {
type: 'sum',
sql: () => orders.amount,
filters: [{ sql: () => gt(orders.amount, 1000) }]
}
}
Dimension Types
| Type | Description | Example |
|---|---|---|
string | Text values | { type: 'string', sql: () => table.name } |
number | Numeric values | { type: 'number', sql: () => table.quantity } |
boolean | True/false values | { type: 'boolean', sql: () => table.isActive } |
time | Date/timestamp values | { type: 'time', sql: () => table.createdAt } |
Primary Key Dimension
Mark the primary key for proper aggregations:
dimensions: {
id: {
type: 'number',
sql: () => table.id,
primaryKey: true // Important for multi-cube queries
}
}
Cube Joins (Relationships)
Relationship Types
| Type | Description | SQL Join |
|---|---|---|
belongsTo | Many-to-one | INNER JOIN |
hasOne | One-to-one | LEFT JOIN |
hasMany | One-to-many | LEFT JOIN (with CTE) |
belongsToMany | Many-to-many | LEFT JOIN (through junction) |
belongsTo Example (Many-to-One)
export const employeesCube = defineCube({
name: 'Employees',
sql: (ctx) => eq(employees.organisationId, ctx.organisationId),
joins: {
Departments: {
targetCube: () => departmentsCube,
relationship: 'belongsTo',
on: [
{ source: employees.departmentId, target: departments.id }
]
}
},
measures: { /* ... */ },
dimensions: { /* ... */ }
})
hasMany Example (One-to-Many)
export const departmentsCube = defineCube({
name: 'Departments',
sql: (ctx) => eq(departments.organisationId, ctx.organisationId),
joins: {
Employees: {
targetCube: () => employeesCube,
relationship: 'hasMany',
on: [
{ source: departments.id, target: employees.departmentId }
]
}
},
measures: { /* ... */ },
dimensions: { /* ... */ }
})
belongsToMany Example (Many-to-Many)
Use when relating through a junction table:
export const employeesCube = defineCube({
name: 'Employees',
sql: (ctx) => eq(employees.organisationId, ctx.organisationId),
joins: {
Projects: {
targetCube: () => projectsCube,
relationship: 'belongsToMany',
on: [], // Not used for belongsToMany
through: {
table: employeeProjects, // Junction table
sourceKey: [
{ source: employees.id, target: employeeProjects.employeeId }
],
targetKey: [
{ source: employeeProjects.projectId, target: projects.id }
],
// Optional: Security filter for junction table
securitySql: (securityContext) =>
eq(employeeProjects.organisationId, securityContext.organisationId)
}
}
}
})
Star Schema Pattern
For fact-dimension-fact joins, the dimension cube MUST define hasMany relationships back to all fact cubes:
// Dimension cube - MUST define hasMany to both facts
export const productsCube = defineCube({
name: 'Products',
sql: (ctx) => eq(products.organisationId, ctx.organisationId),
joins: {
Sales: {
targetCube: () => salesCube,
relationship: 'hasMany',
on: [{ source: products.id, target: sales.productId }]
},
Inventory: {
targetCube: () => inventoryCube,
relationship: 'hasMany',
on: [{ source: products.id, target: inventory.productId }]
}
},
dimensions: {
name: { type: 'string', sql: () => products.name },
category: { type: 'string', sql: () => products.category }
}
})
// Fact cube #1
export const salesCube = defineCube({
name: 'Sales',
sql: (ctx) => eq(sales.organisationId, ctx.organisationId),
joins: {
Products: {
targetCube: () => productsCube,
relationship: 'belongsTo',
on: [{ source: sales.productId, target: products.id }]
}
},
measures: {
totalRevenue: { type: 'sum', sql: () => sales.revenue }
}
})
// Fact cube #2
export const inventoryCube = defineCube({
name: 'Inventory',
sql: (ctx) => eq(inventory.organisationId, ctx.organisationId),
joins: {
Products: {
targetCube: () => productsCube,
relationship: 'belongsTo',
on: [{ source: inventory.productId, target: products.id }]
}
},
measures: {
totalStock: { type: 'sum', sql: () => inventory.stockLevel }
}
})
Registering Cubes
import { SemanticLayerCompiler } from 'drizzle-cube/server'
const semanticLayer = new SemanticLayerCompiler({
drizzle: db,
schema: schema
})
// Register cubes
semanticLayer.registerCube(employeesCube)
semanticLayer.registerCube(departmentsCube)
// Execute queries
const result = await semanticLayer.execute({
measures: ['Employees.count'],
dimensions: ['Departments.name']
}, securityContext)
Common Patterns
Calculated Dimension
dimensions: {
fullName: {
type: 'string',
sql: () => sql`${employees.firstName} || ' ' || ${employees.lastName}`
}
}
Date Extraction
dimensions: {
createdYear: {
type: 'number',
sql: () => sql`EXTRACT(YEAR FROM ${orders.createdAt})`
}
}
Conditional Measure
measures: {
completedOrders: {
type: 'count',
sql: () => orders.id,
filters: [{ sql: () => eq(orders.status, 'completed') }]
}
}
Security Best Practices
- Always include security context filter - Never skip the
sqlfunction - Filter junction tables - Use
securitySqlinbelongsToManyrelationships - Use Drizzle query builder - Never construct raw SQL strings
- Test security isolation - Verify queries return only authorized data
Database Support
Cubes work identically across:
- PostgreSQL
- MySQL
- SQLite
The database type is auto-detected from your Drizzle instance.
スコア
総合スコア
リポジトリの品質指標に基づく評価
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
レビュー
レビュー機能は近日公開予定です