Complete Guide to Decorators in Dynamite¶
This guide provides comprehensive documentation for all decorators available in Dynamite ORM, including practical examples, common patterns, and best practices.
Table of Contents¶
- Introduction to Decorators
- @PrimaryKey - Primary Keys
- @Index - GSI Configuration
- @IndexSort - Sort key of the table
- @Default - Default Values
- @Validate - Validation Functions
- @Set - Write Transformation
- @Get and @Set - Bidirectional Transformation
- @NotNull - Required Fields
- @CreatedAt - Creation Timestamp
- @UpdatedAt - Update Timestamp
- @DeleteAt - Soft Delete
- @Name - Custom Names
- @HasMany - One to Many Relationships
- @HasOne - One to One Relationships
- @BelongsTo - Many to One Relationships
- @ManyToMany - Many to Many Relationships
- Lifecycle Hook Decorators
- Combining Multiple Decorators
- Custom Decorator Patterns
- Best Practices
Introduction to Decorators¶
Decorators in Dynamite are special functions that add metadata and behavior to classes and properties. They allow you to define database schemas in a declarative and type-safe manner.
Basic Concepts¶
import { Table, PrimaryKey, Default, CreationOptional } from "@arcaelas/dynamite";
class User extends Table<User> {
// Primary key decorator
@PrimaryKey()
declare id: CreationOptional<string>;
// Simple field without decorators
declare name: string;
// Field with default value
@Default(() => "customer")
declare role: CreationOptional<string>;
}
Types of Decorators¶
Key Decorators: - @PrimaryKey() - Defines the primary key - @Index() - Defines partition key (GSI) - @IndexSort() - Defines the sort key of the table
Data Decorators: - @Default() - Sets default values - @Set() - Transforms values when writing (before saving) - @Get() - Transforms values when reading (from the database) - @Validate() - Validates values before saving - @NotNull() - Marks fields as required
Timestamp Decorators: - @CreatedAt() - Auto-timestamp on creation - @UpdatedAt() - Auto-timestamp on update - @DeleteAt() - Soft delete with timestamp
Relationship Decorators: - @HasMany() - One to many relationship - @HasOne() - One to one relationship - @BelongsTo() - Many to one relationship - @ManyToMany() - Many to many relationship
Configuration Decorators: - @Name() - Custom names for tables/columns
@PrimaryKey - Primary Keys¶
The @PrimaryKey decorator defines the table's primary key. It internally applies @Index and @IndexSort automatically.
Syntax¶
Simple Primary Key¶
import { Table, PrimaryKey, CreationOptional, Default } from "@arcaelas/dynamite";
class User extends Table<User> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare name: string;
declare email: string;
}
// Usage
const user = await User.create({
name: "John Doe",
email: "john@example.com"
// id is optional (CreationOptional) and auto-generated
});
console.log(user.id); // "01ARZ3NDEKTSV4RRFFQ69G5FAV"
Primary Key with Static Value¶
class Product extends Table<Product> {
@PrimaryKey()
declare sku: CreationOptional<string>;
declare name: string;
declare price: number;
}
// Usage
const product = await Product.create({
sku: "PROD-001",
name: "Widget",
price: 29.99
});
Composite Primary Key (Partition + Sort)¶
Although @PrimaryKey applies both decorators, you can define composite keys manually:
class Order extends Table<Order> {
@Index()
declare user_id: string;
@IndexSort()
declare order_date: string;
declare total: number;
declare status: string;
}
// Usage
const order = await Order.create({
user_id: "user-123",
order_date: new Date().toISOString(),
total: 99.99,
status: "pending"
});
// Queries by partition key
const user_orders = await Order.where({ user_id: "user-123" });
// Queries with sort key
const recent_orders = await Order.where({ user_id: "user-123" }, {
order: "DESC",
limit: 10
});
Important Characteristics¶
class Account extends Table<Account> {
@PrimaryKey()
declare account_id: CreationOptional<string>;
// Automatically:
// - Marked as @Index: it is the partition key of the table
// - Registered as the primary key of the schema
// - Filled in with a ULID when no id is given, and immutable afterwards
// - Rejects anything that is not a non-empty string
}
@Index - GSI Configuration¶
The @Index decorator marks a property as Partition Key. It is fundamental for efficient queries in DynamoDB.
Syntax¶
Simple Index¶
class Customer extends Table<Customer> {
@PrimaryKey()
declare id: CreationOptional<string>;
@Index()
declare email: string;
declare name: string;
declare phone: string;
}
// Queries by email (partition key)
const customers = await Customer.where({ email: "john@example.com" });
Global Secondary Index (GSI)¶
class Article extends Table<Article> {
@PrimaryKey()
declare id: CreationOptional<string>;
@Index()
declare category: string;
@Index()
declare author_id: string;
declare title: string;
declare content: string;
declare published_at: string;
}
// Queries by category
const tech_articles = await Article.where({ category: "technology" });
// Queries by author
const author_articles = await Article.where({ author_id: "author-123" });
Decorator Validations¶
class InvalidModel extends Table<InvalidModel> {
@Index()
declare field1: string;
@Index() // Error: Only one @Index per table allowed
declare field2: string;
// Throws: "Table invalid_models already has a PartitionKey defined"
}
@IndexSort - Sort key of the table¶
The @IndexSort decorator marks a property as Sort Key. It requires a Partition Key to be defined.
Syntax¶
Basic Sort Key¶
class Message extends Table<Message> {
@Index()
declare conversation_id: string;
@IndexSort()
declare timestamp: string;
declare sender_id: string;
declare content: string;
}
// Create messages
await Message.create({
conversation_id: "conv-123",
timestamp: "2025-01-15T10:30:00Z",
sender_id: "user-1",
content: "Hello!"
});
// Queries ordered by timestamp
const messages = await Message.where({ conversation_id: "conv-123" }, {
order: "ASC" // Ascending order by timestamp
});
// Most recent messages
const recent = await Message.where({ conversation_id: "conv-123" }, {
order: "DESC",
limit: 20
});
Range Queries with Sort Key¶
class Event extends Table<Event> {
@Index()
declare venue_id: string;
@IndexSort()
declare event_date: string;
declare name: string;
declare capacity: number;
}
// Events in a date range
const upcoming = await Event.where("event_date", ">=", "2025-01-01");
const past = await Event.where("event_date", "<", "2025-01-01");
Sort key of the table¶
class Transaction extends Table<Transaction> {
@Index()
declare account_id: string;
@IndexSort()
declare transaction_date: string;
declare amount: number;
declare type: string;
declare description: string;
}
// Account transactions ordered by date
const transactions = await Transaction.where({ account_id: "acc-123" }, {
order: "DESC",
limit: 50
});
// Last transaction
const last_transaction = await Transaction.last({ account_id: "acc-123" });
Validations¶
class InvalidSort extends Table<InvalidSort> {
@IndexSort() // Error: @Index required first
declare date: string;
// Throws: "Cannot define a SortKey without a PartitionKey"
}
class DuplicateSort extends Table<DuplicateSort> {
@Index()
declare id: string;
@IndexSort()
declare date1: string;
@IndexSort() // Error: Only one @IndexSort allowed
declare date2: string;
// Throws: "Table already has a SortKey defined"
}
@Default - Default Values¶
The @Default decorator sets static or dynamic default values for properties.
Syntax¶
Static Values¶
class Settings extends Table<Settings> {
@PrimaryKey()
declare id: CreationOptional<string>;
@Default("dark")
declare theme: CreationOptional<string>;
@Default(true)
declare notifications: CreationOptional<boolean>;
@Default(100)
declare volume: CreationOptional<number>;
@Default([])
declare tags: CreationOptional<string[]>;
}
// Usage
const settings = await Settings.create({}); // All fields optional
console.log(settings.theme); // "dark"
console.log(settings.notifications); // true
console.log(settings.volume); // 100
console.log(settings.tags); // []
Dynamic Values (Functions)¶
class Document extends Table<Document> {
@PrimaryKey()
declare id: CreationOptional<string>;
@Default(() => new Date().toISOString())
declare created: CreationOptional<string>;
@Default(() => `DOC-${Date.now()}`)
declare code: CreationOptional<string>;
@Default(() => Math.floor(Math.random() * 1000000))
declare reference_number: CreationOptional<number>;
}
// Each instance gets unique values
const doc1 = await Document.create({});
const doc2 = await Document.create({});
console.log(doc1.id !== doc2.id); // true
console.log(doc1.code !== doc2.code); // true
Complex Default Values¶
class UserProfile extends Table<UserProfile> {
@PrimaryKey()
declare id: CreationOptional<string>;
@Default(() => ({
theme: "light",
language: "en",
timezone: "UTC"
}))
declare preferences: CreationOptional<Record<string, string>>;
@Default(() => ({
email: true,
sms: false,
push: true
}))
declare notifications: CreationOptional<Record<string, boolean>>;
@Default(() => [])
declare recent_searches: CreationOptional<string[]>;
}
// Usage
const profile = await UserProfile.create({});
console.log(profile.preferences); // { theme: "light", language: "en", ... }
console.log(profile.notifications); // { email: true, sms: false, push: true }
Combining with CreationOptional¶
import { CreationOptional } from "@arcaelas/dynamite";
class Task extends Table<Task> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare title: string; // Required
@Default(() => "pending")
declare status: CreationOptional<string>; // Optional
@Default(() => false)
declare completed: CreationOptional<boolean>; // Optional
@Default(() => new Date().toISOString())
declare due_date: CreationOptional<string>; // Optional
}
// Only title is required
const task = await Task.create({ title: "Complete project" });
console.log(task.status); // "pending"
console.log(task.completed); // false
@Validate - Validation Functions¶
The @Validate decorator allows defining custom validation functions that run before saving data.
Syntax¶
@Validate(validator: (value: unknown) => true | string): PropertyDecorator
@Validate(validators: Array<(value: unknown) => true | string>): PropertyDecorator
Simple Validation¶
class User extends Table<User> {
@PrimaryKey()
declare id: CreationOptional<string>;
@Validate((value) => {
const email = value as string;
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || "Invalid email";
})
declare email: string;
@Validate((value) => {
const age = value as number;
return age >= 18 || "Must be 18 or older";
})
declare age: number;
}
// Valid
const user1 = await User.create({
id: "user-1",
email: "john@example.com",
age: 25
});
// Invalid - throws error
try {
await User.create({
id: "user-2",
email: "invalid-email",
age: 25
});
} catch (error) {
console.error(error.message); // "Invalid email"
}
Multiple Validators¶
class Password extends Table<Password> {
@PrimaryKey()
declare user_id: CreationOptional<string>;
@Validate([
(v) => (v as string).length >= 8 || "Minimum 8 characters",
(v) => /[A-Z]/.test(v as string) || "Must contain uppercase",
(v) => /[a-z]/.test(v as string) || "Must contain lowercase",
(v) => /[0-9]/.test(v as string) || "Must contain number",
(v) => /[^A-Za-z0-9]/.test(v as string) || "Must contain symbol"
])
declare password: string;
}
// All validations must pass
try {
await Password.create({
user_id: "user-1",
password: "weak"
});
} catch (error) {
console.error(error.message); // "Minimum 8 characters"
}
// Valid
await Password.create({
user_id: "user-1",
password: "Str0ng!Pass"
});
Complex Validations¶
class Product extends Table<Product> {
@PrimaryKey()
declare sku: CreationOptional<string>;
@Validate((value) => {
const price = value as number;
if (price < 0) return "Price cannot be negative";
if (price > 999999.99) return "Price is too high";
if (!/^\d+(\.\d{1,2})?$/.test(price.toString())) {
return "Price must have at most 2 decimal places";
}
return true;
})
declare price: number;
@Validate((value) => {
const stock = value as number;
return Number.isInteger(stock) && stock >= 0 || "Stock must be a positive integer";
})
declare stock: number;
@Validate((value) => {
const url = value as string;
try {
new URL(url);
return true;
} catch {
return "Invalid URL";
}
})
declare image_url: string;
}
Validations with Context¶
class DateRange extends Table<DateRange> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare start_date: string;
@Validate(function(value) {
const end = new Date(value as string);
const start = new Date(this.start_date);
return end > start || "End date must be after start date";
})
declare end_date: string;
}
@Set - Write Transformation¶
The @Set decorator transforms values when writing them, before they are saved to the database.
Syntax¶
The transformer receives the incoming value (next) and the current stored value (current). Most transformations only need next.
Basic Transformations¶
class User extends Table<User> {
@PrimaryKey()
declare id: CreationOptional<string>;
@Set((v) => (v as string).toLowerCase().trim())
declare email: string;
@Set((v) => (v as string).trim())
@Set((v) => v.charAt(0).toUpperCase() + v.slice(1).toLowerCase())
declare name: string;
@Set((v) => (v as string).replace(/\D/g, ""))
declare phone: string;
}
// Usage
const user = await User.create({
id: "user-1",
email: " JOHN@EXAMPLE.COM ",
name: " jOhN dOe ",
phone: "+1 (555) 123-4567"
});
console.log(user.email); // "john@example.com"
console.log(user.name); // "John doe"
console.log(user.phone); // "15551234567"
Multiple Transformations¶
Mutations are executed in declaration order:
class Article extends Table<Article> {
@PrimaryKey()
declare id: CreationOptional<string>;
@Set((v) => (v as string).trim())
@Set((v) => (v as string).replace(/\s+/g, " "))
@Set((v) => (v as string).substring(0, 200))
declare title: string;
@Set((v) => (v as string).trim())
@Set((v) => (v as string).replace(/<[^>]*>/g, ""))
@Set((v) => (v as string).substring(0, 5000))
declare content: string;
}
Numeric Transformations¶
class Financial extends Table<Financial> {
@PrimaryKey()
declare transaction_id: CreationOptional<string>;
@Set((v) => Math.round((v as number) * 100) / 100)
declare amount: number;
@Set((v) => Math.max(0, Math.min(100, v as number)))
declare percentage: number;
@Set((v) => Math.abs(v as number))
declare quantity: number;
}
// Usage
const transaction = await Financial.create({
transaction_id: "txn-1",
amount: 123.456789,
percentage: 150,
quantity: -10
});
console.log(transaction.amount); // 123.46
console.log(transaction.percentage); // 100
console.log(transaction.quantity); // 10
Object Transformations¶
class Settings extends Table<Settings> {
@PrimaryKey()
declare user_id: CreationOptional<string>;
@Set((v) => {
const config = v as Record<string, any>;
return Object.keys(config).reduce((acc, key) => {
acc[key.toLowerCase()] = config[key];
return acc;
}, {} as Record<string, any>);
})
declare preferences: Record<string, any>;
@Set((v) => Array.from(new Set(v as string[])))
declare tags: string[];
}
@Get and @Set - Bidirectional Transformation¶
Pairing @Get and @Set on the same property transforms values in both directions: @Get runs when reading from the database and @Set runs when saving. Unlike a lone @Set (write-only), combining both decorators handles the complete data cycle conversion.
Syntax¶
@Get(fromDB: (value: any) => any): PropertyDecorator
@Set(toDB: (next: any, current: any) => any): PropertyDecorator
Parameters¶
| Decorator | Type | Description |
|---|---|---|
@Get | (value) => any | Transforms the value when reading from the database. Omit to skip. |
@Set | (next, current) => any | Transforms the value when saving to the database. Omit to skip. |
Bidirectional Transformation¶
import { Get, Set, CreationOptional } from "@arcaelas/dynamite";
class User extends Table<User> {
@PrimaryKey()
declare id: CreationOptional<string>;
// Boolean stored as number in DynamoDB
@Get((from) => from === 1) // DB: 1 -> App: true
@Set((to) => to ? 1 : 0) // App: true -> DB: 1
declare active: boolean;
// JSON stored as string
@Get((from) => JSON.parse(from)) // DB: '{"a":1}' -> App: {a:1}
@Set((to) => JSON.stringify(to)) // App: {a:1} -> DB: '{"a":1}'
declare metadata: Record<string, any>;
}
// Usage
const user = await User.create({
id: "user-1",
active: true, // Saved as 1 in DynamoDB
metadata: { role: "admin" } // Saved as '{"role":"admin"}'
});
// When reading
const fetched = await User.first({ id: "user-1" });
console.log(fetched.active); // true (not 1)
console.log(fetched.metadata); // { role: "admin" } (not string)
Transform Only on Save¶
Use a lone @Set to skip transformation when reading:
class Product extends Table<Product> {
@PrimaryKey()
declare sku: CreationOptional<string>;
// Only normalize when saving, no transformation when reading
@Set((to) => (to as string).toUpperCase().trim())
declare code: string;
}
// Code is saved in uppercase
await Product.create({ sku: "prod-1", code: " abc123 " });
// In DB: code = "ABC123"
Transform Only on Read¶
Use a lone @Get to only transform when reading:
class Settings extends Table<Settings> {
@PrimaryKey()
declare user_id: CreationOptional<string>;
// Parse JSON only when reading (saved as string directly)
@Get((from) => JSON.parse(from))
declare preferences: Record<string, any>;
// Convert timestamp to Date only when reading
@Get((from) => new Date(from))
declare last_login: Date;
}
Common Use Cases¶
Encrypting Sensitive Data¶
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!;
const IV_LENGTH = 16;
function encrypt(text: string): string {
const iv = randomBytes(IV_LENGTH);
const cipher = createCipheriv("aes-256-cbc", Buffer.from(ENCRYPTION_KEY), iv);
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
return iv.toString("hex") + ":" + encrypted.toString("hex");
}
function decrypt(text: string): string {
const [ivHex, encryptedHex] = text.split(":");
const iv = Buffer.from(ivHex, "hex");
const encrypted = Buffer.from(encryptedHex, "hex");
const decipher = createDecipheriv("aes-256-cbc", Buffer.from(ENCRYPTION_KEY), iv);
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString();
}
class UserSecret extends Table<UserSecret> {
@PrimaryKey()
declare user_id: CreationOptional<string>;
@Get(decrypt)
@Set(encrypt)
declare api_key: string;
@Get(decrypt)
@Set(encrypt)
declare secret_token: string;
}
Data Compression¶
import { gzipSync, gunzipSync } from "zlib";
class Document extends Table<Document> {
@PrimaryKey()
declare id: CreationOptional<string>;
@Get((from) => gunzipSync(Buffer.from(from, "base64")).toString())
@Set((to) => gzipSync(to).toString("base64"))
declare content: string;
}
DynamoDB Type Conversion¶
class Analytics extends Table<Analytics> {
@PrimaryKey()
declare event_id: CreationOptional<string>;
// DynamoDB Set to JavaScript Array
@Get((from) => Array.from(from)) // Set -> Array
@Set((to) => new Set(to)) // Array -> Set
declare tags: string[];
// BigInt for large numbers
@Get((from) => BigInt(from))
@Set((to) => to.toString())
declare large_number: bigint;
}
@Set only vs @Get + @Set¶
| Feature | @Set only | @Get + @Set |
|---|---|---|
| Direction | Save only | Bidirectional |
| Decorators | One decorator | Two decorators (@Get, @Set) |
| Use case | Normalization | Type conversion |
class Example extends Table<Example> {
@PrimaryKey()
declare id: CreationOptional<string>;
// @Set only: Normalizes when saving
@Set((v) => (v as string).toLowerCase())
declare email: string; // "JOHN@EXAMPLE.COM" -> "john@example.com" (write only)
// @Get + @Set: Transforms in both directions
@Get((from) => from === 1)
@Set((to) => to ? 1 : 0)
declare active: boolean; // true <-> 1 (read and write)
}
@NotNull - Required Fields¶
The @NotNull decorator marks fields as required, validating that they are not null, undefined, or empty strings.
Syntax¶
Required Fields¶
class Customer extends Table<Customer> {
@PrimaryKey()
declare id: CreationOptional<string>;
@NotNull()
declare name: string;
@NotNull()
declare email: string;
@NotNull()
declare phone: string;
declare address: string; // Optional
}
// Valid
const customer1 = await Customer.create({
name: "John Doe",
email: "john@example.com",
phone: "555-1234"
});
// Invalid - throws error
try {
await Customer.create({
name: "",
email: "john@example.com",
phone: "555-1234"
});
} catch (error) {
console.error("Validation failed"); // name is empty
}
Combining with @Validate¶
class Registration extends Table<Registration> {
@PrimaryKey()
declare id: CreationOptional<string>;
@NotNull()
@Set((v) => (v as string).toLowerCase().trim())
@Validate((v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v as string) || "Invalid email")
declare email: string;
@NotNull()
@Validate((v) => (v as string).length >= 8 || "Minimum 8 characters")
declare password: string;
}
Validation on Arrays and Objects¶
class Project extends Table<Project> {
@PrimaryKey()
declare id: CreationOptional<string>;
@NotNull()
declare title: string;
@NotNull()
@Validate((v) => Array.isArray(v) && v.length > 0 || "Must have at least one member")
declare team_members: string[];
@NotNull()
@Validate((v) => {
const config = v as Record<string, any>;
return Object.keys(config).length > 0 || "Configuration cannot be empty";
})
declare config: Record<string, any>;
}
@CreatedAt - Creation Timestamp¶
The @CreatedAt decorator automatically sets the creation date and time in ISO 8601 format.
Syntax¶
Basic Usage¶
class Post extends Table<Post> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare title: string;
declare content: string;
@CreatedAt()
declare created_at: CreationOptional<string>;
}
// Date is set automatically
const post = await Post.create({
title: "My first post",
content: "Post content"
});
console.log(post.created_at); // "2025-01-15T10:30:00.123Z"
Complete Auditing¶
class AuditLog extends Table<AuditLog> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare user_id: string;
declare action: string;
declare resource: string;
@CreatedAt()
declare timestamp: CreationOptional<string>;
declare ip_address: string;
declare user_agent: string;
}
// Audit log with automatic timestamp
const log = await AuditLog.create({
user_id: "user-123",
action: "DELETE",
resource: "document-456",
ip_address: "192.168.1.1",
user_agent: "Mozilla/5.0..."
});
Queries by Date¶
class Event extends Table<Event> {
@Index()
declare category: string;
@IndexSort()
@CreatedAt()
declare created_at: CreationOptional<string>;
declare name: string;
declare description: string;
}
// Recent events by category
const recent_events = await Event.where({ category: "news" }, {
order: "DESC",
limit: 20
});
// Events in a date range
const events = await Event.where("created_at", ">=", "2025-01-01T00:00:00Z");
@UpdatedAt - Update Timestamp¶
The @UpdatedAt decorator automatically updates the date and time each time the record is saved.
Syntax¶
Basic Usage¶
class Document extends Table<Document> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare title: string;
declare content: string;
@CreatedAt()
declare created_at: CreationOptional<string>;
@UpdatedAt()
declare updated_at: CreationOptional<string>;
}
// Creation
const doc = await Document.create({
title: "Document",
content: "Initial content"
});
console.log(doc.created_at); // "2025-01-15T10:00:00Z"
console.log(doc.updated_at); // "2025-01-15T10:00:00Z"
// Update
doc.content = "Updated content";
await doc.save();
console.log(doc.created_at); // "2025-01-15T10:00:00Z" (unchanged)
console.log(doc.updated_at); // "2025-01-15T10:15:00Z" (updated)
Version System¶
class Article extends Table<Article> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare title: string;
declare content: string;
declare author_id: string;
@Default(() => 1)
declare version: CreationOptional<number>;
@CreatedAt()
declare created_at: CreationOptional<string>;
@UpdatedAt()
declare updated_at: CreationOptional<string>;
declare last_edited_by: string;
}
// Update with version
const article = await Article.first({ id: "article-123" });
if (article) {
article.content = "New content";
article.version = article.version + 1;
article.last_edited_by = "user-456";
await article.save();
// updated_at is automatically updated
}
Change Tracking¶
class UserProfile extends Table<UserProfile> {
@PrimaryKey()
declare user_id: CreationOptional<string>;
declare name: string;
declare email: string;
declare phone: string;
@CreatedAt()
declare created_at: CreationOptional<string>;
@UpdatedAt()
declare last_modified: CreationOptional<string>;
declare modification_count: number;
}
// Increment counter on each modification
const profile = await UserProfile.first({ user_id: "user-123" });
if (profile) {
profile.name = "New Name";
profile.modification_count = (profile.modification_count || 0) + 1;
await profile.save();
// last_modified is automatically updated
}
@DeleteAt - Soft Delete¶
The @DeleteAt decorator marks a property as a soft delete column. When destroy() is called, instead of physically deleting the record, this column is set with an ISO 8601 timestamp.
Syntax¶
Basic Usage¶
import { DeleteAt, CreationOptional } from "@arcaelas/dynamite";
class User extends Table<User> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare name: string;
declare email: string;
@DeleteAt()
declare deleted_at?: string;
}
// Create user
const user = await User.create({
name: "John Doe",
email: "john@example.com"
});
// Soft delete - does NOT delete the record, marks deleted_at
await user.destroy();
console.log(user.deleted_at); // "2025-01-15T10:30:00.123Z"
// The record remains in the database with deleted_at set
Query Behavior¶
With @DeleteAt, normal queries automatically exclude soft-deleted records:
class Article extends Table<Article> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare title: string;
declare content: string;
@DeleteAt()
declare deleted_at?: string;
}
// Create articles
await Article.create({ id: "1", title: "Article 1", content: "..." });
await Article.create({ id: "2", title: "Article 2", content: "..." });
await Article.create({ id: "3", title: "Article 3", content: "..." });
// Soft delete one
const article = await Article.first({ id: "2" });
await article.destroy();
// Normal query - excludes soft-deleted automatically
const active = await Article.where({});
console.log(active.length); // 2 (articles 1 and 3)
// Include soft-deleted records
const all = await Article.where({}, { deleted: true });
console.log(all.length); // 3 (all)
// Only soft-deleted records
const deleted = await Article.where({ deleted_at: { $ne: null } }, { deleted: true });
console.log(deleted.length); // 1 (article 2)
Restore Records¶
class Document extends Table<Document> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare title: string;
@DeleteAt()
declare deleted_at?: string;
}
// Soft delete
const doc = await Document.first({ id: "doc-1" });
await doc.destroy();
// Restore
const deleted_doc = await Document.where({ id: "doc-1" }, { deleted: true });
if (deleted_doc[0]) {
deleted_doc[0].deleted_at = undefined;
await deleted_doc[0].save();
// Document appears again in normal queries
}
Trash System¶
class File extends Table<File> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare name: string;
declare path: string;
declare owner_id: string;
@CreatedAt()
declare created_at: CreationOptional<string>;
@DeleteAt()
declare deleted_at?: string;
}
// Move to trash
async function move_to_trash(file_id: string): Promise<void> {
const file = await File.first({ id: file_id });
if (file) await file.destroy();
}
// Empty trash (permanently delete)
async function empty_trash(owner_id: string): Promise<void> {
const trashed = await File.where({ deleted_at: { $ne: null } }, { deleted: true });
const user_trashed = trashed.filter(f => f.owner_id === owner_id);
for (const file of user_trashed) {
// Force permanent deletion
await File.delete({ id: file.id });
}
}
// Restore from trash
async function restore_from_trash(file_id: string): Promise<void> {
const files = await File.where({ id: file_id }, { deleted: true });
if (files[0]?.deleted_at) {
files[0].deleted_at = undefined;
await files[0].save();
}
}
// List trash
async function list_trash(owner_id: string): Promise<File[]> {
const trashed = await File.where({ deleted_at: { $ne: null } }, { deleted: true });
return trashed.filter(f => f.owner_id === owner_id);
}
Combining with Timestamps¶
class Post extends Table<Post> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare title: string;
declare content: string;
@CreatedAt()
declare created_at: CreationOptional<string>;
@UpdatedAt()
declare updated_at: CreationOptional<string>;
@DeleteAt()
declare deleted_at?: string;
}
// Complete lifecycle
const post = await Post.create({
title: "My Post",
content: "Content"
});
// created_at = "2025-01-15T10:00:00Z"
// updated_at = "2025-01-15T10:00:00Z"
// deleted_at = undefined
post.title = "Updated Title";
await post.save();
// updated_at = "2025-01-15T11:00:00Z"
await post.destroy();
// deleted_at = "2025-01-15T12:00:00Z"
Soft Delete in Transactions¶
const dynamite = new Dynamite({
region: "us-east-1",
tables: [User, Order]
});
await dynamite.connect();
// Atomic soft delete of user and their orders
await dynamite.tx(async (tx) => {
const user = await User.first({ id: "user-123" });
const orders = await Order.where({ user_id: "user-123" });
// Soft delete all orders
for (const order of orders) {
await order.destroy({ tx });
}
// Soft delete user
await user.destroy({ tx });
});
Automatic Characteristics¶
When applying @DeleteAt:
- softDelete = true:
destroy()writes the timestamp instead of removing the record - Automatic filtering:
where()andfirst()exclude the records that carry it - Opt-in:
{ deleted: true }in the query options brings them back - Static
delete()is unaffected: it always removes the record, soft delete lives in the instance
@Name - Custom Names¶
The @Name decorator allows customizing table and column names in the database.
Syntax¶
Custom Table Name¶
@Name("custom_users_table")
class User extends Table<User> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare name: string;
declare email: string;
}
// Table is created with the name "custom_users_table"
Custom Column Names¶
class Customer extends Table<Customer> {
@PrimaryKey()
@Name("customer_id")
declare id: string;
@Name("full_name")
declare name: string;
@Name("email_address")
declare email: string;
@Name("phone_number")
declare phone: string;
}
// In DynamoDB: { customer_id, full_name, email_address, phone_number }
Legacy System Compatibility¶
@Name("legacy_orders")
class Order extends Table<Order> {
@PrimaryKey()
@Name("ORDER_ID")
declare id: string;
@Name("CUSTOMER_ID")
declare customer_id: string;
@Name("ORDER_DATE")
declare order_date: string;
@Name("TOTAL_AMOUNT")
declare total: number;
@Name("ORDER_STATUS")
declare status: string;
}
@HasMany - One to Many Relationships¶
The @HasMany decorator defines relationships where one model has multiple instances of another model.
Syntax¶
Parameters¶
| Parameter | Type | Description |
|---|---|---|
model | () => Model | Function that returns the related model class |
foreignKey | string | Foreign key in the related model |
localKey | string | Local key (default: 'id') |
Basic Relationship¶
import { HasMany, NonAttribute, CreationOptional } from "@arcaelas/dynamite";
class User extends Table<User> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare name: string;
declare email: string;
@HasMany(() => Order, "user_id", "id")
declare orders: NonAttribute<Order[]>;
}
class Order extends Table<Order> {
@PrimaryKey()
declare id: CreationOptional<string>;
@NotNull()
declare user_id: string;
declare total: number;
declare status: string;
}
// Load user with orders
const users = await User.where({ id: "user-123" }, {
include: {
orders: {}
}
});
console.log(users[0].orders); // Order[]
Filtered Relationships¶
// Get user with completed orders
const users = await User.where({ id: "user-123" }, {
include: {
orders: {
where: { status: "completed" },
limit: 10,
order: "DESC"
}
}
});
Nested Relationships¶
class User extends Table<User> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare name: string;
@HasMany(() => Order, "user_id", "id")
declare orders: NonAttribute<Order[]>;
}
class Order extends Table<Order> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare user_id: string;
declare total: number;
@HasMany(() => OrderItem, "order_id", "id")
declare items: NonAttribute<OrderItem[]>;
}
class OrderItem extends Table<OrderItem> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare order_id: string;
declare product_id: string;
declare quantity: number;
declare price: number;
}
// Load users with orders and items
const users = await User.where({}, {
include: {
orders: {
include: {
items: {}
}
}
}
});
@HasOne - One to One Relationships¶
The @HasOne decorator defines relationships where one model has exactly one instance of another model.
Syntax¶
Parameters¶
| Parameter | Type | Description |
|---|---|---|
model | () => Model | Function that returns the related model class |
foreignKey | string | Foreign key in the related model |
localKey | string | Local key (default: 'id') |
Basic Relationship¶
import { HasOne, NonAttribute, CreationOptional } from "@arcaelas/dynamite";
class User extends Table<User> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare name: string;
declare email: string;
@HasOne(() => Profile, "user_id", "id")
declare profile: NonAttribute<Profile | null>;
}
class Profile extends Table<Profile> {
@PrimaryKey()
declare id: CreationOptional<string>;
@NotNull()
declare user_id: string;
declare bio: string;
declare avatar_url: string;
declare website: string;
}
// Load user with profile
const users = await User.where({ id: "user-123" }, {
include: {
profile: {}
}
});
console.log(users[0].profile?.bio); // "Software developer..."
User with Settings¶
class User extends Table<User> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare name: string;
@HasOne(() => UserSettings, "user_id", "id")
declare settings: NonAttribute<UserSettings | null>;
}
class UserSettings extends Table<UserSettings> {
@PrimaryKey()
declare user_id: CreationOptional<string>;
declare theme: string;
declare language: string;
declare notifications_enabled: boolean;
}
// Load user with settings
const user = await User.first({ id: "user-123" }, {
include: {
settings: {}
}
});
if (user?.settings) {
console.log(user.settings.theme); // "dark"
}
@BelongsTo - Many to One Relationships¶
The @BelongsTo decorator defines relationships where a model belongs to another model.
Syntax¶
Parameters¶
| Parameter | Type | Description |
|---|---|---|
model | () => Model | Function that returns the related model class |
localKey | string | Local key that references the parent model |
foreignKey | string | Key in the parent model (default: 'id') |
Basic Relationship¶
import { BelongsTo, NonAttribute, CreationOptional } from "@arcaelas/dynamite";
class Order extends Table<Order> {
@PrimaryKey()
declare id: CreationOptional<string>;
@NotNull()
declare user_id: string;
declare total: number;
declare status: string;
@BelongsTo(() => User, "id", "user_id")
declare user: NonAttribute<User | null>;
}
class User extends Table<User> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare name: string;
declare email: string;
}
// Load order with user
const orders = await Order.where({ id: "order-123" }, {
include: {
user: {}
}
});
console.log(orders[0].user?.name); // "John Doe"
Multiple Relationships¶
class OrderItem extends Table<OrderItem> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare order_id: string;
declare product_id: string;
declare quantity: number;
@BelongsTo(() => Order, "id", "order_id")
declare order: NonAttribute<Order | null>;
@BelongsTo(() => Product, "id", "product_id")
declare product: NonAttribute<Product | null>;
}
// Load item with order and product
const items = await OrderItem.where({ id: "item-123" }, {
include: {
order: {},
product: {}
}
});
@ManyToMany - Many to Many Relationships¶
The @ManyToMany decorator defines relationships where multiple instances of one model can be associated with multiple instances of another model through a pivot table.
Syntax¶
@ManyToMany(
model: () => Model,
pivotTable: string,
foreignKey: string,
relatedKey: string,
localKey?: string,
relatedPK?: string
): PropertyDecorator
Parameters¶
| Parameter | Type | Description |
|---|---|---|
model | () => Model | Function that returns the related model class |
pivotTable | string | Name of the pivot table |
foreignKey | string | Foreign key in pivot table pointing to current model |
relatedKey | string | Foreign key in pivot table pointing to related model |
localKey | string | Local key (default: 'id') |
relatedPK | string | Related model's primary key (default: 'id') |
Basic Relationship¶
import { ManyToMany, NonAttribute, CreationOptional } from "@arcaelas/dynamite";
class User extends Table<User> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare name: string;
declare email: string;
@ManyToMany(() => Role, "users_roles", "user_id", "role_id", "id", "id")
declare roles: NonAttribute<Role[]>;
}
class Role extends Table<Role> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare name: string;
declare permissions: string[];
@ManyToMany(() => User, "users_roles", "role_id", "user_id", "id", "id")
declare users: NonAttribute<User[]>;
}
// Load user with roles
const users = await User.where({ id: "user-123" }, {
include: {
roles: {}
}
});
console.log(users[0].roles); // Role[]
Tags System¶
class Article extends Table<Article> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare title: string;
declare content: string;
@ManyToMany(() => Tag, "articles_tags", "article_id", "tag_id", "id", "id")
declare tags: NonAttribute<Tag[]>;
}
class Tag extends Table<Tag> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare name: string;
declare slug: string;
@ManyToMany(() => Article, "articles_tags", "tag_id", "article_id", "id", "id")
declare articles: NonAttribute<Article[]>;
}
// Load articles with tags
const articles = await Article.where({}, {
include: {
tags: {}
}
});
// Load tag with articles
const tags = await Tag.where({ slug: "javascript" }, {
include: {
articles: {
limit: 10,
order: "DESC"
}
}
});
Course Enrollment System¶
class Student extends Table<Student> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare name: string;
declare email: string;
@ManyToMany(() => Course, "enrollments", "student_id", "course_id", "id", "id")
declare courses: NonAttribute<Course[]>;
}
class Course extends Table<Course> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare title: string;
declare instructor_id: string;
@ManyToMany(() => Student, "enrollments", "course_id", "student_id", "id", "id")
declare students: NonAttribute<Student[]>;
}
// Load student with enrolled courses
const student = await Student.first({ id: "student-123" }, {
include: {
courses: {}
}
});
console.log(student?.courses); // Course[]
Lifecycle Hook Decorators¶
Method decorators that run automatically around persistence operations. They are opt-in: they only run when the operation receives { hook: true }. Inside a hook, this is the entity instance.
| Decorator | Runs | Argument |
|---|---|---|
@BeforeCreate() | Before insert. May mutate this. | ā |
@AfterCreate() | After insert (this already persisted). | ā |
@BeforeUpdate() | Before update. | changes (delta) |
@AfterUpdate() | After update. | changes (delta) |
@BeforeDestroy() | Before delete. | ā |
@AfterDestroy() | After delete. | ā |
Example¶
import { Table, PrimaryKey, CreationOptional, BeforeCreate, AfterCreate, BeforeUpdate } from "@arcaelas/dynamite";
class User extends Table<User> {
@PrimaryKey()
declare id: CreationOptional<string>;
declare name: string;
declare email: string;
declare slug: string;
@BeforeCreate()
normalize() {
// `this` is the entity; mutations are persisted
this.email = this.email.toLowerCase().trim();
this.slug = this.name.toLowerCase().replace(/[^a-z0-9]+/g, "-");
}
@AfterCreate()
async notify() {
// Runs after the record has been persisted
await sendWelcomeEmail(this.email);
}
@BeforeUpdate()
syncEmail(changes: Partial<User>) {
// `changes` holds the delta about to be written
if (changes.email) {
this.email = changes.email.toLowerCase().trim();
}
}
}
// Hooks only run when explicitly enabled
const user = await User.create(
{ name: "John Doe", email: " JOHN@EXAMPLE.COM " },
{ hook: true }
);
console.log(user.email); // "john@example.com"
console.log(user.slug); // "john-doe"
- Multiple hooks of the same type run in declaration order; async hooks are awaited.
- Activate per operation:
User.create(data, { hook: true }),user.update(data, { hook: true }),user.destroy({ hook: true }). - In mass
update/delete, hooks run once per affected entity. - Inside a transaction (
{ hook: true, tx }),before*run when queued andafter*after commit. increment()/decrement()accept{ tx }but do not trigger hooks.
Combining Multiple Decorators¶
Complete Model with All Decorators¶
import { Table, PrimaryKey, Index, IndexSort, Default, Validate, Get, Set, NotNull, CreatedAt, UpdatedAt, DeleteAt, Name, HasMany, HasOne, BelongsTo, ManyToMany, CreationOptional, NonAttribute } from "@arcaelas/dynamite";
@Name("users")
class User extends Table<User> {
@PrimaryKey()
declare id: CreationOptional<string>;
@NotNull()
@Set((v) => (v as string).toLowerCase().trim())
@Validate((v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v as string) || "Invalid email")
@Name("email_address")
declare email: string;
@NotNull()
@Set((v) => (v as string).trim())
@Validate([
(v) => (v as string).length >= 2 || "Name too short",
(v) => (v as string).length <= 50 || "Name too long"
])
declare name: string;
@Default(() => 18)
@Validate((v) => (v as number) >= 0 && (v as number) <= 150 || "Invalid age")
declare age: CreationOptional<number>;
@Default(() => "customer")
@Validate((v) => ["customer", "admin", "moderator"].includes(v as string) || "Invalid role")
declare role: CreationOptional<string>;
@Default(() => true)
@Get((from) => from === 1)
@Set((to) => to ? 1 : 0)
declare active: CreationOptional<boolean>;
@Get((from) => JSON.parse(from))
@Set((to) => JSON.stringify(to))
declare preferences: CreationOptional<Record<string, any>>;
@CreatedAt()
declare created_at: CreationOptional<string>;
@UpdatedAt()
declare updated_at: CreationOptional<string>;
@DeleteAt()
declare deleted_at?: string;
@HasOne(() => Profile, "user_id", "id")
declare profile: NonAttribute<Profile | null>;
@HasMany(() => Order, "user_id", "id")
declare orders: NonAttribute<Order[]>;
@HasMany(() => Review, "user_id", "id")
declare reviews: NonAttribute<Review[]>;
@ManyToMany(() => Role, "users_roles", "user_id", "role_id", "id", "id")
declare roles: NonAttribute<Role[]>;
// Computed property
declare display_name: NonAttribute<string>;
constructor(data?: any) {
super(data);
Object.defineProperty(this, 'display_name', {
get: () => `${this.name} (${this.role})`,
enumerable: true
});
}
}
Custom Decorator Patterns¶
Every decorator in Dynamite is built with the same factory the library exports, decorator(). There is no privileged API: @PrimaryKey, @CreatedAt and @HasMany are written exactly like the ones you write yourself.
API of decorator()¶
import { decorator } from "@arcaelas/dynamite";
function decorator(
callback: (table_class: any, col: Column, params: any[]) => void
): (...params: any[]) => PropertyDecorator;
table_classis the constructor of the model the property belongs tocolis the column being decoratedparamsare the arguments the decorator was called with
The Column object¶
interface Column {
name: string; // column name in DynamoDB
get: Array<(current: any) => any>; // read pipeline
set: Array<(next: any, current: any) => any>; // write pipeline
store: {
index?: boolean; // partition key of the <field>_index GSI
indexSort?: boolean; // sort key of the table
primaryKey?: boolean; // primary key
softDelete?: boolean; // soft delete flag
createdAt?: boolean; // creation timestamp
updatedAt?: boolean; // update timestamp
readsCurrent?: boolean; // the pipeline needs the stored value
relation?: { // relation metadata
type: 'HasMany' | 'HasOne' | 'BelongsTo' | 'ManyToMany';
model: () => any;
foreignKey: string;
localKey: string;
relatedKey?: string;
pivotTable?: string;
relatedPK?: string;
};
};
}
You extend the pipelines by pushing onto the arrays: col.get.push(fn) and col.set.push(fn). Anything you drop into col.store travels with the schema and is readable through the SCHEMA symbol.
Decorator without parameters¶
import { decorator } from "@arcaelas/dynamite";
export const Uppercase = decorator((_model, col) => {
col.set.push((next) => typeof next === "string" ? next.toUpperCase() : next);
});
class User extends Table<User> {
@Uppercase()
declare country_code: string;
}
await User.create({ country_code: "us" }); // stored as "US"
Decorator with parameters¶
export const Length = decorator((_model, col, params) => {
const [min, max] = params;
col.set.push((next) => {
if (typeof next !== "string") throw new TypeError(`${col.name} must be a string`);
if (next.length < min) throw new Error(`${col.name} needs at least ${min} characters`);
if (next.length > max) throw new Error(`${col.name} cannot exceed ${max} characters`);
return next;
});
});
class User extends Table<User> {
@Length(3, 50)
declare username: string;
}
Decorator with both pipelines¶
export const Json = decorator((_model, col) => {
col.set.push((next) => typeof next === "object" && next !== null ? JSON.stringify(next) : next);
col.get.push((current) => {
if (typeof current !== "string") return current;
try { return JSON.parse(current); } catch { return current; }
});
});
class Settings extends Table<Settings> {
@Json()
declare preferences: Record<string, unknown>;
}
The write pipeline runs on assignment and the read pipeline on every access, which is what makes @Get a transformation of the value you read and not of the value you store.
Reading the stored value¶
A function in col.set receives (next, current): the incoming value and the one the instance already held. That is how @CreatedAt stays immutable:
export const Immutable = decorator((_model, col) => {
col.store.readsCurrent = true;
col.set.push((next, current) => current ?? next);
});
Declaring the second argument has a cost. update() by primary key resolves in a single write precisely because it does not need to read the record first; a column whose pipeline asks for current forces that read. Mark it with col.store.readsCurrent = true so the library knows, and only declare current when you truly use it.
@Set and @Validate set that flag on their own by inspecting the function you pass them.
Decorator that writes metadata¶
import { decorator, SCHEMA } from "@arcaelas/dynamite";
export const Searchable = decorator((model, col) => {
col.store.index = true; // <field>_index GSI
(model as any)[SCHEMA].gsis.add(col.name);
});
The schema behind SCHEMA holds the table name, the primary key, the expected GSIs, the hooks and every column. @Name writes to it to rename a table, and @PrimaryKey to record which column is the key.
Composing decorators¶
A decorator that is just another one with a fixed argument is written as a plain function, the way @Default and @NotNull are:
import { Set, Validate } from "@arcaelas/dynamite";
export const Slug = () => Set((next: any) =>
typeof next === "string" ? next.toLowerCase().replace(/\W+/g, "-") : next
);
export const Email = (message = "Invalid email") => Validate((next: any) =>
/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(String(next)) || message
);
class User extends Table<User> {
@Slug() declare handle: string;
@Email() declare email: string;
}
Order of execution¶
TypeScript applies decorators bottom to top, so the one closest to the property is pushed into the pipeline first and therefore runs first on write:
class Example extends Table<Example> {
@Validate((v) => v <= 100 || "Max 100") // runs third
@Set((v) => Math.abs(v)) // runs second
@NotNull("Required") // runs first
declare value: number;
}
// -50 -> NotNull passes -> Set turns it into 50 -> Validate passes
Best Practices¶
1. Use CreationOptional Appropriately¶
class User extends Table<User> {
// Always CreationOptional with @Default
@Default(() => crypto.randomUUID())
declare id: CreationOptional<string>;
// Always CreationOptional with @CreatedAt/@UpdatedAt
@CreatedAt()
declare created_at: CreationOptional<string>;
// Required fields without CreationOptional
@NotNull()
declare email: string;
}
2. Decorator Order¶
class User extends Table<User> {
// Recommended order: Key -> Validation -> Transformation -> Defaults -> Timestamps
@PrimaryKey()
declare id: CreationOptional<string>;
@NotNull()
@Validate((v) => /^[^\s@]+@/.test(v as string) || "Invalid")
@Set((v) => (v as string).toLowerCase())
@Name("email_address")
declare email: string;
}
3. Descriptive Validations¶
// Bad
@Validate((v) => (v as number) > 0)
declare price: number;
// Good
@Validate((v) => (v as number) > 0 || "Price must be greater than 0")
declare price: number;
4. Relationships with NonAttribute¶
class User extends Table<User> {
// Always mark relationships as NonAttribute
@HasMany(() => Order, "user_id", "id")
declare orders: NonAttribute<Order[]>;
@HasOne(() => Profile, "user_id", "id")
declare profile: NonAttribute<Profile | null>;
@BelongsTo(() => Company, "id", "company_id")
declare company: NonAttribute<Company | null>;
@ManyToMany(() => Role, "users_roles", "user_id", "role_id", "id", "id")
declare roles: NonAttribute<Role[]>;
}
5. Use snake_case for Field Names¶
class User extends Table<User> {
// Good: snake_case
declare user_id: string;
declare created_at: string;
declare first_name: string;
// Bad: camelCase
// declare userId: string;
// declare createdAt: string;
// declare firstName: string;
}
This guide covers all decorators available in Dynamite with practical examples and recommended patterns for building robust and type-safe models.