Validation API

Column validation rules and how write paths enforce them

Validation Column Options

typescript
interface ColumnConfig {
required?: boolean; // Value must be present
minLength?: number; // Min string length (validation)
maxLength?: number; // Max string length (validation)
pattern?: RegExp; // Regex validation
customValidator?: (val: any) => boolean | string; // Custom validation
unique?: boolean; // UNIQUE constraint
}

Parameters:

  • required Value must be present on the entity
  • minLength Minimum string length
  • maxLength Maximum string length
  • pattern A RegExp the value must match
  • customValidator Caller-supplied predicate
  • unique Enforced by the database index, not the runtime validator

There is no numeric min or max option. minLength and maxLength measure string length only.

At most one message per column is reported: each rule that fails ends that column's checks, so a value that is both too short and fails its pattern reports only the length failure.

required is not enforced on the auto-increment primary key. When the id column has a numeric type, that column is skipped by the required check — so a missing id is left for the database to generate rather than rejected.

customValidator

typescript
customValidator(val: any): boolean | string

Return true to pass, or a string to fail with that message. The returned string is used verbatim as the validation message.

models/User.ts
const User = defineModel({
tableName: "users",
columns: {
id: { type: DataTypes.STRING, required: true, unique: true },
email: {
type: DataTypes.STRING,
length: 255,
required: true,
customValidator: (val) =>
val.includes("@") ? true : "email must contain @",
},
},
});

validateAll()

typescript
validateAll(entity: Partial<T>, skipRequired?: boolean): string[]

Returns one message per invalid column. Empty array when valid. Never throws.

Parameters:

  • entity Partial entity to check
  • skipRequired Defaults to false. When true, only the required rule is skipped
example/validate.ts
const errors = userRepo.validateAll({ email: "nope" });
// ["email must contain @", "Field name is required"]
const partialErrors = userRepo.validateAll(
{ email: "nope" },
true,
);
// ["email must contain @"] - required rule skipped

Write-Path Enforcement

typescript
validate(entity: Partial<T>, skipRequired?: boolean) // private

Write paths run the private validate(), which throws on the FIRST failure only. It does not collect every message the way validateAll() does.

example/errors.ts
import { StabilizeError, generateUUID } from "stabilize-orm";
try {
await userRepo.create({ id: generateUUID(), email: "nope" });
} catch (err) {
if (err instanceof StabilizeError && err.code === "VALIDATION_ERROR") {
console.log(err.name); // "StabilizeError"
}
}

VALIDATION_ERROR

typescript
class StabilizeError extends Error {
code: string;
originalError?: unknown;
}

Validation failures are thrown as StabilizeError with name === "StabilizeError", a .code, and a .originalError. The code for a failed rule is "VALIDATION_ERROR".

Message strings:

typescript
Field ${key} is required
Field ${key} too short
Field ${key} too long
Field ${key} does not match pattern
// or the string returned by customValidator, verbatim

unique

unique exists on the column interface but is NOT enforced by the runtime validator. It is enforced by the database index, so a duplicate raises a constraint error, not a VALIDATION_ERROR.

example/unique.ts
const User = defineModel({
tableName: "users",
columns: {
email: { type: DataTypes.STRING, required: true, unique: true },
},
});
// A duplicate insert fails at the database index,
// not as a StabilizeError with code "VALIDATION_ERROR".