Skip to main content

Model Lifecycle and Events

Every Webda model emits typed events before and after each store operation. You can listen to these events to implement side effects like sending notifications, updating caches, or enforcing cross-model constraints.

Lifecycle event phases​

For each mutation operation, two events fire — one before (allows interception) and one after (notifications only):

OperationBefore eventAfter event
CreateCreateCreated
Update (full)UpdateUpdated
Partial updatePartialUpdatePartialUpdated
DeleteDeleteDeleted
PatchPatchPatched
QueryQueryQueried

Event type definitions​

// From packages/models/src/model.ts
export type ModelEvents<T = any> = {
Create: { object_id: string; object: T };
PartialUpdate: any;
Delete: { object_id: string };
Update: { object_id: string; object: T; previous: T };
Patch: { object_id: string; object: T; previous: T };
Query: { query: string };

// After-events
Created: { object_id: string; object: T };
PartialUpdated: any;
Deleted: { object_id: string };
Patched: { object_id: string; object: T; previous: T };
Updated: { object_id: string; object: T; previous: T };
Queried: { query: string; results: T[]; continuationToken?: string };
};

Declaring custom events​

Extend ModelEvents<T> with your own custom events using WEBDA_EVENTS:

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

export class PostEvents<T extends Post> {
Publish: { post: T };
}

export class Post extends Model {
[WEBDA_PRIMARY_KEY] = ["slug"] as const;
[WEBDA_EVENTS]: ModelEvents<this> & PostEvents<this>;

slug!: string;
title!: string;
status!: "draft" | "published" | "archived";
}

Emitting events from a model​

// In an @Operation method or service
export class Post extends Model {
// ...

async publish(destination: "linkedin" | "twitter"): Promise<string> {
this.status = "published";
await this.save();

// Emit a custom event
this.emit("Publish", { post: this });
return `${destination}_${this.slug}_${Date.now()}`;
}
}

Listening to events on a repository​

import { Post } from "./models/Post";

// Listen on the Post repository for any Created event
Post.getRepository().on("Created", ({ object }) => {
console.log(`Post created: ${object.slug}`);
});

// Listen to a specific object's events
const post = await Post.ref("hello-world").get();
post.on("Updated", ({ object, previous }) => {
console.log(`Post updated: ${previous.title} → ${object.title}`);
});

Listening to events in a service using @On​

In Webda services, use the @On decorator to subscribe to store events:

import { Bean } from "@webda/core";
import { On } from "@webda/core";
import { Service } from "@webda/core";
import { useLog } from "@webda/workout";

@Bean
export class NotificationService extends Service {
protected log = useLog("NotificationService");

@On("Store.Save:WebdaSample/Post")
async onPostSaved(event: any): Promise<void> {
const post = event.object;
this.log.info(`Post saved: ${post.slug}`);
// Send notification, update search index, etc.
}

@On("Store.Delete:WebdaSample/Post")
async onPostDeleted(event: any): Promise<void> {
const { object_id } = event;
this.log.info(`Post deleted: ${object_id}`);
}
}

Dirty tracking​

Models automatically track which fields have changed since the last load or save. The dirty set is stored under model[WEBDA_DIRTY]:

import { WEBDA_DIRTY } from "@webda/utils";

const post = await Post.ref("hello-world").get();
console.log(post[WEBDA_DIRTY]); // Set {} — clean

post.title = "New Title";
console.log(post[WEBDA_DIRTY]); // Set { "title" } — title is dirty

await post.save();
console.log(post[WEBDA_DIRTY]); // Set {} — clean again

This is used internally by stores to emit partial update events and to optimize database writes.

User-defined lifecycle hooks (prepareCreate, canAct)​

canAct is a permission check: keep it free of side effects. The framework asks the static canAct(context, action, object?) of the model class (which forwards to the instance canAct(context, action) when the model defines one, and refuses otherwise). On update and patch it runs on the stored object, before the client input is applied, so it cannot see or transform the new values; on create it runs on the new, unsaved object; for a static action it runs without object.

To set fields on a new object, define prepareCreate(context): the DomainService (and GraphQL) call it after loading the client input and before the "create" check:

export class Post extends Model {
// ...

prepareCreate(context: IOperationContext): void {
// Auto-populate slug from title if not provided
if (!this.slug && this.title) {
this.slug = this.title.toLowerCase().replace(/[^a-z0-9]+/g, "-");
}
}
}

See Permissions for the checks each operation runs.

Sequence diagram​

Verify​

# Run model lifecycle tests
cd packages/models
pnpm test
✓ packages/models/src/model.spec.ts — lifecycle tests pass

See also​