Data Lifecycle
Application patterns for slugs, validation, and schema changes that need to stay safe as content changes over time.
Use these patterns when the hard part is not the initial schema, but keeping data safe and predictable as editors create, update, and evolve content. They help protect URL structure, catch invalid changes, and handle schema evolution more carefully.
For self-hosted Dyrected, lifecycle patterns can use your application runtime directly. That is the right fit for function hooks, database migrations, transaction-aware changes, and side effects that must run inside your server.
Generate a slug from a title
Problem: you want readable URLs, but you do not want editors hand-authoring slugs for every document.
This pattern generates the slug on the server and can also mirror it live in the Admin UI. It is a strong default for article pages, documentation content, and any collection that needs stable URL-friendly paths.
Example implementation
import { defineCollection, defineTextField } from "@dyrected/core";
export const toSlug = (value: unknown) =>
String(value ?? "")
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "");
export const Posts = defineCollection({
slug: "posts",
hooks: {
beforeChange: [
({ data, operation }) => {
if (operation === "create" || data.title !== undefined) {
return { ...data, slug: toSlug(data.title) };
}
return data;
},
],
},
fields: [
defineTextField({ name: "title", label: "Title", required: true }),
defineTextField({
name: "slug",
label: "Slug",
required: true,
unique: true,
promoted: true,
admin: {
hooks: {
onChange:
"value == '' || value == null ? (siblingData.title != null ? slugify(siblingData.title) : value) : value",
},
},
}),
],
});
[!TIP] You can use Dyrected's built-in Jexl helper functions—such as
slugify(siblingData.title)orlower(siblingData.name)—directly in declarative string hooks for 100% Cloud-safe live field derivation.
Read the full docs:
Validate related fields before saving
Problem: one field value is only valid in relation to another field, such as a start date and an end date.
This pattern uses a collection hook to reject invalid combinations before they reach the database. It is a good fit for dates, time windows, numeric ranges, and any rule that depends on multiple values together.
Example implementation
import { defineCollection, defineDateTimeField, defineTextField } from "@dyrected/core";
export const Events = defineCollection({
slug: "events",
hooks: {
beforeChange: [
({ data, doc }) => {
const startsAt = data.startsAt ?? doc?.startsAt;
const endsAt = data.endsAt ?? doc?.endsAt;
const start = startsAt ? new Date(startsAt) : undefined;
const end = endsAt ? new Date(endsAt) : undefined;
if (start && Number.isNaN(start.getTime())) {
throw new Error("The event start time must be a valid date.");
}
if (end && Number.isNaN(end.getTime())) {
throw new Error("The event end time must be a valid date.");
}
if (start && end && end <= start) {
throw new Error("The event end time must be after its start time.");
}
return data;
},
],
},
fields: [
defineTextField({ name: "title", label: "Title", required: true }),
defineDateTimeField({
name: "startsAt",
label: "Starts at",
required: true,
}),
defineDateTimeField({
name: "endsAt",
label: "Ends at",
required: true,
}),
],
});
Read the full docs:
Rename a field without orphaning existing data
Problem: you need to change a field name on a live schema without breaking the documents that already exist.
This pattern uses renameTo and a safe default so old content can continue working while the schema migrates. It is the safer path when a schema needs to evolve without forcing a risky big-bang rename.
Example implementation
import { defineCollection, defineTextField } from "@dyrected/core";
export const Customers = defineCollection({
slug: "customers",
fields: [
defineTextField({
name: "fullName",
label: "Full name",
renameTo: "name",
defaultValue: "",
required: true,
}),
],
});
Read the full docs:
Archive records instead of deleting them
Problem: content should disappear from normal views without being permanently deleted from the database.
This pattern adds an archive flag, hides archived rows from normal readers, and disables destructive deletion. It is useful for announcements, listings, or internal records that may need to be restored or audited later.
Example implementation
import { defineBooleanField, defineCollection, defineTextField } from "@dyrected/core";
export const Announcements = defineCollection({
slug: "announcements",
access: {
read: ({ user }) =>
user?.roles?.includes("admin") ? true : { archived: { equals: false } },
create: ({ user }) => Boolean(user),
update: ({ user }) => Boolean(user),
delete: () => false,
},
fields: [
defineTextField({ name: "title", label: "Title", required: true }),
defineBooleanField({
name: "archived",
label: "Archived",
defaultValue: false,
}),
],
});
Read the full docs: