Field Types

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:

typescript
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: Boolean
  • validation.length.min: Minimum length
  • validation.length.max: Maximum length
  • ui.displayMode: 'input' or 'textarea'

Integer Field

Number field with validation:

typescript
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: Boolean
  • validation.min: Minimum value
  • validation.max: Maximum value
  • defaultValue: Default integer value

Checkbox Field

Boolean field:

typescript
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:

typescript
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 timestamp
  • db.updatedAt: Boolean - auto-update on record changes

Password Field

String field automatically excluded from reads:

typescript
import { password } from '@opensaas/stack-core/fields'

fields: {
  password: password({
    validation: {
      isRequired: true,
      length: { min: 8 },
    },
  }),
}

Options:

  • validation.isRequired: Boolean
  • validation.length.min: Minimum length
  • validation.length.max: Maximum length

Password fields are automatically excluded from all read operations for security.

Select Field

Enum field with predefined options:

typescript
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 } pairs
  • defaultValue: Default selected value
  • validation.isRequired: Boolean
  • ui.displayMode: 'select' | 'radio' | 'segmented-control'

Relationship Field

Foreign key relationship:

typescript
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

Third-Party Field Types

Rich Text Field

From @opensaas/stack-tiptap:

typescript
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:

typescript
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

typescript
text({
  access: {
    read: ({ session }) => !!session,
    create: ({ session }) => !!session,
    update: ({ session, item }) => session?.userId === item.authorId,
  },
})

Hooks

typescript
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

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
getPrismaType: (fieldName) => {
  return { type: 'String', modifiers: '?' }
}

getTypeScriptType()

Returns the TypeScript type and optionality:

typescript
getTypeScriptType: () => {
  return { type: 'string', optional: true }
}

Best Practices

1. Use Appropriate Field Types

typescript
// ✅ 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

typescript
// ✅ Good: Validate email format
email: text({
  validation: {
    isRequired: true,
    length: { max: 255 },
  },
})

// ❌ Bad: No validation
email: text()

3. Use Relationships for Foreign Keys

typescript
// ✅ Good: Use relationship field
author: relationship({ ref: 'User.posts' })

// ❌ Bad: Don't use text for IDs
authorId: text()

Next Steps