This post is based on a discussion on Velo discord about data hooks that I believe is worth sharing
A few days ago, Velo Dev team released a new video about Data hooks. The video is a good introduction to Data hooks and how to use them but I believe it falls short on examples of how to use them properly. But rather than complaint endlessly, I decided to create my own presentation.
Stay here for more examples
The problems
The tutorial uses data hooks to enforce business logic. I firmly believe that business logic and data logic are 2 different concerns and therefore should not be mixed:
Business logic: rules and processes associated with the conduct of business. This logic will likely evolve with the business and change throughout the lifetime of the application to match reality.
Data Logic: rules for storing data in order to ensure its safety and integrity. Those are rules should not be business dependent and therefore should remain unchanged for most of the business's existence.
In Wix's case, business logic should be placed in web modules(.jsw), or public modules(public files), whereas data logic should be enforced with data hooks.
Here are a few reasons why we don't want business logic to be implemented at the database level
we can't bypass logic
If the business logic is enforced via data hooks, we won't be able to adjust the database manually. Even though this should not happen in theory, in reality, bugs, errors, and network issues will corrupt the database. Eventually, we will need to manually correct the database. But since the content manager is subject to webhooks we won't be able to bypass rules in order to fix the issue.
Hard to debug
When implementing business logic, we need to log users' actions in order to debug issues/monitor performance. Data hooks are called independently of any context (page, endpoint, user action) and thus provide limited information about where and why it was executed.
all use cases are mixed together
Because data hooks are triggered on all operations, regardless of their origin, we have to combine logic for all use cases in the same place. The larger the business logic the more complex the hooks become and the harder it's to maintain. It also means that one buggy feature will prevent any operation on the collection to execute.
Complex data hook slows down queries
The more complex is a data hook the more it will increase execution time. This is a normal trade-off for some use cases but it might not be necessary for all scenarios. Since data hooks apply to all queries, they will impact performance for all use cases regardless of it was needed or not.
On top of those main reasons, there are also limitations in regard to reference fields that are hard to manipulate from within hooks.
Authorization and Permission
Even though it's possible to restrict write operation (but not read) via data hooks, we do not want to mix security logic with data logic. Having security embedded in our database means that a security check will be executed on every operation instead of being done once at the beginning of the use case (when calling a function from a web module for instance). Also, a single collection might be accessible to different types of users with different permissions which makes the authorization system event more complex
The Good way
let's see now why and how to use data hook to the best of their ability. Those recommendations are especially important when you start building larger applications on Wix;
To illustrated those examples, we'll have the collection "Candies" that contains a list of products sold by myCandiesFactory.
Read vs Write Hooks
Most of the time, we want to use write hooks rather than read hooks. The main reason is write operations occur way less often than read operations. Secondly, users accept more easily slow writing operations rather than slow read operations.
We rarely use the after query hook because it modifies the Wix data API output (and therefore what we see in the content manager). Data presentation rules and logic should be done by the data consumer (AKA frontend) in order to present the data based on user preference (language, location, use case)
Data Integrity
We use data hooks to enforce simple data integrity for instance
function Candies_BeforeInsert(candy, context) {
checkCandyIntegrity(candy);
return customer;
}
function Candies_BeforeUpdate(candy, context) {
checkCandyIntegrity(candy);
return candy;
}
function checkCandyIntegrity(candy) {
if(!candy.weight) throw new Error("missing weight for candy("+candy._id+")")
if(!candy.brand) throw new Error("missing brand for candy("+candy._id+")")
return candy;
}
Computed Field
Computed fields are fields in which the value is derived from other fields. Those values need to be adjusted anytime the main fields are updated.
function Candies_BeforeInsert(candy, context) {
const updatedCandy = addComputedFields(candy);
return updatedCandy;
}
function Candies_BeforeUpdate(candy, context) {
const updatedCandy = addComputedFields(candy);
return updatedCandy;
}
function addComputedFields(candy) {
return {
...candy,
fullTitle: candy.brand + " - " + candy.title,
isSugerFree: candy.sugar === 0,
version: customer.version ? customer.version + 1 : 0
}
}
Again, we want to stick to simple data logic and avoid enforcing business rules.
Security Concern
Access Control
Something important to keep in mind when we use data hooks for security is that the aggregation operator does not use hooks. This means that protecting data access with hooks won't work. on the other hand, before write hooks can be used to control data editing but as stated at the beginning, we don't want to do that.
Unicity
We cannot use data hook to enforce true unicity of a field throughout a collection. Nothing prevents 2 users to execute the same write query at the same. Both hooks will be executed at the same time therefore not detecting the other attempt to insert the same data.
Advanced use case: Public view of a private collection
Views are collections that copy the content of another one. The purpose of those views is to make some complex queries easier by pre-computing their results and saving them in a collection.
For instance let's say our candies list is viewable only by approved wholesalers but we allow unlogged visitors to see myCandiesFatory's products (title, image, description) but without access to price and ingredients.
In order to let users search our products database, we can have a collection "ProductsPreview" (publicly viewable) that is a view of the "Candies" (private collection). We are using hooks to sync document between the 2 collections:
function Candies_AfterInsert(candy, context) {
return updateProductPreview(candy)
.then(() => candy)
.catch(() => {
console.error("could not update preview")
return candy
}
}
function Candies_AfterUpdate(candy, context) {
return updateProductPreview(candy)
.then(() => candy)
.catch(() => {
console.error("could not update preview")
return candy
}
}
function Candies_AfterRemove(candy, context) {
return removeProductPreview(candy)
.then(() => candy)
.catch(() => {
console.error("could not revome preview")
return candy;
}
}
async function updateProductPreview(candy) {
const preview = {
_id: candy._id, //we use the same id for easier matching
title: candy.fullTitle,
description: candy.description,
image: candy.image,
}
return wixData.save("ProductsPreview", preview);
}
function removeProductPreview(candy) {
return wixData.remove("ProductsPreview", candy._id)
}
We can now use datasets to let visitors access "ProductsPreview" collection without exposing restricted data.
I hope those examples will help harness the full power of Velo Data Hooks.
If you have any questions, tag me in Velo Forum(@kentin) with a link to this article and I'll be glad to help you.
Thanks for the article. This helps explain quite a few quirks of data hooks. One aspect that's not been adequately demonstrated and documented is the use of the context (HookContext). Is this something that can be used to pass it data from the original page/form? It would be great to have some examples that illustrate the usage of HookContext in various situations and unleash the real power of data hooks.
Great article Kentin! well explained. Where should I handle thrown errors from checkCandyIntegrity? functioncheckCandyIntegrity(candy){if(!candy.weight)thrownewError("missing weight for candy("+candy._id+")")if(!candy.brand)thrownewError("missing brand for candy("+candy._id+")")return candy;}