Defining a model
A model describes one kind of content: its fields, how they are validated, and how they appear in the admin panel. You define it with makeModel() in src/models/.
Basic example
// src/models/post.ts
import { makeModel, text, richtext } from "@njinlabs/njin";
import z from "zod";
const post = makeModel("post", {
name: "Post",
searchFields: ["title", "body"],
schema: z.object({
title: text({ label: "Title" }),
body: richtext({ label: "Body" }),
}),
});
export default post;makeModel(prefix, config) takes:
| Option | Required | What it does |
|---|---|---|
prefix (first argument) |
Yes | The API path (/api/post), the name of the global you use in templates, and the table name |
name |
Yes | The label shown in the admin panel |
schema |
Yes | A z.object() whose fields are built with njin's field helpers. See Field types |
searchFields |
Yes | Fields used by search. Use "author.name" to search a field of a related record |
draftable |
No | Adds a draft and publish workflow. See Draft and publish |
i18n |
No | Lets fields hold one value per language. See Languages |
A nested entry like "author.name" is checked when the file loads: makeModel() throws if author is not a relation or file field, or if the related model has no name field.
Registering a model
A model only exists once it is listed in config.ts:
// config.ts
export default defineConfig({
models: [() => import("./src/models/post")],
});Restart the dev server after changing config.ts. On startup njin creates or updates the model's table for you.
A registered model gets:
GETandPOSTon/api/{prefix}GET,PUTandDELETEon/api/{prefix}/:id- A section in the admin panel, built from its schema
- A global in templates, named after the prefix:
await post.read() - Its schema in
GET /api/schema, which is what the admin panel reads
What you get for free
Every record has these fields in addition to your own:
| Field | Value |
|---|---|
id |
The record's id |
createdAt, updatedAt |
ISO 8601 timestamps |
createdBy |
The id of the user who created the record, or null |
You cannot declare a field named createdBy: makeModel() throws, because njin fills it in. On a draftable model, status and publishedAt are reserved in the same way.
Working with a model in code
The value returned by makeModel() is the model. Use it in templates, hooks, events, routes and tests:
| Method | Returns |
|---|---|
read(options?) |
{ data, meta }, where meta is { total, page, limit, pageCount }. See Reading data |
show(id) |
The record, or undefined when there is no record with that id |
create(data) |
The new record |
update(id, data) |
The updated record. Only the fields you pass are changed |
destroy(id) |
The deleted record |
create() also accepts { createdBy: userId } as a second argument to set the owner.
const created = await post.create({ title: "Hello", body: "<p>Hi</p>" });
await post.update(created.id, { title: "Hello again" });
await post.destroy(created.id);These calls run the model's hooks and its unique checks, but they are not checked against the user's permissions. Code you write calls the model directly, so guarding it is up to you. See Access control.
Validation happens at the API
The rules in your schema are enforced on requests to the REST API (POST and PUT). Calling post.create() yourself skips them, and it does not apply .default() values either. If your code accepts data from outside, validate it first:
const data = post.validation.parse(input);
await post.create(data);See Validation.