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
| Option | Type | Description |
|---|---|---|
slug | string | Unique identifier for the collection |
fields | array | Field definitions |
auth | boolean | Enable auth collection features |
authPrefix | string | Prefix for auth endpoints |
access | object | Access control rules |
hooks | object | Collection-specific hooks |
timestamps | boolean | Auto-manage createdAt/updatedAt |
softDelete | boolean | Enable soft delete |
rateLimit | object | Rate limiting rules |
Field Types
| Type | Description |
|---|---|
text | Single-line text input |
textarea | Multi-line text input |
number | Floating-point number |
integer | Integer number |
boolean | True/false toggle |
email | Email address |
password | Hashed password field |
date | ISO 8601 date/datetime |
select | Single selection from options |
multiselect | Multiple selections from options |
json | Arbitrary JSON data |
richtext | Rich text (HTML) content |
relationship | Relation to another collection |
array | Nested array of sub-fields |
upload | File 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" },
},
});