Collections

Collections define the data models in your CMS. Each collection represents a type of content that can be created, read, updated, and deleted.

Creating a Collection

You can scaffold a new collection using the CLI:

lucent collection Posts

This creates a new file in src/collections/Posts.ts with a default structure. Alternatively, you can define it manually:

Manual Definition

import { defineCollection } from "@codesordinatestudio/lucent";

export const Posts = defineCollection({
  slug: "posts",
  fields: [
    { name: "title", type: "text", required: true },
    { name: "slug", type: "text", required: true, unique: true },
    { name: "content", type: "richtext" },
    { name: "publishedAt", type: "date" },
    { name: "status", type: "select", options: ["draft", "published", "archived"] },
    { name: "views", type: "integer", defaultValue: 0 },
    { name: "featured", type: "boolean", defaultValue: false },
    { name: "tags", type: "multiselect", options: ["tech", "news", "tutorial"] },
    { name: "metadata", type: "json" },
    { name: "author", type: "relationship", relationTo: "users" },
    { name: "images", type: "upload", multiple: true, maxSize: 5 * 1024 * 1024 },
  ],
  access: {
    read: () => true,
    create: ({ user }) => !!user,
    update: ({ user, doc }) => user?.id === doc.author?.id,
    delete: ({ user, doc }) => user?.id === doc.author?.id,
  },
  timestamps: true,
  softDelete: true,
});

Collection Options

OptionTypeDescription
slugstringUnique identifier for the collection
fieldsarrayField definitions
authbooleanEnable auth collection features
authPrefixstringPrefix for auth endpoints
accessobjectAccess control rules
hooksobjectCollection-specific hooks
timestampsbooleanAuto-manage createdAt/updatedAt
softDeletebooleanEnable soft delete
rateLimitobjectRate limiting rules

Field Types

TypeDescription
textSingle-line text input
textareaMulti-line text input
numberFloating-point number
integerInteger number
booleanTrue/false toggle
emailEmail address
passwordHashed password field
dateISO 8601 date/datetime
selectSingle selection from options
multiselectMultiple selections from options
jsonArbitrary JSON data
richtextRich text (HTML) content
relationshipRelation to another collection
arrayNested array of sub-fields
uploadFile upload

Field Options

Common Options

All fields support these base options:

{ name: "title", type: "text" }
{ name: "title", type: "text", required: true }
{ name: "title", type: "text", unique: true }
{ name: "title", type: "text", hidden: true }
{ name: "title", type: "text", readonly: true }
{ name: "title", type: "text", defaultValue: "Untitled" }
{ name: "title", type: "text", description: "The post title" }

Computed Fields

Virtual fields computed on read:

{
  name: "fullName",
  type: "text",
  compute: (doc) => `${doc.firstName} ${doc.lastName}`,
}

Field Validation

{
  name: "email",
  type: "email",
  validate: (value) => {
    if (!value.includes("@")) return "Invalid email address";
    return true;
  },
}

Field Access Control

{
  name: "internalNote",
  type: "text",
  access: {
    read: ({ user }) => user?.role === "admin",
    create: ({ user }) => user?.role === "admin",
    update: ({ user }) => user?.role === "admin",
  },
}

API Options

export const posts = defineCollection({
  slug: "posts",
  fields: [
    /* ... */
  ],
  api: {
    // Enable/disable operations
    read: true,
    create: true,
    update: true,
    delete: true,

    // Pagination
    perPage: 20,
    maxPerPage: 100,

    // Filtering & sorting
    filterable: ["status", "author", "createdAt", "publishedAt"],
    sortable: ["createdAt", "updatedAt", "title", "publishedAt"],
  },
});

Collection Hooks

export const posts = defineCollection({
  slug: "posts",
  fields: [
    /* ... */
  ],
  hooks: {
    beforeOperation: [
      async ({ operation, data, db }) => {
        console.log(`About to ${operation}:`, data);
        return data;
      },
    ],
    beforeRead: [
      async ({ query, db }) => {
        // Modify query before read
        query.where.published = true;
        return query;
      },
    ],
    afterRead: [
      async ({ doc, db }) => {
        // Transform document after read
        doc.viewCount = (doc.viewCount || 0) + 1;
        return doc;
      },
    ],
    beforeCreate: [
      async ({ data, db }) => {
        // Modify data before creation
        data.slug = slugify(data.title);
        return data;
      },
    ],
    afterCreate: [
      async ({ doc, db }) => {
        // Perform actions after creation
        await notifySubscribers(doc);
      },
    ],
    beforeUpdate: [
      async ({ data, doc, db }) => {
        data.updatedAt = new Date().toISOString();
        return data;
      },
    ],
    afterUpdate: [
      async ({ data, doc, previousDoc, db }) => {
        if (doc.status === "published" && previousDoc.status !== "published") {
          await sendNotification(doc);
        }
      },
    ],
    beforeDelete: [
      async ({ doc, db }) => {
        // Cleanup related data
        await deleteComments(doc.id);
      },
    ],
    afterDelete: [
      async ({ doc, db }) => {
        // Post-deletion cleanup
        await cleanupAssets(doc);
      },
    ],
  },
});

Access Control

Function-Based Access

export const posts = defineCollection({
  slug: "posts",
  fields: [
    /* ... */
  ],
  access: {
    // Public read
    read: () => true,

    // Authenticated create
    create: ({ user }) => !!user,

    // Own document update
    update: ({ user, doc }) => user?.id === doc.author?.id,

    // Admin or owner delete
    delete: ({ user, doc }) => user?.role === "admin" || user?.id === doc.author?.id,
  },
});

Access Context

The access functions receive:

interface AccessContext {
  user: User | null; // Current authenticated user
  doc?: Record<string, unknown>; // Document being accessed
  id?: string; // Document ID
  operation: "read" | "create" | "update" | "delete";
}

Timestamps & Soft Delete

Automatic Timestamps

export const posts = defineCollection({
  slug: "posts",
  fields: [
    /* ... */
  ],
  timestamps: true, // Adds createdAt and updatedAt
});

Soft Delete

export const posts = defineCollection({
  slug: "posts",
  fields: [
    /* ... */
  ],
  softDelete: true, // Records are marked deleted rather than removed
});

With soft delete, deleted records have a deletedAt timestamp. Use includeDeleted=true to query deleted records:

GET /api/posts?includeDeleted=true

Rate Limiting

export const posts = defineCollection({
  slug: "posts",
  fields: [
    /* ... */
  ],
  rateLimit: {
    read: { max: 100, window: "1 minute" },
    write: { max: 10, window: "1 minute" },
  },
});