Decorators
Mantle uses TC39 Stage 3 decorators to declare database and AWS service permissions on entity query classes -- not on handler functions, which get observability automatically via define*Handler.
Where decorators apply
| Decorator | Target | Package | Purpose |
|---|---|---|---|
@RequiresTable | Static methods | @mantleframework/database | Per-method table permissions |
@RequiresDatabase | Classes | @mantleframework/core | Class-level database permissions |
@RequiresS3 | Static methods | @mantleframework/aws | S3 operation permissions |
@RequiresSQS | Static methods | @mantleframework/aws | SQS operation permissions |
@RequiresEventBridge | Static methods | @mantleframework/aws | EventBridge permissions |
@RequiresLambda | Static methods | @mantleframework/aws | Lambda invoke permissions |
@RequiresTable
Declare which tables and operations a query method requires. mantle generate permissions reads these declarations to produce least-privilege PostgreSQL roles.
The decorator accepts an array of TablePermission objects:
import { DatabaseOperation, RequiresTable, withQueryMetrics, getDrizzleClient } from '@mantleframework/database'
import { eq } from '@mantleframework/database/orm'
import { items } from '../schema'
class ItemQueries {
@RequiresTable([{ table: 'items', operations: [DatabaseOperation.Select] }])
static async getById(id: string) {
return withQueryMetrics('Items.getById', async () => {
const db = await getDrizzleClient()
const result = await db.select().from(items).where(eq(items.id, id))
return result[0] ?? null
})
}
@RequiresTable([{ table: 'items', operations: [DatabaseOperation.Select, DatabaseOperation.Insert] }])
static async create(data: NewItem) {
return withQueryMetrics('Items.create', async () => {
const db = await getDrizzleClient()
const [item] = await db.insert(items).values(data).returning()
return item!
})
}
@RequiresTable([{ table: 'items', operations: [DatabaseOperation.Select, DatabaseOperation.Update] }])
static async updateName(id: string, name: string) {
return withQueryMetrics('Items.updateName', async () => {
const db = await getDrizzleClient()
const [updated] = await db.update(items).set({ name }).where(eq(items.id, id)).returning()
return updated!
})
}
@RequiresTable([{ table: 'items', operations: [DatabaseOperation.Delete] }])
static async deleteById(id: string) {
return withQueryMetrics('Items.deleteById', async () => {
const db = await getDrizzleClient()
await db.delete(items).where(eq(items.id, id))
})
}
}Key rules:
@RequiresTableis a method decorator -- Stage 3, uses WeakMap internally- Static methods only -- entity query classes are never instantiated
- Always wrap the body in
withQueryMetrics()for X-Ray tracing and CloudWatch metrics - INSERT and UPDATE with
.returning()requireDatabaseOperation.Selecttoo (DSQL needs SELECT for RETURNING clauses)
Multi-table permissions
A single method can declare permissions on multiple tables:
@RequiresTable([
{ table: 'files', operations: [DatabaseOperation.Select] },
{ table: 'file_downloads', operations: [DatabaseOperation.Select, DatabaseOperation.Insert] },
])
static getFileWithDownload(fileId: string) { ... }DatabaseOperation enum
enum DatabaseOperation {
Select = 'SELECT',
Insert = 'INSERT',
Update = 'UPDATE',
Delete = 'DELETE',
All = 'ALL',
}TablePermission interface
interface TablePermission {
table: string // Unquoted PostgreSQL table name
operations: DatabaseOperation[] // SQL operations required on this table
}@RequiresDatabase
A class decorator (Stage 3, uses Symbol + Object.defineProperty) for declaring class-level database access. Applied to entity query classes that need database initialization.
import { RequiresDatabase } from '@mantleframework/core'
@RequiresDatabase
class UserQueries {
// ...
}Permission extraction
mantle generate permissionsScans the codebase with ts-morph AST analysis:
- Finds
@RequiresTabledecorators on entity query methods - Finds
@RequiresS3,@RequiresSQS, etc. on AWS vendor wrappers - Traces which entity queries each Lambda imports
Produces per-Lambda PostgreSQL roles with least-privilege grants:
# permissions/generated_dsql_permissions.tf
resource "aws_dsql_cluster_role" "lambda_list_items" {
cluster_id = module.database.cluster_id
role_name = "lambda_list_items"
grants = [
{ table = "items", operations = ["SELECT"] }
]
}Apply the generated SQL to Aurora DSQL:
mantle db apply-permissionsStage 3 decorator syntax
All Mantle decorators use TC39 Stage 3 syntax. Never set experimentalDecorators: true in tsconfig.json.
The any constraint rule
Method decorator targets must use any, not unknown:
// Correct
<T extends (...args: any[]) => any>(target: T, context: ClassMethodDecoratorContext)
// Wrong -- causes "Unable to resolve signature of method decorator"
<T extends (...args: unknown[]) => unknown>(target: T, context: ClassMethodDecoratorContext)TypeScript cannot match concrete method signatures against unknown constraints.
Handler decorators (not needed)
@Traced, @LogMetrics, and @InjectContext exist but are built into every define*Handler call via withObservability(). Do not apply them to handler code.
Next steps
- Entity Queries -- full query class patterns with
withQueryMetrics - Infrastructure --
mantle generate infraand IAM policy auto-wiring