Skip to main content

Syncing Models

Syncing models is a major powerful feature in Cascade, it allows you to embed documents inside other documents, and it will auto update the embedded documents whenever the original document is updated.

The Problem

Let's say we have a category model, and post model, the post has an embedded document for category, the problem here is whenever the category is updated, the post keeps the same information about the category when the post is saved.

The Solution

The solution here is with Syncing Models, the concept is simple, when the category is updated, search for all categories that are embedded inside posts, and update them.

We have here two scenarios:

  1. Single embedded document
  2. Array of embedded documents

Let's see each one of them.

Single Embedded Document

Let's go with the single embedded document scenario, as we mentioned we need to update the post's category when the category itself is updated.

Let's take a look at the post model:

src/models/post.ts
import { Model, Casts, castModel } from "@warlock.js/cascade";
import { Category } from "./category";

export class Post extends Model {
/**
* Collection name
*/
public static collection = "posts";

/**
* {@inheritDoc}
*/
protected casts: Casts = {
title: "string",
content: "string",
category: castModel(Category),
};
}

Let's take a look at the category model:

src/models/category.ts
import { Model, Casts } from "@warlock.js/cascade";

export class Category extends Model {
/**
* Collection name
*/
public static collection = "categories";

/**
* {@inheritDoc}
*/
protected casts: Casts = {
name: "string",
isActive: "boolean",
};
}

This is just a normal category model, let's update the code to make it synced with the post model:

src/models/category.ts
import { Model, Casts, ModelSync } from "@warlock.js/cascade";

export class Category extends Model {
/**
* Collection name
*/
public static collection = "categories";

/**
* Sync with posts
*/
public syncWith: ModelSync[] = [
// sync post
Post.sync("category"),
];

/**
* {@inheritDoc}
*/
protected casts: Casts = {
name: "string",
isActive: "boolean",
};
}

Let's understand what's happening here:

First off, We added a new property called syncWith that is an array of ModelSync instances.

We can create an instance of ModelSync that is linked to the model using the static method sync, this method returns a new instance of ModelSync that is linked to the model.

The sync method takes first argument with the name of the field that we need to update in our case it will be the category field in the post model.

So the scenario here as follows, category data is updated, the ModelSync class will be instantly called after the model is saved, it will search in the posts collection for all posts that has the same category id, which internally searches in category.id field, but we only pass the top field name which is category

Array of Embedded Documents

The second scenario is when we have an array of embedded documents, let's say we have a post model that has an array of comments, we want to update the comments list inside the post when a comment is updated.

warning

This is just an example, it is not recommended to store comments inside each post document, it is better to store them in a separate collection.

Let's take a look at the post model:

src/models/post.ts
import { Model, Casts, castModel } from "@warlock.js/cascade";
import { Comment } from "./comment";

export class Post extends Model {
/**
* Collection name
*/
public static collection = "posts";

/**
* {@inheritDoc}
*/
protected casts: Casts = {
title: "string",
content: "string",
comments: castModel(Comment),
};
}

Let's take a look at the comment model:

src/models/comment.ts
import { Model, Casts, ModelSync } from "@warlock.js/cascade";
import { Post } from "./post";

export class Comment extends Model {
/**
* Collection name
*/
public static collection = "comments";

/**
* Sync with posts
*/
public syncWith: ModelSync[] = [
// sync comments inside post whenever a comment is updated
Post.syncMany("comments"),
];

/**
* {@inheritDoc}
*/
protected casts: Casts = {
content: "string",
isActive: "boolean",
};
}

The syncMany method works exactly like sync except that it will search inside an array of embedded documents.

info

The rest of the coming documentation works in both scenarios sync and syncMany but we will use sync for simplicity

Embedded custom data

By default the ModelSync will call the embeddedData property to get the data from, but if we want to use another property we can pass it as a second argument to the sync method.

Let's say we want to sync the category id and name only, we can do it like this:

src/models/category.ts
import { Model, Casts, ModelSync } from "@warlock.js/cascade";

export class Category extends Model {
/**
* Collection name
*/
public static collection = "categories";

/**
* Sync with posts
*/
public syncWith: ModelSync[] = [
// sync post
Post.sync("category", "embedIdAndNameOnly"),
];

/**
* {@inheritDoc}
*/
protected casts: Casts = {
name: "string",
isActive: "boolean",
};

/**
* Embed id and name only
*/
public get embedIdAndNameOnly() {
return this.only(["id", "name"]);
}
}

Update multiple fields

Another scenario where we want to update multiple columns when the model is updated, let's say we want to update createdBy and updatedBy fields in the post when the user is updated, in that case, pass an array of fields to the sync method:

src/models/user.ts
import { Model, Casts, ModelSync } from "@warlock.js/cascade";

export class User extends Model {
/**
* Collection name
*/
public static collection = "users";

/**
* Sync with posts
*/
public syncWith: ModelSync[] = [
// sync post
Post.sync(["createdBy", "updatedBy"]),
];

/**
* {@inheritDoc}
*/
protected casts: Casts = {
name: "string",
isActive: "boolean",
};
}

What happens here the ModelSync will search in all columns that has the same value as the user id, which internally searches in createdBy.id and updatedBy.id fields, but we only pass the top field name which is createdBy and updatedBy

Unset On Delete

Another scenario is being taking care of is when the actual document of the embedded document is deleted, in that case we can perform multiple actions based on our needs.

  1. Unset the embedded document.
  2. Delete the document that has the embedded document.
  3. Do nothing.

Let's see each one of them:

Unset the embedded document

Call unsetOnDelete method on the ModelSync instance, this will unset the embedded document when the actual document is deleted.

src/models/category.ts
import { Model, Casts, ModelSync } from "@warlock.js/cascade";
import { Post } from "./post";

export class Category extends Model {
/**
* Collection name
*/
public static collection = "categories";

/**
* Sync with posts
*/
public syncWith: ModelSync[] = [
// sync post
Post.sync("category").unsetOnDelete(),
];

/**
* {@inheritDoc}
*/
protected casts: Casts = {
name: "string",
isActive: "boolean",
};
}

This is the default behavior, so you don't have to call unsetOnDelete method.

Delete the document that has the embedded document

The second scenario we can think of, when the original document is deleted, delete all related documents, for example we can delete all posts when their category is deleted.

src/models/category.ts
import { Model, Casts, ModelSync } from "@warlock.js/cascade";
import { Post } from "./post";

export class Category extends Model {
/**
* Collection name
*/
public static collection = "categories";

/**
* Sync with posts
*/
public syncWith: ModelSync[] = [
// sync post
Post.sync("category").removeOnDelete(),
];

/**
* {@inheritDoc}
*/
protected casts: Casts = {
name: "string",
isActive: "boolean",
};
}

By calling removeOnDelete, the ModelSync will search for all posts that have the deleted category and remove them.

Ignoring the delete action

The third scenario is when we want to ignore the delete action, in that case we can call ignoreOnDelete method.

src/models/category.ts
import { Model, Casts, ModelSync } from "@warlock.js/cascade";
import { Post } from "./post";

export class Category extends Model {
/**
* Collection name
*/
public static collection = "categories";

/**
* Sync with posts
*/
public syncWith: ModelSync[] = [
// sync post
Post.sync("category").ignoreOnDelete(),
];

/**
* {@inheritDoc}
*/
protected casts: Casts = {
name: "string",
isActive: "boolean",
};
}

This will keep the category document inside the post when the category is deleted.

Sync only when certain fields are updated

Because this is a costy operation, we can limit the sync to only when certain fields are updated, for example we can sync the category when the name is updated, any other updated fields will not trigger the sync, in that case use updateWhenChange by passing the fields that we want to sync when they are updated.

src/models/category.ts
import { Model, Casts, ModelSync } from "@warlock.js/cascade";
import { Post } from "./post";

export class Category extends Model {
/**
* Collection name
*/
public static collection = "categories";

/**
* Sync with posts
*/
public syncWith: ModelSync[] = [
// sync post
Post.sync("category").updateWhenChange(["name"]),
];

/**
* {@inheritDoc}
*/
protected casts: Casts = {
name: "string",
isActive: "boolean",
};
}

Update with certain criteria

Sometimes it is not good to update all documents that has the same id, for example we can update the category in the post only when the post is active, in that case we can use the where method that returns a query builder instance.

src/models/category.ts
import { Model, Casts, ModelSync } from "@warlock.js/cascade";
import { Post } from "./post";

export class Category extends Model {
/**
* Collection name
*/
public static collection = "categories";

/**
* Sync with posts
*/
public syncWith: ModelSync[] = [
// sync post
Post.sync("category").where(query => {
query.where("isActive", true);
}),
}),
];

/**
* {@inheritDoc}
*/
protected casts: Casts = {
name: "string",
isActive: "boolean",
};
}

The query here is an instance of the Model Aggregate Query so you can easily apply whatever filter you would like when fetching posts.

A final Note about Sync

All sync operations first fetch the documents then perform a save or destroy actions, this will allow multiple sync operations to be performed in one query.

For example, if category is updated, find all posts for that category, and update each one of them, this will call all syncs inside the post, in that sense, all comments will be updated as well if they are synced with the posts.

Also, all related events like onSaving, onSaved, onDeleting, onDeleted...etc will be triggered as well.