Homepage

Implementing Business Rules Using Commands

Last edit: Sep 16, 2024

As the next step, we will implement business rules to ensure the form submission meets specific criteria, using the Command approach described in the core module's documentation.

Specifically, we want to:

  • Validate that the email provided by the user looks like a valid email.
  • Ensure that the body is at least 10 characters long and has no more than 200 characters.

Create the folder structure

First, create the following directories and files:


mkdir -p app/lib/commands/contacts/create
  1. In the app directory, create a lib directory.

  2. Within the lib directory, create a commands directory.

  3. Inside the commands directory, create a contacts directory.

  4. Within the contacts directory, create a file named create.liquid.

  5. In the contacts directory, create another directory named create.

  6. Inside the create directory, create the following files:


build.liquid
check.liquid

Your folder structure should look like this:


  app
  └── lib
      └── commands
          └── contacts
              ├── create.liquid
              └── create
                  ├── build.liquid
                  └── check.liquid
  

This folder structure is designed to keep your project organized, easy to maintain, and scalable.

  1. lib Directory: This directory holds all your custom libraries and business logic. By placing your commands here, you keep the core logic separate from the views, making the codebase cleaner and easier to manage.

  2. commands Directory: Commands are specific actions or operations that can be invoked. Having a dedicated commands directory helps in grouping all such operations together, making them easy to find and modify.

  3. contacts Directory: This directory is specifically for commands related to the contacts functionality. Grouping related commands in their own directories prevents clutter and makes it easier to locate the relevant files.

  4. create.liquid File: This file acts as an entry point for the create operation. It coordinates the different steps involved in creating a contact, such as building the contact object, validating it, and executing the creation.

  5. Nested create Directory: Inside the contacts directory, the create subdirectory contains the specific steps (build, check, etc.) involved in the create operation. This separation of steps into individual files keeps each file simpler and easier to test.

By structuring your project this way, each piece of functionality is contained in its own module, promoting reuse and reducing the risk of conflicts. This approach also makes it easier for multiple developers to work on the project simultaneously without interfering with each other.

Using the function tag

To create a functional Contact Us form in platformOS, you’ll use the function tag. The function tag works similarly to functions in traditional programming languages. It allows you to call a partial and store the returned result in a variable, which is essential for processing form data like user contacts.

First, you'll need to use the function tag and specify a variable name to store the result. Here’s the basic syntax:

{% function variable_name = 'path/to/my/partial', argument: value %}

Let’s add a function tag to your app/views/pages/contacts/create.liquid file:

app/views/pages/contacts/create.liquid

---
method: post
---
{% function contact %}

In our example, contact is the variable that will store the result of the function.

To use it, you need to make sure that the partial returns data using the return tag. If you're using an editor that supports Language Server Protocol (LSP), you can explore examples and get more details about the function tag there.


LSP support for function tag

If you previously added Hello {{ context.params.contact }} for demonstration purposes, you can now remove it from the app/views/pages/contacts/create.liquid file. It was just a placeholder to illustrate passing data.

Specify the path to the partial

You can see that LSP complains about invalid syntax for the function tag, and shows you what’s missing:


LSP error for function tag

{% function variable_name = 'path/to/my/partial', argument: value %}

To use a function tag, you need to specify the path to the partial file that contains the code you want to execute. The next step is to provide this path. You can use LSP to autocomplete the path, so you don’t need to type it manually. In our example, the path leads to the app/lib/commands/contacts/create.liquid file:

app/views/pages/contacts/create.liquid

`

---
method: post
---

{% function contact = 'commands/contacts/create' %}

Inside the app/views/pages/contacts/create.liquid file, we have Liquid code that uses the function tag. This tag evaluates the code from app/lib/commands/contacts/create.liquid. This is how you invoke the function.

You don't need to specify the 'lib' directory when using the function tag because it automatically knows where to look for the specified file. This simplifies the code and reduces the need for redundant path specifications.

Provide arguments to the function

To make our Contact Us form work, we want to provide arguments to this function:

app/views/pages/contacts/create.liquid

---
method: post
---

{% function contact = 'commands/contacts/create', object: context.params.contact %}

Here, we pass context.params.contact as the object argument. This object contains the email and body parameters that you will use in the partial. You can choose any name for the function. By default, the partial won't have access to any data unless specified.


LSP feedback indicating unused 'contact' variable

You might notice that LSP complains that the contact variable is never used and we have an unused object argument. This is true for now, and we will address it in later steps.

Understanding Business Logic and Commands

The build/check/execute pattern in platformOS is a structured approach to handle business logic within commands. This method ensures your code is organized, maintainable, and reusable.

  • The build stage prepares the input for the command. It normalizes and processes the data received from the user, ensuring it is in the correct format.

  • The check stage validates the input data. It confirms that all required fields are present, checks for uniqueness, and ensures the data meets specific criteria. If validation fails, it provides detailed error messages.

  • The execute stage performs the main action of the command, such as saving data to the database. This step only occurs if the validation is successful.

By splitting these stages into separate files, you promote clean, modular, and testable code. This approach also makes it easier for multiple developers to work on the project simultaneously.

We recommend placing your commands in lib/commands directory.

The naming conventions that we use are <resource>/<action>, for example, contacts/create.liquid.

Commands are designed to be easily executed as background jobs [heavy commands - external API call, expensive operations computations, reports]. Each command might produce an Event.

Set up a Build Command

As the next step, we want to use a build command in the app/lib/commands/contacts/create/build.liquid file.

The build command is an essential part of the data processing workflow. It allows you to normalize and prepare data before it is stored or used further in your application. By defining a build command, you can manipulate incoming data, perform validations, and ensure the data meets your application's requirements. This is especially useful for tasks like cleaning up user input, setting default values, and enforcing data consistency.

Build commands are defined in Liquid templates and typically involve transforming and returning JSON objects. This process ensures that your data is well-structured and ready for subsequent operations or storage.

To set up a build command, you don’t need to write the code from scratch. Let’s use the dummy build command from the core module documentation.

Here's the dummy build command from the core module documentation:

{% parse_json data %}
  {
    "title": {{ object.title | downcase | json }},
    "uuid": {{ object.uuid | json }},
    "c__score": 0
  }
{% endparse_json %}

{% liquid
  if data['uuid'] == blank
    hash_assign data["uuid"] = '' | uuid | json
  endif

  return data
%}

Copy this code into your app/lib/commands/contacts/create/build.liquid file.

Customize the Build Command

Now, we will modify the build command to suit our needs. In the build phase, you normalize the parameters, ensuring you only take what's relevant and perform any necessary transformations. The goal is to clean up and prepare the contact data (email and body) before further processing.

For example, you want to ensure that the email is always in lowercase:

app/lib/commands/contacts/create/build.liquid

{% parse_json contact %}
  {
    "email": {{ object.email | downcase | json }},
    "body": {{ object.body | json }}
  }
{% endparse_json %}

{% liquid
  return contact
%}

The parse_json contact tag parses the JSON object (email and body) into a variable named contact.

Inside the JSON object, we transform object.email to lowercase and ensure it is properly formatted as JSON.

Every function needs to return an object. In this case, the function takes user parameters, processes them (downcasing the email), and returns an object with the email and body properties.

Invoking the function

To process the contact data using the build command, we need to invoke the function inside the /app/lib/commands/contacts/create.liquid file:

Here's the code snippet for invoking the function:

app/lib/commands/contacts/create.liquid

{% function contact = 'commands/contacts/create/build', object: object %}

This line of code will call the build command, passing the object containing the email and body parameters to it. The build command will then process these parameters and return a normalized contact object.

We used the function tag to specify the path to the build command partial. Remember, you can use LSP to autocomplete paths, so you don’t need to type them manually. In our example, the path leads to the app/lib/commands/contacts/create/build.liquid file.

You also need to pass the object that you defined earlier in your /app/views/pages/contacts/create.liquid file, which is object.

You might notice that LSP complains that the contact variable is never used. This is true for now, and we will address it in later steps.

Using the log tag for debugging

The log tag is a basic and effective way to debug your platformOS application. Here's how to use it in your app/lib/commands/contacts/create.liquid file:

app/lib/commands/contacts/create.liquid

{% function contact = 'commands/contacts/create/build', object: object %}
{% log contact, type: 'result of invoking build' %}

{% return contact %}

contact: this is the variable containing the processed object. It includes the email and body parameters, with email being downcased.

type: 'result of invoking build': this provides a label for the log entry, making it easier to identify in the logs.

Accessing logs

To view your logs, you need to use the pos-cli GUI.

To start the GUI, in your command line, run:


pos-cli gui serve --sync staging

Note

Replace staging with the environment name you want to develop on. To list all available environments use pos-cli env list. Refer to the platformOS documentation for detailed instructions on starting the GUI.

Note
Replace staging with the environment name you want to develop on. To list all available environments use pos-cli env list.
Refer to the platformOS documentation for detailed instructions on starting the GUI.

After starting the GUI, you will see output similar to:


  [07:23:19] Connected to https://contactus.staging.oregon.platform-os.com/
  [07:23:19] Admin: http://localhost:3333
  [07:23:19] ---
  [07:23:19] GraphiQL IDE: http://localhost:3333/gui/graphql
  [07:23:19] Liquid evaluator: http://localhost:3333/gui/liquid
  [07:23:19] Instance Logs: http://localhost:3333/logs
  [07:23:20] [Sync] Synchronizing changes to: https://contactus.staging.oregon.platform-os.com/
  

Open your browser and go to http://localhost:3333/logs to view the logs:


Viewing logs in the local server

Alternatively, you can use the CLI to render the logs by running the following command:


pos-cli logs staging


CLI command to view logs

Example of introducing a syntax error

Using the log tag effectively helps in diagnosing problems and ensuring your application runs smoothly.
If you encounter a syntax error, such as an unnecessary comma, you can debug it by viewing the logs.
For testing purposes, let's add an extra comma after "body" in the app/lib/commands/contacts/create/build.liquid file:

app/lib/commands/contacts/create/build.liquid

{% parse_json contact %}
  {
    "email": {{ object.email | downcase | json }},
    "body": {{ object.body | json }},
  }
{% endparse_json %}

{% liquid
  return contact
%}

Now, with this unnecessary comma added, submit the form with test data.

When you introduce an error like this and then access the logs at http://localhost:3333/logs, you will see a Liquid error indicating the issue. This helps you identify and correct syntax errors.


Liquid error log example

Now, remove the extra comma and continue our task.

app/lib/commands/contacts/create/build.liquid

{% parse_json contact %}
  {
    "email": {{ object.email | downcase | json }},
    "body": {{ object.body | json }}
  }
{% endparse_json %}

{% liquid
  return contact
%}

Questions?

We are always happy to help with any questions you may have.

contact us