Model
Define the multi-tenant post schema with the fields that power isolation and soft-delete.
Create the post schema under schemas/cms/. Two fields do the heavy lifting later: tenant is what isolationFields will populate on every write, and deletedAt powers our custom soft-delete.
import z from "zod";
import { mongodb } from "@/database.ts";
import { $objectId } from "@/core/utils/createCRUD.ts";
// What clients send when creating/updating a post
export const postInputSchema = z.object({
title: z.string().min(1).max(200),
body: z.string(),
published: z.boolean().default(false),
});
// The full stored document
export const postSchema = postInputSchema.extend({
_id: $objectId.optional(),
tenant: $objectId, // injected by isolationFields on create
deletedAt: z.date().optional(), // set by the custom soft-delete
createdAt: z.date().default(() => new Date()),
});
export const postModel = mongodb.db().collection<
z.infer<typeof postSchema>
>("posts");Why two schemas?
postInputSchemadescribes the shape clients are allowed to send. It deliberately omitstenant,_id, and timestamps - a client should never set those.postSchemaextends the input with server-owned fields.createCRUDuses it to validate stored documents and to generate accurate response types.
$objectId is exported from @/core/utils/createCRUD.ts and validates MongoDB ObjectId values in Zod. See Models for more on the $-prefixed schema convention and ObjectId handling.
With the model in place, we can generate the entire API in a single call. On to the API.