1. guides
prorm
  • prorm documentation
  • Known test gaps
  • Architecture
  • Core concepts
  • Database types & coverage
  • Getting started
  • Going further
  • compliance
    • Compliance
    • The security toolkit: `prorm audit`
    • Checklists by regulation
    • Declaring personal data
    • Evidence bundles
    • Evidence
    • Frameworks: `@Security`
    • Jurisdictions
    • Platform policies
    • Protecting data
    • The regulation dataset
    • Residency and cross-border transfers
    • Retention & integrity
    • Status
    • Subject rights
  • decorators
    • prorm with decorators
    • Models with decorators
    • Querying a decorated model
    • Relations with decorators
  • diagrams
    • Diagram generators
    • Chen diagram
    • Class diagram
    • Dependency diagram
    • ER diagram
    • Flow diagram
    • From your models to a diagram
    • Gantt diagram
    • Index diagram
    • Migration diagram
    • Model diagram
    • Package diagram
    • Crow’s-foot diagram
    • Schema documentation
    • Sequence diagram
    • State diagram
    • Tree diagram
  • dialects
    • SQL dialects
    • AlloyDB Dialect
    • AlloyDB Omni Dialect
    • Amazon Athena Dialect
    • Aurora DSQL Dialect
    • Amazon Aurora MySQL Dialect
    • Amazon Aurora PostgreSQL Dialect
    • Azure Database for MySQL Dialect
    • Azure Database for PostgreSQL Dialect
    • Azure SQL Database Dialect
    • Azure SQL Managed Instance Dialect
    • Google BigQuery Dialect
    • Citus Dialect
    • ClickHouse dialect
    • Cloud Authentication (`cloud-auth`)
    • Cloud SQL for SQL Server Dialect
    • Cloud SQL for MySQL Dialect
    • Cloud SQL for PostgreSQL Dialect
    • CockroachDB Dialect
    • Azure Cosmos DB for PostgreSQL Dialect
    • CrateDB Dialect
    • Cloudflare D1 Dialect
    • Databricks dialect
    • IBM Db2 Dialect
    • Dolt Dialect
    • Apache Doris Dialect
    • Druid Dialect
    • DuckDB Dialect
    • Exasol dialect
    • Microsoft Fabric Data Warehouse Dialect
    • Firebird Dialect
    • Greenplum Dialect
    • SAP HANA Dialect
    • MySQL HeatWave Dialect
    • Azure Data Explorer (Kusto) Dialect — T-SQL
    • MariaDB Dialect
    • Materialize Dialect
    • MonetDB Dialect
    • MotherDuck Dialect
    • Microsoft SQL Server Dialect
    • MySQL Dialect
    • Neon Dialect
    • OceanBase Dialect
    • openGauss Dialect
    • Oracle Dialect
    • Oracle Autonomous Database Dialect
    • ParadeDB Dialect
    • PolarDB Dialect
    • PostgreSQL Dialect
    • Presto Dialect
    • QuestDB Dialect
    • Aurora RDS Data API Dialect (`rdsdata`)
    • Amazon RDS for Db2 Dialect
    • Amazon RDS for MariaDB Dialect
    • Amazon RDS for SQL Server Dialect
    • Amazon RDS for MySQL Dialect
    • Amazon RDS for Oracle Dialect
    • Amazon RDS for PostgreSQL Dialect
    • Amazon Redshift
    • Amazon Redshift Data API Dialect
    • RisingWave Dialect
    • rqlite Dialect
    • SingleStore Dialect
    • Snowflake Dialect
    • Google Cloud Spanner Dialect
    • Spanner (PostgreSQL interface) Dialect
    • SQLite Dialect
    • StarRocks Dialect
    • Supabase Dialect
    • Azure Synapse Analytics (dedicated SQL pool)
    • Teradata Dialect
    • TiDB Dialect
    • TimescaleDB Dialect
    • Trino Dialect
    • Turso / libSQL Dialect
    • Vertica dialect
    • Vitess Dialect
    • Yellowbrick Dialect
    • YugabyteDB Dialect
  • guides
    • Access rules and behaviour profiles
    • ArangoDB (AQL)
    • Associations
    • Audit levels and escalation
    • Audit logging
    • In the browser
    • Bulk operations
    • Caching
    • Capabilities, tiers and team roles
    • CLI
    • Cloudflare
    • Compliance & data protection
    • Connection Pooling & Performance
    • Security in the console
    • Couchbase: structured N1QL queries
    • Data types
    • Decorators
    • Eager loading
    • Enums and constants
    • Error handling & error types
    • Extensions
    • External fields
    • Foreign data wrappers
    • Graph databases
    • Hooks & lifecycle events
    • Indexes & constraints
    • InfluxDB: structured Flux pipelines
    • klock-errors
    • klock — the `.klock` file
    • Local databases (`prorm db up`)
    • Structured log search — Splunk, Graylog, Papertrail, Sumo Logic
    • Logging
    • Grafana Loki: structured LogQL queries
    • MCP server
    • Migrate — the data transfer subsystem
    • Migrations & Schema Sync
    • Models from an existing database
    • Defining models
    • Multiple databases
    • OrientDB, Neptune and TigerGraph
    • Passwords
    • Importing a Prisma schema
    • Structured PromQL and MetricsQL
    • Query firewall
    • QueryInterface
    • Query operators reference
    • Query optimization
    • Composing find options with `pipe()` and `Q`
    • Query safety: the fail-closed WHERE guard
    • Querying
    • Raw Queries & Query Building
    • Read replicas
    • Schema diffing & migration generation
    • Views, triggers & other schema objects
    • Scopes & default scopes
    • Seeders
    • SPARQL / RDF triplestores
    • SQL constants
    • SQL features
    • SQL function builders
    • SQLite advanced features
    • StoreKey — a prod-orm database as the storage
    • Streaming results
    • Terraform
    • Transactions & Isolation Levels
    • Type mapping: legacy and modern
    • TypeScript types
    • Database users, roles & privileges
    • Validation & constraints
    • Virtual & Computed Fields
    • Writing docs
  • javascript
    • prorm in JavaScript
    • Models in JavaScript
    • Querying in JavaScript
    • Relations in JavaScript
  • runbooks
    • Operational runbooks
    • Connection failures
    • Credential rotation
    • Failover
    • Incident checklist
    • Deploying klock v2
    • Migration rollback
  • running-databases
    • Running databases in Docker
    • Running prorm under Bun in Docker
    • Running Cassandra in Docker with prorm
    • Running ClickHouse in Docker with prorm
    • Running CockroachDB in Docker with prorm
    • Running prorm under Deno in Docker
    • Running Elasticsearch in Docker with prorm
    • Running MariaDB in Docker with prorm
    • Running MongoDB in Docker with prorm
  • prorm control plane
    • Introduction
    • Authentication
    • Errors
    • Getting started
      • Service
        • Is it up
        • This description
    • Account
      • Sign-up and verification
        • Create an account
        • Exchange credentials for a token
        • Verify with the emailed code
        • Verify by clicking the emailed link
        • Name the parent or guardian
        • What a consent link is about
        • Give consent for a child's account
        • Ask the parent or guardian again
        • Send a new code and link
      • Family
        • Everyone this account is responsible for
        • One child, in detail
        • Turn a limit on or off
      • Profile
        • Who am I
        • Change the account
        • Every address on the account
        • Add an address
        • Confirm an address with the code mailed to it
        • Mail a fresh code to an unconfirmed address
        • Make an address primary
        • Remove an address
        • What this caller may do
    • Security
      • Sessions
        • Everywhere this account is signed in
        • Sign out everywhere else
        • Sign one out
        • A token for the CLI on a named machine
        • A token for the customer console, from the staff one
        • Park a sign-in for the CLI to collect
        • The CLI collecting its sign-in
        • The team's members' live sessions
        • Sign a member's session out
        • Sign an account out everywhere
        • Everywhere the account is signed in
        • Sign an account's session out
      • Single sign-on
        • Begin a single sign-on
        • The provider's return
        • Link the signed-in account to a team's provider
        • Which team an address signs in to
        • The team's single sign-on, as a member sees it
        • Set the team's OpenID Connect provider up, or change it
        • Remove the provider
        • The team's domain claims and the DNS record each needs
        • Prove a claimed domain
      • MFA and passwords
        • Start enrolling a second factor
        • Prove the app has it
        • Remove the second factor
        • Ask for a reset link
        • Set a new password with the emailed token
      • Klock
        • Open a device-bound klock session
        • Check in, for the next grant
        • Step up the session asking
        • Rotate the device key
        • Freeze a session
        • Unfreeze a session
        • A guest grant
        • The revocation feed
        • The published keyset
        • What this control plane supports
      • Devices
        • Your devices, or a team's
        • Approve a new device's pairing code
        • One device
        • Rename a device
        • Revoke a device
        • Suspend a device
        • Reinstate a suspended device
        • Wipe a device
        • Ask a device to rotate its key
        • The team's members' devices
        • Revoke a member's device
        • An account's devices
        • Revoke an account's device
        • Wipe an account's device
        • The device fleet
      • Access rules and behaviour
        • The access rules that bear on you
        • Add an access rule
        • This request, as the rules see it
        • One access rule
        • Remove an access rule
        • The behaviour profiles that bear on you
        • Add or change a behaviour profile
        • The profile presets
        • Your behaviour findings
        • Acknowledge a finding
        • One behaviour profile
        • Remove a behaviour profile
        • The team's access rules
        • Add a team access rule
        • Remove a team access rule
        • The team's behaviour profiles
        • Add or change a team behaviour profile
        • Remove a team behaviour profile
        • What the team's profiles caught
        • Acknowledge a team finding
      • Recovery and lockdown
        • Ask for a recovery link
        • Recover access with the emailed code
        • Is the account locked down
        • Lock the account down
        • Release a lockdown
        • Lock a member down
        • Release a member's lockdown
      • MCP tokens
        • Make a delegated token for one MCP client
        • The account's delegated tokens
        • Revoke a delegated token
        • What a delegated token may do now
        • Run one MCP tool with a delegated token
        • Run a device-signed MCP tool call relayed by the MCP Worker
      • Kerberos
        • Give the team its own Kerberos realm
        • The team's realm
        • Remove the team's realm
    • Logging and monitoring
      • Audit log
        • What has been done to the account
        • What has happened to this account
        • The account's audit trail
      • Operations
        • Everything that has been asked of this database
        • One, with its steps
        • Stop it
        • Put a failed one back in the queue
        • Every operation, across all accounts
      • Metrics and events
        • Every measurement, for a graph
        • Who did what to it
    • Billing
      • Plans and pricing
        • What the plans allow
        • The rate card
        • What one database would cost
        • What this account is paying for, itemised
      • Subscriptions and invoices
        • What this month's invoice will say
        • What this account is on
        • What it has been charged
        • Start a Stripe Checkout session
        • Open the customer portal
        • Stripe's events
        • Buy more connection pools
      • Team billing
        • The team's billing account
        • What the team has been charged
        • What this month's invoice will say
        • The team's cards
        • Add a card, through a Stripe-hosted setup session
        • Make a card the team's default
        • Remove a card
        • Start the team paying
        • Open the Stripe portal on the team's customer
        • Change the team's plan
        • Who receives the team's bills
        • How the team pays for transfers
    • Support
      • Support chat
        • My conversations
        • Start a conversation
        • Every support conversation, open first
        • A ticket for the socket
        • One conversation, with who is in it and who is connected
        • Rename a group; open or close a support conversation
        • The lines, oldest first
        • Say something
        • Add somebody
        • Mark read
    • Administration
      • Users
        • Every account
        • One account, with everything hanging off it
        • Grant or revoke admin
        • Verify an account by hand
        • Lock or unlock sign-in
        • Allow or hold provisioning
        • Send the verification mail again
        • Set the plan by hand
      • Platform
        • Every database and store, across all accounts
        • The counts worth putting on a dashboard
      • Staff and permissions
        • What kind of admin the caller is, and which scopes they hold
        • The admin presets
        • Every admin, and the scopes each resolves to
        • Make an account an admin of a kind
        • Change an admin's kind, or a custom admin's scopes
        • Revoke admin
      • Team administration
        • Every team, with its owner and size
        • One team in full — members with their accounts, and projects
        • Hand the team to another member
        • Put an account in the team, active at once
        • Change a member's role
        • Remove a member
      • Assumed identities
        • Assume an identity — act as a customer account
        • The identities the caller holds open
        • End an assumed identity
      • Support inbox
        • The platform's addresses
        • Add a platform address
        • One address, with its counts
        • Change an address's label, routing, or whether it accepts mail
        • Remove an address that holds no mail
        • Make, adopt or repair the address's routing rule
        • Every admin's personal address, made where missing
        • File a raw message, as if it had arrived
        • The inbox
        • One message, whole
        • Mark a message
        • Hand a message to an admin, or take it back
        • Leave an internal note
        • Reply, from the address the message came to
        • What is waiting
    • Databases
      • Databases
        • Data stores
          • Every data store on the account
          • Provision a data store
          • Change a data store that is already running
          • One data store
          • Deprovision it
          • Report what it is using
          • How to reach a data store
        • Connections and credentials
          • The api keys on a database
          • Issue a key
          • Revoke a key
          • The engine users we created
          • Create a user inside the database
          • Give a user a new password
          • Drop a user
          • How the database is reached
          • Set how it is reached
          • Revoke every api key on it
          • How to connect to it
          • The ssh keys that open its bastion
          • Add a key, or have one made
          • The private half of a generated key, once
          • Revoke an ssh key
        • Schema and objects
          • The SQL objects we put in it
          • Create one, and apply it
          • What is in the database, as the engine reports it
          • One table
          • What it would run, without running it
          • Redefine an object in place
          • Drop it
          • The extensions installed in it
        • Migrations
          • A plan compiled to statements, recording nothing
          • The migrations recorded for it
          • Record one
          • Apply everything recorded and not yet run
          • Apply everything pending, oldest version first
        • Lifecycle
          • Grow or shrink it
          • Stop it accepting connections
          • Start it again
          • Move it to another region
        • Backups and replicas
          • Take a backup
          • The backups it has
          • Restore one into it
          • What has been restored into it, and from where
          • Its read copies
          • Add one
        • Policies
          • The catalog
          • One policy
          • What this database is held to
          • Switch a policy on, or change its knobs
          • Switch it off
          • Mark the policies reviewed
          • Evaluate the periodic policies now
          • Who is behind a credential
        • Passwordless
          • The database's passwordless sessions
          • Issue a passwordless session (v1)
          • Revoke a passwordless session
          • Bind a session to a device key
          • Issue a v2 session for a host to bind
          • Redeem an enrolment ticket on the host
          • Check a passwordless credential
          • Run one described query under a passwordless credential
        • Database roles
          • The database's roles
          • Define a role
          • One role
          • Change what a role permits
          • Delete a role, and revoke every key granted it
          • Grant a role, which mints a key
          • Who holds a role
          • The api keys bound to a role
        • Pools
          • A database's connection pools, and how busy each is
          • Open another pool
          • Rename a pool or change its size
          • Close a pool
        • Data plane
          • Run a query against a database
          • Several statements, in order, over one connection
          • Is this database answering
          • What the proxy itself is holding
        • Transfers
          • The strategies this account's tier may use
          • Price a transfer without creating it
          • The account's transfer jobs, newest first
          • Create a transfer job
          • One job, and whether its certificate holds
          • Cancel an unfinished job
          • Start a created job, confirming its price
          • Report a job's status (the runner, or the owner's side of a local run)
        • Every database on the account
        • Provision a database
        • Change a database that is already running
        • One database
        • Deprovision it
        • Read or write it
        • Report what it is using
        • The engines this API will provision
        • Where a managed database can be put
    • Teams
      • Teams
        • Members
          • Who is on it
          • Invite somebody
          • Accept an invitation
          • Change somebody's role
          • Remove somebody
        • Projects
          • The projects a team runs
          • Start one
          • Rename a project, or move its environment
          • Delete a project
          • One project, and what is in it
          • Put a database or store in it
          • What is in a project
          • Take one out
        • The teams this account is on
        • Start one
        • The twelve roles, and what each may do
        • Rename a team
        • Close a team
        • One team
        • Hand the team to somebody else
        • Leave a team
    • Schemas
      • Error
      • ValidationErrors
      • Token
      • PolicyCategory
      • PolicySeverity
      • PolicyAction
      • PolicyTrigger
      • PolicyParam
      • Policy
      • PolicyCatalog
      • PolicyAssignment
      • PolicyOutcome
      • PolicyFiring
      • CredentialTrace
      • Relation
      • Restrictions
      • ChildSummary
      • SignupRequest
      • SigninRequest
      • UpdateMeRequest
      • User
      • Address
      • GeoPoint
      • D1Region
      • Usage
      • UsageReport
      • Resource
      • Database
      • DataStore
      • NewDatabase
      • NewDataStore
      • QueryRequest
      • DatabasePrivilege
      • ApiKey
      • DbUser
      • Access
      • AccessRequest
      • DatabaseObject
      • NewDatabaseObject
      • NewMigration
      • SchemaOperation
      • Migration
      • Operation
      • Session
      • AdminPowers
      • Admin
      • AuditEntry
      • TeamRole
      • Conversation
      • ChatMessage
      • TeamSso
      • Team
      • TeamMember
      • SshKey
      • ConnectionInfo
      • InboxAddress
      • InboxMailbox
      • InboxMessageSummary
      • InboxMessage
      • InboxNote
      • Page
      • Quote
      • Plan
      • ApiError
      • CapabilityRefusal
      • KlockErrorCode
      • PageWindow
      • SessionView
      • KlockStepUpRequest
      • KlockOpenRequest
      • KlockOpenedSession
      • UpdateMeResult
      • CapabilitiesView
      • SessionLimits
      • Device
      • DeviceListed
      • DeviceTeamRef
      • DeviceCommand
      • DeviceAuditLine
      • DeviceActionRequest
      • StaffActionRequest
      • ManagedSession
      • SessionRevoked
      • AccessRule
      • AccessRuleRequest
      • BehaviourProfile
      • BehaviourProfileRequest
      • BehaviourProfileSaved
      • BehaviourFinding
      • BehaviourFindingAck
      • StepUpProof
      • Removed
      • MemberLockdownRequest
      • McpClient
      • McpDelegatedView
      • McpToolCall
      • McpForwardRequest
      • PasswordlessScope
      • RoleScope
      • PasswordlessSession
      • PasswordlessTlsMaterial
      • PasswordlessKerberosMaterial
      • ServiceDeviceKeys
      • ServiceSession
      • DatabaseRole
      • TransferEndpoint
      • TransferJobRequest
      • TransferQuote
      • TransferStrategy
      • TransferJob
      • TransferJobAccess
      • Pool
      • PoolAllowance
      • BillingSubscription
      • Invoice
      • BillingContact
      • PaymentMethod
      • UpcomingCharges
      • TeamBilling
      • TeamKerberosRealm
      • TeamSsoDomain
      • TeamSsoResponse
      • StoreConnection
      • BatchFailure
      • ProxyStatus
  1. guides

Validation & constraints

prorm validates records before they hit the database. You attach validation
rules to a model's attributes, and the ORM runs them automatically on create,
and instance.update(). When a record fails, prorm throws a
ValidationError carrying a structured list of every failure. This guide
covers the built-in rule set, custom rules, the allowNull/unique
constraints, model-level validation, when validation fires, and the exact shape
of the error you get back.
Validation lives in src/validators/validator.ts. It is wired into models
through Model.create, instance.save, and instance.validate in
src/models/model.ts.

Field-level validators#

Attach a validate object to any attribute. Each key is a rule name and each
value is the rule's argument. The value is boolean | string | number | RegExp | array.
import { Prorm, INTEGER, STRING } from 'ts-prorm-orm';

const prorm = new Prorm({ dialect: 'sqlite', storage: ':memory:' });

const User = prorm.define('User', {
  id: { type: INTEGER, autoIncrement: true, primaryKey: true },
  username: {
    type: STRING(50),
    allowNull: false,
    validate: {
      notEmpty: true,     // rule enabled with `true`
      len: [3, 20],       // string length between 3 and 20
    },
  },
  email: {
    type: STRING(255),
    validate: { isEmail: true },
  },
  age: {
    type: INTEGER,
    validate: { isInt: true, min: 0, max: 150 },
  },
});
A boolean-style rule (isEmail, notEmpty, isInt, …) is turned on with
true. A parameterized rule takes its argument directly as the value: len
takes a number or a [min, max] tuple, min/max take a number, isIn/notIn
take an array, matches takes a RegExp.
Setting a rule value to false disables it, which is handy when overriding a
base configuration:
validate: { isEmail: false } // rule is skipped entirely

On a decorated model#

The same rules, declared with @Validate:
import { Model, DataTypes, Table, Column, PrimaryKey, AutoIncrement,
         Validate, IsEmail, Len, Min, IsIn } from 'ts-prorm-orm';

@Table({ tableName: 'users' })
class User extends Model {
  @Column(DataTypes.INTEGER())
  @PrimaryKey
  @AutoIncrement
  declare id: number;

  @Column(DataTypes.STRING(50))
  @Validate({ notEmpty: true, len: [3, 20] })
  declare username: string;

  @Column(DataTypes.STRING(255))
  @IsEmail()
  declare email: string;

  @Column(DataTypes.INTEGER())
  @Min(0)
  declare age: number;
}
@Validate(rules) takes the same map as the validate option, and applying it
more than once merges — so rules can be split across lines, or added to a base
class's. Each shorthand is one rule: @IsEmail(), @IsUrl(), @IsIP(),
@IsAlpha(), @IsAlphanumeric(), @IsNumeric(), @IsInt(), @IsFloat(),
@IsDecimal(), @IsBoolean(), @IsDate(), @IsUUID(), @IsJSON(),
@NotEmpty(), @IsLowercase(), @IsUppercase(), @IsHexColor(),
@IsCreditCard() take an optional message; @Len(), @Min(), @Max(),
@Range(), @IsIn(), @NotIn(), @Matches(), @NotMatches(), @Equals()
and @NotEquals() take their argument.
class Post extends Model {
  @IsEmail('give a real address')     // rule + custom message
  declare contact: string;

  @Len([3, 20])                       // parameterised rule
  declare slug: string;

  @IsIn(['draft', 'live'])
  declare status: string;
}

Custom error messages#

Pass a string instead of true to supply your own message. The rule still runs;
the string is only used as the failure message.
import { INTEGER, STRING } from 'ts-prorm-orm';

const Account = prorm.define('Account', {
  id: { type: INTEGER, autoIncrement: true, primaryKey: true },
  email: {
    type: STRING(255),
    validate: { isEmail: 'Please enter a valid email address' },
  },
});
Note that isIn and notIn treat a string value as data, not as a message —
always pass those an array.

The built-in validator set#

Every rule below is implemented as a static method on the Validator class and
dispatched by name. Rules skip null, undefined, and empty-string values (use
notEmpty to require a value), so an optional field only validates when present.
Format and content:
isEmail — valid email address
isUrl — parseable URL
isIP — IPv4 or IPv6 address
isAlpha — letters only
isAlphanumeric — letters and digits only
isNumeric — digits only
isInt — integer
isFloat — floating-point number
isDecimal — decimal number (123.45)
isBoolean — boolean-ish value
isDate — parseable date
isTime — HH:MM or HH:MM:SS
isDateString — ISO 8601 date string
isUUID — UUID v1/v4/v5
isJSON — valid JSON value or string
isCreditCard — passes the Luhn check
isPostalCode — postal code (Validator.isPostalCode(value, locale), default US)
isMobilePhone — phone number (default locale en-US)
isHexColor — hex color such as #ff8800
isLowercase / isUppercase — case check
Presence, length, and range:
notEmpty — rejects null, undefined, empty string, empty array
isEmpty — the inverse
len — len: 8 for an exact length, len: [3, 20] for a range
min / max — numeric bounds
range — range: [1, 10]
Sets, equality, and patterns:
isIn: [...] — value must be one of the listed values
notIn: [...] — value must not be in the list
equals / notEquals — strict equality against the rule value
matches: /pattern/ — value matches the RegExp (string patterns are compiled)
notMatches: /pattern/ — value must not match
import { INTEGER, STRING } from 'ts-prorm-orm';

const Product = prorm.define('Product', {
  id: { type: INTEGER, autoIncrement: true, primaryKey: true },
  sku: {
    type: STRING(20),
    validate: {
      notEmpty: true,
      matches: /^[A-Z]{3}-\d{4}$/, // e.g. ABC-1234
    },
  },
  status: {
    type: STRING(20),
    validate: { isIn: ['active', 'inactive', 'archived'] },
  },
  quantity: {
    type: INTEGER,
    validate: { isInt: true, range: [0, 9999] },
  },
});

Where validation happens#

Two distinct layers. Validators run in the process and never reach the
database — a ValidationError means no statement was sent. Constraints run in
the database and produce a different error. A uniqueness rule can only be
truly enforced by the second: between a validator's SELECT and the INSERT,
another writer can take the value.
Every failing rule is collected before anything is thrown, so one bad request
reports all of its problems at once:
await User.create({ username: 'ab', email: 'not-an-email', age: -5 });
ValidationError: Validation len on username failed, Validation isEmail on email failed, Validation min on age failed

Custom validators#

For logic outside the built-in set, use matches with your own regular
expression, or run rules programmatically with the Validator class and the
standalone validate / validateField helpers. These are the same functions
the model uses internally. They are exported from the package root, and from
the ts-prorm-orm/validators subpath.
import { Validator, validate, validateField } from 'ts-prorm-orm';

// Call a single built-in method directly.
Validator.isEmail('user@example.com'); // true
Validator.len('abcd', [3, 20]);        // true
Validator.matches('ABC-1234', /^[A-Z]{3}-\d{4}$/); // true

// Validate a whole object against a rule config.
const result = validate(
  { email: 'nope', age: 200 },
  {
    email: { isEmail: true },
    age: { isInt: true, max: 150 },
  },
);
// result.valid === false
// result.errors -> one item per failed rule

// Validate a single value.
const one = validateField('ab', { len: [3, 20] });
// one.valid === false
To enforce rules that span several fields or need to run I/O, put the logic in a
beforeValidate (or beforeSave) hook and throw when it fails. A throw from the
hook aborts the write before it reaches the database:
import { INTEGER, STRING } from 'ts-prorm-orm';

const Booking = prorm.define('Booking', {
  id: { type: INTEGER, autoIncrement: true, primaryKey: true },
  startDate: { type: STRING(20) },
  endDate: { type: STRING(20) },
});

Booking.addHook('beforeValidate', (instance: any) => {
  if (instance.endDate < instance.startDate) {
    throw new Error('endDate must be on or after startDate');
  }
});
[!NOTE]
A throw from a hook is re-thrown as it is — a plain Error, not a
ValidationError — so a form handler that only looks for ValidationError
will not see it as a field problem. For cross-field rules that should read as
validation failures, use a model-level validator
instead: its message lands in err.errors like any other rule.

Uniqueness#

There are two independent notions of "unique":
The unique attribute option is a database constraint applied when the
table is created or synced. It does not run during application-side validation.
import { INTEGER, STRING } from 'ts-prorm-orm';

const Member = prorm.define('Member', {
  id: { type: INTEGER, autoIncrement: true, primaryKey: true },
  email: { type: STRING(255), unique: true },
  // Named constraint, or an object form: { name, msg }
  slug: { type: STRING(60), unique: 'uq_member_slug' },
});
When the database rejects a duplicate, the dialect throws a
UniqueConstraintError (a ValidationError subclass, code
UNIQUE_CONSTRAINT_ERROR) whose fields names the colliding columns, with the
driver's error on .parent and the statement on .sql. Turn it into the same
shape as a validation failure, so the client sees one kind of error per field:
import { UniqueConstraintError } from 'ts-prorm-orm';

async function register(email: string) {
  try {
    return { member: await Member.create({ email }) };
  } catch (err) {
    if (err instanceof UniqueConstraintError) {
      return { errors: Object.fromEntries((err.fields ?? ['email']).map((f) => [f, ['Already registered']])) };
    }
    throw err;
  }
}
Before 4.9.2 the duplicate arrived as a plain Error carrying the driver's
text (Query error: SqliteError: UNIQUE constraint failed: …), and had to be
matched by message.
For an application-level uniqueness check that queries the database, use the
isUnique validation rule. Because it needs an async lookup, it only runs when
validation is executed asynchronously — which is exactly what create, save,
and the instance validate() method do. The synchronous entry points
(validate, validateField, isValid, validateSync) cannot query the
database and always pass isUnique, logging a warning instead.
isUnique resolves its target model either from a model registered with
registerValidatorModel(name, model) or from a live model reference passed as
options.model. When validation runs on an update, prorm automatically excludes
the current record (by primary key) so a row does not conflict with itself.

Model-level validation#

The model's validate map holds two different things, told apart by what the
value is.
A function is a validator run against the whole instance — the place for a
rule that spans columns, which no per-column rule can express. It is called
with this bound to the instance and reports by throwing; it may be async.
import { INTEGER, DATE } from 'ts-prorm-orm';

const Event = prorm.define(
  'Event',
  {
    id: { type: INTEGER, autoIncrement: true, primaryKey: true },
    startsAt: { type: DATE },
    endsAt: { type: DATE },
  },
  {
    validate: {
      endsAfterStart(this: { endsAt: Date; startsAt: Date }) {
        if (this.endsAt <= this.startsAt) throw new Error('endsAt must be after startsAt');
      },
    },
  },
);
A model-level failure is reported under the validator's name, not a column:
{ "field": "endsAfterStart", "message": "endsAt must be after startsAt", "type": "endsAfterStart", "path": "endsAfterStart" }
On a decorated model that is a class-level @Validate:
@Table({ tableName: 'events' })
@Validate({
  endsAfterStart() {
    if (this.endsAt <= this.startsAt) throw new Error('endsAt must be after startsAt');
  },
})
class Event extends Model { /* … */ }
An object keyed by a column name attaches extra rules to that column, under
a validators key, composing with the attribute's own validate.
import { INTEGER, STRING } from 'ts-prorm-orm';

const Signup = prorm.define(
  'Signup',
  {
    id: { type: INTEGER, autoIncrement: true, primaryKey: true },
    email: { type: STRING(255), validate: { isEmail: true } },
    referralCode: { type: STRING(20) },
  },
  {
    validate: {
      referralCode: {
        validators: { len: [6, 6], isAlphanumeric: true },
      },
    },
  },
);

When validation runs#

Validation runs on every write path that takes values from you, except a
plain bulkCreate. Measured on a prorm.define() model with an isEmail
rule, writing 'bad' (SQLite):
CallValidated?
Model.create(values)yes
instance.update(values)yes
instance.save() on a new instance (Model.build(…))yes
instance.save() on a loaded, changed instanceyes: the changed columns and the model validators
Model.update(values, { where }) (static)yes: the values written; SQL expressions such as literal() are skipped
Model.bulkCreate(rows, { validate: true })yes: every row is checked before any INSERT, and the error names the row (row 1: …)
Model.bulkCreate(rows, { individualHooks: true })yes, per row
Model.bulkCreate(rows)no: the INSERT is sent as is
Model.create(values, { validate: false }), Model.update(values, { where, validate: false })no: validation (and its hooks) is skipped on purpose
[!NOTE]
Before 4.9.2, save() on a loaded row, the static Model.update() and
bulkCreate({ validate: true }) sent the write without validating, and
create(values, { validate: false }) validated anyway. Code that relied on
those writes getting through with invalid values now gets a
ValidationError; pass validate: false where that is intended.
For a bulk import, pass validate: true: one bad row stops the whole batch
before anything is written.
The switches:
Per model: validateOnInsert: false skips validation on inserts,
validateOnUpdate: false skips it on updates.
Globally for the model: skipValidations: true turns off all validation.
Per call: { validate: false } on create() and the static update()
skips validation for that call.
// Turn validation off for a whole model.
const Audit = prorm.define('Audit', { /* ... */ }, { skipValidations: true });
You can also validate on demand without writing:
// Async: runs every rule, including isUnique. Returns { valid, errors }.
const result = await user.validateRules();
if (!result.valid) console.log(result.errors);

// Validate only some fields.
await user.validateRules(['email', 'username']);

// Run beforeValidate / afterValidate hooks as part of the check.
await user.validateRules({ hooks: true });

// Synchronous checks (isUnique is skipped). Throws a ValidationError
// listing every failure when the instance is invalid.
user.validateSync();
user.isValid();                 // boolean, cached
await user.getValidationErrors(); // ValidationErrorItem[]

// Static: validate stored rows matching a query.
const errors = await User.validate({ where: { active: true } });
validate() on an instance is the password check. It was the rules form
until the name went to the thing people call far more often:
await user.validate(submittedPassword) answers true or false against a
@Secure field. The rules are validateRules(), with the
same arguments and the same result as before. The static User.validate()
above is unrelated and unchanged.

The ValidationError shape#

When an automatic write fails validation, prorm throws a ValidationError
(exported from the package root, so err instanceof ValidationError works).
Its message joins every failure's message with , , and its errors
property is the full list.
import { ValidationError } from 'ts-prorm-orm';

try {
  await User.create({ username: 'ab', email: 'not-an-email', age: -5 });
} catch (err) {
  if (err instanceof ValidationError) {
    console.log(err.name);    // 'ValidationError'
    console.log(err.errors);  // one item per failed rule
  }
}
The errors from that call, verbatim:
[
  { "field": "username", "message": "Validation len on username failed", "type": "len", "path": "username", "value": "ab" },
  { "field": "email", "message": "Please enter a valid email address", "type": "isEmail", "path": "email", "value": "not-an-email" },
  { "field": "age", "message": "Validation min on age failed", "type": "min", "path": "age", "value": -5 }
]
type is the rule that failed (notNull for a missing allowNull: false
value, whose message is username cannot be null), path is the field, and
message is your custom message when you gave one. The on-demand
validateRules() result uses the older item shape — type is always
'Validation error' and there is no field:
interface ValidationErrorItem {
  message: string; // human-readable failure message
  type: string;    // 'Validation error'
  path: string;    // the field name that failed
  value: any;      // the value that was rejected
}
The exported ValidationError class (used by the standalone helpers via
createValidationError) offers a few conveniences on top of a plain error:
import { ValidationError } from 'ts-prorm-orm';

const err = new ValidationError('Validation error', [
  { message: 'Bad email', type: 'Validation error', path: 'email', value: 'x' },
]);
err.errors;                    // the ValidationErrorItem[] it was built with
err.getMessages();             // ['Bad email']
err.getFieldErrors('email');   // items whose path === 'email'
Turning that into a 422 for an HTTP API — the usual end of the pipeline:
import express from 'express';
import { ValidationError } from 'ts-prorm-orm';
// `User` is the model from your db module — see Defining models.

export const userRoutes = express.Router();

userRoutes.post('/users', async (req, res, next) => {
  try {
    const user = await User.create(req.body);
    res.status(201).json(user);
  } catch (err) {
    if (err instanceof ValidationError) {
      const fields: Record<string, string[]> = {};
      for (const e of err.errors) (fields[e.path ?? '_model'] ??= []).push(e.message);
      return res.status(422).json({ error: 'validation_failed', fields });
    }
    next(err);
  }
});
{
  "error": "validation_failed",
  "fields": {
    "username": ["Validation len on username failed"],
    "email": ["Please enter a valid email address"],
    "age": ["Validation min on age failed"]
  }
}

Next: Hooks — running your own logic around the lifecycle.

Related reading#

Hooks — logic around the lifecycle, once values are valid
Error handling — what a validation failure looks like when it throws
Modified at 2026-10-04 00:40:16
Previous
Database users, roles & privileges
Next
Virtual & Computed Fields
Built with