Query and manage your database with CanxJS's elegant ORM. Zero-config setup with MySQL and PostgreSQL support.
MySQL primary, PostgreSQL secondary support
Fluent API for building complex queries
Type-safe models with static methods
Built-in connection pooling for performance
1import { initDatabase, closeDatabase } from "canxjs";23// Initialize database connection4await initDatabase({5driver: "mysql", // or "postgresql"6host: "localhost",7port: 3306,8database: "myapp",9username: "root",10password: "password",11pool: { min: 2, max: 10 },12logging: true // Log SQL queries13});1415// Close connection on shutdown16process.on("SIGTERM", async () => {17await closeDatabase();18});
1import { Model } from "canxjs";23// Define a User model4export class User extends Model {5protected static tableName = "users";6protected static primaryKey = "id";7protected static timestamps = true; // auto: created_at, updated_at89// Type definition for the model10id!: number;11name!: string;12email!: string;13role!: string;14created_at!: Date;15updated_at!: Date;1617// Relations18posts() {19return this.hasMany(Post, "user_id");20}2122profile() {23return this.hasOne(Profile, "user_id");24}25}2627export class Post extends Model {28// ...29user() {30return this.belongsTo(User, "user_id");31}3233tags() {34return this.belongsToMany(Tag, "post_tags", "post_id", "tag_id");35}36}
To prevent unauthorized modifications, define $fillable (whitelist) or $guarded (blacklist) properties.
1export class User extends Model {2// Allow only these fields to be mass-assigned3protected static fillable = ["name", "email", "password"];45// OR block these fields (everything else is allowed)6protected static guarded = ["id", "is_admin", "balance"];7}
1// Find by primary key2const user = await User.find(1);34// Get all records5const users = await User.all();67// Create a new record8const newUser = await User.create({9name: "John Doe",10email: "john@example.com",11role: "user"12});1314// Update by ID15await User.updateById(1, { name: "Jane Doe" });1617// Delete by ID18await User.deleteById(1);
Define relationships as methods on your model class using hasOne, hasMany, belongsTo, and belongsToMany.
1import { Model } from "canxjs";23// Define a User model4export class User extends Model {5protected static tableName = "users";6protected static primaryKey = "id";7protected static timestamps = true; // auto: created_at, updated_at89// Type definition for the model10id!: number;11name!: string;12email!: string;13role!: string;14created_at!: Date;15updated_at!: Date;1617// Relations18posts() {19return this.hasMany(Post, "user_id");20}2122profile() {23return this.hasOne(Profile, "user_id");24}25}2627export class Post extends Model {28// ...29user() {30return this.belongsTo(User, "user_id");31}3233tags() {34return this.belongsToMany(Tag, "post_tags", "post_id", "tag_id");35}36}
Beyond the basics, models support polymorphic relations (morphOne, morphMany,morphTo, morphToMany) and hasManyThrough. All of them work with eager loading via with().
1import { Model } from "canxjs";23export class User extends Model {4static tableName = "users";56// One-to-many7posts() { return this.hasMany(Post, "user_id"); }89// Polymorphic one-to-one: the related row stores10// imageable_id + imageable_type columns.11image() { return this.morphOne(Image, "imageable"); }1213// Has-many-through: comments on all of this user's posts.14// (Comment -> Post -> User)15comments() {16return this.hasManyThrough(Comment, Post, "user_id", "post_id");17}18}1920export class Post extends Model {21static tableName = "posts";2223// Polymorphic one-to-many24tags() { return this.morphMany(Tag, "taggable"); }25}2627export class Image extends Model {28static tableName = "images";29// Inverse of a polymorphic relation30imageable() { return this.morphTo(); }31}3233// Eager-load exactly like normal relations34const user = await User.with("image", "comments")35.where("id", "=", 1)36.first();3738console.log(user.relations.image); // Image | null39console.log(user.relations.comments); // Comment[]
belongsToMany (and morphToMany) relations expose pivot helpersattach(), detach(), and sync() to manage the join table. Duplicate pivot rows are skipped automatically, and extra pivot columns can be passed as a second argument.
1import { Model } from "canxjs";23export class User extends Model {4static tableName = "users";56// Many-to-many via the "role_user" pivot table7roles() {8return this.belongsToMany(Role, "role_user", "user_id", "role_id");9}10}1112const user = await User.find(1);1314// Attach one or many related ids (skips duplicate pivot rows)15await user.roles().attach(5);16await user.roles().attach([2, 3], { assigned_by: "admin" }); // extra pivot columns1718// Detach specific ids, or all when called without arguments19await user.roles().detach(5);20await user.roles().detach(); // remove every role2122// Replace the entire set in one call23await user.roles().sync([1, 2, 3]);2425// Read the related records26const roles = await user.roles().get();
Set static softDeletes = true to keep rows in the database and mark them with adeleted_at timestamp instead of removing them. Default queries hide trashed rows; usewithTrashedResults(), restore(), and forceDelete() to manage them.
1import { Model } from "canxjs";23export class Post extends Model {4static tableName = "posts";5static softDeletes = true; // enables the deleted_at column6}78const post = await Post.find(1);910// Soft delete: sets deleted_at instead of removing the row11await post.delete();1213// Default queries automatically exclude soft-deleted rows14await Post.find(1); // => null1516// Include trashed rows explicitly17const trashed = await Post.query()18.withTrashedResults()19.where("id", "=", 1)20.first();2122// Restore a soft-deleted model23await trashed.restore();2425// Permanently remove (ignores soft-delete scope)26await Post.query().where("id", "=", 1).forceDelete();
Declare a casts map to convert attributes to native types on read and serialize them on write — e.g. boolean, json, array, integer, datetime.
1import { Model } from "canxjs";23export class User extends Model {4static tableName = "users";56// Values are cast on read and serialized on write7protected casts = {8is_admin: "boolean", // 1/0 <-> true/false9meta: "json", // TEXT column <-> object/array10settings: "array",11last_login: "datetime" // string <-> Date12} as any;13}1415const user = await User.create({16name: "Ada",17is_admin: true,18meta: { theme: "dark" }19});2021typeof user.is_admin; // "boolean"22typeof user.meta; // "object"
1// Using the query builder for complex queries2const activeAdmins = await User.query()3.select("id", "name", "email")4.where("role", "=", "admin")5.where("status", "=", "active")6.orderBy("created_at", "desc")7.limit(10)8.get();910// With pagination11const page = 1;12const perPage = 20;13const users = await User.query()14.orderBy("id", "asc")15.limit(perPage)16.offset((page - 1) * perPage)17.get();1819// First record matching condition20const admin = await User.query()21.where("role", "=", "admin")22.first();
Paginate results automatically with the paginate() method.
1// Get paginated results (page 1, 15 items per page)2const result = await User.query()3.where("status", "=", "active")4.paginate(1, 15);56console.log(result);7/*8{9data: [...],10total: 45,11perPage: 15,12currentPage: 1,13lastPage: 314}15*/
1// Basic where2const users = await User.query()3.where("status", "=", "active")4.get();56// Multiple conditions (AND)7const results = await User.query()8.where("role", "=", "admin")9.where("status", "=", "active")10.get();1112// OR condition13const results = await User.query()14.where("role", "=", "admin")15.orWhere("role", "=", "moderator")16.get();1718// WHERE IN19const users = await User.query()20.whereIn("id", [1, 2, 3, 4, 5])21.get();2223// NULL checks24const unverified = await User.query()25.whereNull("email_verified_at")26.get();2728const verified = await User.query()29.whereNotNull("email_verified_at")30.get();
1// Inner join2const postsWithUsers = await Post.query()3.select("posts.*", "users.name as author")4.join("users", "posts.user_id", "=", "users.id")5.get();67// Left join8const results = await User.query()9.select("users.*")10.leftJoin("posts", "users.id", "=", "posts.user_id")11.get();
1// Count records2const totalUsers = await User.query().count();34// Sum5const totalSales = await Order.query()6.where("status", "=", "completed")7.sum("amount");89// Average10const avgRating = await Review.query().avg("rating");1112// Group by with aggregates13const salesByCategory = await Product.query()14.select("category")15.groupBy("category")16.get();
1// Execute raw SQL2const results = await User.query().raw(3"SELECT * FROM users WHERE created_at > ?",4["2024-01-01"]5);
CanxJS provides powerful eager loading capabilities to solve the N+1 query problem. You can load relationships at query time using with() or on existing models using load().
1// Eager load 'posts' relationship2const users = await User.query()3.with("posts")4.get();56// Eager load multiple relationships7const posts = await Post.query()8.with("author", "comments")9.get();1011// Lazy Eager Loading (on existing instance)12const user = await User.find(1);13await user.load("posts");