ActiveRecord-style ORM for Dart with GORM-like auto-migrations.
Supports PostgreSQL, MySQL, and SQLite.
dependencies:
duxt_orm: ^0.3.0import 'package:duxt_orm/duxt_orm.dart';
class User extends Entity {
int? _id;
String? email;
String? name;
bool isActive;
User({int? id, this.email, this.name, this.isActive = true}) : _id = id;
@override dynamic get id => _id;
@override set id(dynamic value) => _id = value as int?;
@override
Map<String, dynamic> toMap() => {
'id': id,
'email': email,
'name': name,
'is_active': isActive ? 1 : 0,
};
factory User.fromRow(Map<String, dynamic> row) => User(
id: row['id'] as int?,
email: row['email'] as String?,
name: row['name'] as String?,
isActive: (row['is_active'] as int?) == 1,
);
static void register() {
Entity.registerModel<User>(
User.fromRow,
schema: {
'id': Column.integer().primaryKey().autoIncrement(),
'email': Column.string(255).unique().notNull(),
'name': Column.string(100).notNull(),
'is_active': Column.boolean().defaultValue(true),
'created_at': Column.dateTime().nullable(),
'updated_at': Column.dateTime().nullable(),
},
);
}
}// Register models
User.register();
Post.register();
// Initialize with config
await DuxtOrm.init(DuxtConfig.database);
// Auto-create tables
await DuxtOrm.migrate();final users = Model<User>();
// Create
final user = User(email: 'test@example.com', name: 'Test');
await user.save();
// Query all
final all = await users.all();
// Find by ID
final found = await users.find(1);
// Query with conditions
final admins = await users.where('role', 'admin').get();
// Query builder
final results = await users.query()
.where('is_active', 1)
.orderByDesc('created_at')
.limit(10)
.get();
// Update
found?.name = 'Updated Name';
await found?.save();
// Delete (soft delete by default)
await found?.destroy();Configure in your duxt.config.dart:
class DuxtConfig {
static const database = (
driver: 'postgres', // 'postgres', 'mysql', 'sqlite'
host: String.fromEnvironment('DB_HOST', defaultValue: 'localhost'),
port: int.fromEnvironment('DB_PORT', defaultValue: 5432),
database: String.fromEnvironment('DB_NAME', defaultValue: 'myapp'),
username: String.fromEnvironment('DB_USER', defaultValue: ''),
password: String.fromEnvironment('DB_PASS', defaultValue: ''),
path: 'data/app.db', // SQLite only
ssl: false,
);
}Soft deletes are enabled by default. destroy() sets deleted_at instead of removing the row.
final posts = Model<Post>();
final post = await posts.find(1);
// Soft delete
await post!.destroy();
print(post.trashed); // true
// Restore
await post.restore();
// Permanently delete
await post.forceDelete();
// Query scopes
final all = await posts.withTrashed().get(); // Include trashed
final trashed = await posts.onlyTrashed().get(); // Only trashedOpt out per model:
Entity.registerModel<LogEntry>(LogEntry.fromRow, softDeletes: false, schema: { ... });Override hooks to run logic around persistence:
class Post extends Entity {
@override
Future<void> beforeSave() async {
slug ??= title?.toLowerCase().replaceAll(' ', '-');
}
@override
Future<void> afterDelete() async {
await FileStorage.delete('posts/$id');
}
}Available hooks: beforeSave(), afterSave(), beforeDelete(), afterDelete().
class Post extends Entity {
Category? get category => getRelation<Category>('category');
List<Comment> get comments => getRelation<List<Comment>>('comments') ?? [];
static void register() {
Entity.registerModel<Post>(Post.fromRow, schema: { ... });
Entity.registerRelation<Post>('category', BelongsTo<Category>(foreignKey: 'category_id'));
Entity.registerRelation<Post>('comments', HasMany<Comment>(foreignKey: 'post_id'));
}
}// Load relations (no N+1 queries)
final posts = await Model<Post>().include(['category', 'comments']).get();
print(posts.first.category?.name);
// Nested dot notation
final posts = await Model<Post>().include(['author.profile', 'comments.user']).get();
print(posts.first.author?.profile?.bio);HasMany<T>(foreignKey: ...)— One-to-manyHasOne<T>(foreignKey: ...)— One-to-oneBelongsTo<T>(foreignKey: ...)— Inverse of has-many/has-oneBelongsToMany<T>(pivotTable: ..., foreignPivotKey: ..., relatedPivotKey: ...)— Many-to-many
await post.attach('tags', tagId);
await post.detach('tags', tagId);
await post.sync('tags', [1, 2, 3]);Table names are auto-inferred from class names:
| Class Name | Table Name |
|---|---|
User |
users |
UserProfile |
user_profiles |
Category |
categories |
Override with tableName in registerModel().
Column.integer() // INTEGER
Column.bigInteger() // BIGINT
Column.string(255) // VARCHAR(255)
Column.text() // TEXT
Column.boolean() // BOOLEAN
Column.dateTime() // TIMESTAMP
Column.date() // DATE
Column.time() // TIME
Column.decimal(10, 2) // DECIMAL(10, 2)
Column.float() // FLOAT
Column.json() // JSON
Column.binary() // BLOBColumn.integer().primaryKey() // PRIMARY KEY
Column.integer().autoIncrement() // AUTO_INCREMENT
Column.string(255).notNull() // NOT NULL
Column.string(255).nullable() // NULL (default)
Column.string(255).unique() // UNIQUE
Column.boolean().defaultValue(true) // DEFAULT true
Column.integer().references('users') // FOREIGN KEYfinal users = Model<User>();
users.where('is_active', 1)
users.where('age', 18, '>=')
users.query()
.where('is_active', 1)
.orWhere('role', 'admin')
.whereNull('deleted_at')
.whereIn('id', [1, 2, 3])
.whereBetween('age', 18, 65)
.whereLike('name', '%john%')
.get();users.query().orderByDesc('created_at').skip(20).take(10).get();
users.query().latest().get(); // created_at DESCawait users.where('is_active', 1).count();
await users.query().sum('total');
await users.query().avg('age');
await users.query().max('price');
await users.query().min('price');await users.query().where('is_active', 0).delete();
await users.query().where('id', 1).increment('views');await DuxtOrm.transaction((tx) async {
await tx.query('INSERT INTO users (email, name) VALUES (?, ?)',
['test@example.com', 'Test']);
await tx.query('INSERT INTO profiles (user_id, bio) VALUES (?, ?)',
[1, 'Hello!']);
});final result = await DuxtOrm.raw(
'SELECT * FROM users WHERE email = ?',
['test@example.com'],
);Full docs at duxt.dev/duxt-orm.
MIT