Property Modifiers
Property modifiers in Routier allow you to customize the behavior, constraints, and metadata of your schema properties. They can be chained together to create powerful, flexible schemas that accurately represent your database structure.
Quick Summary
- Default: Define default values for properties.
- Deserialize: Custom deserializer (e.g., parse ISO strings to Date).
- Distinct: Mark property as unique (distinct index).
- Identity: Mark property as database/computed identity (auto-generated).
- Index: Define single or composite indexes.
- Key: Define primary key.
- Nullable: Allow null.
- Optional: Allow undefined (omit the field).
- Readonly: Disallow modification after creation.
- Serialize: Custom serializer (e.g., Date to ISO string).
- Tracked: Persist computed value for indexing and faster reads.
Available Modifiers
All schema types support these core modifiers:
.optional()- Makes the property optional.nullable()- Allows the property to be null.default(value)- Sets a default value.readonly()- Makes the property read-only.deserialize(fn)- Custom deserialization function.serialize(fn)- Custom serialization function.array()- Converts the property to an array type.index(...names)- Creates database indexes
Additional modifiers are available on specific types:
.key()- Marks as primary key (string, number, date).identity()- Auto-generates values (string, number, date, boolean).distinct()- Ensures unique values (string, number, date, boolean)
Tracked (for computed values)
.tracked()
Persists a computed value to the underlying store. Use when:
- You need to index or sort/filter by the computed value
- Recomputing is expensive and you want to cache post-save
import { s } from "@routier/core/schema";
import { v4 as uuidv4 } from 'uuid';
// Tracked computed properties within schemas
const productSchema = s.define("products", {
id: s.string().key().identity(),
name: s.string(),
price: s.number(),
category: s.string(),
}).modify(w => ({
// Computed property with tracking for indexing
displayName: w.computed((entity, collectionName) =>
`${entity.name} (${entity.category})`,
{}
).tracked(),
// Computed with injected dependencies
slug: w.computed((entity, collectionName, injected) =>
injected.slugify(entity.name),
{ slugify: (str: string) => str.toLowerCase().replace(/\s+/g, '-') }
).tracked(),
})).compile();Notes:
.tracked()applies to computed properties. It does not change the computation, only persistence/indexability.- Use
.tracked()sparingly; it increases write costs but can greatly improve read performance. - Computed function parameters:
(entity, collectionName, injected)whereentityis the current entity,collectionNameis the schema collection name, andinjectedcontains your dependencies.
Identity and Keys
.key()
Marks a property as a primary key for the entity.
import { s } from "@routier/core/schema";
// Key properties within schemas
const userSchema = s.define("users", {
// String key
email: s.string().key(),
// Number key
userId: s.number().key(),
// Date key
timestamp: s.date().key(),
// Key with identity (auto-generated)
id: s.string().key().identity(),
}).compile();Available on: string, number, date
.identity()
Marks the property for automatic value generation by the datastore. The datastore will generate unique values for this property.
import { s } from "@routier/core/schema";
// Identity properties within schemas
const entitySchema = s.define("entities", {
// String identity (datastore generates UUID)
id: s.string().key().identity(),
// Number identity (datastore generates auto-incrementing value)
userId: s.number().key().identity(),
// Date identity (datastore generates current timestamp)
createdAt: s.date().key().identity(),
// Boolean identity (datastore generates true/false)
isActive: s.boolean().identity(),
}).compile();Available on: string, number, date, boolean
Indexing
.index()
Creates a database index for efficient querying.
import { s } from "@routier/core/schema";
// Indexed properties within schemas
const searchSchema = s.define("search", {
id: s.string().key().identity(),
// Single index
email: s.string().index("email_idx"),
// Multiple indexes
category: s.string().index("category_idx", "search_idx"),
// Distinct index (unique)
username: s.string().distinct(),
// Indexed with other modifiers
status: s.string("active", "inactive").index("status_idx").default("active"),
}).compile();Available on: All types
Compound Indexes
Multiple fields can share the same index name for compound indexing.
import { s } from "@routier/core/schema";
// Compound indexes within schemas
const orderSchema = s.define("orders", {
id: s.string().key().identity(),
// Multiple fields sharing the same index name for compound indexing
userId: s.string().index("user_orders"),
orderDate: s.date().index("user_orders"),
status: s.string().index("user_orders"),
// Another compound index
category: s.string().index("category_status"),
priority: s.string().index("category_status"),
}).compile();Defaults and Values
.default()
Sets a default value for the property. Can accept either a direct value or a function that returns a value.
Function Parameters:
.default((injected) => value, { injected })- Function with injected dependencies.default((injected, collectionName) => value, { injected })- Function with injected dependencies and collection name
import { s } from "@routier/core/schema";
import { v4 as uuidv4 } from 'uuid';
// Default values within schemas
const postSchema = s.define("posts", {
id: s.string().key().identity(),
title: s.string(),
content: s.string(),
// Static default value
status: s.string("draft", "published").default("draft"),
// Function default value
createdAt: s.date().default(() => new Date()),
// Default with injected dependencies
slug: s.string().default((injected) => injected.uuidv4(), { uuidv4 }),
// Default with collection name context
collectionType: s.string().default((injected, collectionName) =>
`post_${collectionName}`,
{}
),
}).compile();Available on: All types
Note: When using a function, it’s evaluated each time a default is needed, making it perfect for dynamic values like timestamps or context-dependent defaults. The function parameters are (injected, collectionName) where injected contains your dependencies and collectionName is the schema collection name.
Insert semantics
- If a property has
.default(...), it is considered optional during inserts. When the value is omitted, Routier will supply the default.
import { s } from "@routier/core/schema";
// Default values with insert semantics
const userSchema = s.define("users", {
id: s.string().key().identity(),
name: s.string(),
email: s.string().distinct(),
// Properties with defaults are optional during inserts
createdAt: s.date().default(() => new Date()),
isActive: s.boolean().default(true),
role: s.string("user", "admin").default("user"),
}).compile();
// Usage example - these properties can be omitted during creation
const newUser = {
name: "John Doe",
email: "[email protected]",
// createdAt, isActive, and role will use their defaults
};Behavior Control
.optional()
Makes the property optional (can be undefined).
import { s } from "@routier/core/schema";
// Optional properties within schemas
const profileSchema = s.define("profiles", {
id: s.string().key().identity(),
name: s.string(),
// Optional properties (can be undefined)
bio: s.string().optional(),
avatar: s.string().optional(),
website: s.string().optional(),
// Optional with default
theme: s.string("light", "dark").optional().default("light"),
}).compile();Available on: All types
.nullable()
Makes the property nullable (can be null).
import { s } from "@routier/core/schema";
// Nullable properties within schemas
const dataSchema = s.define("data", {
id: s.string().key().identity(),
name: s.string(),
// Nullable properties (can be null)
description: s.string().nullable(),
metadata: s.object({}).nullable(),
// Nullable with default
status: s.string().nullable().default(null),
// Both optional and nullable
notes: s.string().optional().nullable(),
}).compile();Available on: All types
.readonly()
Makes the property read-only after creation.
import { s } from "@routier/core/schema";
// Readonly properties within schemas
const auditSchema = s.define("audit", {
id: s.string().key().identity(),
action: s.string(),
// Readonly properties (cannot be modified after creation)
createdAt: s.date().readonly().default(() => new Date()),
createdBy: s.string().readonly(),
// Readonly with other modifiers
version: s.number().readonly().default(1),
}).compile();Available on: All types
Serialization
.serialize()
Custom serialization function for the property.
import { s } from "@routier/core/schema";
// Serialization within schemas
const configSchema = s.define("config", {
id: s.string().key().identity(),
name: s.string(),
// Custom serialization
settings: s.object({}).serialize((obj) => JSON.stringify(obj))
.deserialize((str) => JSON.parse(str)),
// Date serialization
lastModified: s.date().serialize((date) => date.toISOString())
.deserialize((str) => new Date(str)),
// Number serialization
count: s.number().serialize((num) => num.toString())
.deserialize((str) => parseFloat(str)),
}).compile();Available on: All types
.deserialize()
Custom deserialization function for the property.
import { s } from "@routier/core/schema";
// Deserialization within schemas
const dataSchema = s.define("data", {
id: s.string().key().identity(),
name: s.string(),
// Custom deserialization
metadata: s.object({}).deserialize((str) => JSON.parse(str)),
// Date deserialization
timestamp: s.date().deserialize((str) => new Date(str)),
// Number deserialization
value: s.number().deserialize((str) => parseFloat(str)),
}).compile();Available on: All types
Type Conversion
.array()
Converts any property type to an array of that type. This allows you to combine base types with array functionality.
import { s } from "@routier/core/schema";
// Type combination examples within schemas
const combinedSchema = s.define("combined", {
id: s.string().key().identity(),
// Combining string with array conversion
singleTag: s.string().array(), // Converts string to string[]
// Combining number with array conversion
singleScore: s.number().array(), // Converts number to number[]
// Combining object with array conversion
singleItem: s.object({
name: s.string(),
value: s.number(),
}).array(), // Converts object to object[]
// Combining boolean with array conversion
singleFlag: s.boolean().array(), // Converts boolean to boolean[]
// Combining date with array conversion
singleDate: s.date().array(), // Converts date to date[]
}).compile();Available on: All types
Type Combinations:
s.string().array()→string[]s.number().array()→number[]s.boolean().array()→boolean[]s.date().array()→Date[]s.object({...}).array()→object[]
.distinct()
Ensures the property value is unique across all entities.
import { s } from "@routier/core/schema";
// Distinct properties within schemas
const userSchema = s.define("users", {
id: s.string().key().identity(),
name: s.string(),
// Distinct properties (unique values)
email: s.string().distinct(),
username: s.string().distinct(),
// Distinct with other modifiers
phoneNumber: s.string().distinct().optional(),
employeeId: s.number().distinct(),
}).compile();Available on: string, number, date, boolean
Chaining Modifiers
Modifiers can be chained together in any order:
import { s } from "@routier/core/schema";
// Modifier chaining within schemas
const complexSchema = s.define("complex", {
id: s.string().key().identity(),
// Recommended order: type -> constraints -> behavior -> defaults
email: s.string().distinct().optional().default(""),
// Complex chaining
age: s.number(18, 19, 20, 21).default(18).readonly(),
// Serialization with other modifiers
profile: s.object({
bio: s.string().optional().nullable(),
avatar: s.string().optional(),
}).optional().serialize((obj) => JSON.stringify(obj))
.deserialize((str) => JSON.parse(str)),
// Indexed with defaults
status: s.string("active", "inactive").index("status_idx").default("active"),
}).compile();Modifier Compatibility
Not all modifiers can be used together. Here are the key rules based on the source code:
Mutually Exclusive Modifiers
.key()and.optional()- Cannot be used together (keys are always required).identity()and.default()- Cannot be used together (identity generates values).optional()and.nullable()- Can be used together
Modifier Support by Type
| Modifier | string | number | boolean | date | object | array |
|---|---|---|---|---|---|---|
.optional() | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
.nullable() | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
.default() | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
.readonly() | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
.serialize() | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
.deserialize() | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
.array() | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
.index() | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
.key() | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ |
.identity() | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
.distinct() | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
Modifier Order
While modifiers can be chained in any order, it’s recommended to follow this pattern:
import { s } from "@routier/core/schema";
// Modifier order examples within schemas
const orderSchema = s.define("orders", {
// Recommended order: type -> constraints -> behavior -> defaults
id: s.string().key().identity(),
email: s.string().distinct().optional(),
age: s.number(18, 19, 20, 21).default(18),
status: s.string("active", "inactive").default("active").readonly(),
// Complex chaining
profile: s.object({
bio: s.string().optional().nullable(),
avatar: s.string().optional(),
}).optional().serialize((obj) => JSON.stringify(obj))
.deserialize((str) => JSON.parse(str)),
}).compile();Best Practices
1. Use Built-in Modifiers
import { s } from "@routier/core/schema";
// Built-in modifiers examples within schemas
const bestPracticeSchema = s.define("bestPractice", {
id: s.string().key().identity(),
// Use built-in modifiers instead of custom logic
email: s.string().distinct(), // Instead of custom uniqueness validation
createdAt: s.date().default(() => new Date()), // Instead of manual timestamp setting
isActive: s.boolean().default(true), // Instead of conditional logic
// Define constraints early
status: s.string("pending", "approved", "rejected").default("pending"),
priority: s.number(1, 2, 3, 4, 5).default(1),
}).compile();2. Define Constraints Early
import { s } from "@routier/core/schema";
// Constraints defined early within schemas
const earlyConstraintsSchema = s.define("earlyConstraints", {
id: s.string().key().identity(),
// Define constraints early in the schema
email: s.string().distinct(), // Unique constraint
username: s.string().distinct().index("username_idx"), // Unique + indexed
role: s.string("admin", "user", "guest").default("user"), // Literal constraint
// Then add behavior modifiers
isActive: s.boolean().default(true),
lastLogin: s.date().optional(),
}).compile();3. Leverage Type Safety
import { s, InferType, InferCreateType } from "@routier/core/schema";
// Type safety examples within schemas
const typeSafeSchema = s.define("typeSafe", {
id: s.string().key().identity(),
name: s.string(),
email: s.string().distinct(),
age: s.number().default(18),
}).compile();
// Leverage TypeScript type safety
type User = InferType<typeof typeSafeSchema>;
type CreateUser = InferCreateType<typeof typeSafeSchema>;
// Type-safe function parameters
function createUser(userData: CreateUser): User {
// TypeScript will enforce correct types
return userData as User;
}
// Type-safe API responses
function getUser(): User {
return {
id: "user-123",
name: "John Doe",
email: "[email protected]",
age: 30,
};
}4. Use Appropriate Modifiers
import { s } from "@routier/core/schema";
// Appropriate modifiers examples within schemas
const appropriateSchema = s.define("appropriate", {
id: s.string().key().identity(),
// Use appropriate modifiers for the use case
email: s.string().distinct(), // For unique email addresses
username: s.string().distinct().index("username_idx"), // For searchable usernames
createdAt: s.date().default(() => new Date()).readonly(), // For audit timestamps
isActive: s.boolean().default(true), // For status flags
metadata: s.object({}).optional().serialize(obj => JSON.stringify(obj)), // For flexible data
// Avoid over-engineering
// Don't use .distinct() on every field
// Don't use .readonly() unless you need immutability
// Don't use .serialize() unless you need custom format
}).compile();Next Steps
- Property Types - Available property types
- Creating A Schema - Back to schema creation