Defining Models
Every model maps to a Spatie QueryBuilder::for(...) resource on the backend.
Anatomy
ts
import { Model } from '@bir-tan/crisp-oquent';
export class User extends Model {
static override uri = '/users';
static override primaryKey = 'id'; // default
declare id?: number;
declare name?: string;
declare email?: string;
declare created_at?: string;
}| Field | Required | Default | Purpose |
|---|---|---|---|
static uri | ✅ | '' | Path appended to CrispOquentConfig.baseUri |
static primaryKey | ❌ | 'id' | Used by find(), save(), delete() |
declare ... | ❌ | — | Typed attribute access via Proxy |
Attribute access (Proxy)
ts
const user = await User.crispy().find(1);
user.name; // string — proxied through to internal attribute store
user.id; // number
user.getKey(); // 1
user.isPersisted(); // trueThe internal attribute store is private. You read/write via the model instance directly:
ts
user.name = 'New name'; // updates the attribute store
user.toJSON(); // { id: 1, name: 'New name', email: '...' }Static crispy() factory
crispy() returns a fresh Builder<T> bound to the model class:
ts
User.crispy() // Builder<User>
.filter('status', 'active')
.get(); // Promise<User[]>The Builder remembers the model class, not an instance — so User.uri is resolved without instantiating anything.
TypeScript tips
For strict typing, declare your attributes with explicit types:
ts
export class User extends Model {
static override uri = '/users';
declare id?: number;
declare name: string; // required after fetch
declare email?: string;
declare roles?: Array<'admin' | 'user'>;
}declare tells TypeScript the field exists at runtime (provided by the proxy) without emitting a class field initializer that would shadow the proxy.