SkillAgentSearch skills...

Jao

Just Another Orm - A type-safe, model-first ORM for Dart development. Supports PostgreSQL, SQLite, and MySQL.

Install / Use

/learn @nexlabstudio/Jao

README

JAO - Django-inspired ORM for Dart

Tests codecov pub package License: MIT

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 CLIjao 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 configuration
  • lib/config/database.dart — Database configuration
  • lib/migrations/ — Migrations directory
  • bin/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

View on GitHub
GitHub Stars18
CategoryDevelopment
Updated12d ago
Forks0

Languages

Dart

Security Score

80/100

Audited on Mar 16, 2026

No findings