Skip to content

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;
}
FieldRequiredDefaultPurpose
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(); // true

The 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.