Syncing Models
Syncing models is a major powerful feature in Monpulse
, 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:
- Single embedded document
- 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:
import { Model, Casts, castModel } from "@mongez/monpulse";
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:
import { Model, Casts } from "@mongez/monpulse";
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:
import { Model, Casts, ModelSync } from "@mongez/monpulse";
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.
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:
import { Model, Casts, castModel } from "@mongez/monpulse";
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:
import { Model, Casts, ModelSync } from "@mongez/monpulse";
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.
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:
import { Model, Casts, ModelSync } from "@mongez/monpulse";
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:
import { Model, Casts, ModelSync } from "@mongez/monpulse";
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.
- Unset the embedded document.
- Delete the document that has the embedded document.
- 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.
import { Model, Casts, ModelSync } from "@mongez/monpulse";
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.
import { Model, Casts, ModelSync } from "@mongez/monpulse";
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.
import { Model, Casts, ModelSync } from "@mongez/monpulse";
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.
import { Model, Casts, ModelSync } from "@mongez/monpulse";
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.
import { Model, Casts, ModelSync } from "@mongez/monpulse";
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.