Implementing Business Rules Using Commands
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
-
In the
app
directory, create alib
directory. -
Within the
lib
directory, create a commands directory. -
Inside the commands directory, create a
contacts
directory. -
Within the
contacts
directory, create a file namedcreate.liquid
. -
In the
contacts
directory, create another directory namedcreate
. -
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.
-
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. -
commands
Directory: Commands are specific actions or operations that can be invoked. Having a dedicatedcommands
directory helps in grouping all such operations together, making them easy to find and modify. -
contacts
Directory: This directory is specifically for commands related to thecontacts
functionality. Grouping related commands in their own directories prevents clutter and makes it easier to locate the relevant files. -
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. -
Nested
create
Directory: Inside thecontacts
directory, thecreate
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.
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:
{% 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.
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:
Alternatively, you can use the CLI to render the logs by running the following command:
pos-cli logs staging
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.
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
%}