Field Types
OpenSaaS Stack provides a comprehensive set of field types for building your schema. Each field type includes validation, access control, and UI configuration options.
Core Field Types
Text Field
String field with validation options:
import { text } from '@opensaas/stack-core/fields'
fields: {
title: text({
validation: {
isRequired: true,
length: { min: 3, max: 100 },
},
ui: {
displayMode: 'input', // or 'textarea'
},
}),
description: text({
ui: {
displayMode: 'textarea',
},
}),
}
Options:
validation.isRequired: Booleanvalidation.length.min: Minimum lengthvalidation.length.max: Maximum lengthui.displayMode:'input'or'textarea'
Integer Field
Number field with validation:
import { integer } from '@opensaas/stack-core/fields'
fields: {
age: integer({
validation: {
isRequired: true,
min: 0,
max: 150,
},
}),
score: integer({
validation: {
min: 0,
max: 100,
},
defaultValue: 0,
}),
}
Options:
validation.isRequired: Booleanvalidation.min: Minimum valuevalidation.max: Maximum valuedefaultValue: Default integer value
Checkbox Field
Boolean field:
import { checkbox } from '@opensaas/stack-core/fields'
fields: {
isPublished: checkbox({
defaultValue: false,
}),
emailVerified: checkbox(),
}
Options:
defaultValue: Boolean default value
Timestamp Field
Date/time field with auto-now support:
import { timestamp } from '@opensaas/stack-core/fields'
fields: {
publishedAt: timestamp(),
createdAt: timestamp({
defaultValue: { kind: 'now' },
}),
updatedAt: timestamp({
db: { updatedAt: true }, // Auto-update on changes
}),
}
Options:
defaultValue.kind:'now'for current timestampdb.updatedAt: Boolean - auto-update on record changes
Password Field
String field automatically excluded from reads:
import { password } from '@opensaas/stack-core/fields'
fields: {
password: password({
validation: {
isRequired: true,
length: { min: 8 },
},
}),
}
Options:
validation.isRequired: Booleanvalidation.length.min: Minimum lengthvalidation.length.max: Maximum length
Password fields are automatically excluded from all read operations for security.
Select Field
Enum field with predefined options:
import { select } from '@opensaas/stack-core/fields'
fields: {
status: select({
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
{ label: 'Archived', value: 'archived' },
],
defaultValue: 'draft',
validation: {
isRequired: true,
},
ui: {
displayMode: 'select', // or 'radio', 'segmented-control'
},
}),
}
Options:
options: Array of{ label, value }pairsdefaultValue: Default selected valuevalidation.isRequired: Booleanui.displayMode:'select'|'radio'|'segmented-control'
Relationship Field
Foreign key relationship:
import { relationship } from '@opensaas/stack-core/fields'
fields: {
// One-to-many (User has many posts)
posts: relationship({
ref: 'Post.author',
many: true,
}),
// Many-to-one (Post belongs to one user)
author: relationship({
ref: 'User.posts',
}),
// One-to-one
profile: relationship({
ref: 'Profile.user',
}),
}
Options:
ref: String in format'ListName.fieldName'many: Boolean - true for one-to-many relationships
Virtual Field
Computed field that is not stored in the database:
import { virtual } from '@opensaas/stack-core/fields'
fields: {
firstName: text(),
lastName: text(),
// Computed from other fields
fullName: virtual({
type: 'string', // TypeScript output type
hooks: {
resolveOutput: ({ item }) => {
return `${item.firstName} ${item.lastName}`
},
},
}),
// External API sync example
syncStatus: virtual({
type: 'boolean',
hooks: {
resolveInput: async ({ item }) => {
// Side effect: sync to external API
await syncToExternalAPI(item)
return undefined // Don't store anything
},
resolveOutput: () => true,
},
}),
}
Options:
type: TypeScript type string, import string, or type descriptor (see Custom Scalar Types below)hooks.resolveOutput: Required - Compute field value from other fieldshooks.resolveInput: Optional - Side effects during create/update
Custom Scalar Types
Virtual fields support custom scalar types (like Decimal for financial precision) through three approaches:
1. Primitive type strings (for built-in JavaScript types):
fullName: virtual({
type: 'string',
hooks: {
resolveOutput: ({ item }) => `${item.firstName} ${item.lastName}`,
},
})
2. Import strings (for custom types, explicit format):
import Decimal from 'decimal.js'
totalPrice: virtual({
type: "import('decimal.js').Decimal",
hooks: {
resolveOutput: ({ item }) => {
return new Decimal(item.price).times(item.quantity)
},
},
})
3. Type descriptor objects (recommended for custom types):
import Decimal from 'decimal.js'
totalPrice: virtual({
type: { value: Decimal, from: 'decimal.js' },
hooks: {
resolveOutput: ({ item }) => {
return new Decimal(item.price).times(item.quantity)
},
},
})
The TypeScript type generator automatically collects and generates the necessary import statements. This enables precise financial calculations and integration with third-party types while maintaining full type safety.
Use Cases:
- Financial calculations: Use
Decimalfromdecimal.jsfor precise currency calculations - Custom data structures: Return domain-specific types from virtual fields
- Third-party libraries: Integrate types from any npm package
Key Features:
- Does not create a database column
- Only computed when explicitly selected/included in queries
- Can combine data from multiple fields
- Useful for derived values, computed properties, and external sync
- Supports custom scalar types for financial precision
Usage Example:
// Query with virtual field
const user = await context.db.user.findUnique({
where: { id },
select: {
firstName: true,
lastName: true,
fullName: true, // Virtual field is computed on demand
},
})
console.log(user.fullName) // "John Doe"
Virtual fields are only computed when explicitly included in select or include clauses. They are not computed by default to optimize performance.
JSON Field
Field for storing arbitrary JSON data:
import { json } from '@opensaas/stack-core/fields'
fields: {
metadata: json({
validation: { isRequired: false },
ui: {
placeholder: 'Enter JSON data...',
rows: 10,
formatted: true,
},
}),
settings: json({
validation: { isRequired: true },
}),
}
Options:
validation.isRequired: Booleanui.placeholder: Placeholder textui.rows: Number of textarea rowsui.formatted: Format JSON with indentation
Third-Party Field Types
Rich Text Field
From @opensaas/stack-tiptap:
import { richText } from '@opensaas/stack-tiptap/fields'
fields: {
content: richText({
ui: {
minHeight: 300,
maxHeight: 800,
},
}),
}
See the Tiptap package documentation for more details.
Image Field
From @opensaas/stack-storage:
import { image } from '@opensaas/stack-storage/fields'
fields: {
avatar: image({
storage: 's3',
validation: {
isRequired: true,
},
}),
}
See the Storage package documentation for more details.
Common Field Options
All field types support these common options:
Access Control
text({
access: {
read: ({ session }) => !!session,
create: ({ session }) => !!session,
update: ({ session, item }) => session?.userId === item.authorId,
},
})
Hooks
text({
hooks: {
resolveInput: async ({ resolvedData, fieldKey }) => {
// Transform input data
return resolvedData[fieldKey]?.toLowerCase()
},
resolveOutput: async ({ item, fieldKey }) => {
// Transform output data
return item[fieldKey]?.toUpperCase()
},
},
})
UI Configuration
text({
ui: {
label: 'Custom Label',
description: 'Help text shown below the field',
placeholder: 'Enter text here...',
// Field-type-specific options
},
})
Creating Custom Field Types
You can create custom field types by implementing the BaseFieldConfig interface:
import type { BaseFieldConfig } from '@opensaas/stack-core'
import { z } from 'zod'
export type SlugField = BaseFieldConfig & {
type: 'slug'
from?: string // Field to generate slug from
}
export function slug(options?: Omit<SlugField, 'type'>): SlugField {
return {
type: 'slug',
...options,
getZodSchema: (fieldName, operation) => {
return z
.string()
.regex(/^[a-z0-9-]+$/)
.optional()
},
getPrismaType: (fieldName) => {
return { type: 'String', modifiers: '?' }
},
getTypeScriptType: () => {
return { type: 'string', optional: true }
},
}
}
See the Custom Fields guide for a complete tutorial.
Field Validation
Validation rules are defined in the validation object:
text({
validation: {
isRequired: true,
length: { min: 3, max: 100 },
},
})
integer({
validation: {
isRequired: true,
min: 0,
max: 100,
},
})
Validation errors are thrown during create/update operations and include:
- Field name
- Error type
- Validation rule that failed
Field Methods
Every field config object provides these methods used by the generator:
getZodSchema(fieldName, operation)
Returns a Zod schema for validation:
getZodSchema: (fieldName, operation) => {
let schema = z.string()
if (validation?.length) {
if (validation.length.min) schema = schema.min(validation.length.min)
if (validation.length.max) schema = schema.max(validation.length.max)
}
return validation?.isRequired ? schema : schema.optional()
}
getPrismaType(fieldName)
Returns the Prisma type and modifiers:
getPrismaType: (fieldName) => {
return { type: 'String', modifiers: '?' }
}
getTypeScriptType()
Returns the TypeScript type and optionality:
getTypeScriptType: () => {
return { type: 'string', optional: true }
}
Best Practices
1. Use Appropriate Field Types
// ✅ Good: Use integer for numbers
age: integer({ validation: { min: 0, max: 150 } })
// ❌ Bad: Don't use text for numbers
age: text({ validation: { length: { max: 3 } } })
2. Add Validation Rules
// ✅ Good: Validate email format
email: text({
validation: {
isRequired: true,
length: { max: 255 },
},
})
// ❌ Bad: No validation
email: text()
3. Use Relationships for Foreign Keys
// ✅ Good: Use relationship field
author: relationship({ ref: 'User.posts' })
// ❌ Bad: Don't use text for IDs
authorId: text()
Next Steps
- Hooks System - Transform field data
- Custom Fields Guide - Create custom field types
- API Reference - Complete field API