Models
Netpress ships with an Eloquent-style ORM that now works across SQL and MongoDB through one shared model API. Application models live in app/Models/ and resolve their driver at query time.
Quick Example
// app/Models/Post.js
import { BaseModel } from "@admicaa/netpress";
import User from "./User.js";
export default class Post extends BaseModel {
static connection = "mysql";
static fillable = ["userId", "title", "slug", "body", "status", "publishedAt"];
static casts = {
publishedAt: "date",
createdAt: "date",
updatedAt: "date",
};
author() {
return this.belongsTo(User, "userId");
}
}
const post = await Post.create({
userId: 1,
title: "Shared models",
body: "One API across SQL and Mongo.",
status: "published",
});
const rows = await Post.whereLike("title", "Shared%")
.with("author")
.latest()
.get();
How Model Resolution Works
BaseModel keeps the application-facing behavior shared and delegates storage details to runtime adapters:
SqlModelAdapterhandles table introspection and SQL writesMongoModelAdapterbuilds schemas from the migration manifest and handles Mongo writesModel.query()resolves toSqlQueryBuilderorMongoQueryBuilder
You do not create different model classes per driver anymore.
Static Configuration
| Static | Purpose | Default |
|---|---|---|
connection | Pin the model to a connection or driver | active DB_CONNECTION |
table | Force a table name | pluralized snake_case of the class name |
collection | Force a Mongo collection name | null |
primaryKey | Primary key field | "id" |
schema | Legacy Mongo schema override | null |
schemaOptions | Extra Mongoose schema options merged onto the inferred schema | {} |
hidden | Fields stripped by toJSON() | [] |
casts | Per-field coercion on hydration | {} |
fillable | Mass-assignment allow list | null |
guarded | Mass-assignment deny list | [] |
softDeletes | Enable soft deletes | false |
deletedAtColumn | Soft-delete column name | "deletedAt" |
Supported Casts
| Cast | Effect |
|---|---|
int, integer | parseInt(value, 10) |
float, number | Number(value) |
boolean, bool | coercion to boolean |
string | String(value) |
date, datetime | new Date(value) |
json, object | JSON.parse(...) when the stored value is a string |
Query Entry Points
The model exposes both explicit and convenience entry points:
Post.query();
Post.newQuery();
Post.where("status", "published");
Post.whereLike("title", "Shared%");
Post.whereFullText(["title", "body"], "shared models");
Post.with("author");
Post.latest();
Post.withTrashed();
Query Builder Surface
Filtering
| Method | Notes |
|---|---|
where(column, value) | equality shorthand |
where(column, operator, value) | explicit operator |
where({ ... }) | object shorthand |
where(q => ...) | nested group |
orWhere(...) | OR variant |
whereNot(column, value) | != |
whereIn(column, values) | includes orWhereIn and whereNotIn |
whereNull(column) | includes orWhereNull and whereNotNull / orWhereNotNull |
whereBetween(column, [a, b]) | includes whereNotBetween |
whereRaw(sql, bindings) | SQL only; Mongo throws |
whereLike(column, value) | SQL uses native LIKE behavior; Mongo compiles to $regex |
whereFullText(columns, value) | SQL compiles per dialect; Mongo compiles to $text |
whereHas(name, constraint?) | relation existence |
orWhereHas(name, constraint?) | OR relation existence |
whereDoesntHave(name, constraint?) | negative relation existence |
orWhereDoesntHave(name, constraint?) | OR negative relation existence |
has(name) | alias of whereHas(name) |
doesntHave(name) | alias of whereDoesntHave(name) |
Eager Loading And Flow
| Method | Notes |
|---|---|
with("posts") | eager load one relation |
with(["posts", "profile"]) | eager load many |
with("posts.author") | nested eager loading |
with("posts", q => q.where(...)) | constrained eager loading |
with({ posts: q => q.where(...) }) | object syntax |
when(condition, cb, otherwise?) | conditional builder branch |
tap(cb) | inspect or mutate the builder inline |
Ordering, Paging, And Soft Deletes
| Method | Notes |
|---|---|
orderBy(column, direction) | base ordering |
latest(column) | descending order |
oldest(column) | ascending order |
reorder(column?, direction?) | clears existing ordering first |
limit(n), take(n) | limit rows |
offset(n), skip(n) | offset rows |
forPage(page, perPage) | page slice |
withTrashed() | include soft-deleted rows |
onlyTrashed() | only soft-deleted rows |
withoutTrashed() | reset to default behavior |
Terminal Methods
| Method | Result |
|---|---|
get() | array of model instances |
first() | single instance or null |
find(id) | primary-key lookup |
find([ids]) | whereIn(primaryKey, ids) |
firstWhere(...) | where(...).first() |
firstOrFail(where?) | throws ModelNotFoundError |
findOrFail(id) | throws ModelNotFoundError |
findOr(id, fallback) | resolves fallback when not found |
value(column) | first row value |
pluck(column, key?) | values or keyed object |
count() | count of matching rows |
exists() | boolean |
doesntExist() | boolean |
sum, avg, min, max | aggregates |
paginate(page, perPage) | { data, total, page, perPage, lastPage } |
chunk(size, callback) | batch iteration |
each(callback) | row iteration |
firstOrCreate(attrs, values?) | find or insert |
firstOrNew(attrs, values?) | find or instantiate unsaved |
updateOrCreate(attrs, values) | upsert-style helper |
update(data) | bulk update |
delete() | bulk delete |
forceDelete() | bulk hard delete |
restore() | bulk restore |
Static Convenience Methods
Most common builder methods are also available directly on the model:
await Post.all();
await Post.find(1);
await Post.findOrFail(1);
await Post.first({ status: "draft" });
await Post.firstWhere("slug", "hello-world");
await Post.whereIn("id", [1, 2, 3]).get();
await Post.whereLike("title", "Shared%");
await Post.whereFullText(["title", "body"], "shared");
await Post.count({ status: "published" });
await Post.sum("views");
await Post.paginate({ status: "published" }, 1, 15);
await Post.destroy(1, 2, 3);
await Post.truncate();
Instance Methods
Hydrated rows are real model instances, not plain objects. That gives you instance-level helpers:
const post = await Post.find(1);
post.title = "Renamed";
await post.save();
await post.update({ status: "draft" });
await post.delete();
await post.forceDelete();
await post.restore();
post.trashed();
post.getKey();
post.getKeyName();
post.is(otherPost);
post.isNot(otherPost);
const fresh = await post.fresh();
await post.refresh();
const copy = post.replicate(["slug"]);
Mass Assignment And Writes
All write paths filter payloads through the model's writable fields:
- if
fillableis set, only those fields are written - if
guardedcontains"*", nothing is mass assignable - otherwise unknown fields are dropped automatically
That behavior applies across create, update, relation writes, and bulk builder updates.
Hooks
BaseModel.on(event, handler) supports:
creatingcreatedupdatingupdateddeletingdeletedforceDeletingforceDeletedrestoringrestored
Returning false from the “before” hooks stops the operation.
Soft Deletes
Enable soft deletes with:
static softDeletes = true;
The model must have a nullable deletedAt column, or whatever name you set in static deletedAtColumn.
Relations
Relation factories still live as instance methods on the model:
hasOnehasManybelongsTobelongsToManymorphOnemorphManymorphTomorphToManymorphedByMany
See Relationships for the detailed guide.