Skip to content

Latest commit

ย 

History

38 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Elegance ORM

Elegance is a small, fluent ORM for Node.js. It is designed for applications that want a Laravel-like database experience without hiding the underlying SQL.

It provides:

  • PostgreSQL, MySQL/MariaDB, and SQLite drivers
  • fluent queries with parameter binding
  • active-record models with dirty tracking and timestamps
  • schema builders and migrations
  • transactions that work with raw queries, builders, and models
  • a CLI for initializing projects and managing migrations

Elegance supports TypeScript and JavaScript projects and requires Node.js 18 or newer.

Elegance is currently beta software. Its API may change, and it is not yet intended for production use without your own testing and review.

Installation

npm install elegance-orm

The package also provides the elegance command:

npx elegance <command>

Quick start

1. Create the project files

From your project root, run:

npx elegance init

This creates an elegance.config.ts file and the directories used by the CLI.

2. Configure a data source

// src/database/dataSource.ts
import { DataSource } from "elegance-orm";

const dataSource = new DataSource({
  driver: "sqlite",
  host: "",
  port: 0,
  user: "",
  password: "",
  database: "./database/app.sqlite"
});

export default dataSource;

For PostgreSQL or MySQL, provide the corresponding host, port, credentials, and database name. See Configuration for the complete option list.

3. Initialize the connection

import dataSource from "./database/dataSource";

await dataSource.initialize();

// Use models, Schema, or dataSource queries here.

await dataSource.destroy();

DataSource is a convenient application-facing wrapper. Internally, Elegance uses one active connection, so initialize the data source before creating builders or running schema operations.

4. Define a model

import { Model } from "elegance-orm";

export default class User extends Model {
  protected table = "users";
  protected primaryKey = "id";
  protected fillable = ["name", "email", "status"];
  protected hidden = ["password"];
}

Models should explicitly declare the attributes that may be mass-assigned through create() and fill().

5. Query and save records

const activeUsers = await User.query()
  .select("id", "name", "email")
  .where("status", "=", "active")
  .orderByDesc("created_at")
  .limit(10)
  .get();

const user = await User.create({
  name: "Ada",
  email: "ada@example.com",
  status: "active"
});

user.status = "inactive";
await user.save();

Models

A model represents one database table. Attribute values are exposed through the model instance, while the model keeps the original values so that save() can update only changed columns.

Model properties

class Article extends Model {
  protected table = "articles";
  protected primaryKey = "id";
  protected fillable = ["title", "body", "status"];
  protected guarded = ["user_id", "is_admin"];
  protected hidden = ["user_id"];
  protected timestamps = true;
  protected softDeletes = false;
}
Property Default Purpose
table "" Physical table name.
primaryKey "id" Primary-key column used by find() and model updates.
fillable ["*"] Mass-assignment allowlist. Declare an explicit list for predictable behavior.
guarded [] Attributes excluded from mass assignment. Use ["*"] to deny all by default.
hidden [] Attributes removed by toJSON().
timestamps true Adds created_at and updated_at values during model saves.
softDeletes false Uses deleted_at instead of removing records.
CREATED_AT "created_at" Name of the creation timestamp column.
UPDATED_AT "updated_at" Name of the update timestamp column.
DELETED_AT "deleted_at" Name of the soft-delete timestamp column.

Creating and updating

const article = await Article.create({
  title: "Hello",
  body: "World",
  status: "draft"
});

article.status = "published";
await article.save();

create() inserts a record immediately. save() inserts new models and updates existing models. Existing models only write dirty attributes. Timestamp columns are managed automatically when timestamps are enabled.

Soft deletes

Add a nullable soft-delete column in the migration and enable it on the model:

await Schema.create("articles", table => {
  table.id();
  table.string("title");
  table.softDeletes();
  table.timestamps();
});

class Article extends Model {
  protected table = "articles";
  protected fillable = ["title"];
  protected softDeletes = true;
}

Soft-deleting models exclude deleted records from queries by default:

await article.delete();

await Article.query().get();               // active records
await Article.query().withTrashed().get(); // active and deleted records
await Article.query().onlyTrashed().get(); // deleted records

await article.restore();
await article.forceDelete();

delete(), restore(), and forceDelete() are also available on the query builder and return the number of affected rows. trashed() reports whether a model instance is deleted. Soft-delete-specific methods throw when softDeletes is disabled.

Override DELETED_AT when the table uses a different column name. Use table.softDeletes("removed_at") in the matching migration.

Model methods

Method Returns Description
Model.create(attributes) Promise<Model> Creates and inserts a model using mass-assignment rules.
model.fill(attributes) void Assigns allowed attributes. Totally guarded models throw MassAssignmentException for rejected input.
model.save() Promise<boolean> Inserts a new model or updates dirty attributes on an existing model.
model.delete() Promise<boolean> Soft deletes the model when enabled; otherwise deletes it permanently.
model.restore() Promise<boolean> Restores a soft-deleted model.
model.forceDelete() Promise<boolean> Permanently deletes a soft-deleting model.
model.trashed() boolean Reports whether the model has been soft deleted.
model.toJSON() object Returns model attributes without hidden columns.

Mass assignment

class AdminUser extends Model {
  protected table = "users";
  protected fillable = ["name", "email"];
  protected guarded = ["is_admin"];
}

await AdminUser.create({
  name: "Ada",
  email: "ada@example.com",
  is_admin: true // throws MassAssignmentException
});

For untrusted input, keep fillable explicit and use guarded = ["*"] when you want to deny all attributes unless they are listed in fillable.

query().insert() is intended for bulk or trusted data imports. It bypasses model fillable checks and does not add model timestamps.

Query builder

Call Model.query() to create a builder. Builders are fluent and use bound values for conditions and mutations.

Use toSql() when you want to inspect the generated SQL:

const query = User.query("u")
  .select("u.id", "u.name", "profiles.bio")
  .leftJoin("profiles", "u.id", "=", "profiles.user_id")
  .where("u.status", "=", "active")
  .whereBetween("u.created_at", ["2024-01-01", "2024-12-31"])
  .orderBy("u.created_at", "desc")
  .limit(5)
  .offset(0);

console.log(query.toSql());

The SQL uses ? placeholders. PostgreSQL converts those placeholders to its numbered parameter format before execution.

Selecting

await User.query()
  .select("id", "name")
  .addSelect("email")
  .distinct()
  .get();
Method Description
select(...columns) Adds columns to the selection list.
addSelect(...columns) Adds columns to the selection list.
distinct() Adds distinct to the select statement.

Filtering

const users = await User.query()
  .where("status", "=", "active")
  .whereIn("role", ["admin", "editor"])
  .whereBetween("created_at", ["2024-01-01", "2024-12-31"])
  .whereNull("deleted_at")
  .get();
Method Description
where(column, operator, value) Adds an and basic condition.
andWhere(column, operator, value) Adds an and basic condition. A null operator is treated as =.
orWhere(column, operator, value) Adds an or basic condition. A null operator is treated as =.
whereIn(column, values) Matches one of a non-empty list of values.
whereNotIn(column, values) Excludes a non-empty list of values.
whereBetween(column, [from, to]) Matches an inclusive range.
whereNotBetween(column, [from, to]) Excludes an inclusive range.
whereNull(column) Matches NULL.
whereNotNull(column) Excludes NULL.

Joins

const posts = await Post.query("p")
  .select("p.id", "p.title", "u.name as author")
  .leftJoin("users as u", "p.user_id", "=", "u.id")
  .get();
Method Description
join(table, first, operator, second) Adds an inner join.
innerJoin(table, first, operator, second) Alias for an inner join.
leftJoin(table, first, operator, second) Adds a left join.

Table aliases and qualified column names are supported.

Grouping and aggregates

const count = await User.query()
  .where("status", "=", "active")
  .count();

const totalVisits = await User.query().sum("visits");

const summaries = await User.query()
  .select("status")
  .groupBy("status")
  .having("status", "!=", "archived")
  .get();
Method Returns Description
count(column = "*") Promise<number> Counts matching rows.
sum(column) Promise<number> Sums a column.
avg(column) Promise<number> Calculates the average.
min(column) Promise<number> Returns the minimum value.
max(column) Promise<number> Returns the maximum value.

Ordering and pagination

const page = await Article.query()
  .latest("published_at")
  .limit(10)
  .offset(20)
  .get();
Method Description
orderBy(column, direction = "asc") Adds ascending or descending ordering.
orderByDesc(column) Adds descending ordering.
latest(column = primaryKey) Orders by a column descending.
oldest(column = primaryKey) Orders by a column ascending.
limit(value) Limits the number of rows.
offset(value) Skips rows before returning results.

Fetching records

Method Returns Description
get() Promise<Model[]> Returns all matching models.
first() Promise<Model | null> Returns the first matching model.
find(id) Promise<Model | null> Looks up a model by its primary key.

Mutating records

await User.query().insert([
  { name: "Grace", email: "grace@example.com" },
  { name: "Katherine", email: "katherine@example.com" }
]);

await User.query()
  .where("status", "=", "draft")
  .update({ status: "published" });

await User.query()
  .where("id", "=", 10)
  .delete();
Method Returns Description
insert(records) Promise<void> Performs a bulk insert. Bypasses fillable checks and model timestamps.
insertGetId(attributes) Promise<number | null> Inserts one row and returns its generated key when supported by the driver.
update(attributes) Promise<number> Updates rows matching the current constraints.
delete() Promise<number> Deletes matching rows, or soft deletes them when enabled on the model.
restore() Promise<number> Restores matching soft-deleted rows.
forceDelete() Promise<number> Permanently deletes matching rows.
withTrashed(include = true) Builder Includes soft-deleted rows.
withoutTrashed() Builder Excludes soft-deleted rows.
onlyTrashed() Builder Selects only soft-deleted rows.
toSql() string Returns the compiled select SQL without executing it.

Schema builder

Schema operations are available through the static Schema facade:

import { Schema } from "elegance-orm";

await Schema.create("users", table => {
  table.id();
  table.string("name");
  table.string("email").unique();
  table.string("status").default("pending");
  table.timestamps();
});

To modify an existing table:

await Schema.table("users", table => {
  table.string("display_name").nullable();
  table.index(["display_name"]);
});

Schema methods

Method Returns Description
Schema.create(table, callback) Promise<void> Creates a table from a blueprint.
Schema.table(table, callback) Promise<void> Alters an existing table from a blueprint.
Schema.drop(table) Promise<void> Drops a table.
Schema.dropIfExists(table) Promise<void> Drops a table if it exists.
Schema.renameColumn(table, from, to) Promise<void> Renames a column.
Schema.dropColumns(table, columns) Promise<void> Removes one or more columns.
Schema.hasTable(table) Promise<boolean> Checks whether a table exists.
Schema.hasColumn(table, column) Promise<boolean> Checks whether a column exists.

Column methods

Group Methods
IDs id, increment, bigIncrement, smallIncrement
Integers integer, bigInteger, smallInteger
Text char, string, text, longText
Date and time date, dateTime, time, timestamp, year, timestamps, softDeletes
Other uuid, json, binary, boolean, float, double

Column definitions support nullable(), default(), unsigned(), useCurrent(), useCurrentOnUpdate(), onUpdate(), after(), first(), comments(), index(), primary(), and unique().

Use dropSoftDeletes() when removing the default deleted_at column, or pass its custom name.

Indexes and foreign keys

await Schema.create("posts", table => {
  table.id();
  table.integer("author_id");
  table.string("title");

  table.foreign("author_id")
    .references("id")
    .on("users")
    .onDelete("cascade")
    .onUpdate("cascade");
});

Blueprint methods for indexes and constraints are primary(), unique(), index(), dropPrimary(), dropUnique(), dropIndex(), and dropForeign().

The generated SQL is driver-specific. Some schema features, especially table alterations and foreign-key changes, have different capabilities across SQLite, PostgreSQL, and MySQL.

Migrations

A migration is a versioned pair of up() and down() methods:

import { Migration, Schema } from "elegance-orm";

export default class CreateUsersTable extends Migration {
  async up() {
    await Schema.create("users", table => {
      table.id();
      table.string("name");
      table.string("email").unique();
      table.timestamps();
    });
  }

  async down() {
    await Schema.dropIfExists("users");
  }
}

Generate migration files with:

npx elegance make:migration create_users_table --create
npx elegance make:migration add_status_to_users_table --update

You can provide a table explicitly with --table=users. The migration repository records executed files in the configured migrations table.

Run migrations with:

npx elegance migrate
npx elegance migrate:rollback
npx elegance migrate:rollback --step=2
npx elegance migrate:refresh

migrate:rollback rolls back the latest batch by default. migrate:refresh rolls back recorded migrations and runs them again.

Transactions

Use DataSource.transaction() when several operations must succeed or fail together:

await dataSource.transaction(async trx => {
  await trx.insert(
    "insert into orders (user_id, total) values (?, ?)",
    [userId, total]
  );

  await User.query()
    .where("id", "=", userId)
    .update({ last_order_total: total } as any);

  await User.create({
    name: "Transaction User",
    email: "tx@example.com"
  });
});

Raw queries, builders, and models called inside the callback automatically use the active transaction. If the callback throws, the transaction is rolled back. Nested transactions reuse the current transaction; savepoints are not currently provided.

The transaction callback also receives a low-level client with rawQuery, select, insert, update, and delete methods.

Raw database access

const rows = await dataSource.select(
  "select * from users where email like ?",
  ["%example.com"]
);

await dataSource.insert(
  "insert into audit_log (payload) values (?)",
  [JSON.stringify(rows)]
);

DataSource methods

Method Returns Description
initialize(callback?) Promise<void> Opens the configured connection.
destroy() Promise<void> Closes the connection.
isInitialized() boolean Reports whether the connection is initialized.
rawQuery(query, bindings?) Promise<any> Executes a raw statement.
select(query, bindings?) Promise<any[]> Executes a query and returns rows.
insert(query, bindings?) Promise<any> Executes an insert and returns the driver result or generated ID.
update(query, bindings) Promise<number> Executes an update and returns affected rows.
delete(query, bindings) Promise<number> Executes a delete and returns affected rows.
transaction(callback) Promise<T> Runs the callback inside a transaction.

CLI

Command Purpose Options
init Create the project configuration and directories. โ€”
make:migration <name> Generate a timestamped migration. --table, --create, --update
make:model <path> Generate a model file. Nested paths are supported.
migrate Run pending migrations. โ€”
migrate:rollback Roll back the latest migration batch. --step=2
migrate:refresh Roll back and rerun migrations. โ€”

Configuration

The CLI looks for one of these files in the project root:

elegance.config.ts
elegance.config.js
elegance.config.cjs
elegance.config.mjs
elegance.config.json

Example:

const config = {
  dataSource: "./src/database/dataSource",
  migrations: {
    directory: "./database/migrations",
    table: "migrations"
  },
  models: {
    directory: "./src/app/models"
  },
  language: "typescript"
};

export default config;
Option Description
dataSource Module path exporting a DataSource instance.
migrations.directory Directory containing migration files.
migrations.table Table used to record executed migrations. Defaults to migrations.
models.directory Destination directory for generated models.
language typescript or javascript; controls generated files.

Data source options

type DataSourceOptions = {
  driver: "mysql" | "postgresql" | "sqlite";
  host: string;
  port: number;
  user: string;
  password: string;
  database: string;
  maxPoolSize?: number;
};

For SQLite, host, port, user, and password are unused; database is the path to the SQLite file.

About

๐˜ˆ ๐˜ญ๐˜ช๐˜จ๐˜ฉ๐˜ต๐˜ธ๐˜ฆ๐˜ช๐˜จ๐˜ฉ๐˜ต ๐˜ฐ๐˜ฃ๐˜ซ๐˜ฆ๐˜ค๐˜ต ๐˜ณ๐˜ฆ๐˜ญ๐˜ข๐˜ต๐˜ช๐˜ฐ๐˜ฏ๐˜ข๐˜ญ ๐˜ฎ๐˜ข๐˜ฑ๐˜ฑ๐˜ฆ๐˜ณ ๐˜ง๐˜ฐ๐˜ณ ๐˜•๐˜ฐ๐˜ฅ๐˜ฆ.๐˜ซ๐˜ด

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages