DeesseJS Collections

Collections

Advanced features

Collections Deep Dive

Advanced collection features for building robust data models.

File-Based Collections

Collections are defined in separate files for better organization:

collections/
├── users.ts
├── posts.ts
├── comments.ts
config/
├── config.ts
// collections/posts.ts
export const posts = collection({
  slug: 'posts',
  fields: { ... }
})

// config/database.ts
import { posts } from '../collections/posts'

export const { collections } = defineConfig({
  collections: [posts]
})

Relations

Collections can reference each other through relation fields.

One-to-One

// collections/users.ts
export const users = collection({
  slug: 'users',
  fields: {
    profile: field({
      type: relation('profiles', { singular: true })
    })
  }
})

// collections/profiles.ts
export const profiles = collection({
  slug: 'profiles',
  fields: {
    userId: field({
      type: relation('users')
    })
  }
})

One-to-Many

// collections/posts.ts
export const posts = collection({
  slug: 'posts',
  fields: {
    authorId: field({
      type: relation('users')
    }),

    // Reverse relation (virtual field)
    comments: field({
      type: reverseRelation({
        collection: 'comments',
        via: 'postId'
      })
    })
  }
})

// collections/comments.ts
export const comments = collection({
  slug: 'comments',
  fields: {
    postId: field({
      type: relation('posts')
    })
  }
})

Many-to-Many

export const posts = collection({
  slug: 'posts',
  fields: {
    categories: field({
      type: relation('categories', {
        many: true,
        through: 'postCategories' // Join table
      })
    })
  }
})

Hooks

Hooks let you run code at specific points during collection operations.

Hook Types

export const posts = collection({
  slug: 'posts',

  hooks: {
    beforeCreate: [
      async ({ data }) => {
        // Modify data before create
        data.slug = slugify(data.title)
      }
    ],

    afterCreate: [
      async ({ result }) => {
        // Run side effects after create
        await searchEngine.index(result)
      }
    ],

    beforeUpdate: [
      async ({ data, where }) => {
        // Validate or transform before update
        await logUpdate('posts', where.id, data)
      }
    ],

    afterUpdate: [
      async ({ result }) => {
        // Invalidate caches, notify, etc.
        await cache.invalidate(`posts:${result.id}`)
      }
    ]
  },

  fields: { ... }
})

Multiple Hooks

Hooks can be arrays for composition:

hooks: {
  beforeCreate: [
    validateTitle,
    generateSlug,
    setDefaults
  ],

  afterUpdate: [
    updateSearchIndex,
    invalidateCache
  ]
}

// Defined as separate functions
function validateTitle({ data }) {
  if (data.title.length < 3) {
    throw new Error('Title is too short')
  }
}

Hook Context

Hooks receive context about the operation:

beforeCreate: async ({ data, context }) => {
  // data: Input data for the operation
  // context: { collection, locale, currentUser, ... }
}

Plugins

Plugins extend collections with additional functionality.

Applying Plugins

Plugins are applied at configuration level:

// config/database.ts
export const { collections } = defineConfig({
  plugins: [
    timestampsPlugin(),
    softDeletePlugin({
      collections: ['posts', 'comments']
    }),
    seoPlugin({
      collections: ['posts', 'pages']
    })
  ],

  collections: [posts, comments]
})

What Plugins Can Do

Plugins can:

  • Add fields to collections (timestamps, soft delete, SEO fields)
  • Add field types (custom field types available globally)
  • Add collections (create new collections automatically)
  • Add hooks (inject behavior at operation boundaries)
  • Add operations (new methods on collections)
  • Add utilities (helper functions)

Native Plugins

Built-in plugins for common needs:

  1. Versioning - Track all changes to records
  2. Cache - Intelligent caching layer
  3. Soft Delete - Safe deletion with recovery
  4. SEO - Meta tags and optimization

Metadata

Collections can include metadata for better documentation and tooling.

export const posts = collection({
  slug: 'posts',

  // Human-readable labels
  label: 'Blog Post',
  plural: 'Posts',
  description: 'Blog posts and articles',

  // Admin UI configuration
  admin: {
    icon: 'document',
    defaultSort: 'createdAt',
    defaultSortDirection: 'desc'
  },

  // Custom metadata
  metadata: {
    featureFlags: ['full-text-search', 'revisions'],
    permissions: ['create', 'read', 'update', 'delete']
  },

  fields: { ... }
})

Validation

Cross-field and custom validation:

export const posts = collection({
  slug: 'posts',

  validation: {
    // Cross-field validation
    password: {
      matches: (value, { data }) => {
        return value === data.confirmPassword
      }
    }
  },

  fields: {
    password: field({ type: text() }),
    confirmPassword: field({ type: text() })
  }
})

Collection Operations

Collections provide automatic CRUD operations:

// Find
await collections.posts.findMany({
  where: { status: { equals: 'published' } },
  orderBy: { createdAt: 'desc' },
  limit: 10
})

// Find unique
await collections.posts.findUnique({
  where: { id: 1 }
})

// Create
await collections.posts.create({
  data: {
    title: 'Hello World',
    content: '...'
  }
})

// Update
await collections.posts.update({
  where: { id: 1 },
  data: { title: 'Updated' }
})

// Delete
await collections.posts.delete({
  where: { id: 1 }
})

// Count
await collections.posts.count({
  where: { status: { equals: 'published' } }
})

Collections are Composable: Everything about collections—from fields to hooks to plugins—is designed to be composed from simple, reusable pieces. This makes your data model maintainable and scalable.

Next Steps

  • Plugins - Learn about native and custom plugins
  • i18n - Internationalization support
  • Examples - Real-world examples

On this page