Skip to main content

My First Model

A model is a class in src/models/<Name>.model.ts extending UuidModel (generated uuid key) or Model (your own key) from @webda/models. Fields are class properties; validation comes from JSDoc tags, which the compiler turns into the JSON schema.

src/models/Task.model.ts
import type { IOperationContext } from "@webda/core";
import { UuidModel } from "@webda/models";

/**
* A task to do
*/
export class Task extends UuidModel {
/**
* What needs to be done
* @minLength 1
* @maxLength 200
*/
title!: string;

/**
* Whether the task is finished
*/
done!: boolean;

/**
* Optional fields use `?`
* @maxLength 1000
*/
notes?: string;

/**
* Who can do what on this task
* @param context - the caller context
* @param action - "create", "get", "update", "delete" or an operation name
* @returns true, or the reason of the refusal
*/
async canAct(_context: IOperationContext, _action: string): Promise<boolean | string> {
// Open to everyone while you get started: restrict it before going to production
return true;
}
}

The file name must end with .model.ts: the build fails on a model file without that suffix.

Build the application after changing a model: it regenerates webda.module.json and the .webda/ folder.

npm run build

Permissions are required​

Permissions are deny-by-default: a model without a canAct method is refused on every transport (REST, GraphQL, gRPC, MCP). canAct returns true to allow the action, or false or a string explaining the refusal.

A rule that only depends on the caller can be written as a static method instead:

export class Task extends UuidModel {
static canAct(context: IOperationContext, action: string): boolean | string {
if (action === "get") {
return true;
}
return context.getCurrentUserId() ? true : "Login required";
}
}

The Post model of the blog tutorial shows a complete rule, with drafts only visible to their author.

Use it in code​

Read and write data through the model; never through a store:

const task = await Task.create({ title: "Write the docs", done: false });
const same = await Task.ref(task.getUUID()).get();
const { results } = await Task.query("done = FALSE ORDER BY title LIMIT 20");
const mine = await Task.query("title = ?", ["Write the docs"]);

Queries use WebdaQL. The model is stored in the application's default store, chosen with --store when the application was created.

Use it from a client​

Webda exposes the create, get, update, patch, delete and query operations of every model on each configured transport. With the REST transport, after npm run debug:

# Create
curl -X POST http://localhost:18080/tasks -H "Content-Type: application/json" \
-d '{"title": "Write the docs", "done": false}'
# Get, update, patch, delete
curl http://localhost:18080/tasks/<uuid>
curl -X PUT http://localhost:18080/tasks/<uuid> -H "Content-Type: application/json" \
-d '{"title": "Write the docs", "done": true}'
curl -X PATCH http://localhost:18080/tasks/<uuid> -H "Content-Type: application/json" -d '{"done": true}'
curl -X DELETE http://localhost:18080/tasks/<uuid>
# Query
curl -X PUT http://localhost:18080/tasks -H "Content-Type: application/json" -d '{"q": "done = FALSE"}'

Add a custom operation​

A method decorated with @Operation() becomes an operation of the model, called on the instance loaded from its key. It goes through canAct with the method name as action.

import { Operation } from "@webda/core";

export class Task extends UuidModel {
// ...

/**
* Operation id: Task.Complete; REST: PUT /tasks/{uuid}/complete
*/
@Operation()
async complete(): Promise<void> {
this.done = true;
await this.save();
}
}

Relations​

A field typed with a relation links models together:

src/models/Project.model.ts
import { OneToMany, UuidModel } from "@webda/models";
import type { Task } from "./Task.model.js";

export class Project extends UuidModel {
name!: string;
tasks!: OneToMany<Task, Project, "project">;
}
src/models/Task.model.ts
import { BelongTo, UuidModel } from "@webda/models";
import type { Project } from "./Project.model.js";

export class Task extends UuidModel {
// ...
project!: BelongTo<Project>;
}
TypeMeaning
BelongTo<Parent>this model belongs to a parent; deleting the parent does not delete it
RelateTo<Other>a link to another model, no cascade
OneToMany<Child, ThisModel, "attribute">the children whose attribute points to this model
ManyToMany<Other>many-to-many links

A BelongTo or RelateTo field stores the key of the target: Task.create({ title, done: false, project: project.getUUID() }). Import related models with import type to avoid circular imports. See Relations.

Your own primary key​

Extend Model and declare the key fields:

import { Model, WEBDA_PRIMARY_KEY } from "@webda/models";

export class Category extends Model {
[WEBDA_PRIMARY_KEY] = ["slug"] as const;

/**
* @pattern ^[a-z0-9-]+$
*/
slug!: string;

name!: string;
}

Common mistakes​

  • @Model() / @Expose() decorators and CoreModel are Webda v3 APIs: extend Model or UuidModel, no decorators.
  • Injecting a store (@Inject("taskStore")): use Task.create, Task.ref(uuid).get(), Task.query(...).
  • Editing webda.module.json or .webda/: they are generated, change the model and run npm run build.

Next: My First Service.