import { createBillableMethods } from './traits/billable'; import { createCategorizableMethods } from './traits/categorizable'; import { createCommentableMethods } from './traits/commentable'; import { createLikeableMethods } from './traits/likeable'; import { createSoftDeleteMethods } from './traits/soft-deletes'; import { createTaggableMethods } from './traits/taggable'; import { createTwoFactorMethods } from './traits/two-factor'; import { type OrmModelDefinition as BQBModelDefinition, type OrmModelStatic } from '@stacksjs/query-builder'; import type { ApiMiddleware, DashboardModelOptions } from '@stacksjs/types'; import type { BelongsToForeignKeys } from './model-types'; import type { Faker } from '@stacksjs/faker'; import type { InferRelationNames } from '@stacksjs/query-builder'; import type { Validator } from '@stacksjs/validation'; // Re-export types from bun-query-builder for convenience export type { ModelDefinition, InferRelationNames, ModelAttributes, InferModelAttributes, SystemFields, ColumnName, AttributeKeys, FillableKeys, HiddenKeys, ModelInstance, ModelQueryBuilder } from '@stacksjs/query-builder'; /** * Run a callback with model lifecycle events suppressed for its entire * (synchronous + async) duration. Any nested awaits inside the callback * inherit the suppression via the AsyncLocalStorage propagation. * * @example * ```ts * await User.withoutEvents(async () => { * for (const row of importedRows) await User.create(row) // no events fire * }) * ``` */ export declare function withoutEvents(fn: () => T | Promise): Promise; /** * Run a callback with model validation suppressed for its entire duration. * * @example * ```ts * await User.withoutValidation(async () => { * for (const row of legacyRows) await User.create(row) // rules do not run * }) * ``` */ export declare function withoutValidation(fn: () => T | Promise): Promise; /** * Standalone projection helper (#1891) — used by both the lifecycle * sync and the bulk-reindex path so the document shape stays * consistent. Mirrors the closure inside `buildSearchHooks` but * lives at module scope so the static-helpers code can reach it. */ /** * Project a whole chunk at once. * * The entry point for every bulk indexing path. `shapeMany` gets the batch and * decides for itself how many queries that costs; without it this falls back to * projecting each row, which is what a column-rearranging `shape` wants anyway. */ export declare function projectDocumentsFromTrait(models: any[], config: SearchableTraitConfig): Promise[]>; export declare function defineModel(definition: TDef): StacksModelStatic; /** * Normalize a ModelInstance (or array of them, or already-plain row) into * a serialization-ready plain object. * * Resolves the three shapes a Stacks model query can return: * - ModelInstance (find/first/get) → calls toJSON() → strips `hidden` attrs * - Bare attribute bag with `_attributes` → returns _attributes as-is * - Plain row (already normalized) → returns it unchanged * * Use `toAttrs(inst)` in actions instead of `inst._attributes ?? inst` — * the latter pattern silently leaks `hidden: true` fields (e.g. license_plate, * vin, password hashes) into responses. */ export declare function toAttrs(value: any): T; /** * Thrown when a direct write fails a declared `validation.rule`. * * Carries `status = 422` and a per-field `errors` map, matching the shape the * generated REST routes already return, so a handler that catches this can * respond with the same body it would have produced through auto-CRUD. It is * duck-typed by `mapWriteError`, which preserves any integer `status` in * 400-599 — so an over-length value now surfaces as a 422 instead of the * driver's raw 22001 becoming a 500 (stacksjs/stacks#2233). */ /** * Where `defineModel` keeps the definition it was handed. * * `Symbol.for`, so two copies of the ORM in one process still agree — the * dist-only-app split makes that a real arrangement, not a hypothetical. */ export declare const MODEL_DEFINITION: unique symbol; /** * Custom caster interface for user-defined attribute transformations. */ export declare interface CasterInterface { get(value: unknown): unknown set(value: unknown): unknown } export declare interface StacksModelDefinition extends Omit { name: string table: string primaryKey?: string autoIncrement?: boolean dashboard?: DashboardModelOptions traits?: Omit, 'useApi'> & { useApi?: boolean | { readonly uri?: string readonly prefix?: string readonly routes?: readonly string[] readonly middleware?: ApiMiddleware } } & Record indexes?: Array<{ name: string, columns: string[], unique?: boolean, where?: string }> casts?: Record attributes: Record } /** * Scout-style search index sync when `traits.useSearch` is set. * Indexes on create/update and removes on delete without requiring `observe: true`. */ /** * Per-model search-trait config (stacksjs/stacks#1891). Accepts * either `true` (legacy boolean — uses the model's * `toSearchableObject()` if defined) OR a declarative object that * spells out index name, document projection, and whether to * dispatch the sync via a queued job. */ declare interface SearchableTraitConfig { index?: string displayable?: string[] searchable?: string[] sortable?: string[] filterable?: string[] shape?: (model: any) => Record | null | undefined | Promise | null | undefined> shapeMany?: (models: any[]) => Record[] | Promise[]> hidden?: Set queueable?: boolean } export declare interface TraitMethods { _taggable?: ReturnType _categorizable?: ReturnType _commentable?: ReturnType _billable?: ReturnType _likeable?: ReturnType _twoFactor?: ReturnType _softDeletes?: ReturnType } /** * Built-in cast types for model attributes. * * ### Timezone contract (stacksjs/stacks#1876 O-5, D-5) * * `datetime` and `date` casts persist values in **UTC** regardless of * which driver is connected. The `set` direction uses * `Date.toISOString()`, which always emits `Z`-suffixed UTC. The * `get` direction parses the stored string back into a JavaScript * `Date`, which represents an instant on the universal timeline — * timezone presentation is the caller's responsibility (typically via * `Intl.DateTimeFormat` at the render layer, or a Temporal-API * adapter). * * **Why UTC-only:** Per-driver behavior diverges sharply on * timezone-aware columns. PostgreSQL has `timestamptz` (timezone- * aware); MySQL stores `TIMESTAMP` as UTC but presents in the * session timezone; SQLite has no timezone concept at all and stores * ISO strings verbatim. The ORM normalizes them to a single * convention (UTC on the wire) so multi-driver apps behave the same * across environments. Apps that need original-timezone preservation * should store the user's TZ as a separate column and convert at * the render layer. */ export type CastType = 'string' | 'number' | 'boolean' | 'json' | 'datetime' | 'date' | 'array' | 'integer' | 'float'; declare type BQBModelAttribute = BQBModelDefinition['attributes'][string]; export type StacksModelAttribute = Omit & { factory?: (faker: Faker) => unknown } declare type ModelDefinition = StacksModelDefinition; declare type ValidationRuleOf = TAttribute extends { validation: { rule: infer TRule } } ? TRule : never; declare type ValidationInferenceRule = TRule extends Validator ? 'number' : TRule; declare type BQBFaker = Parameters>[0]; declare type FactoryReturnOf = TAttribute extends { factory: (...args: never[]) => infer TResult } ? TResult : never; declare type DefaultTypeToken = TAttribute extends { default: infer TDefault } ? TDefault extends string ? 'string' : TDefault extends number ? 'number' : TDefault extends boolean ? 'boolean' : TDefault extends Date ? 'date' : TDefault extends Record ? 'json' : never : never; declare type InferenceHint = TAttribute extends { type: unknown } | { factory: (...args: never[]) => unknown } ? object : [ValidationRuleOf] extends [never] ? [DefaultTypeToken] extends [never] ? object : { type: DefaultTypeToken } : { type: ValidationInferenceRule> } /** * `required: false` is what makes the emitted column nullable, so it has to * imply `nullable: true` for the inferred value type as well. * * Without this the two halves of a definition disagree. Value types come from * the seed factory's return type, and a factory exists to produce a *useful* * sample row, so optional columns are routinely written as * `factory: () => new Date().toISOString()`. That inferred a bare `string`, * which made `update({ nextPollAt: null })` a type error against a column the * migration had already created as nullable. An explicit `nullable` on the * attribute still wins, since it is the more specific declaration. */ declare type IsOptionalAttribute = TAttribute extends { required: false } ? true : false; /** Covers attributes with no factory, where the validation rule drives the type. */ declare type NullabilityOf = TAttribute extends { nullable: unknown } ? object : IsOptionalAttribute extends true ? { nullable: true } : object; /** * Covers attributes that do have a factory, whose return type takes precedence * over `nullable` when the value type is inferred. */ declare type FactoryValueOf = IsOptionalAttribute extends true ? FactoryReturnOf | null : FactoryReturnOf; declare type QueryAttribute = Omit & InferenceHint & NullabilityOf & ([FactoryReturnOf] extends [never] ? object : { factory: (faker: BQBFaker) => FactoryValueOf }); declare type QueryTraits = TDef extends { traits: infer TTraits } ? { traits: TTraits & NonNullable } : { traits?: BQBModelDefinition['traits'] } declare type QueryDefinition = TDef & { attributes: { [TKey in keyof TDef['attributes']]: QueryAttribute } } & QueryTraits; declare type QueryModel = OrmModelStatic>; declare type ModelWriteData = Parameters['create']>[0]; declare type ModelForceWriteData = Parameters['make']>['forceFill']>[0] & Partial>>; /** * Stacks-enhanced model definition. * * Wraps bun-query-builder's `createModel()` with: * - Event dispatching via `traits.observe` * - Trait methods (billable, taggable, categorizable, commentable, likeable, 2FA) * - Full backward compatibility with generators (migration, routes, dashboard) * * ### Relationships * Each entry in `belongsTo`, `hasMany`, `hasOne`, `belongsToMany`, * `hasOneThrough`, and `hasManyThrough` declares a typed relation * usable via `.with('relationName')`: * * ```ts * defineModel({ * belongsTo: ['Author'], // ↪ post.author * hasMany: ['Comment'], // ↪ post.comments (lowercase + pluralized) * hasOne: ['Cover'], // ↪ post.cover * }) * ``` * * After eager loading the related row(s) are reachable as a property * on the instance — `(await Post.with('author').first()).author`. * * @example * ```ts * import { defineModel } from '@stacksjs/orm' * import { schema } from '@stacksjs/validation' * * export default defineModel({ * name: 'Post', * table: 'posts', * attributes: { * title: { fillable: true, validation: { rule: schema.string() } }, * views: { fillable: true, validation: { rule: schema.number() } }, * }, * belongsTo: ['Author'], * hasMany: ['Tag', 'Category', 'Comment'], * traits: { useTimestamps: true, useUuid: true }, * }) * * // Result: Post.where('title', 'test') — 'title' narrowed to valid columns * // Result: Post.with('author') — 'author' narrowed to valid relations * ``` */ export type StacksModelStatic = QueryModel & TDef & TraitMethods & { readonly [MODEL_DEFINITION]: TDef update: (id: number | string, data: ModelWriteData) => ReturnType['find']> forceUpdate: (id: number | string, data: ModelForceWriteData) => ReturnType['find']> forceCreate: (data: ModelForceWriteData) => ReturnType['create']> delete: (id: number | string) => Promise withoutEvents: (fn: () => T | Promise) => Promise /** Run `fn` with declared `validation.rule`s suppressed (bulk imports, backfills). */ withoutValidation: (fn: () => T | Promise) => Promise } export declare class ModelValidationError extends Error { readonly status: number; readonly errors: Record; constructor(modelName: string, errors: Record); } /** * Thrown by `Model.findOrFail(id)` (and other strict lookups) when no row matches. * Callers can `instanceof` against this to distinguish "missing" from other errors. */ export declare class ModelNotFoundError extends Error { readonly model: string; readonly id: number | string | undefined; constructor(model: string, id?: number | string); } /** * Thrown when a write payload (`Model.create` / `Model.update` / * `firstOrCreate` / `updateOrCreate`) contains an attribute the model * forbids from mass assignment. There are two reasons this fires: * * • `guarded` — the attribute is explicitly marked `guarded: true`. * • `not-fillable` — the model is in *allowlist* mode (at least one * attribute has `fillable: true`) and the write payload contains a * non-allowlisted field. * * The check exists to stop unfiltered request payloads from landing * directly in the DB. If you genuinely need to write a normally-protected * column, use the `force*` escape hatches (`Model.forceCreate(...)`, * `Model.forceUpdate(id, ...)`) — those bypass the check by design and * make the bypass auditable in code review. */ export declare class MassAssignmentException extends Error { readonly model: string; readonly attribute: string; readonly reason: 'guarded' | 'not-fillable'; constructor(model: string, attribute: string, reason: 'guarded' | 'not-fillable'); }