Skip to content

Validations

ActiveDrizzle offers two validation mechanisms: synchronous @validate (runs client and server) and asynchronous @serverValidate (server only).

@validate — synchronous

ts
// schema.ts
export const users = pgTable('users', {
  id:    integer('id').primaryKey().generatedAlwaysAsIdentity(),
  email: text('email').notNull(),
  age:   integer('age'),
})
ts
// models/User.model.ts
import { ApplicationRecord, model, validate } from 'active-drizzle'

@model('users')
export class User extends ApplicationRecord {
  @validate()
  validateEmail() {
    const email = this.email as string
    if (!email?.includes('@')) {
      this.errors.add('email', 'must be a valid email address')
    }
  }

  @validate()
  validateAge() {
    const age = this.age as number
    if (age !== null && age < 0) {
      this.errors.add('age', 'must be non-negative')
    }
  }
}
ts
const user = new User({ email: 'not-an-email' })
const valid = await user.isValid()    // → false
user.errors.all()                     // { email: ['must be a valid email address'] }
user.errors.on('email')               // ['must be a valid email address']
user.errors.full()                    // ['email must be a valid email address']

When you save() or create(), validation runs automatically:

ts
try {
  await User.create({ email: 'bad' })
} catch (e) {
  // ValidationError thrown
  e.errors   // { email: ['must be a valid email address'] }
}

@serverValidate — async, server only

For validations that require a DB lookup or external service call:

ts
import { serverValidate } from 'active-drizzle'

@model('users')
export class User extends ApplicationRecord {
  @serverValidate()
  async validateEmailUnique() {
    const existing = await User.findBy({ email: this.email })
    if (existing && existing.id !== this.id) {
      this.errors.add('email', 'is already taken')
    }
  }
}

@serverValidate runs during save() / create() on the server. In client-generated code (via codegen), @serverValidate methods are excluded from the client bundle.

Inline validation with Attr.new

For simple per-field rules, use the validate option on Attr.new:

ts
static price = Attr.new({
  get: (v: number) => v / 100,
  set: (v: number) => Math.round(v * 100),
  validate: (v: number) => v >= 0 ? null : 'must be non-negative',
})

Multiple validators as an array:

ts
static slug = Attr.new({
  validate: [
    (v: string) => v?.length > 0 ? null : 'cannot be blank',
    (v: string) => /^[a-z0-9-]+$/.test(v) ? null : 'must be lowercase alphanumeric',
  ],
})

Conditional validation

Run a validation only under certain conditions:

ts
@validate('statusChanged')
ensureTransitionAllowed() {
  const from = this.statusWas()
  const to   = this.status
  const allowed = { pending: ['paid'], paid: ['shipped', 'cancelled'] }
  if (from && !allowed[from]?.includes(to)) {
    this.errors.add('status', `cannot transition from ${from} to ${to}`)
  }
}

errors API

MethodReturns
errors.add(field, message)Add an error
errors.on(field)string[] — messages for that field
errors.all()Record<string, string[]> — all errors
errors.full()string[]['field message', ...] format
errors.clear()Remove all errors
errors.isEmpty()boolean

isValid() / isInvalid()

ts
const user = new User({ email: '' })

await user.isValid()    // → false (runs all validations)
await user.isInvalid()  // → true

user.errors.all()       // { email: ['must be a valid email address'] }

Skipping validation

If you need to save without running validations (use sparingly):

ts
await user.save({ validate: false })

Released under the MIT License.