Don't Forget the Update!: How to Enforce Uniqueness in Wix Collections

Introduction


Wix Collections is an amazing tool for developers - database tables that are flexible, easy to create, and visible through a front-end interface for field creation and content management. However, if you have any experience with other database solutions, like SQL, the term flexible carries a lot of weight here for Wix Collections. Collections hardly enforce any rules out of the box. So, how do you enforce uniqueness on a field in a collection?


The solution for uniqueness lies within the use of Wix-Data Hooks. Hooks present Corvid Developers with the opportunity to run custom logic before or after certain collection operations: insert, update, and query amongst a few others. In this solution, we will be focusing on the beforeInsert() and beforeUpdate() hooks. We will be using these hooks to create our own rules, returning acceptance or rejection based on the criteria we set.


A Case for Uniqueness: The EasyChatAi Api Key


In order to enforce our business model here at EasyChatAi, we assign api keys that are unique to each instance of EasyChatAi. When a business creates a new instance of EasyChatAi to install on a Wix website, they are assigned an api key that gives them access to our service for that specific instance only. However, once we assign a business an api key, we need to make sure that they cannot create a new instance and apply the same api key, or share that api key with another business. EasyChatAi api keys can only be used on one instance at a time - they must be unique in our collection of instances.


Preparation & Implementation


In order to create Wix-Data Hooks, you must have Dev Mode turned on, and you must create a data.js javascript file (Introduction, paragraph 3) in your site backend.


Creating a Generic Function to Check for Duplicates on Insert


When writing code, generic and reusable functions are always better than specific, one-off functions. Here is our function for checking for duplicates on insert. Note: this is a separate function that we will later use within the beforeInsert() hook. It should still go in your data.js file, preferably at the top or somewhere above the beforeInsert() function we will create.

import wixData from 'wix-data';

//Collection: the collection we are checking against
//Field: the field in the collection we are checking against
//Item: the new record we are checking against
export function searchForDuplicatesOnInsert(collection, field, item) {
     let options = {
          "suppressAuth": true,
          "supressHooks": false,
     };
     //Here we check if there is a value. We only want to check           
     //for duplicates if there is a value, not on blank.
     if (item[field]){
          //Now that we know there is a value, query for records
          return wixData.query(collection)
               //We want records that have the same field value
               .eq(field, item[field])
               .find(options)
               .then((results) => {
                    //Return the length so we can check against            
                    //it later
                    return results.items.length;
               })
               .catch((err) => {
                    let errorMsg = err;
               });
      }
      //This clause handles the return if we are checking a blank 
      //value. We return 0 because blank values are okay.
      else{
           //Tip: you have to return a promise that resolves to 0 
           //here; you cannot just return the value 0.
           return Promise.resolve(0);
      }
 }

Calling the Generic Function within beforeInsert()


This is how we put the above function to use. Without calling our generic function in the beforeInsert() hook, no validation will occur.

export function yourCollectionName_beforeInsert(item, context) {    
       return searchForDuplicatesOnInsert(context.collectionName,
              "Name of field to check", item).then((res) => {
                     if(res > 0) {   
                            return Promise.reject('Your custom error message here.');
                     }
                     return item;
       });
}

You can try testing this out by creating a record in your collection, then creating another record with a duplicate field value for the field you are validating.


Great! Looks like we are done.


Not So Fast: There's a Vulnerability You Might Not Have Noticed


After implementing the beforeInsert() validation, it may appear that your job is done. New records that are inserted will be validated within our beforeInsert function, and duplicates will be rejected. Hooray! Not so fast, there's a catch. Only new records are being checked. What happens when we expose these records on our website with a form and allow users to update them? If someone is clever enough, they could simply create a new record that follows our rules, then update that record to a value that breaks our rules without any consequence. We need to create some special validation for beforeUpdate() that is slightly different than our searchForDuplicatesOnInsert().


Creating a Generic Function to Check for Duplicates on Update


The caveat with checking for duplicate records before an update is that the record you are updating actually already exists. We are setting ourselves up for failure here (not really, we can do this!).


The generic function for checking for duplicates before update is nearly the same as the before insert function. However, there's one crucial difference: we need to look for records that don't have the same ID as the one we are updating. This filters out the record we are updating from the query by telling our code that it's okay if a duplicate exists, as long as the duplicate is the within the same record. Check out the code below and try to spot this difference:

export function searchForDuplicatesOnUpdate(collection, field, item) {

           let options = {
                      "suppressAuth": true,
                      "suppressHooks": false
           };
           if (item[field]){
                      return wixData.query(collection)
                                 .eq(field, item[field])
                                 .ne("_id", item["_id"])
                                 .find(options)
                                 .then((results) => {
                                 return results.items.length;
                                 })
                      .catch((err) => {
                                 let errorMsg = err;
                      });
           }
           else{
                      return Promise.resolve(0);
           }
}

We have added an extra query filter: .ne("_id", item["_id"]). If we do not add this extra filter, our search function will fail every single time. We need to make sure that the record being updated is ignored in our search for duplicates. We do this by leveraging the system-generated _id field and taking it into account by using .ne (not equal).


Tip: All system fields begin with an underscore.


Calling the Generic Function within beforeUpdate()


Here's how we put the function to use:

export function yourCollectionName_beforeUpdate(item, context) {    
       return searchForDuplicatesOnUpdate(context.collectionName, "Field Name to Check", item).then((res) => {
              if(res > 0) {   
                     return Promise.reject('Error message.');
              }
              return item;
       });
}

And that's it! We now have proper uniqueness enforcement that considers both the insert and update of records in our collection. Try it out in the content manager or on a new form on your website.


Wrap-up & Takeaways


The most important part about this post to take away is that uniqueness must be enforced on both the insert and update of a record. Failing to consider updated values will open your application up to vulnerabilities that will either A) cause problems with queries where you might only be expecting one record or B) open loopholes into your business model. Secondly, the generic functions that we created today can be used on all respective beforeInsert or beforeUpdate hooks for any collection. Keep in mind, you may have unique cases that need to be covered for different collections or cases (ex. do not allow blank records either). These functions can be altered to your own liking.


Thank you for reading EasyChatAi's first article on Corvid by Wix Development :) If you would like to chat about Corvid, feel free to shoot me an email at stan@xy.consulting; I would be happy to help in any way.




41 views0 comments