create,instance.update(). When a record fails, prorm throws aValidationError carrying a structured list of every failure. This guideallowNull/uniquesrc/validators/validator.ts. It is wired into modelsModel.create, instance.save, and instance.validate insrc/models/model.ts.validate object to any attribute. Each key is a rule name and eachboolean | 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 },
},
});isEmail, notEmpty, isInt, …) is turned on withtrue. A parameterized rule takes its argument directly as the value: len[min, max] tuple, min/max take a number, isIn/notInmatches takes a RegExp.false disables it, which is handy when overriding avalidate: { isEmail: false } // rule is skipped entirely@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@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()@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;
}true to supply your own message. The rule still runs;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' },
},
});isIn and notIn treat a string value as data, not as a message —Validator class andnull, undefined, and empty-string values (usenotEmpty to require a value), so an optional field only validates when present.isEmail — valid email addressisUrl — parseable URLisIP — IPv4 or IPv6 addressisAlpha — letters onlyisAlphanumeric — letters and digits onlyisNumeric — digits onlyisInt — integerisFloat — floating-point numberisDecimal — decimal number (123.45)isBoolean — boolean-ish valueisDate — parseable dateisTime — HH:MM or HH:MM:SSisDateString — ISO 8601 date stringisUUID — UUID v1/v4/v5isJSON — valid JSON value or stringisCreditCard — passes the Luhn checkisPostalCode — postal code (Validator.isPostalCode(value, locale), default US)isMobilePhone — phone number (default locale en-US)isHexColor — hex color such as #ff8800isLowercase / isUppercase — case checknotEmpty — rejects null, undefined, empty string, empty arrayisEmpty — the inverselen — len: 8 for an exact length, len: [3, 20] for a rangemin / max — numeric boundsrange — range: [1, 10]isIn: [...] — value must be one of the listed valuesnotIn: [...] — value must not be in the listequals / notEquals — strict equality against the rule valuematches: /pattern/ — value matches the RegExp (string patterns are compiled)notMatches: /pattern/ — value must not matchimport { 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] },
},
});ValidationError means no statement was sent. Constraints run inSELECT and the INSERT,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 failedmatches with your own regularValidator class and thevalidate / validateField helpers. These are the same functionsts-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 === falsebeforeValidate (or beforeSave) hook and throw when it fails. A throw from theimport { 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 plainError, not aValidationError— so a form handler that only looks forValidationError
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 inerr.errorslike any other rule.
unique attribute option is a database constraint applied when theimport { 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' },
});UniqueConstraintError (a ValidationError subclass, codeUNIQUE_CONSTRAINT_ERROR) whose fields names the colliding columns, with the.parent and the statement on .sql. Turn it into the sameimport { 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;
}
}Error carrying the driver'sQuery error: SqliteError: UNIQUE constraint failed: …), and had to beisUnique validation rule. Because it needs an async lookup, it only runs whencreate, save,validate() method do. The synchronous entry pointsvalidate, validateField, isValid, validateSync) cannot query theisUnique, logging a warning instead.isUnique resolves its target model either from a model registered withregisterValidatorModel(name, model) or from a live model reference passed asoptions.model. When validation runs on an update, prorm automatically excludesvalidate map holds two different things, told apart by what thethis 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');
},
},
},
);{ "field": "endsAfterStart", "message": "endsAt must be after startsAt", "type": "endsAfterStart", "path": "endsAfterStart" }@Validate:@Table({ tableName: 'events' })
@Validate({
endsAfterStart() {
if (this.endsAt <= this.startsAt) throw new Error('endsAt must be after startsAt');
},
})
class Event extends Model { /* … */ }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 },
},
},
},
);bulkCreate. Measured on a prorm.define() model with an isEmail'bad' (SQLite):| Call | Validated? |
|---|---|
Model.create(values) | yes |
instance.update(values) | yes |
instance.save() on a new instance (Model.build(…)) | yes |
instance.save() on a loaded, changed instance | yes: 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 staticModel.update()andbulkCreate({ validate: true })sent the write without validating, andcreate(values, { validate: false })validated anyway. Code that relied on
those writes getting through with invalid values now gets aValidationError; passvalidate: falsewhere that is intended.
validate: true: one bad row stops the whole batchvalidateOnInsert: false skips validation on inserts,validateOnUpdate: false skips it on updates.skipValidations: true turns off all validation.{ validate: false } on create() and the static update()// Turn validation off for a whole model.
const Audit = prorm.define('Audit', { /* ... */ }, { skipValidations: true });// 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)answerstrueorfalseagainst a@Securefield. The rules arevalidateRules(), with the
same arguments and the same result as before. The staticUser.validate()
above is unrelated and unchanged.
ValidationErrorerr instanceof ValidationError works).message joins every failure's message with , , and its errorsimport { 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
}
}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: falseusername cannot be null), path is the field, andmessage is your custom message when you gave one. The on-demandvalidateRules() 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
}ValidationError class (used by the standalone helpers viacreateValidationError) 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'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"]
}
}