Jao
Just Another Orm - A type-safe, model-first ORM for Dart development. Supports PostgreSQL, SQLite, and MySQL.
Install / Use
/learn @nexlabstudio/JaoREADME
JAO - Django-inspired ORM for Dart
A type-safe, model-first ORM for Dart backend development. Built for PostgreSQL, SQLite, and MySQL. Works seamlessly with Dart Frog, Shelf, or any Dart server framework.
JAO (Just Another ORM) — Because writing raw SQL shouldn't be your only option.
Why JAO?
The problem: Dart's backend ecosystem lacks a Django style ORM. You're stuck with raw SQL strings, manual result mapping, and runtime errors waiting to happen.
The solution: JAO brings Django's elegant query API to Dart.
// ❌ Before: Raw SQL, manual mapping, runtime errors
final result = await db.query(
'SELECT * FROM authors WHERE age >= ? AND is_active = ? ORDER BY name LIMIT ?',
[18, true, 10]
);
final authors = result.map((row) => Author.fromMap(row)).toList();
// ✅ After: Type-safe, chainable, IDE autocomplete
final authors = await Authors.objects
.filter(Authors.$.age.gte(18) & Authors.$.isActive.eq(true))
.orderBy(Authors.$.name.asc())
.limit(10)
.toList();
Features
- Type-safe queries — Catch errors at compile time, not runtime
- Lazy QuerySets — Chain filters without hitting the DB until needed
- Django-style API — Familiar patterns:
filter(),exclude(),orderBy() - Cross-database — PostgreSQL, SQLite, MySQL with zero code changes
- Django-style CLI —
jao makemigrations,jao migrate,jao rollback - Code generation — Define models once, get queries and serialization for free
- Framework agnostic — Works with Dart Frog, Shelf, or any Dart backend
- No middleware required — Query directly in your route handlers
Quick Start
1. Install
dart pub add jao
dart pub add --dev build_runner jao_generator
dart pub global activate jao_cli
2. Initialize Project
jao init
This creates:
jao.yaml— Paths configurationlib/config/database.dart— Database configurationlib/migrations/— Migrations directorybin/migrate.dart— Migration CLI entry point
3. Define Models
import 'package:jao/jao.dart';
part 'models.g.dart';
@Model()
class Author {
@AutoField()
late int id;
@CharField(maxLength: 100)
late String name;
@EmailField(unique: true)
late String email;
@IntegerField(min: 0)
late int age;
@BooleanField(defaultValue: true)
late bool isActive;
@TextField(nullable: true)
late String? bio;
@DateTimeField(autoNowAdd: true)
late DateTime createdAt;
@DateTimeField(autoNow: true)
late DateTime updatedAt;
}
@Model()
class Post {
@AutoField()
late int id;
@CharField(maxLength: 200)
late String title;
@TextField()
late String content;
@ForeignKey(Author, onDelete: OnDelete.cascade)
late int authorId;
@BooleanField(defaultValue: false)
late bool isPublished;
@DateTimeField(autoNowAdd: true)
late DateTime createdAt;
}
4. Generate Code
dart run build_runner build
5. Configure Database
Edit lib/config/database.dart:
import 'package:jao/jao.dart';
// SQLite (default)
final databaseConfig = DatabaseConfig.sqlite('database.db');
const databaseAdapter = SqliteAdapter();
// PostgreSQL
// final databaseConfig = DatabaseConfig.postgres(
// database: 'myapp',
// username: 'postgres',
// password: 'password',
// );
// const databaseAdapter = PostgresAdapter();
// MySQL
// final databaseConfig = DatabaseConfig.mysql(
// database: 'myapp',
// username: 'root',
// password: 'password',
// );
// const databaseAdapter = MySqlAdapter();
6. Register Model Schemas
In bin/migrate.dart:
import 'dart:io';
import 'package:jao_cli/jao_cli.dart';
import 'package:your_app/models/models.dart';
import '../lib/config/database.dart';
import '../lib/migrations/migrations.dart';
void main(List<String> args) async {
final config = MigrationRunnerConfig(
database: databaseConfig,
adapter: databaseAdapter,
migrations: allMigrations,
modelSchemas: [
Authors.schema,
Posts.schema,
],
);
final cli = JaoCli(config);
exit(await cli.run(args));
}
7. Create & Run Migrations
jao makemigrations
jao migrate
8. Initialize Database
Call once at application startup:
import 'package:jao/jao.dart';
import 'lib/config/database.dart';
Future<void> main() async {
await Jao.configure(
adapter: databaseAdapter,
config: databaseConfig,
);
// Start your server...
}
9. Query Your Data
import 'package:your_app/models/models.dart';
Future<Response> onRequest(RequestContext context) async {
// Get all authors
final authors = await Authors.objects.all().toList();
// Filter with type-safe field accessors
final activeAdults = await Authors.objects
.filter(Authors.$.age.gte(18))
.filter(Authors.$.isActive.eq(true))
.orderBy(Authors.$.name.asc())
.toList();
// Create
final author = await Authors.objects.create({
'name': 'John Doe',
'email': 'john@example.com',
'age': 30,
});
// Update
await Authors.objects
.filter(Authors.$.id.eq(1))
.update({'name': 'Jane Doe'});
// Delete
await Authors.objects
.filter(Authors.$.isActive.eq(false))
.delete();
return Response.json(body: authors);
}
Query API
Filtering
// Exact match
Authors.objects.filter(Authors.$.name.eq('John'));
// Comparisons
Authors.objects.filter(Authors.$.age.gte(18));
Authors.objects.filter(Authors.$.age.lt(65));
Authors.objects.filter(Authors.$.age.between(18, 65));
// String lookups
Authors.objects.filter(Authors.$.name.contains('John'));
Authors.objects.filter(Authors.$.email.endsWith('@gmail.com'));
Authors.objects.filter(Authors.$.name.startsWith('Dr.'));
// Case-insensitive
Authors.objects.filter(Authors.$.name.iContains('john'));
// Null checks
Authors.objects.filter(Authors.$.bio.isNull());
Authors.objects.filter(Authors.$.bio.isNotNull());
// In list
Authors.objects.filter(Authors.$.status.inList(['active', 'pending']));
Boolean Logic
// AND (chained filters)
Authors.objects
.filter(Authors.$.age.gte(18))
.filter(Authors.$.isActive.eq(true));
// AND (& operator)
Authors.objects.filter(
Authors.$.age.gte(18) & Authors.$.isActive.eq(true)
);
// OR (| operator)
Authors.objects.filter(
Authors.$.age.lt(18) | Authors.$.age.gte(65)
);
// NOT (~ operator)
Authors.objects.filter(~Authors.$.name.eq('Admin'));
// Complex queries
Authors.objects.filter(
(Authors.$.age.gte(18) & Authors.$.isActive.eq(true)) |
Authors.$.role.eq('admin')
);
Ordering & Pagination
// Ascending
Authors.objects.orderBy(Authors.$.name.asc());
// Descending
Authors.objects.orderBy(Authors.$.createdAt.desc());
// Multiple columns
Authors.objects.orderBy(
Authors.$.isActive.desc(),
Authors.$.name.asc(),
);
// Pagination
Authors.objects.offset(20).limit(10);
// Slice
Authors.objects.slice(20, 30);
Aggregations
final stats = await Authors.objects.aggregate({
'count': Count.all(),
'avg_age': Avg(Authors.$.age.col),
'max_age': Max(Authors.$.age.col),
'min_age': Min(Authors.$.age.col),
});
// stats = {'count': 150, 'avg_age': 34.5, 'max_age': 89, 'min_age': 18}
CRUD Operations
// Create
final author = await Authors.objects.create({
'name': 'John',
'email': 'john@example.com',
'age': 30,
});
// Bulk create
final authors = await Authors.objects.bulkCreate([
{'name': 'Alice', 'email': 'alice@example.com', 'age': 25},
{'name': 'Bob', 'email': 'bob@example.com', 'age': 35},
]);
// Get by primary key
final author = await Authors.objects.get(1);
// Get or null
final author = await Authors.objects.getOrNull(999);
// First / Last
final first = await Authors.objects.orderBy(Authors.$.name.asc()).first();
final last = await Authors.objects.orderBy(Authors.$.name.asc()).last();
// Exists / Count
final hasAdmins = await Authors.objects.filter(Authors.$.role.eq('admin')).exists();
final count = await Authors.objects.count();
// Get or create
final (author, created) = await Authors.objects.getOrCreate(
condition: Authors.$.email.eq('john@example.com'),
defaults: {'name': 'John', 'age': 30},
);
// Update
final updatedCount = await Authors.objects
.filter(Authors.$.isActive.eq(false))
.update({'isActive': true});
// Delete
final deletedCount = await Authors.objects
.filter(Authors.$.email.endsWith('@spam.com'))
.delete();
Field Types
| Annotation | Dart Type | Database Type | Description |
|------------|-----------|---------------|-------------|
| @AutoField() | int | SERIAL | Auto-increment primary key |
| @BigAutoField() | int | BIGSERIAL | Big auto-increment primary key |
| @CharField(maxLength: n) | String | VARCHAR(n) | Limited-length string |
| @TextField() | String | TEXT | Unlimited-length string |
| @EmailField() | String | VARCHAR(254) | Email with validation |
| @IntegerField() | int | INTEGER | Standard integer |
| @BigIntegerField() | int | BIGINT | Large integer |
| @SmallIntegerField() | int | SMALLINT | Small integer |
| @PositiveIntegerField() | int | INTEGER | Positive integer (min: 0) |
| @FloatField() | double | REAL | Floating point |
| @DecimalField(decimalPlaces: n) | double | DECIMAL | Fixed precision decimal |
| @BooleanField() | bool | BOOLEAN | True/False |
| @DateField() | DateTime | DATE | Date only |
| @DateTimeField() | DateTime | TIMESTAMPTZ | Date and time w
