npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lara-node/db

v0.1.29

Published

Lara-Node ORM — Eloquent-style models, migrations, schema builder

Downloads

885

Readme

@lara-node/db

Eloquent-style ORM for Lara-Node — MySQL and MongoDB with a unified API for models, migrations, seeders, relationships, observers, and transactions.

Installation

pnpm add @lara-node/db

Runtime Flag: --expose-gc

Always start your application with --expose-gc. It lets the ORM release connection pool and MongoDB client memory promptly after heavy operations.

# Dev server
node --expose-gc -r @swc-node/register src/server.ts

# Artisan
node --expose-gc -r @swc-node/register src/artisan.ts migrate

The create-lara-node scaffold injects --expose-gc into all generated package.json scripts automatically.

Quick Start

import { Model, use } from "@lara-node/db";
import { SoftDeletes, Timestamps } from "@lara-node/db";

@use(SoftDeletes, Timestamps)
export class User extends Model {
  static table = "users";
  static fillable = ["name", "email", "password"];
  static hidden = ["password"];
  static casts = { created_at: "datetime", updated_at: "datetime" };
}

// Create
const user = await User.create({ name: "Alice", email: "[email protected]" });

// Query
const active = await User.where("status", "active").orderBy("name").get();

// Find
const found = await User.findOrFail(1);

// Update
found.name = "Alice B.";
await found.save();

// Soft delete
await found.delete();
await found.restore();

Model Basics

Static properties

| Property | Type | Default | Description | | --------------- | ---------- | -------------------- | ----------------------------------------------------------------- | | table | string | plural of class name | Table / collection name | | primaryKey | string | 'id' | Primary key column | | fillable | string[] | [] | Mass-assignable attributes | | guarded | string[] | [] | Blocked attributes | | hidden | string[] | [] | Excluded from toJSON() | | appends | string[] | [] | Virtual accessors included in toJSON() | | casts | Casts | {} | Attribute casting (datetime, json, int, float, boolean) | | timestamps | boolean | true | Auto-manage created_at / updated_at | | softDeletes | boolean | false | Set automatically by SoftDeletes trait | | autoIncrement | boolean | true | Whether PK is auto-incrementing |

Querying

// All records
const users = await User.all();

// Find by PK
const user = await User.find(1);
const user = await User.findOrFail(1); // throws 404 if missing

// Where / orWhere
const active = await User.where("status", "active").get();
const admin = await User.where("role", "=", "admin").first();
const found = await User.where("email", email).orWhere("phone", phone).first();

// WhereIn / WhereNull
const users = await User.whereIn("id", [1, 2, 3]).get();
const unverified = await User.whereNull("email_verified_at").get();

// Count / exists / aggregate
const count = await User.where("status", "active").count();
const exists = await User.where("email", email).exists();
const max = await User.max("age");
const avg = await User.avg("score");
const sum = await User.sum("points");

// Select / pluck
const names = await User.select(["id", "name"]).get();
const emails = await User.pluck("email");

// Ordering / limiting
const newest = await User.latest("created_at").get();
const oldest = await User.oldest().limit(5).get();

// Pagination
const page = await User.paginate(15, 1);
// { data, total, per_page, current_page, last_page }

Creating and updating

const user = await User.create({ name: "Alice", email: "[email protected]" });

user.name = "Alice B.";
await user.save();

// Mass update
await User.where("status", "pending").update({ status: "active" });

// firstOrCreate / updateOrCreate
const user = await User.firstOrCreate(
  { email: "[email protected]" },
  { name: "Bob", password: hash },
);

const user = await User.updateOrCreate({ email: "[email protected]" }, { name: "Bob Updated" });

Deleting

await user.delete(); // soft delete (if trait applied), otherwise hard delete
await user.delete(true); // force hard delete regardless of trait
await User.where("status", "banned").delete();

Casts

static casts = {
  created_at:  'datetime',  // Date object
  metadata:    'json',      // JSON.parse on read, JSON.stringify on write
  is_active:   'boolean',
  score:       'float',
  retry_count: 'int',
};

Accessors and Mutators

export class User extends Model {
  // Accessor: getFullNameAttribute → user.full_name
  getFullNameAttribute() {
    return `${this.getAttribute("first_name")} ${this.getAttribute("last_name")}`;
  }

  // Mutator: setPasswordAttribute → called when user.password = '...'
  setPasswordAttribute(value: string) {
    this.setAttribute("password", bcrypt.hashSync(value, 12));
  }
}

// Expose in toJSON via appends
export class User extends Model {
  static appends = ["full_name"];
}

Traits

Apply with the @use() decorator:

import { Model, use } from "@lara-node/db";
import { SoftDeletes, Timestamps, Sluggable, Sortable, Searchable, Cacheable } from "@lara-node/db";

@use(SoftDeletes, Timestamps, Sluggable)
export class Post extends Model {}

| Trait | Adds | | ------------- | --------------------------------------------------------------------------------------------------------- | | SoftDeletes | delete() sets deleted_at; withTrashed(), onlyTrashed(), restore(), forceDelete(), trashed() | | Timestamps | Auto-fills created_at / updated_at; touch() | | Sluggable | Auto-generates slug from name on save; scopeFindBySlug() | | Sortable | moveUp(), moveDown(), reorder(ids[]), scopeOrdered() | | Searchable | scopeSearch(query, fields[]), scopeAdvancedSearch(params) | | Cacheable | cached(fn, key, ttl), clearCache(), getCached(id) |

Scopes

// Local scope
export class User extends Model {
  static localScopes = {
    active: (builder) => builder.where("status", "active"),
  };

  // Instance scope method (resolved automatically)
  scopeVerified(builder) {
    builder.whereNotNull("email_verified_at");
  }
}

const active = await User.scope("active").get();
const verified = await User.scope("verified").get();

// Global scope
User.addGlobalScope("tenant", (builder) => {
  builder.where("tenant_id", currentTenantId());
});

// Remove global scope for one query
const all = await User.withoutGlobalScope("tenant").get();
const all = await User.query().withoutGlobalScope("tenant").get();

Relationships

hasOne / hasMany

export class User extends Model {
  profile() {
    return this.hasOne(UserProfile, "user_id", "id");
  }
  posts() {
    return this.hasMany(Post, "user_id", "id");
  }
}

const profile = await user.profile().first();
const posts = await user.posts().where("published", 1).get();
const newPost = await user.posts().create({ title: "Hello World" });

belongsTo

export class Comment extends Model {
  post() {
    return this.belongsTo(Post, "post_id", "id");
  }
}

const post = await comment.post().first();
await comment.post().associate(post);
await comment.post().dissociate();

belongsToMany

export class User extends Model {
  roles() {
    return this.belongsToMany(Role, "roles_users", "user_id", "role_id");
  }
}

const roles = await user.roles().get();

await user.roles().attach([1, 2]);
await user.roles().attach([{ role_id: 1, granted_at: new Date() }]); // pivot data
await user.roles().detach([1]);
await user.roles().sync([1, 2, 3]);
await user.roles().toggle([1, 2]);
await user.roles().updateExistingPivot(1, { granted_at: new Date() });

const roles = await user.roles().withPivot("granted_at").get();
roles[0].pivot.getAttribute("granted_at");

Polymorphic

export class Post extends Model {
  image() {
    return this.morphOne(Image, "imageable", "imageable_id", "id");
  }
  comments() {
    return this.morphMany(Comment, "commentable", "commentable_id", "id");
  }
}

export class Image extends Model {
  imageable() {
    return this.morphTo("imageable_type", "imageable_id", { Post, Product });
  }
}

Eager loading

const users = await User.with(["profile"]).get();
const users = await User.with(["roles", "roles.permissions"]).get();

// Constrained eager loading
const users = await User.with({
  roles: (query) => query.where("active", 1).orderBy("name"),
}).get();

Observers

import { Observer, Observe } from "@lara-node/db";
import { User } from "../Models/User";

@Observe(User)
export class UserObserver extends Observer<User> {
  creating(user: User): void {
    if (!user.getAttribute("status")) user.setAttribute("status", "active");
  }

  created(user: User): void {
    console.log(`Created: ${user.getAttribute("email") as string}`);
  }

  updating(user: User): void {}
  updated(user: User): void {}
  deleting(user: User): void {}
  deleted(user: User): void {}
  restoring(user: User): void {}
  restored(user: User): void {}
  retrieved(user: User): void {}
}

Importing the observer file is sufficient — @Observe(User) wires it automatically. For manual registration: User.observe(UserObserver).

| Operation | Events fired (in order) | | --------------- | ------------------------------------------- | | create | savingcreatingcreatedsaved | | save (update) | savingupdatingupdatedsaved | | delete | deletingdeleted | | restore | restoringrestored | | find / all | retrieved |

DB Facade

Raw queries (MySQL)

import DB from "@lara-node/db";

const rows = await DB.select("SELECT * FROM users WHERE id = ?", [1]);
const id = await DB.insert("INSERT INTO users (name) VALUES (?)", ["Alice"]);
const count = await DB.update("UPDATE users SET status = ? WHERE id = ?", ["active", 1]);
await DB.statement("ALTER TABLE users ADD COLUMN phone VARCHAR(20)");

Table builder (MySQL)

const users = await DB.table("users").where("status", "active").get();
await DB.table("users").where("id", 1).update({ status: "inactive" });
await DB.table("users").insert({ name: "Bob", email: "[email protected]" });
const user = await DB.table("users").where("id", 1).first();

MongoDB collection

const col = DB.collection("users");
await col.insertOne({ name: "Alice" });
await col.findOne({ email: "[email protected]" });
await col.updateOne({ _id }, { $set: { name: "Alice B." } });
await col.deleteOne({ _id });

Transactions

// Auto-commit / rollback wrapper
await DB.transaction(async (trx) => {
  await User.create({ name: "Alice" });
  await Invoice.create({ user_id: 1 });
});

// Manual
const trx = await DB.beginTransaction();
try {
  await User.create({ name: "Bob" });
  await DB.commit();
} catch {
  await DB.rollback();
}

// Type-safe MySQL
await DB.mysqlTransaction(async (trx) => {
  const id = await trx.insert("INSERT INTO users (name) VALUES (?)", ["Bob"]);
  await trx.update("UPDATE counters SET total = total + 1");
});

// Type-safe MongoDB
await DB.mongoTransaction(async (trx) => {
  const col = trx.collection("users");
  await col.insertOne({ name: "Carol" }, { session: trx.getSession() });
});

Schema Builder (Migrations)

MySQL migration

import Schema, { TableBuilder } from "@lara-node/db";
import type { MigrationSchema, QueryFn } from "@lara-node/db";

export async function up(schema: MigrationSchema, query: QueryFn) {
  await query(
    schema.createTable("users", (table: TableBuilder) => {
      table.id();
      table.string("name");
      table.string("email", 191).unique();
      table.string("password");
      table.enum("status", ["active", "inactive"]).default("active");
      table.boolean("email_verified").default(false);
      table.json("metadata").nullable();
      table.timestamps();
      table.softDeletes();
    }),
  );
}

export async function down(schema: MigrationSchema, query: QueryFn) {
  await query(schema.dropTableIfExists("users"));
}

Column types

| Method | MySQL type | | --------------------------------- | ----------------------------------- | | id() | BIGINT UNSIGNED AUTO_INCREMENT | | string(name, length?) | VARCHAR(255) | | text() / longText() | TEXT variants | | integer() / bigInteger() | INT variants | | decimal(name, precision, scale) | DECIMAL | | boolean() | TINYINT(1) | | datetime() / date() | date/time | | timestamps() | created_at + updated_at | | softDeletes() | deleted_at TIMESTAMP NULL | | json() | JSON | | enum(name, values[]) | ENUM | | foreignId(name) | BIGINT UNSIGNED | | uuid() | CHAR(36) | | morphs(name) | {name}_type + {name}_id + index |

Indexes, foreign keys, and constraints

// Named indexes
table.index(["tenant_id", "status"], "tenant_status_idx");
table.uniqueIndex(["email", "tenant_id"]);
table.fullText(["title", "body"]);
table.compositeUnique(["user_id", "role_id"]);
table.compositePrimary(["user_id", "tenant_id"]);

// Foreign keys (fluent builder)
table.foreign("user_id").references("id").inTable("users").onDelete("CASCADE");
// Shorthand
table.foreign("user_id").references("id").inTable("users").cascadeOnDelete();
table.foreign("deleted_by").references("id").inTable("users").nullOnDelete();

// foreignIdFor — one-liner for common FK patterns
table.foreignIdFor("users");                  // → user_id BIGINT UNSIGNED
table.foreignIdFor("users").constrained();    // + FK constraint user_id → users.id
table.foreignIdFor("roles")
     .constrained({ onDelete: "CASCADE" });   // + CASCADE

Constraint shorthand helpers

| Helper | Equivalent | |---|---| | .cascadeOnDelete() | .onDelete("CASCADE") | | .cascadeOnUpdate() | .onUpdate("CASCADE") | | .nullOnDelete() | .onDelete("SET NULL") | | .restrictOnDelete() | .onDelete("RESTRICT") | | .noActionOnDelete() | .onDelete("NO ACTION") |

MongoDB migration

import { MongoSchema, TableBuilder } from "@lara-node/db";

export async function up(schema: MongoSchema) {
  schema.createTable("users", (table: TableBuilder) => {
    table.unique("email");
    table.index(["status", "created_at"], "users_status_date_idx");
  });
  await schema.apply();
}

Seeders

// database/seeders/UserSeeder.ts
export async function run() {
  await User.create({ name: "Admin", email: "[email protected]", role: "admin" });
}
node artisan db:seed

DatabaseServiceProvider

import { DatabaseServiceProvider } from "@lara-node/db";

// In AppServiceProvider
this.app.register(DatabaseServiceProvider);

Set SKIP_DB=1 to bypass initialization in test or CI environments.

Environment Variables

| Variable | Default | Description | | ----------------------------------- | ----------- | ---------------------------------------------------- | | DB_CONNECTION | mysql | Driver: mysql or mongodb | | DB_NAME | test | Database / schema name | | DB_HOST | localhost | MySQL host / MongoDB host (when MONGO_URI not set) | | DB_PORT | 3306 | MySQL port (27017 for MongoDB) | | DB_USER | root | MySQL user | | DB_PASSWORD | (empty) | MySQL password | | DB_POOL_LIMIT | 10 | MySQL connection pool size | | DB_SOCKET_PATH | — | Unix socket path for MySQL (overrides host/port) | | MONGO_URI | — | Full MongoDB connection string | | MONGO_REPLICA_SET | — | Replica set name; enables replica-set mode | | MONGO_SERVER_SELECTION_TIMEOUT_MS | 10000 | Server selection timeout (ms) | | SKIP_DB | — | Set to 1 to skip DB init in CI |