Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Wheels uses a Docker-based development environment that provides consistent, containerized development with support for multiple CFML engines and databases.
Docker: Install Docker Desktop from docker.com
Wheels CLI: Install the Wheels CommandBox module:
box install wheels-cliEnsure you are in the application root directory.
Initialize your Docker development environment:
wheels docker init cfengine=adobe cfversion=2018 db=mysqlcfengine options:
lucee - Lucee CFML engine
adobe - Adobe ColdFusion
boxlang - BoxLang CFML engine
cfversion options:
Major versions for Adobe ColdFusion: 2018, 2021, 2023, 2025
Major versions for Lucee: 5, 6, 7
Major versions for BoxLang: 1
db options:
mysql - MySQL database
postgres - PostgreSQL database
mssql - Microsoft SQL Server
h2 - H2 embedded database
The wheels docker init command creates several files in your project:
.dockerignore - Specifies files to exclude from Docker build context
Dockerfile - Container definition for your chosen CFML engine
docker-compose.yml - Multi-container application definition
CFConfig.json - CFML engine configuration with datasource setup
After running the init command, start your containers:
docker-compose up -dThe containers will take a few minutes to start the first time as Docker downloads the necessary images. Once started, your application will be available at:
Default: http://localhost:8080
Custom port: Check your server.json file for the configured port
# Stop the containers
docker-compose down
# View running containers
docker-compose ps
# View container logs
docker-compose logs
# Rebuild and restart
docker-compose up -d --buildThe default port is 8080, but you can customize this by modifying the server.json:
{
"name":"wheels",
"web":{
"host":"localhost",
"http":{
"port":3000
},
"webroot":"public",
"rewrites":{
"enable":true,
"config":"public/urlrewrite.xml"
}
}
}The generated CFConfig.json file automatically configures a datasource for your chosen database. The configuration includes:
Connection settings for your selected database type
Default datasource named wheels-dev
Appropriate drivers for the database engine
Make code changes in your directory
Changes are reflected immediately due to Docker volume mounting
Database changes persist between container restarts
Use standard Wheels commands like migrations, generators, etc.
Containers won't start:
# Check if ports are in use
docker-compose ps
netstat -an | grep 8080
# Force recreate containers
docker-compose down
docker-compose up -d --force-recreateDatabase connection issues:
# Check database container logs
docker-compose logs db
# Restart just the database
docker-compose restart dbPerformance issues:
Ensure Docker Desktop has adequate memory allocated (4GB+ recommended)
On Windows/Mac, enable file sharing for your project directory
Install Wheels and get a local development server running
By far the quickest way to get started with Wheels is via . CommandBox brings a whole host of command line capabilities to the CFML developer. It allows you to write scripts that can be executed at the command line written entirely in CFML. It allows you to start a CFML server from any directory on your machine and wire up the code in that directory as the web root of the server. What's more is, those servers can be either Lucee servers or Adobe ColdFusion servers. You can even specify what version of each server to launch. Lastly, CommandBox is a package manager for CFML. That means you can take some CFML code and package it up into a module, host it on ForgeBox.io, and make it available to other CFML developers. In fact we make extensive use of these capabilities to distribute Wheels plugins and templates. More on that later.
One module that we have created is a module that extends CommandBox itself with commands and features specific to the Wheels framework. The Wheels CLI module for CommandBox is modeled after the Ruby on Rails CLI module and gives similar capabilities to the Wheels developer.
The first step is to get downloaded and running. CommandBox is available for Windows, Mac & Linux, and can be installed manually or using one of the respective package managers for each OS. You can use on Windows, on MacOS, or Yum/Apt on Linux depending on your flavor of Linux. Please follow the instructions on how to install CommandBox on your particular operating system. At the end of the installation process you want to make sure the box command is part of your system path so you can call the command from any directory on your system.
Once installed, you can either double-click on the box executable which opens the CommandBox shell window, or run box from a CMD window in Windows, Terminal window in MacOS, or shell prompt on a Linux server. Sometimes you only want to call a single CommandBox command and don't need to launch a whole CommandBox shell window to do that, for these instances you can call the CommandBox command directly from your default system terminal window by prefixing the command with the box prefix.
So to run the CommandBox version command you could run box version from the shell or you could launch the CommandBox shell and run version inside it.
box version
version
This is a good concept to grasp, cause depending on your workflow, you may find it easier to do one versus the other. Most of the commands you will see in these CLI guides will assume that you are entering the command in the actual CommandBox shell so the box prefix is left off.
Okay, now that we have CommandBox installed, let's add the Wheels CLI module.
install wheels-cli
Installing this module will add a number of commands to your default CommandBox installation. All of these commands are prefixed by the wheels name space. There are commands to create a brand new Wheels application or scaffold out sections of your application. We'll see some of these commands in action momentarily.
To install a new application using version 3.0, we can use the new application wizard and select Bleeding Edge when prompted to select the template to use.
wheels new
Now that we have CommandBox installed and extended it with the Wheels CLI module, let's start our first Wheels app from the command line. We'll look at the simplest method for creating a Wheels app and starting our development server.
wheels generate app myApp server start
A few minutes after submitting the above commands a new browser window should open up and display the default Wheels congratulations screen.
So what just happened? Since we only passed the application name myApp to the wheels generate app command, it used default values for most of its parameters and downloaded our Base template (wheels-base-template) from ForgeBox.io, then downloaded the framework core files (wheels.dev) from ForgeBox.io and placed it in the vendor/wheels directory, then configured the application name and reload password, and started a Lucee server on a random port.
This Getting Started guide has taken you from the very beginning and gotten you to the point where you can go into any empty directory on your local development machine and start a Wheels project by issuing a couple of CLI commands. In later guides we'll explore these options further and see what else the CLI can do for us.
Using Wheels to develop web applications with AJAX features is a breeze. You have several options and tools at your disposal, which we'll cover in this chapter.
Wheels was designed to be as lightweight as possible, so this keeps your options fairly open for developing AJAX features into your application.
While there are several flavors of JavaScript libraries out there with AJAX support, we will be using the jQuery framework in this tutorial. Let's assume that you are fairly familiar with the basics of jQuery and know how to set it up.
For this tutorial, let's create the simplest example of all: a link that will render a message back to the user without refreshing the page.
In this example, we'll wire up some simple JavaScript code that calls a Wheels action asynchronously. All of this will be done with basic jQuery code and built-in Wheels functionality.
First, let's make sure we've got an appropriate route setup. It might be you're still using the default wildcard() route which will create some default GET routes for the controller/action pattern, but we'll add a new route here just for practice. We are going to create a route named sayHello and direct it to the hello action of the say controller. There are two ways you could write this code a long hand method specifying the controller and action separately as well as a short hand method that combines the two into a single parameter.
The longhand way would look like:
mapper()
.get(name="sayHello", controller="say", action="hello")
.end()The shorthand method would look like:
mapper()
.get(name="sayHello", to="say##hello")
.end()You can decide which method you prefer. Both sets of code above are equivalent.
Then, let's create a link to a controller's action in a view file, like so:
<cfoutput>
<!--- View code --->
<h1></h1>
<p></p>
#linkTo(text="Alert me!", route="sayHello", id="alert-button")#
</cfoutput>That piece of code by itself will work just like you expect it to. When you click the link, you will load the hello action inside the say controller.
But let's make it into an asynchronous request. Add this JavaScript (either on the page inside script tags or in a separate .js file included via javaScriptIncludeTag() ):
(function($) {
// Listen to the "click" event of the "alert-button" link and make an AJAX request
$("#alert-button").on("click", function(event) {
$.ajax({
type: "GET",
// References "/say/hello?format=json"
url: $(this).attr("href") + "?format=json",
dataType: "json",
success: function(response) {
$("h1").html(response.message);
$("p").html(response.time);
}
});
event.preventDefault();
});
})(jQuery);With that code, we are listening to the click event of the hyperlink, which will make an asynchronous request to the hello action in the say controller. Additionally, the JavaScript call is passing a URL parameter called format set to json.
Note that the success block inserts keys from the response into the empty h1 and p blocks in the calling view. (You may have been wondering about those when you saw the first example. Mystery solved.)
The last thing that we need to do is implement the say/hello action. Note that the request expects a dataType of JSON. By default, Wheels controllers only generate HTML responses, but there is an easy way to generate JSON instead using Wheels's provides() and renderWith() functions:
component extends="Controller" {
function config() {
provides("html,json");
}
function hello() {
// Prepare the message for the user.
greeting = {};
greeting["message"] = "Hi there";
greeting["time"] = Now();
// Respond to all requests with `renderWith()`.
renderWith(greeting);
}
}In this controller's config() method, we use the provides() function to indicate that we want all actions in the controller to be able to respond with the data in HTML or JSON formats. Note that the client calling the action can request the type by passing a URL parameter named format or by sending the format in the request header.
The call to renderWith() in the hello action takes care of the translation to the requested format. Our JavaScript is requesting JSON, so Wheels will format the greeting struct as JSON automatically and send it back to the client. If the client requested HTML or the default of none, Wheels will process and serve the view template at /app/views/say/hello.cfm. For more information about provides() and renderWith(), reference the chapter on Responding with Multiple Formats.
Lastly, notice the lines where we're setting greeting["message"] and greeting["time"]. Due to the case-insensitive nature of ColdFusion, we recommend setting variables to be consumed by JavaScript using bracket notation like that. If you do not use that notation (i.e., greetings.message and greetings.time instead), your JavaScript will need to reference those keys from the JSON as MESSAGE and TIME (all caps). Unless you like turning caps lock on and off, you can see how that would get annoying after some time.
Assuming you already included jQuery in your application and you followed the code examples above, you now have a simple AJAX-powered web application built on Wheels. After clicking that Alert me! link, your say controller will respond back to you the serialized message via AJAX. jQuery will parse the JSON object and populate the h1 and pwith the appropriate data.
That is it! Hopefully now you have a clearer picture on how to create AJAX-based features for your web applications.
Learn the goals of Wheels as well as web development frameworks in general. Then learn more about some key concepts in Wheels.
This chapter will introduce you to frameworks in general and later specifically to Wheels. We'll help you decide if you even need a framework at all and what common problems a framework tries to solve. If we're able to convince you that using a framework is the right thing for you, then we'll present our goals with creating Wheels and show you some key Wheels concepts.
So let's get started.
Short answer, no. If you don't mind doing the same thing over and over again and are getting paid by the hour to do so, then by all means keep doing that.
Slightly longer answer, no. If you're working on a highly customized project that does not fall within what 9 out of 10 web sites/applications normally do then you likely need a high percentage of custom code, and a framework will not help much.
However, if you're like most of us and have noticed that for every new project you start on--or even every new feature you add to an existing project--you waste a lot of time re-creating the wheel, then you should read on because Wheels may just be the solution for you!
Wheels will make starting a new project or building a new feature quick and painless. You can get straight to solving business problems on day one! To understand how this is achieved, we figured that a little background info on frameworks in general may help you out.
All good frameworks rise from the need to solve real problems in real world situations. Wheels is based heavily on the Rails framework for Ruby and also gets inspiration from Django and, though to a lesser extent, other frameworks in the ColdFusion space (like Fusebox, for example). Over the years the contributors to these frameworks have identified problems and tedious tasks in their own development processes, built a solution for it, and abstracted (made it more generic so it suits any project) the solution into the framework in question. Piggy-backing on what all these great programmers have already created and adding a few nice solutions of our own, Wheels stands on solid ground.
OK, so that was the high level overview of what frameworks are meant to do. But let's get a little more specific.
Most web development frameworks set out to address some or all of these common concerns:
Map incoming requests to the code that handles them.
Separate your business logic from your presentation code.
Let you work at a higher level of abstraction, thus making you work faster.
Give you a good code organization structure to follow.
Encourage clean and pragmatic design.
Simplify saving data to a storage layer.
Like all other good frameworks, Wheels does all this. But there are some subtle differences, and certain things are more important in Wheels than in other frameworks and vice versa. Let's have a look at the specific goals with Wheels so you can see how it relates to the overall goals of frameworks in general.
As we've said before, Wheels is heavily based on Ruby on Rails, but it's not a direct port, and there are some things that have been changed to better fit the CFML language. Here's a brief overview of the goals we're striving for with Wheels (most of these will be covered in greater detail in later chapters):
We strive for simplicity on a lot of different levels in Wheels. We'll gladly trade code beauty in the framework's internal code for simplicity for the developers who will use it. This goal to keep things simple is evident in a lot of different areas in Wheels. Here are some of the most notable ones:
The concept of object oriented programming is very simple and data-centric in Wheels, rather than 100% "pure" at all times.
By default, you'll always get a query result set back when dealing with multiple records in Wheels, simply because that is the way we're all used to outputting data.
Wheels encourages best practices, but it will never give you an error if you go against any of them.
With Wheels, you won't program yourself into a corner. If worse comes to worse, you can always drop right out of the framework and go back to old school code for a while if necessary.
Good old CFML code is used for everything, so there is no need to mess with XML for example.
What this means is that you don't have to be a fantastic programmer to use the framework (although it doesn't hurt). It's enough if you're an average programmer. After using Wheels for a while, you'll probably find that you've become a better programmer though!
If you've ever downloaded a piece of open source software, then you know that most projects lack documentation. Wheels hopes to change that. We're hoping that by putting together complete, up-to-date documentation that this framework will appeal, and be usable, by everyone. Even someone who has little ColdFusion programming background, let alone experience with frameworks.
Besides what is already mentioned above, there are some key concepts in Wheels that makes sense to familiarize yourself with early on. If you don't feel that these concepts are to your liking, feel free to look for a different framework or stick to using no framework at all. Too often programmers choose a framework and spend weeks trying to bend it to do what they want to do rather than follow the framework conventions.
Speaking of conventions, this brings us to the first key concept:
Instead of having to set up tons of configuration variables, Wheels will just assume you want to do things a certain way by using default settings. In fact, you can start programming a Wheels application without setting any configuration variables at all!
If you find yourself constantly fighting the conventions, then that is a hint that you're not yet ready for Wheels or Wheels is not ready for you.
Beautiful (for lack of a better word) code is code that you can scan through and immediately see what it's meant to do. It's code that is never repeated anywhere else. And, most of all, it's code that you'll enjoy writing and will enjoy coming back to 6 months from now.
Sometimes the Wheels structure itself encourages beautiful code (separating business logic from request handling, for example). Sometimes it's just something that comes naturally after reading documentation, viewing other Wheels applications, and talking to other Wheels developers.
If you've investigated frameworks in the past, then you've probably heard this terminology before. Model-View-Controller, or MVC, is a way to structure your code so that it is broken down into three easy-to-manage pieces:
Model: Just another name for the representation of data, usually a database table.
View: What the user or their browser sees and interacts with (a web page in most cases).
Controller: The behind-the-scenes guy that's coordinating everything.
"Uh, yeah. So what's this got to do with anything?" you may ask. MVC is how Wheels structures your code for you. As you start working with Wheels applications, you'll see that most of the code you write (database queries, forms, and data manipulation) are very nicely separated into one of these three categories.
The benefits of MVC are limitless, but one of the major ones is that you almost always know right where to go when something needs to change.
If you've added a column to the vehicles table in your database and need to give the user the ability to edit that field, all you need to change is your View. That's where the form is presented to the user for editing.
If you find yourself constantly getting a list of all the red cars in your inventory, you can add a new method to your model called getRedCars() that does all the work for you. Then when you want that list, just add a call to that method in your controller and you've got 'em!
The Object Relational Mapping, or ORM, in Wheels is perhaps the one thing that could potentially speed up your development the most. An ORM handles mapping objects in memory to how they are stored in the database. It can replace a lot of your query writing with simple methods such as user.save(), blogPost.comments(order="date"), and so on. We'll talk a lot more about the ORM in Wheels in the chapters about models.
So there you have it, a completely fair and unbiased introduction to Wheels. ;)
If you've been developing ColdFusion applications for a while, then we know this all seems hard to believe. But trust us; it works. And if you're new to ColdFusion or even web development in general, then you probably aren't aware of most of the pains that Wheels was meant to alleviate!
That's okay. You're welcome in the Wheels camp just the same.
What you need to know and have installed before you start programming in Wheels.
We can identify 3 different types of requirements that you should be aware of:
Project Requirements. Is Wheels a good fit for your project?
Developer Requirements. Do you have the knowledge and mindset to program effectively in Wheels?
System Requirements. Is your server ready for Wheels?
Before you start learning Wheels and making sure all the necessary software is installed on your computer, you really need to take a moment and think about the project you intend to use Wheels on. Is it a ten page website that won't be updated very often? Is it a space flight simulator program for NASA? Is it something in between?
Most websites are, at their cores, simple data manipulation applications. You fetch a row, make some updates to it, stick it back in the database and so on. This is the "target market" for Wheels--simple CRUD (create, read, update, delete) website applications.
A simple ten page website won't do much data manipulation, so you don't need Wheels for that (or even ColdFusion in some cases). A flight simulator program will do so much more than simple CRUD work, so in that case, Wheels is a poor match for you (and so perhaps, is ColdFusion).
If your website falls somewhere in between these two extreme examples, then read on. If not, go look for another programming language and framework. ;)
Another thing worth noting right off the bat (and one that ties in with the simple CRUD reasoning above) is that Wheels takes a very data-centric approach to the development process. What we mean by that is that it should be possible to visualize and implement the database design early on in the project's life cycle. So, if you're about to embark on a project with an extensive period of object oriented analysis and design which, as a last step almost, looks at how to persist objects, then you should probably also look for another framework.
Still reading?
Good!
Moving on...
Yes, there are actually some things you should familiarize yourself with before starting to use Wheels. Don't worry though. You don't need to be an expert on any on of them. A basic understanding is good enough.
CFML. You should know CFML, the ColdFusion programming language. (Surprise!)
Object Oriented Programming. You should grasp the concept of object oriented programming and how it applies to CFML.
Model-View-Controller. You should know the theory behind the Model-View-Controller development pattern.
Simply the best web development language in the world! The best way to learn it, in our humble opinion, is to get the free developer edition of Adobe ColdFusion, buy Ben Forta's ColdFusion Web Application Construction Kit series, and start coding using your programming editor of choice. Remember it's not just the commercial Adobe offering that's available; Lucee offers an excellent open source alternative. Using CommandBox is a great and simple way to get a local development environment of your choice up and running quickly.
This is a programming methodology that uses constructs called objects to design applications. Objects model real world entities in your application. OOP is based on several techniques including inheritance, modularity, polymorphism, and encapsulation. Most of these techniques are supported in CFML, making it a fairly functional object oriented language. At the most basic level, a .cfc file in CFML is a class, and you create an instance of a class by using the CreateObject function or the <cfobject> tag.
Trying to squeeze an explanation of object oriented programming and how it's used in CFML into a few sentences is impossible, and a detailed overview of it is outside the scope of this chapter. There is lots of high quality information online, so go ahead and Google it.
Model-View-Controller, or MVC for short, is a way to structure your code so that it is broken down into 3 easy-to-manage pieces:
Model. Just another name for the representation of data, usually a database table.
View. What the user sees and interacts with (a web page in our case).
Controller. The behind-the-scenes guy that's coordinating everything.
MVC is how Wheels structures your code for you. As you start working with Wheels applications, you'll see that most of the code you write is very nicely separated into one of these 3 categories.
Wheels requires that you use one of these CFML engines:
Adobe ColdFusion 2018 / 2021 / 2023 / 2025
Lucee 5.2.1.9+ / 6 / 7
BoxLang 1
Your ColdFusion or Lucee engine can be installed on Windows, Mac, UNIX, or Linux—they all work just fine.
You also need a web server. Wheels runs on all popular web servers, including Apache, Microsoft IIS, Jetty, and the JRun or Tomcat web server that ships with Adobe ColdFusion. Some web servers support URL rewriting out of the box, some support the cgi.PATH_INFO variable which is used to achieve partial rewriting, and some don't have support for either. For local development, we strongly encourage the use of CommandBox.
Don't worry though. Wheels will adopt to your setup and run just fine, but the URLs that it creates might differ a bit. You can read more about this in the URL Rewriting chapter.
Finally, to build any kind of meaningful website application, you will likely interact with a database. These are the currently supported databases:
SQL Server 7+
MySQL 5+ *
PostgreSQL 8.4+
H2 1.4+
Oracle 19c+
OK, hopefully this chapter didn't scare you too much. You can move on knowing that you have the basic knowledge needed, the software to run Wheels, and a suitable project to start with.
Run the next pending database migration.
Alias: wheels db up
The dbmigrate up command executes the next pending migration in your database migration queue. This command is used to incrementally apply database changes one migration at a time, allowing for controlled and reversible database schema updates.
None.
This will execute the next migration in the sequence and update the database schema version.
When you want to apply database changes one at a time rather than all at once:
Apply migrations one at a time for better control:
Migrations are executed in chronological order based on their timestamps
Each migration is tracked in the database to prevent duplicate execution
If already at latest version, displays: "We're all up to date already!"
If no more versions available, displays: "No more versions to go to?"
Automatically runs dbmigrate info after successful migration
Always backup your database before running migrations in production
- Rollback the last migration
- Run all pending migrations
- View migration status
- Reset all migrations
⚠️ DEPRECATED: This command has been deprecated. Please use wheels security scan instead.
The analyze security command has been moved to provide better organization and expanded functionality.
Better command organization with dedicated security namespace
Enhanced scanning capabilities
Improved reporting options
Integration with security vulnerability databases
- The replacement command with enhanced features
Deprecated: v1.5.0
Warning Added: v1.6.0
Removal Planned: v2.0.0
The command currently redirects to wheels security scan with a deprecation warning.
Display CLI and Wheels framework version information.
The wheels info command displays information about the Wheels CLI module and identifies the Wheels framework version in the current directory.
This command has no arguments.
The command displays:
Wheels ASCII Art - A colorful banner
Current Working Directory - Where you're running the command from
CommandBox Module Root - Where the CLI module is installed
Current Wheels Version - The detected Wheels framework version in this directory
Verify CLI installation location
Check Wheels framework version in current directory
Troubleshoot path issues
Quick visual confirmation of Wheels environment
The Wheels version is detected by looking for box.json files in the vendor/wheels directory
If no Wheels version is found, it will show "Not Found"
The colorful ASCII art helps quickly identify you're using Wheels CLI
- Initialize a Wheels application
- Manage dependencies
wheels dbmigrate upwheels dbmigrate up# Check pending migrations
wheels dbmigrate info
# Apply next migration
wheels dbmigrate up
# Verify the change
wheels dbmigrate info# Check current status
wheels dbmigrate info
# Apply next migration
wheels dbmigrate up
# Verify the change was applied
wheels dbmigrate infowheels analyze securitywheels security scan [path] [--fix] [--output=<format>] [--detailed]wheels info,--. ,--.,--. ,--. ,-----.,--. ,--.
| | | || ,---. ,---. ,---. | | ,---. ' .--./| | | |
| |.'.| || .-. || .-. :| .-. :| |( .-' | | | | | |
| ,'. || | | |\ --.\ --.| |.-' `) ' '--'\| '--.| |
'--' '--'`--' `--' `----' `----'`--'`----' `-----'`-----'`--'
============================ Wheels CLI ============================
Current Working Directory: /Users/username/myapp
CommandBox Module Root: /Users/username/.CommandBox/cfml/modules/wheels-cli/
Current Wheels Version in this directory: 3.0.0-SNAPSHOT
====================================================================Tutorials, demonstrations, and presentations about the ColdFusion on Wheels framework.
Create a basic CRUD interface in Wheels 2.x https://youtu.be/K5HLItTru1g
Create a basic JSON API in Wheels 2.x https://youtu.be/qZr5JzO0vo4
Routing in Wheels 2.x - Part 1 https://youtu.be/BnPGApAvMVQ
Routing in Wheels 2.x - Part 2 https://youtu.be/0CiGxJyJEIQ
Introduction to Unit Testing in Wheels 2.x https://youtu.be/XgMuzzmBQ98
Unit Testing Controllers in Wheels 2.x https://youtu.be/cygj9WDqHjY
Please note that all the webcasts below were created with Wheels 1.x in mind, and are listed here as they might still be useful to those starting out.
Episode 1: "C" Is for "Create" - Basic CRUD Learn about basic create operations when building standard CRUD functionality in Wheels
Episode 2: "R"; Is for "Read" - Basic CRUD Learn about basic read operations when building standard CRUD functionality in Wheels
Episode 3: "U" Is for "Update" - Basic CRUD Chris Peters demonstrates updating data in a simple CRUD Wheels application
Episode 4: "D" Is for Delete - Basic CRUD Learn how simple it is to delete records in a basic CRUD application using Wheels
Episode 1: Setting up ColdFusion on Wheels Chris Peters starts the webcast series by demonstrating how to set up ColdFusion on Wheels;
Episode 2: Form Helpers Chris Peters demonstrates how to bind a Wheels model object to a form through the use of form helpers
Episode 3: Object Validation and Showing Errors
Chris Peters adds data validation to the user registration form
Episode 4: Redirects and the Flash Chris Peters finishes the "success" portion of the registration functionality by adding a success message to the Flash and redirecting the user to their home screen
Episode 5: Object Validation Chris Peters teaches you about more validation options and how you can add them to the registration form quickly and easily
Episode 6: Styling Forms Chris Peters stylizes form markup globally using a Wheels feature called global helpers
Episode 7: Authentication with Filters Learn how to set up simple user authentication on a website by using a Wheels feature called filters
Episode 8: Reading and Displaying a Single Record Learn the mechanics of reading a single record from the database and displaying its data in the view
Episode 9: Adding a Route for User Profiles Creating custom URL patterns is a breeze in ColdFusion on Wheels
Episode 10: Displaying Sets of Records Learn how to fetch multiple records from your model with findAll() and then display them to the user using ColdFusion on Wheels
Episode 11: Custom View Helpers Learn how to factor out logic in your view templates into custom helper functions in ColdFusion on Wheels
Episode 12: Joining Models with Associations Chris Peters demonstrates joining data together with model associations using ColdFusion on Wheels
Episode 13: Pagination All it takes to offer pagination is two extra arguments to findAll() and a call to a view helper called paginationLinks()
Episode 14: Responding with Multiple Formats Learn how to use the provides() and renderWith() functions to automatically serialize data into XML, JSON, and more
Hello World Peter Amiri walks you through setting up a "Hello World" application using the ColdFusion on Wheels framework;
CFUnited 2010: Simplifying Database Code with the ColdFusion on Wheels ORM Chris Peters gives a high level overview of the ORM included with ColdFusion on Wheels
ColdRoute Plugin Chris Peters from Liquifusion demonstrates the ColdRoute plugin for Wheels
Wirebox Plugin for Wheels Doug Boude demonstrates using his new Wirebox plugin for Wheels
Database Migrations Chris Peters from Liquifusion demonstrates creating tables and records using database migrations in ColdFusion on Wheels
CF Meetup, March 10 2011 Online ColdFusion Meetup (coldfusionmeetup.com) session for March 10 2011, "What's New in Wheels 1.1", with Chris Peters:
Wheels Textmate Bundle Demo A quick demo of the Wheels Textmate bundle by Russ Johnson
Instructions for installing Wheels on your system.
Installing Wheels is so simple that there is barely a need for a chapter devoted to it. But we figured we'd better make one anyway in case anyone is specifically looking for a chapter about installation.
So, here are the simple steps you need to follow to get rolling on Wheels...
You have 2 choices when downloading Wheels. You can either use the latest official release of Wheels, or you can take a walk on the wild side and go with the latest committed source code in our Git repository.
The latest official releases can always be found in the Releases section of GitHub, and the Git repository is available at our GitHub repo.
In most cases, we recommend going with the official release because it's well documented and has been through a lot of bug testing. Only if you're in desperate need of a feature that has not been released yet would we advise you to go with the version stored in the Git master branch.
Let's assume you have downloaded the latest official release. (Really, you should go with this option.) You now have a .zip file saved somewhere on your computer. On to the next step...
Getting an empty website running with Wheels installed is an easy process if you already know your way around IIS or Apache. Basically, you need to create a new website in your web server of choice and unzip the contents of the file into the root of it.
In case you're not sure, here are the instructions for setting up an empty Wheels site that can be accessed when typing localhost in your browser. The instructions refer to a system running Windows Server 2003 and IIS, but you should be able to follow along and apply the instructions with minor modifications to your system. (See Requirements for a list of tested systems).
Create a new folder under your web root (usually C:\Inetpub\wwwroot) named wheels_site and unzip the Wheels .zip file into the root of it.
Create a new website using IIS called Wheels Site with localhost as the host header name and C:\Inetpub\wwwroot\mysite as the path to your home directory.
If you want to run a Wheels-powered application from a subfolder in an existing website, this is entirely possible, but you may need to get a little creative with your URL rewrite rules if you want to get pretty URLs--it will only work out of the box on recent versions of Apache. (Read more about this in the URL Rewriting chapter.)
Create a new database in MySQL, PostgreSQL, Microsoft SQL Server, Oracle or H2 and add a new data source for it in the ColdFusion/Lucee Administrator, just as you'd normally do. Now open up /config/settings.cfm and call set(dataSourceName="") with the name you chose for the data source.
If you don't want to be bothered by opening up a Wheels configuration file at all, there is a nice convention you can follow for the naming. Just name your data source with the same name as the folder you are running your website from (mysite in the example above), and Wheels will use that when you haven't set the dataSourceName setting using the Set() function.
When you've followed the steps above, you can test your installation by typing http://localhost/ (or whatever you set as the host header name) in your web browser. You should get a "Congratulations!" page.
That's it. You're done. This is where the fun begins!
In this tutorial, we'll be writing a simple application to make sure we have Wheels installed properly and that everything is working as it should.
Let's make sure we're all on the same page. I'm going to assume that you've followed the Getting Started guide and have CommandBox all setup. If you haven't done that, stop and read that guide get everything setup. It's okay, this web page will wait for you.
Okay, so you have Wheels installed and can see the Wheels "Congratulations!" page as shown below. That wasn't that hard now, was it?
Okay, let's get to some example code. We know that you've been dying to get your hands on some code!
To continue with Programming Tutorial Tradition, we'll create the ubiquitous Hello World! application. But to keep things interesting, let's add a little Wheels magic along the way.
Let's create a controller from scratch to illustrate how easy it is to set up a controller and plug it into the Wheels framework.
First, create a file called Say.cfc in the /app/controllers directory and add the code below to the file.
component extends="Controller"{
}Congratulations, you just created your first Wheels controller! What does this controller do, you might ask? Well, to be honest, not much. It has no methods defined, so it doesn't add any new functionality to our application. But because it extends the base Controller component, it inherits quite a bit of powerful functionality and is now tied into our Wheels application.
So what happens if we try to call our new controller right now? Lets take a look. Open your browser and point your browser to the new controller. Because my local server is installed on port 60000, my URL is http://127.0.0.1:60000/say. You may need to enter a different URL, depending on how your web server is configured. In my case, I'm using CommandBox.
The error says "Could not find the view page for the 'index' action in the 'say' controller." Where did "index" come from? The URL we typed in only specified a controller name but no action. When an action is not specified in the URL, Wheels assumes that we want the default action. Out of the box, the default action in Wheels is set to index. So in our example, Wheels tried to find the index action within the say controller, and it threw an error because it couldn't find its view page.
But let's jump ahead. Now that we have the controller created, let's add an action to it called hello. Change your say controller so it looks like the code block below:
component extends="Controller" {
function hello() {
}
}As you can see, we created an empty method named hello.
Now let's call our new action in the browser and see what we get. To call the hello action, we simply add /hello to the end of the previous URL that we used to call our say controller:
http://127.0.0.1:60000/say/hello
Once again, we get a ColdFusion error. Although we have created the controller and added the hello action to it, we haven't created the view.
By default, when an action is called, Wheels will look for a view file with the same name as the action. It then hands off the processing to the view to display the user interface. In our case, Wheels tried to find a view file for our say/hello action and couldn't find one.
Let's remedy the situation and create a view file. View files are simple CFML pages that handle the output of our application. In most cases, views will return HTML code to the browser. By default, the view files will have the same name as our controller actions and will be grouped into a directory under the view directory. This new directory will have the same name as our controller.
Find the views directory inside the app directory, located at / in your Wheels installation. There will be a few directories in there already. For now, we need to create a new directory in the views directory called say. This is the same name as the controller that we created above.
Now inside the say directory, create a file called hello.cfm. In the hello.cfm file, add the following line of code:
<h1>Hello World!</h1>Save your hello.cfm file, and let's call our say/hello action once again. You have your first working Wheels page if your browser looks like Figure 3 below.
You have just created your first functional Wheels page, albeit it is a very simple one. Pat yourself on the back, go grab a snack, and when you're ready, let's go on and extend the functionality of our Hello World! application a little more.
We will add some simple dynamic content to our hello action and add a second action to the application. We'll then use some Wheels code to tie the 2 actions together. Let's get get to it!
The first thing we are going to do is to add some dynamic content to our say/hello action. Modify your say controller so it looks like the code block below:
component extends="Controller" {
function hello() {
time = Now();
}
}All we are doing here is creating a variable called time and setting its value to the current server time using the basic ColdFusion Now() function. When we do this, the variable becomes immediately available to our view code.
Why not just set up this value directly in the view? If you think about it, maybe the logic behind the value of time may eventually change. What if eventually we want to display its value based on the user's time zone? What if later we decide to pull it from a web service instead? Remember, the controller is supposed to coordinate all of the data and business logic, not the view.
Next, we will modify our say/hello.cfm view file so that it looks like the code block below. When we do this, the value will be displayed in the browser.
<h1>Hello World!</h1>
<p>Current time: <cfoutput>#time#</cfoutput></p>call your say/hello action again in your browser. Your browser should look like Figure 4 below.
This simple example showed that any dynamic content created in a controller action is available to the corresponding view file. In our application, we created a time variable in the say/hello controller action and display that variable in our say/hello.cfm view file.
Now we will expand the functionality of our application once again by adding a second action to our say controller. If you feel adventurous, go ahead and add a goodbye action to the say controller on your own, then create a goodbye.cfm view file that displays a "Goodbye" message to the user. If you're not feeling that adventurous, we'll quickly go step by step.
First, modify the the say controller file so that it looks like the code block below.
component extends="Controller" {
function hello() {
time = Now();
}
function goodbye() {
}
}Now go to the /app/views/say directory and create a goodbye.cfm page.
Add the following code to the goodbye.cfm page and save it.
Goodbye World!If we did everything right, we should be able to call the new say/goodbye action using the following URL:
http://127.0.0.1:60000/say/goodbye
Your browser should look like Figure 5 below:
Now let's link our two actions together. We will do this by adding a link to the bottom of each page so that it calls the other page.
Open the say/hello.cfm view file. We are going to add a line of code to the end of this file so our say/hello.cfm view file looks like the code block below:
<h1>Hello World!</h1>
<p>Current time: <cfoutput>#time#</cfoutput></p>
<p>Time to say <cfoutput>#linkTo(text="goodbye", action="goodbye")#?</cfoutput></p>The linkTo() function is a built-in Wheels function. In this case, we are passing 2 named parameters to it. The first parameter, text, is the text that will be displayed in the hyperlink. The second parameter, action, defines the action to point the link to. By using this built-in function, your application's main URL may change, and even controllers and actions may get shifted around, but you won't suffer from the dreaded dead link. Wheels will always create a valid link for you as long as you configure it correctly when you make infrastructure changes to your application.
Once you have added the additional line of code to the end of the say/hello.cfm view file, save your file and call the say/hello action from your browser. Your browser should look like Figure 6 below.
You can see that Wheels created a link for us and added an appropriate URL for the say/goodbye action to the link.
Let's complete our little app and add a corresponding link to the bottom of our say/goodbye.cfm view page.
Open your say/goodbye.cfm view page and modify it so it looks like the code block below.
<h1>Goodbye World!</h1>
<p>Time to say <cfoutput>#linkTo(text="hello", action="hello")#?</cfoutput></p>If you now call the say/goodbye action in your browser, your browser should look like Figure 7 below.
You now know enough to be dangerous with Wheels. Look out! But there are many more powerful features to cover. You may have noticed that we haven't even talked about the M in MVC.
No worries. We will get there. And we think you will enjoy it.
Bootstrap an existing Wheels application for CLI usage by creating necessary configuration files.
Bootstrap an existing Wheels application for CLI usage.
The wheels init command initializes an existing Wheels application to work with the Wheels CLI. It's an interactive command that helps set up necessary configuration files (box.json and server.json) for an existing Wheels installation.
This command has no arguments - it runs interactively and prompts for required information.
When you run wheels init, you'll be prompted for:
Confirmation - Confirm you want to proceed with initialization
Application Name - Used to make server.json server name unique (if box.json doesn't exist)
CF Engine - Default CFML engine (e.g., lucee5, adobe2021) (if server.json doesn't exist)
Example interaction:
Creates vendor/wheels/box.json - Tracks the Wheels framework version
Creates server.json - Configures CommandBox server settings with:
Unique server name based on application name
Selected CF engine
Default port and settings
Creates box.json - Main project configuration file with:
Application name
Wheels version dependency
Project metadata
Before running wheels init:
Have an existing Wheels application
Database/datasource already configured
Reload password already set in your application settings
Run this command in the root directory of your Wheels application
Files are only created if they don't already exist
The command detects your current Wheels version automatically
Special characters are stripped from application names
- Create a new Wheels application
- Reload the application
- Display version information
Rollback the last executed database migration.
Alias: wheels db down
The dbmigrate down command reverses the last executed migration by running its down() method. This is useful for undoing database changes when issues are discovered or when you need to modify a migration. The command ensures safe rollback of schema changes while maintaining database integrity.
None.
This will execute the down() method of the most recently applied migration, reverting the database changes.
When a migration contains errors or needs modification:
During development when refining migrations:
When a migration causes issues:
Rolling back migrations that drop columns or tables will result in data loss. Always ensure you have backups before rolling back destructive migrations.
For a migration to be rolled back, it must have a properly implemented down() method that reverses the changes made in the up() method.
Be cautious when rolling back migrations that other migrations depend on. This can break the migration chain.
Always implement down() methods: Even if you think you'll never need to rollback
Test rollbacks: In development, always test that your down() method works correctly
Backup before rollback: Especially in production environments
Document destructive operations: Clearly indicate when rollbacks will cause data loss
Only the last executed migration can be rolled back with this command
To rollback multiple migrations, run the command multiple times
If already at version 0, displays: "We're already on zero! No migrations to go to"
Automatically runs dbmigrate info after successful rollback
The migration version is removed from the database tracking table upon successful rollback
Some operations (like dropping columns with data) cannot be fully reversed
When migrating to version 0, displays: "Database should now be empty."
- Run the next migration
- Reset all migrations
- View migration status
- Run a specific migration
Lists installed Wheels plugins from the /plugins folder or shows available plugins from ForgeBox.
The plugins list command displays information about Wheels plugins. By default, it shows plugins installed locally in the /plugins folder. With the --available flag, it queries ForgeBox to show all available cfwheels-plugins packages.
When listing installed plugins, the command displays:
Plugin name
Version number
Description (if available)
When using --available, the command shows all cfwheels-plugins type packages from ForgeBox.
Output:
Output:
Output:
Output:
Local Plugin Detection: Scans the /plugins folder for subdirectories
Metadata Extraction: Reads each plugin's box.json file for name, version, slug, and description
Dynamic Formatting: Calculates column widths based on content for clean alignment
ForgeBox Integration: Uses forgebox show type=cfwheels-plugins for available plugins
Only lists plugins from the /plugins folder (not box.json dependencies)
Only works with cfwheels-plugins type packages
Plugins without a valid box.json are ignored
Column widths adjust dynamically for optimal display
JSON output includes plugin count for programmatic use
Use wheels plugin info <name> to see detailed information about a specific plugin
Serves generated documentation locally for development and review.
The docs serve command starts a local web server to preview your generated documentation.
If documentation is not found:
With --open=true (default), the server automatically opens your default browser to the documentation URL.
Server is intended for development/review only
For production, deploy static files to web server
Large documentation sets may take time to generate
Offline mode caches documentation locally
wheels plugins list [--format=<format>] [--available]format
No
string
table, json
table
Output format for the plugin list
available
No
boolean
true, false
false
Show available plugins from ForgeBox
wheels plugins list===========================================================
Installed Wheels Plugins (3)
===========================================================
Plugin Name Version Description
---------------------------------------------------------------
bcrypt 0.0.4 Bcrypt encryption for Wheels
shortcodes 0.0.4 Shortcode support
wheels-test 1.0.0 Testing utilities
-----------------------------------------------------------
[OK] 3 plugins installed
Commands:
wheels plugin info <name> View plugin details
wheels plugin update:all Update all plugins
wheels plugin outdated Check for updateswheels plugins list===========================================================
Installed Wheels Plugins
===========================================================
No plugins installed in /plugins folder
Install plugins with:
wheels plugin install <plugin-name>
See available plugins:
wheels plugin list --availablewheels plugins list --format=json{
"plugins": [
{
"name": "bcrypt",
"slug": "cfwheels-bcrypt",
"version": "0.0.4",
"description": "Bcrypt encryption for Wheels"
},
{
"name": "shortcodes",
"slug": "cfwheels-shortcodes",
"version": "0.0.4",
"description": "Shortcode support"
}
],
"count": 2
}wheels plugins list --available===========================================================
Available Wheels Plugins on ForgeBox
===========================================================
[Lists all cfwheels-plugins type packages from ForgeBox using 'forgebox show']wheels docs serve [--root=<dir>] [--port=<port>] [--open]--root
Root directory to serve
docs/api
--port
Port to serve on
35729
--open
Open browser automatically
true
wheels docs servewheels docs serve --port=8080wheels docs serve --root=public/api-docswheels docs serve --open=falsewheels docs serve --root=docs/generated --port=3000Output directory: D:\Command Box\wheels\templates\base\src\docs\api\
CommandBox:src> wheels docs serve
√ | Starting Server
| √ | Setting site [wheels-docs-BBAA12EF-7A83-4D03-BD6DBFE4AC17C1F9] Profile to [development]
| √ | Loading CFConfig into server
Status: starting
Server is still starting... waiting...
Server is up and running!
Starting documentation server...
Documentation server started!
Serving: D:\Command Box\wheels\templates\base\src\docs\api\
URL: http://localhost:35729
Opening browser...
Press Ctrl+C to stop the serverDocumentation directory not found: /docs/api
💡 Tip: Run 'wheels docs generate' first to create documentation# Step 1: Generate documentation
wheels docs generate
# Step 2: Serve documentation
wheels docs serve
# Step 3: Make changes and regenerate
wheels docs generate
# Browser will show updated docs# Generate and serve from custom location
wheels docs generate --output=public/docs
wheels docs serve --root=public/docs --port=8080# Use a different port
wheels docs serve --port=8081# Make sure to generate docs first
wheels docs generate
wheels docs serve# Manually navigate to the URL shown
# Or check your default browser settingswheels initwheels init==================================== Wheels init ===================================
This function will attempt to add a few things
to an EXISTING Wheels installation to help
the CLI interact.
We're going to assume the following:
- you've already setup a local datasource/database
- you've already set a reload password
We're going to try and do the following:
- create a box.json to help keep track of the wheels version
- create a server.json
====================================================================================
Sound ok? [y/n] y
Please enter an application name: myapp
Please enter a default cfengine: lucee5{
"name": "myapp",
"web": {
"http": {
"port": 60000
}
},
"app": {
"cfengine": "lucee5"
}
}{
"name": "myapp",
"version": "1.0.0",
"dependencies": {
"wheels": "^2.5.0"
}
}wheels dbmigrate downwheels dbmigrate down# Run the migration
wheels dbmigrate up
# Discover an issue
# Rollback the migration
wheels dbmigrate down
# Edit the migration file
# Re-run the migration
wheels dbmigrate up# Apply migration
wheels dbmigrate up
# Test the changes
# Need to modify? Rollback
wheels dbmigrate down
# Make changes to migration
# Apply again
wheels dbmigrate up# Check current migration status
wheels dbmigrate info
# Rollback the problematic migration
wheels dbmigrate down
# Verify rollback
wheels dbmigrate infoReset all database migrations by migrating to version 0.
wheels dbmigrate resetAlias: wheels db reset
The dbmigrate reset command resets your database by migrating to version 0, effectively rolling back all executed migrations. This is useful during development when you need to start fresh.
None.
wheels dbmigrate resetThis will migrate the database to version 0, rolling back all migrations.
Start with a clean slate during development:
# Reset all migrations
wheels dbmigrate reset
# Re-run all migrations
wheels dbmigrate latest
# Seed with test data
wheels db seedVerify that all migrations run correctly from scratch:
# Reset all migrations
wheels dbmigrate reset
# Run migrations one by one to test
wheels dbmigrate up
wheels dbmigrate up
# ... continue as neededWhen migrations have dependency problems:
# Reset all migrations
wheels dbmigrate reset
# Manually fix migration files
# Re-run all migrations
wheels dbmigrate latestReset database for each test run:
# CI script
wheels dbmigrate reset
wheels dbmigrate latest
wheels test runWARNING: This command will result in complete data loss as it rolls back all migrations. Always ensure you have proper backups before running this command, especially in production environments.
Using this command in production is strongly discouraged. If you must use it in production:
Take a complete database backup
Put the application in maintenance mode
Have a rollback plan ready
The reset process rolls back migrations in reverse chronological order. Ensure all your down() methods are properly implemented.
Development Only: Primarily use this command in development environments
Backup First: Always backup your database before resetting
Test Down Methods: Ensure all migrations have working down() methods
Document Usage: If used in production, document when and why
Displays "Resetting Database Schema"
Executes dbmigrate exec version=0
Automatically runs dbmigrate info to show the reset status
The command will fail if any migration's down() method fails
Migration files must still exist for rollback to work
The migration tracking table itself is preserved
Use wheels dbmigrate info after reset to verify status
wheels dbmigrate up - Run the next migration
wheels dbmigrate down - Rollback last migration
wheels dbmigrate latest - Run all pending migrations
wheels dbmigrate info - View migration status
wheels db seed - Seed the database with data
Reload the Wheels application in different modes.
wheels reload [options]
wheels r [options]The wheels reload command reloads your Wheels application, clearing caches and reinitializing the framework. This is useful during development when you've made changes to configuration, routes, or framework settings. Note: the server must be running for this command to work.
mode
Reload mode: development, testing, maintenance, production
development
password
Required - The reload password configured in your application
None
wheels reload password=mypasswordEnables debugging
Shows detailed error messages
Disables caching
Ideal for active development
wheels reload mode=testing password=mypasswordOptimized for running tests
Consistent environment
Predictable caching
wheels reload mode=maintenance password=mypasswordShows maintenance page to users
Allows admin access
Useful for deployments
wheels reload mode=production password=mypasswordFull caching enabled
Minimal error information
Optimized performance
wheels reload password=wheelswheels reload mode=production password=mySecretPasswordwheels r password=wheelswheels reload mode=testing password=wheelsThe reload password must match the one configured in your Wheels application
Password is sent via URL parameter to the running application
Always use a strong password in production environments
Set the reload password in your Wheels settings.cfm:
set(reloadPassword="mySecretPassword");Reload clears all application caches
Session data may be lost during reload
Database connections are refreshed
All singletons are recreated
The server must be running for this command to work
Invalid password: Check password in settings.cfm
Server not running: Start server with box server start
Connection refused: Ensure server is accessible on expected port
Timeout: Large applications may take time to reload
wheels init - Initialize application configuration=
wheels info - Display application information
Execute a specific database migration by version number.
wheels dbmigrate exec version=<version>Alias: wheels db exec
The dbmigrate exec command allows you to migrate to a specific version identified by its version number, regardless of the current migration state. This is useful for moving to any specific point in your migration history.
version
string
Yes
Version to migrate to
wheels dbmigrate exec version=20240115123456wheels dbmigrate exec version=0Move to any point in migration history:
# Check current status
wheels dbmigrate info
# Migrate to specific version
wheels dbmigrate exec version=20240115123456Move to an earlier migration state:
# Check migration history
wheels dbmigrate info
# Go back to specific version
wheels dbmigrate exec version=20240101000000Clear all migrations:
# Migrate to version 0
wheels dbmigrate exec version=0
# Verify empty state
wheels dbmigrate infoExecuting migrations out of order can cause issues if migrations have dependencies. Always ensure that any required preceding migrations have been run.
The command updates the migration tracking table to reflect the execution status.
Check Dependencies: Ensure required migrations are already applied
Test First: Run in development/testing before production
Use Sparingly: Prefer normal migration flow with up/latest
Document Usage: Record when and why specific executions were done
Verify State: Check migration status before and after execution
Migration versions are typically timestamps in the format:
YYYYMMDDHHmmss (e.g., 20240115123456)
Year: 2024
Month: 01
Day: 15
Hour: 12
Minute: 34
Second: 56
The command will migrate UP or DOWN to reach the specified version
Version must be a valid migration version or 0 to reset all
The migration file must exist in the migrations directory
The command displays the migration progress message
Both up() and down() methods should be defined in the migration
wheels dbmigrate up - Run the next migration
wheels dbmigrate down - Rollback last migration
wheels dbmigrate latest - Run all pending migrations
wheels dbmigrate info - View migration status
wheels dbmigrate create blank - Create a new migration
Removes an installed Wheels CLI plugin.
wheels plugins remove <name> [--force]This command supports multiple parameter formats:
Positional parameters: wheels plugins remove cfwheels-bcrypt (plugin name)
Named parameters: name=value (e.g., name=cfwheels-bcrypt)
Flag parameters: --flag equals flag=true (e.g., --force equals force=true)
Parameter Mixing Rules:
✅ ALLOWED:
Positional: wheels plugins remove cfwheels-bcrypt
Positional + flags: wheels plugins remove cfwheels-bcrypt --force
All named: name=cfwheels-bcrypt force=true
❌ NOT ALLOWED:
Positional + named for same param: wheels plugins remove cfwheels-bcrypt name=other
Recommendation: Use positional for plugin name, flags for options: wheels plugins remove cfwheels-bcrypt --force
name
Yes
string
Plugin name to remove
--force
No
boolean
Force removal without confirmation
The plugins remove command safely uninstalls a plugin from your Wheels application. It:
Checks if the plugin is installed
Prompts for confirmation (unless --force is used)
Removes plugin from box.json
Cleans up plugin files
Updates plugin registry
wheels plugins remove wheels-vue-cliwheels plugins remove wheels-testing --forceInstallation Check: Verifies the plugin is installed in box.json or plugins folder
Confirmation: Prompts user to confirm removal (unless --force is used)
Removal: Removes plugin entry from box.json
File Cleanup: Deletes plugin files via CommandBox package service
Verification: Confirms successful removal
Are you sure you want to remove the plugin 'wheels-vue-cli'? (y/n): y
[*] Removing plugin: wheels-vue-cli...
[OK] Plugin removed successfully
Run 'wheels plugins list' to see remaining plugins[*] Removing plugin: wheels-vue-cli...
[OK] Plugin removed successfully
Run 'wheels plugins list' to see remaining pluginsAre you sure you want to remove the plugin 'bcrypt'? (y/n): y
[*] Removing plugin: bcrypt...
[ERROR] Failed to remove plugin: Plugin 'bcrypt' is not installedAre you sure you want to remove the plugin 'wheels-vue-cli'? (y/n): n
Plugin removal cancelled.The --force flag skips the confirmation prompt
Use wheels plugins list to verify removal
The command checks if plugin is actually installed before removal
Plugin must exist in box.json dependencies, devDependencies, or plugins folder
Restart your application after removing plugins that affect core functionality
Display database migration status and information.
Alias: wheels db info
The wheels dbmigrate info command shows the current state of database migrations, including which migrations have been run, which are pending, and the current database version.
None.
The command displays:
Datasource: The database connection being used
Database Type: The type of database (MySQL, PostgreSQL, H2, MSSQL(SQL Server), Oracle.)
Total Migrations: Count of all migration files found
Available Migrations: Number of pending migrations
Current Version: The latest migration that has been run
Latest Version: The newest migration available
Migration List: All migrations with their status (migrated or pending)
Migrations are stored in /app/migrator/migrations/ and follow the naming convention:
Example:
Version numbers are timestamps in format: YYYYMMDDHHmmss
Higher numbers are newer migrations
Migrations run in chronological order
Migration status is tracked in c_o_r_e_migrator_versions table:
Check before deployment
Verify after migration
Troubleshoot issues
See which migrations have run
Identify pending migrations
Confirm database version
Check file is in /app/migrator/migrations/
Verify .cfc extension
Ensure proper timestamp format
Check c_o_r_e_migrator_versions table
Verify migration files haven't been renamed
Look for duplicate timestamps
Verify datasource configuration
Check database credentials
Ensure database server is running
Use in deployment scripts:
Always check info before running migrations
Review pending migrations before deployment
Keep migration files in version control
Don't modify completed migration files
Use info to verify production deployments
- Run all pending migrations
- Run next migration
- Rollback migration
- Create new migration
Shows detailed information about a Wheels plugin, including version, description, author, and links.
The plugins info command displays comprehensive information about a Wheels plugin. It prioritizes local installation data when available, only querying ForgeBox when the plugin is not installed locally.
When the plugin is installed locally, the command shows:
Installation status
Plugin name and version
Slug
Type (mvc, plugin, etc.)
Author information
Description
Homepage URL
Repository URL
Documentation URL
Issues/Bugs URL
Keywords
When the plugin is not installed, the command shows:
Installation status
ForgeBox package information
Available versions
Installation instructions
Output:
Output:
Output:
Check Local Installation: First checks if the plugin is installed in:
box.json dependencies
box.json devDependencies
Reads plugin's local box.json for detailed information
Display Local Information: If installed, shows all metadata from the plugin's box.json
ForgeBox Fallback: Only queries ForgeBox if the plugin is not installed locally
Installation Commands: Shows appropriate commands based on installation status
The command prioritizes local plugin data over ForgeBox data for accuracy
No network call is made for installed plugins (faster response)
Use wheels plugin search to browse all available plugins
Plugin names can include variations (e.g., "wheels-core", "cfwheels-core")
Generates documentation for your Wheels application from code comments and annotations.
--output - (Optional) Output directory for docs. Default: docs/api
--format - (Optional) Documentation format: html, json, markdown. Default: html
--include - (Optional) Components to include: models, controllers, views, services. Default: models,controllers
--serve - (Optional) Start local server after generation
--verbose - (Optional) Verbose output
The docs generate command automatically creates comprehensive documentation from your Wheels application by parsing:
JavaDoc-style comments in CFCs
Model relationships and validations
Controller actions and routes
Configuration files
Database schema
API endpoints
Overview: Application structure and architecture
Models: Properties, methods, relationships, validations
Controllers: Actions, filters, routes
API Reference: Endpoints, parameters, responses
Database Schema: Tables, columns, indexes
Configuration: Settings and environment variables
Class hierarchies and inheritance
Method signatures and parameters
Property types and defaults
Relationship diagrams
Route mappings
Database ERD
Documentation is generated from code comments
Use consistent JavaDoc format for best results
Private methods are excluded by default
With a convention-over-configuration framework like Wheels, it's important to know these conventions. This is your guide.
There is a specific set of standards that Wheels follows when you run it in its default state. This is to save you time. With conventions in place, you can get started coding without worrying about configuring every little detail.
But it is important for you to know these conventions, especially if you're running an operating system and/or DBMS configuration that's picky about things like case sensitivity.
Wheels uses a very flexible routing system to match your application's URLs to controllers, views, and parameters.
Within this routing system is a default route that handles many scenarios that you'll run across as a developer. The default route is mapped using the pattern [controller]/[action]/[key].
Consider this example URL: http://localhost:8080/users/edit/12
http://localhost:8080/users/edit/12
This maps to the Users controller, edit action, and a key of 12. For all intents and purposes, this will load a view for editing a user with a primary key value in the database of 12.
This URL pattern works up the chain and will also handle the following example URLs:
Note that the above conventions are for GET requests and only apply when you have a wildcard() call in /config/routes.cfm (which is the default). See for instructions on overriding this behavior and how to deal with PUT, POST etc.
Controllers, actions, and views are closely linked together by default. And how you name them will influence the URLs that Wheels will generate.
First, a controller is a CFC file placed in the controllers folder. It should be named in PascalCase. For example, a site map controller would be stored at /app/controllers/SiteMap.cfc.
Multi-word controllers will be delimited by hyphens in their calling URLs. For example, a URL of /site-map will reference the SiteMap controller.
See for instructions on overriding this behavior.
Methods within the controllers, known as actions, should be named in camelCase.
Like with controllers, any time a capital letter is used in camelCase, a hyphen will be used as a word delimiter in the corresponding URL. For example, a URL of /site-map/search-engines will reference the searchEngines action in the SiteMap controller.
See for instructions on overriding this behavior.
By default, view files are named after the action names and are stored in folders that correspond to controller names. Both the folder names and view file names should be all lowercase, and there is no word delimiter.
In our /site-map/search-engines URL example, the corresponding view file would be stored at /app/views/sitemap/searchengines.cfm.
For information on overriding this behavior, refer to documentation for the function and read the chapter.
A special type of view file called a layout defines markup that should surround the views loaded by the application. The default layout is stored at /app/views/layout.cfm and is automatically used by all views in the application.
Controller-level layouts can also be set automatically by creating a file called layout.cfm and storing it in the given controller's view folder. For example, to create a layout for the users controller, the file would be stored at /app/views/users/layout.cfm.
When a controller-level layout is present, it overrides the default layout stored in the root /app/views folder.
For information on overriding the layout file to be loaded by an action, see the chapter on and documentation for the function.
By default, the names of Wheels models, model properties, database tables, and database fields all relate to each other. Wheels even sets a sensible default for the CFML data source used for database interactions.
By default, the datasource is set to wheels-dev in the /config/settings.cfm file. You can change the value in the set(dataSourceName="wheels-dev") function to whatever you want the name of the datasource to be.
Refer to the chapter for instructions on overriding data source information.
Wheels adopts a Rails-style naming conventions for database tables and model files. Think of a database table as a collection of model objects; therefore, it is named with a plural name. Think of a model object as a representation of a single record from the database table; therefore, it is named with a singular word.
For example, a user model represents a record from the users database table. Wheels also recognizes plural patterns like binary/binaries, mouse/mice, child/children, etc.
Like controller files, models are also CFCs and are named in PascalCase. They are stored in the /app/models folder. So the user model would be stored at /app/models/User.cfc.
For instructions on overriding database naming conventions, refer to documentation for the function and the chapter on .
In your database, both table names and column names should be lowercase. The customersegments table could have fields called title, regionid, and incomelevel, for example.
Because of CFML's case-insensitive nature, we recommend that you refer to model names and corresponding properties in camelCase. This makes for easier readability in your application code.
In the customersegments example above, you could refer to the properties in your CFML as title, regionId, and incomeLevel to stick to CFML's Java-style roots. (Built-in CFML functions are often written in camelCase and PascalCase, after all.)
For information on overriding column and property names, refer to documentation for the function and the chapter.
There are many default values and settings that you can tweak in Wheels when you need to. Some of them are conventions and others are just configurations available for you to change. You can even change argument defaults for built-in Wheels functions to keep your code DRYer.
For more details on what you can configure, read the chapter.
wheels plugins info <name>name
Yes
string
Name or slug of the plugin to inspect
wheels plugins info wheels-core===========================================================
Plugin Information: wheels-core
===========================================================
Status:
[OK] Installed locally
Wheels Core
Wheels Framework Core Directory
Details:
Version: 3.0.0-SNAPSHOT+1030
Slug: wheels-core
Type: mvc
Author: Wheels Core Team and Community
Keywords: mvc, rails, wheels, wheels.dev, core
Links:
Homepage: https://wheels.dev/
Repository: https://github.com/wheels-dev/wheels
Docs: https://wheels.dev/docs
Issues: https://github.com/wheels-dev/wheels/issues
Commands:
Update: wheels plugin update wheels-core
Search: wheels plugin searchwheels plugins info wheels-vue-cli===========================================================
Plugin Information: wheels-vue-cli
===========================================================
Status:
[X] Not installed
[ForgeBox package information displayed]
Commands:
Install: wheels plugin install wheels-vue-cli
Search: wheels plugin searchwheels plugins info nonexistent-plugin===========================================================
Plugin Information: nonexistent-plugin
===========================================================
Status:
[X] Not installed
Plugin Not Installed
The plugin 'nonexistent-plugin' was not found in:
Local installation (box.json dependencies)
ForgeBox repository
Possible reasons:
Plugin name may be misspelled
Plugin may not exist on ForgeBox
Network connection issues
Suggestions:
Search for available plugins: wheels plugin list --available
Verify the correct plugin namewheels docs generate [--output=<dir>] [--format=<format>] [--include=<components>] [--serve] [--verbose]wheels docs generatewheels docs generate --format=markdownwheels docs generate --servewheels docs generate --include=models,controllers,services --verbosewheels docs generate --output=public/api-docs --format=html/**
* User model for authentication and authorization
*
* @author John Doe
* @since 1.0.0
*/
component extends="Model" {
/**
* Initialize user relationships and validations
* @hint Sets up the user model configuration
*/
function config() {
// Relationships
hasMany("orders");
belongsTo("role");
// Validations
validatesPresenceOf("email,firstName,lastName");
validatesUniquenessOf("email");
}
/**
* Find active users with recent activity
*
* @param days Number of days to look back
* @return query Active users
*/
public query function findActive(numeric days=30) {
return findAll(
where="lastLoginAt >= :date",
params={date: dateAdd("d", -arguments.days, now())}
);
}
}/**
* Handles user management operations
*
* @displayname User Controller
* @namespace /users
*/
component extends="Controller" {
/**
* Display paginated list of users
*
* @hint GET /users
* @access public
* @return void
*/
function index() {
param name="params.page" default="1";
users = model("user").findAll(
page=params.page,
perPage=20,
order="createdAt DESC"
);
}
}/docs/api/
├── index.html
├── models/
│ ├── model.html/
├── controllers/
│ ├── controller.html
├── views/
| └── view.html
├── services/
└── view.htmlDocumentation Generator
==================================================
Generating documentation...
Scanning source files...
[OK] Found 1 models
[OK] Found 1 controllers
Writing documentation... [OK] HTML files generated
==================================================
[SUCCESS] Documentation generated successfully!
Summary:
- Models: 1 files
- Controllers: 1 files
- Total: 2 components documented
Output directory: C:\path\to\docs\api\wheels dbmigrate info+-----------------------------------------+-----------------------------------------+
| Datasource: myApp | Total Migrations: 4 |
| Database Type: H2 | Available Migrations: 4 |
| | Current Version: 0 |
| | Latest Version: 20250812161449 |
+-----------------------------------------+-----------------------------------------+
+----------+------------------------------------------------------------------------+
| | 20250812161449_cli__create_reporting_procedures |
| | 20250812161302_cli__blacnk |
| | 20250812161250_cli__name |
| | 20250812154338_cli__0 |
+----------+------------------------------------------------------------------------+[timestamp]_[description].cfc20240125160000_create_users_table.cfcSELECT * FROM c_o_r_e_migrator_versions;
+----------------+
| version |
+----------------+
| 20240101100000 |
| 20240105150000 |
| 20240110090000 |
| 20240115120000 |
+----------------+wheels dbmigrate infowheels dbmigrate latest
wheels dbmigrate info#!/bin/bash
# Check migration status
wheels dbmigrate info
# Run if needed
if [[ $(wheels dbmigrate info | grep "pending") ]]; then
echo "Running pending migrations..."
wheels dbmigrate latest
fiusers
edit
12
users
new
users
index
Generate a migration file for dropping a database table.
wheels dbmigrate remove table name=<table_name>Alias: wheels db remove table
This command supports multiple parameter formats:
Named parameters: name=value (e.g., name=users)
Flag parameters: --flag=value (e.g., --name=users)
Parameter Mixing Rules:
✅ ALLOWED:
Named: wheels dbmigrate remove table name=users
Flag: wheels dbmigrate remove table --name=users
❌ NOT ALLOWED:
Positional parameters: This command does not support positional parameters
Recommendation: Use named parameters: wheels dbmigrate remove table name=users
The dbmigrate remove table command generates a migration file that drops an existing database table. The generated migration includes a dropTable() call in the up() method.
name
string
Yes
The name of the table to remove
wheels dbmigrate remove table name=temp_import_datawheels dbmigrate remove table name=userwheels dbmigrate remove table name=orders_archive_2023For the command:
wheels dbmigrate remove table name=product_archiveGenerates:
component extends="wheels.migrator.Migration" hint="remove product_archive table" {
function up() {
transaction {
dropTable("product_archive");
}
}
function down() {
transaction {
// Add code here to recreate the table if needed for rollback
// createTable(name="product_archive") { ... }
}
}
}Clean up temporary or staging tables:
# Remove import staging table
wheels dbmigrate remove table name=temp_customer_import
# Remove data migration table
wheels dbmigrate remove table name=migration_backup_20240115Remove tables during schema refactoring:
# Remove old table after data migration
wheels dbmigrate remove table name=legacy_orders
# Remove deprecated table
wheels dbmigrate remove table name=user_preferences_oldRemove tables from cancelled features:
# Remove tables from abandoned feature
wheels dbmigrate remove table name=beta_feature_data
wheels dbmigrate remove table name=beta_feature_settingsRemove old archive tables:
# Remove yearly archive tables
wheels dbmigrate remove table name=orders_archive_2020
wheels dbmigrate remove table name=orders_archive_2021CRITICAL: Dropping a table permanently deletes all data. Always:
Backup the table data before removal
Verify data has been migrated if needed
Test in development/staging first
Have a rollback plan
Consider objects that depend on the table:
Foreign key constraints
Views
Stored procedures
Triggers
Application code
Be aware of dependent objects when removing tables:
Foreign key constraints
Views that reference the table
Stored procedures using the table
Application code dependencies
Add clear documentation about why the table is being removed:
# Create descriptive migration
wheels dbmigrate remove table name=obsolete_analytics_cache
# Then edit the migration file to add detailed comments about why it's being removedBefore removing tables, create data backups:
# First backup the data
wheels db schema format=sql > backup_before_removal.sql
# Then create removal migration
wheels dbmigrate remove table name=user_preferencesFor production systems, consider staged removal:
# Stage 1: Rename table (keep for rollback)
wheels dbmigrate create blank name=rename_orders_to_orders_deprecated
# Stage 2: After verification period, remove
wheels dbmigrate remove table name=orders_deprecatedVerify no active dependencies before removal:
-- Check foreign keys
SELECT * FROM information_schema.referential_constraints
WHERE referenced_table_name = 'table_name';
-- Check views
SELECT * FROM information_schema.views
WHERE table_schema = DATABASE()
AND view_definition LIKE '%table_name%';The generated migration contains:
An up() method with dropTable()
An empty down() method for you to implement rollback logic if needed
You should edit the down() method to add table recreation logic if you want the migration to be reversible.
Don't run the migration in production
Use wheels dbmigrate down if already run
Restore from backup if down() fails
Before removal, capture structure:
# Export entire database schema
wheels db schema format=sql --save file=schema_backup.sql
# Then remove table
wheels dbmigrate remove table name=user_preferencesThe command analyzes table structure before generating migration
Foreign key constraints must be removed before table removal
The migration is reversible if table structure is preserved
Always review generated migration before running
wheels dbmigrate create table - Create tables
wheels dbmigrate create blank - Create custom migrations
wheels dbmigrate up - Run migrations
wheels dbmigrate down - Rollback migrations
wheels db schema - Export table schemas
Run all pending database migrations to bring database to latest version.
Alias: wheels db latest
The wheels dbmigrate latest command runs all pending migrations in chronological order, updating your database schema to the latest version. This is the most commonly used migration command.
None.
Retrieves current database version and latest version
Executes dbmigrate exec with the latest version
Automatically runs dbmigrate info after completion
Updates version tracking after successful migration
Each migration file must contain:
Migrations run within transactions:
All changes in a migration succeed or fail together
Database remains consistent
Failed migrations can be retried
Test migrations locally first
Backup before production migrations
Use transactions
Make migrations reversible
Migrations can check environment:
Preview migrations before running:
For large tables:
Add to CI/CD pipeline:
If issues occur after migration:
Use down migrations
Restore from backup
Fix and retry
Fix migration file
Run wheels dbmigrate latest
- Check migration status
- Run single migration
- Rollback migration
- Create migration
⚠️ Note: This command depends on configuration values. Please verify your database configuration before executing it.
Drop an existing database.
The wheels db drop command permanently deletes a database. This is a destructive operation that cannot be undone. By default, it requires confirmation unless the --force flag is used.
Examples:
Drop database with confirmation:
Drop without confirmation:
Confirmation Required: By default, you must type "yes" to confirm
Production Warning: Extra warning when dropping production databases
Clear Messaging: Shows database name and environment before dropping
Uses DROP DATABASE IF EXISTS statement
Connects to information_schema to execute command
Automatically handles active connections
Terminates existing connections before dropping
Uses DROP DATABASE IF EXISTS statement
Connects to postgres system database
Sets database to single-user mode to close connections
Uses DROP DATABASE IF EXISTS statement
Connects to master system database
Drops USER/schema (Oracle's equivalent of a database)
Uses DROP USER ... CASCADE to remove all objects
Supports Oracle 12c+ with Container Database (CDB) architecture
Uses _ORACLE_SCRIPT session variable for non-C## users
Important: Database names cannot contain hyphens (use underscores)
Cannot drop system users (SYS, SYSTEM, ADMIN, XDB, etc.)
Deletes database files (.mv.db, .lock.db, .trace.db)
Shows which files were deleted
No server connection required
This operation is irreversible! Always ensure you have backups before dropping a database.
Always backup first:
Use --force carefully: Only in scripts where you're certain
Double-check environment: Especially important for production
The database doesn't exist. No action needed.
The database user doesn't have permission to drop databases. Grant DROP privileges to the user.
Some databases prevent dropping while connections are active. The command attempts to close connections automatically.
Database name contains invalid characters. Oracle usernames can only contain letters, numbers, and underscores.
Fix: Use underscores instead of hyphens:
The Oracle user/schema doesn't exist. No action needed.
Attempting to drop an Oracle system user. System users like SYS, SYSTEM, ADMIN, XDB cannot be dropped.
Fix: Verify you're targeting the correct database. System users are protected and cannot be removed.
- Create a new database
- Drop and recreate database
- Backup before dropping
Remove generated code and files associated with a model, controller, views, and tests.
This command supports multiple parameter formats:
Positional parameters: wheels destroy user (resource name as positional)
Named parameters: name=value (e.g., name=user)
Parameter Mixing Rules:
✅ ALLOWED:
Positional: wheels destroy user
Named: wheels destroy name=user
❌ NOT ALLOWED:
Positional + named for same parameter: wheels destroy user name=other
Recommendation: Use positional parameter for simplicity: wheels destroy user
Note: This command always prompts for confirmation before proceeding. There is no --force flag to skip confirmation.
The wheels destroy command removes all files and code associated with a resource that was previously generated. It's useful for cleaning up mistakes or removing features completely. This command will also drop the associated database table and remove resource routes.
This command has no additional options. It always prompts for confirmation before proceeding.
When you destroy a resource, the following items are deleted:
Model file (/app/models/[Name].cfc)
Controller file (/app/controllers/[Names].cfc)
Views directory (/app/views/[names]/)
Model test file (/tests/specs/models/[Name].cfc)
Controller test file (/tests/specs/controllers/[Names].cfc)
View test directory (/tests/specs/views/[names]/)
Resource route entry in /config/routes.cfm
Database table (if confirmed)
This will prompt:
The command always asks for confirmation and shows exactly what will be deleted:
Confirmation Required: Always asks for confirmation before proceeding
Shows All Changes: Lists all files and directories that will be deleted
Database Migration: Creates and runs a migration to drop the table
Route Cleanup: Automatically removes resource routes from routes.cfm
Files Deleted:
Model file
Controller file
Views directory and all view files
Test files (model, controller, and view tests)
Database Changes:
Creates a migration to drop the table
Runs wheels dbmigrate latest to execute the migration
Route Changes:
Removes .resources("name") from routes.cfm
Cleans up extra whitespace
Commit First: Always commit your changes before destroying
Review Carefully: Read the confirmation list carefully
Check Dependencies: Make sure other code doesn't depend on what you're destroying
Backup Database: Have a database backup before running in production
Cannot be undone - files are permanently deleted
Database table is dropped via migration
Resource routes are automatically removed from routes.cfm
Only works with resources that follow Wheels naming conventions
- Generate resources
- Generate scaffolding
- Remove database tables
Create an empty database migration file with up and down methods.
This command supports multiple parameter formats:
Positional parameters: wheels dbmigrate create blank addIndexes (name as positional)
Named parameters: name=value (e.g., name=addIndexes, description="Add indexes")
Flag parameters: --flag=value (e.g., --name=addIndexes)
Parameter Mixing Rules:
✅ ALLOWED:
Positional: wheels dbmigrate create blank addIndexes
All named: name=addIndexes description="Custom migration"
Positional + named: wheels dbmigrate create blank addIndexes description="Add indexes"
❌ NOT ALLOWED:
Mixing positional + named for same parameter: wheels dbmigrate create blank addIndexes name=other
Recommendation: Use positional for name, named for optional parameters: wheels dbmigrate create blank addIndexes description="My migration"
The dbmigrate create blank command generates a new empty migration file with the basic structure including up() and down() methods. This provides a starting point for custom migrations where you need full control over the migration logic.
--nameType: String
Required: Yes
Description: The name of the migration (will be prefixed with timestamp)
--descriptionType: String
Default: Empty
Description: Add a description comment to the migration file
The command creates a file named YYYYMMDDHHmmss_<name>.cfc with the following structure:
For complex operations not covered by other generators:
When you need to migrate data, not just schema:
For migrations requiring multiple coordinated changes:
For database-specific features not abstracted by Wheels:
Use clear, descriptive names that indicate the migration's purpose:
Always implement both up() and down() methods:
Wrap operations in transactions for atomicity:
Document complex operations:
Within your blank migration, you can use these helper methods:
createTable(name, options) - Create a new table
dropTable(name) - Drop a table
addColumn(table, column, type, options) - Add a column
removeColumn(table, column) - Remove a column
changeColumn(table, column, type, options) - Modify a column
addIndex(table, columnNames, unique, indexName) - Add an index
removeIndex(table, column) - Remove an index
execute(sql) - Execute raw SQL
announce(message) - Output a message during migration
Migration files are created in /app/migrator/migrations/ or your configured migration path
The timestamp ensures migrations run in the correct order
Always test migrations in development before production
Keep migrations focused on a single purpose
- Create a table migration
- Create a column migration
- Run migrations
- Rollback migrations
- View migration status
Generate a migration file for creating a new database table.
Alias: wheels db create table
This command supports multiple parameter formats:
Named parameters: name=value (e.g., name=users, primaryKey=userId)
Flag parameters: --flag equals flag=true (e.g., --force equals force=true)
Flag with value: --flag=value equals flag=value (e.g., --id=false)
Parameter Mixing Rules:
✅ ALLOWED:
All named: name=users primaryKey=userId
Named + flags: name=users --force --id=false
❌ NOT ALLOWED:
Positional parameters: This command does not support positional parameters
Recommendation: Use named for required parameters, flags for booleans: name=users --force
The dbmigrate create table command generates a migration file that creates a new database table. The generated migration includes the table structure following Wheels conventions.
The generated migration file will contain a basic table structure. You'll need to manually edit the migration file to add columns with their types and options. The migration template includes comments showing how to add columns.
For the command:
Generates a migration file that you can customize:
Create a typical entity table:
Create a join table without primary key:
Create a table with non-standard primary key:
Wheels conventions expect singular table names:
After generating the migration, edit it to add columns:
Think through your table structure before creating:
Primary key strategy
Required columns and their types
Foreign key relationships
Indexes needed for performance
The command generates a basic migration template. You'll need to edit it to add columns:
Table names should follow your database naming conventions
The migration automatically handles rollback with dropTable()
Column order in the command is preserved in the migration
Use wheels dbmigrate up to run the generated migration
- Add columns to existing table
- Create custom migration
- Create table removal migration
- Run migrations
- View migration status
Check for outdated Wheels plugins that have newer versions available on ForgeBox.
The plugins outdated command checks all installed plugins in the /plugins folder against ForgeBox to identify which ones have updates available. It performs real-time version checks and displays the results in a clear, formatted output.
Checks only cfwheels-plugins type packages
Real-time version checking via ForgeBox
Color-coded status indicators
Detailed version comparison
Helpful update commands
Output (with outdated plugins):
Output (all up to date):
Output (with errors):
Output:
Output:
During checking, each plugin displays:
[OUTDATED] (yellow) - Newer version available
[OK] (green) - Already at latest version
[ERROR] (red) - Could not check version (network issue, plugin not on ForgeBox, etc.)
Plugin Discovery: Scans /plugins folder for installed plugins
Version Query: Uses forgebox show command for each plugin to get latest version
Version Comparison: Cleans and compares version strings (strips non-numeric characters)
Display Results: Shows outdated plugins with current and latest versions
Update Suggestions: Provides appropriate update commands
The command performs string-based version comparison after cleaning:
Removes non-numeric characters except dots (e.g., "v0.0.4" becomes "0.0.4")
Compares cleaned versions for equality
Marks as outdated if versions differ
If ForgeBox cannot be reached, the plugin is marked with [ERROR] and listed separately.
Only checks plugins from /plugins folder (not box.json dependencies)
Only works with cfwheels-plugins type packages
Requires internet connection to query ForgeBox
Version check is performed in real-time (not cached)
Plugins are checked sequentially with status updates
Use wheels plugin update:all to update all outdated plugins at once
Dynamic table formatting adjusts column widths based on content
- Update a single plugin
- Update all plugins
- List installed plugins
- Show plugin details
Environments that match your development stages.
Wheels allows you to set up different environments that match stages in your development cycle. That way you can configure different values that match what services to call and how your app behaves based on where you are in your development.
The Development environment is the most convenient one to use as you start building your application because it does not cache any data. Therefore, if you make any changes to your controllers and actions, for example, it will immediately be picked up by Wheels.
Other environment modes cache this information in order to speed up your application as much as possible. Making changes to the database in most of these modes will cause Wheels to throw an error. (Although that can be avoided with a reload call. More on that later.)
The fastest environment mode in terms of page load time is the Production mode. This is what you should set your application to run in before you launch your website.
By default, all new applications will start in the Development environment which is middle-of-the-road in terms of convenience versus speed.
Besides the 2 environments mentioned above, there are 2 more. Let's go through them all one by one so you can see the differences between them and choose the most appropriate one given your current stage of development.
Development
Shows friendly Wheels specific errors as well as regular ColdFusion errors on screen.
Does not email you when an error is encountered.
Caches controller and model initialization (the config() methods).
Caches the database schema.
Caches routes.
Caches image information.
Production
Caches everything that the Development mode caches.
Activates all developer controlled caching (actions, pages, queries and partials).
Shows your custom error page when an error is encountered.
Shows your custom 404 page when a controller or action is not found.
Sends an email to you when an error is encountered.
Testing
Same caching settings as the Production mode but using the error handling of the Development mode. (Good for testing an application at its true speed while still getting errors reported on screen.)
Maintenance
Shows your custom maintenance page unless the requesting IP address or user agent is in the exception list (set by calling set(ipExceptions="127.0.0.1") in /config/settings.cfm or passed along in the URL as except=127.0.0.1, or as except=myuseragentstring to match against the user agent instead. Please note that if passing an exception on the URL using the except parameter, you must also provide the password parameter if a reload password has been defined. This eliminates the possibility of a rogue actor breaking out of maintenance mode by simply adding an except to the URL.
This environment mode comes in handy when you want to briefly take your website offline to upload changes or modify databases on production servers.
Wheels provides multiple ways to switch between environments. Choose the method that best fits your workflow:
The easiest and most reliable way to switch environments is using the Wheels CLI command:
This command will:
Update the set(environment="...") setting in /config/environment.cfm
Automatically reload the application to apply the changes
Validate that the target environment exists before switching
Examples:
If you prefer not to use the CLI command, you can manually change the environment by editing the /config/environment.cfm file:
Available environment values:
development - For local development with full debugging
production - For live production servers with caching enabled
testing - For automated testing with production-like caching
maintenance - For temporarily taking your site offline
After manually editing the file, you must reload the application using one of these methods:
Issue a reload request by passing reload as a URL parameter:
This tells Wheels to reload the entire framework (including your /app/events/onapplicationstart.cfm file), picking up any changes made in the /config/environment.cfm file.
For quick testing, you can temporarily switch environments without editing any files:
This will make Wheels skip your /config/environment.cfm file and use the URL value instead (testing in this case). Note: This is temporary and will revert to the configured environment on the next application restart.
Alternatively, you can restart the ColdFusion/Lucee service to reload the application and apply environment changes.
When using URL-based reload methods (Options A and B above), you should add protection by setting the reloadPassword variable in /config/settings.cfm:
When a password is set, reload requests must include the password parameter:
⚠️ WARNING: Always use a reload password in production
You really don't want random users hitting
?reload=developmenton a production server, as it could expose sensitive data about your application and error messages. Always set a reload password for production environments!
Note: The wheels env switch CLI command bypasses URL-based reloads and directly updates the configuration file, so it's not affected by the reload password setting.
If you're deploying to a container based environment, or one that you know you'll never want to switch out of production mode, you can disable URL based environment switching completely via:
set(allowEnvironmentSwitchViaUrl = false);
This is just an additional check to ensure that your production mode acts in the way you expect! Application reloading is still allowed, but the configuration can not be altered.
By default, Wheels only enables the debug GUI (wheels interface) and debug information in the development environment. However, there may be situations where you need access to these debugging tools in other environments like testing, production, or maintenance - but only for specific IP addresses.
Wheels provides IP-based access control for the debug GUI and debug information through two configuration settings:
Add these settings to your environment-specific configuration files (e.g., /config/production/settings.cfm):
When these settings are configured:
In the development environment, the debug GUI is always accessible regardless of IP address.
In other environments (testing, production), the debug GUI and debug information will only be enabled for requests coming from IP addresses listed in the debugAccessIPs array.
If a request comes from an IP address not in the list, the debug GUI and debug information remain disabled, maintaining security in production environments.
Here's a typical setup for different environments:
Development (/config/development/settings.cfm):
Testing (/config/testing/settings.cfm):
Production (/config/production/settings.cfm):
This feature allows administrators to access debugging tools in production or other environments while keeping them secure from regular users, providing a flexible way to troubleshoot issues in all environments without compromising security.
wheels env switch production# Switch to production environment
wheels env switch production
# Switch to testing environment
wheels env switch testing
# Switch to development environment
wheels env switch development// /config/environment.cfm
<cfscript>
// Change this value to your desired environment
set(environment="production");
</cfscript>http://www.mysite.com/?reload=truehttp://www.mysite.com/?reload=testing// /config/settings.cfm
<cfscript>
set(reloadPassword="mySecurePassword");
</cfscript>http://www.mysite.com/?reload=true&password=mySecurePassword
http://www.mysite.com/?reload=testing&password=mySecurePassword// Define allowed IP addresses that can access debug features
set(debugAccessIPs = ["203.0.113.45", "198.51.100.22", "127.0.0.1"]);
// Enable IP-based debug access control
set(allowIPBasedDebugAccess = true);// Debug GUI is enabled by default in development, no additional settings neededset(debugAccessIPs = ["127.0.0.1", "192.168.1.100"]);
set(allowIPBasedDebugAccess = true);set(debugAccessIPs = ["10.0.0.5"]); // Only specific admin IPs
set(allowIPBasedDebugAccess = true);wheels dbmigrate latest+-----------------------------------------+-----------------------------------------+
| Datasource: myApp | Total Migrations: 4 |
| Database Type: H2 | Available Migrations: 0 |
| | Current Version: 20250812161449 |
| | Latest Version: 20250812161449 |
+-----------------------------------------+-----------------------------------------+
+----------+------------------------------------------------------------------------+
| migrated | 20250812161449_cli__create_reporting_procedures |
| migrated | 20250812161302_cli__example_1 |
| migrated | 20250812161250_cli__example_2 |
| migrated | 20250812154338_cli__example_3 |
+----------+------------------------------------------------------------------------+component extends="wheels.migrator.Migration" {
function up() {
// Database changes go here
transaction {
// Use transaction for safety
}
}
function down() {
// Rollback logic (optional)
transaction {
// Reverse the up() changes
}
}
}function up() {
transaction {
t = createTable("products");
t.string("name");
t.decimal("price");
t.timestamps();
t.create();
}
}function up() {
transaction {
addColumn(table="users", columnNames="email", type="string");
}
}function up() {
transaction {
addIndex(table="users", columnNames="email", unique=true);
}
}function up() {
transaction {
changeColumn(table="products", columnNames="price", type="decimal", precision=10, scale=2);
}
}# Test on development database
wheels dbmigrate latest
# Verify
wheels dbmigrate info# Backup database
mysqldump myapp_production > backup.sql
# Run migrations
wheels dbmigrate latestfunction up() {
transaction {
// All changes here
}
}function down() {
transaction {
dropTable("products");
}
}function up() {
transaction {
// Always run
addColumn(table="users", column="lastLogin", type="datetime");
// Development only
if (get("environment") == "development") {
// Add test data
sql("INSERT INTO users (email) VALUES ('[email protected]')");
}
}
}# Check what would run
wheels dbmigrate info
# Review migration files
ls app/migrator/migrations/function up() {
transaction {
// Add index concurrently (if supported)
if (get("databaseType") == "postgresql") {
sql("CREATE INDEX CONCURRENTLY idx_users_email ON users(email)");
} else {
addIndex(table="users", columns="email");
}
}
}# .github/workflows/deploy.yml
- name: Run migrations
run: |
wheels dbmigrate latest
wheels test appwheels dbmigrate down
wheels dbmigrate downmysql myapp_production < backup.sqlfunction up() {
// Increase timeout for large operations
setting requestTimeout="300";
transaction {
// Long running operation
}
}function up() {
transaction {
// Disable checks temporarily
sql("SET FOREIGN_KEY_CHECKS=0");
// Make changes
dropTable("orders");
// Re-enable
sql("SET FOREIGN_KEY_CHECKS=1");
}
}wheels db drop [--datasource=<name>] [--environment=<env>] [--database=<dbname>] [--force]datasource
string
Current datasource
Specify which datasource's database to drop. If not provided, uses the default datasource from your Wheels configuration.
environment
string
Current environment
Specify the environment to use. Defaults to the current environment.
database
string
wheels_dev
Specify the database name to drop. Note for Oracle: Database names cannot contain hyphens. Use underscores instead (e.g., myapp_dev not myapp-dev).
force
boolean
false
Skip the confirmation prompt. Useful for scripting.
# Use specific datasource
wheels db drop --datasource=myapp_dev
# Specify environment
wheels db drop --environment=testing
# Custom database name
wheels db drop --database=myapp_test
# Force drop without confirmation
wheels db drop --forcewheels db drop
# Will prompt: Are you sure you want to drop the database 'myapp_dev'? Type 'yes' to confirm:wheels db drop --forcewheels db drop --datasource=myapp_test --environment=testing --forcewheels db dump --output=backup-before-drop.sql
wheels db drop# Wrong
wheels db drop --database=my-app-dev
# Correct
wheels db drop --database=my_app_devwheels destroy <name>
wheels d <name>name
Name of the resource to destroy
Yes
# Positional (recommended)
wheels destroy user
# OR named
wheels destroy name=user
# OR alias (positional)
wheels d user================================================
= Watch Out! =
================================================
This will delete the associated database table 'users', and
the following files and directories:
/app/models/User.cfc
/app/controllers/Users.cfc
/app/views/users/
/tests/specs/models/User.cfc
/tests/specs/controllers/Users.cfc
/tests/specs/views/users/
/config/routes.cfm
.resources("users")
Are you sure? [y/n]# Positional with alias (recommended)
wheels d product
# OR named with alias
wheels d name=product================================================
= Watch Out! =
================================================
This will delete the associated database table 'users', and
the following files and directories:
/app/models/User.cfc
/app/controllers/Users.cfc
/app/views/users/
/tests/specs/models/User.cfc
/tests/specs/controllers/Users.cfc
/tests/specs/views/users/
/config/routes.cfm
.resources("users")
Are you sure? [y/n]# Generated the wrong name
wheels generate resource prduct # Oops, typo!
wheels destroy prduct # Remove it
wheels generate resource product # Create correct one# Try out a feature
wheels generate scaffold blog_post title:string,content:text
# Decide you don't want it
wheels destroy blog_postwheels dbmigrate create blank <name># Positional (recommended)
wheels dbmigrate create blank add_custom_indexes
# OR flag syntax
wheels dbmigrate create blank --name=add_custom_indexes
# OR named
wheels dbmigrate create blank name=add_custom_indexes# Positional + named (recommended)
wheels dbmigrate create blank update_user_permissions description="Add role-based permissions to users"
# OR all flags
wheels dbmigrate create blank --name=update_user_permissions --description="Add role-based permissions to users"
# OR all named
wheels dbmigrate create blank name=update_user_permissions description="Add role-based permissions to users"component extends="wheels.migrator.Migration" hint="<description>" {
function up() {
transaction {
// Add your migration code here
}
}
function down() {
transaction {
// Add code to reverse the migration
}
}
}# Create migration for custom stored procedure
wheels dbmigrate create blank --name=create_reporting_procedures
# Edit the file to add:
# - CREATE PROCEDURE statements
# - Complex SQL operations
# - Multiple related changes# Create data migration
wheels dbmigrate create blank --name=normalize_user_emails
# Edit to add data transformation logic
# Example: lowercase all email addresses# Create complex migration
wheels dbmigrate create blank --name=refactor_order_system
# Edit to include:
# - Create new tables
# - Migrate data
# - Drop old tables
# - Update foreign keys# Create migration for PostgreSQL-specific features
wheels dbmigrate create blank --name=add_json_columns
# Edit to use PostgreSQL JSON operations# Good
wheels dbmigrate create blank --name=add_user_authentication_tokens
# Bad
wheels dbmigrate create blank --name=update1function up() {
transaction {
execute("CREATE INDEX idx_users_email ON users(email)");
}
}
function down() {
transaction {
execute("DROP INDEX idx_users_email");
}
}function up() {
transaction {
// All operations succeed or all fail
createTable("new_table");
execute("INSERT INTO new_table SELECT * FROM old_table");
dropTable("old_table");
}
}function up() {
transaction {
// Create composite index for query optimization
// This supports the findActiveUsersByRegion() query
execute("
CREATE INDEX idx_users_active_region
ON users(is_active, region_id)
WHERE is_active = 1
");
}
}wheels dbmigrate create table name=<table_name> [--force] [--id] primaryKey=<key_name>name
string
Yes
-
The name of the table to create
--force
boolean
No
false
Force the creation of the table
--id
boolean
No
true
Auto create ID column as autoincrement ID
primaryKey
string
No
"id"
Overrides the default primary key column name
# Named parameter (required)
wheels dbmigrate create table name=users# Named + flag (recommended)
wheels dbmigrate create table name=user_roles --id=false
# OR all named
wheels db create table name=user_roles id=false# Named parameters (recommended)
wheels dbmigrate create table name=products primaryKey=productCode# Named + flag (recommended)
wheels dbmigrate create table name=users --force
# OR all named
wheels db create table name=users force=truewheels dbmigrate create table name=userscomponent extends="wheels.migrator.Migration" hint="create users table" {
function up() {
transaction {
t = createTable(name="users", force=false, id=true, primaryKey="id");
// Add your columns here
// t.string(columnNames="name");
// t.integer(columnNames="age");
t.timestamps();
t.create();
}
}
function down() {
transaction {
dropTable("users");
}
}
}# Generate the migration
wheels dbmigrate create table name=customer
# Then edit the migration file to add columnswheels dbmigrate create table name=products_categories --id=falsewheels dbmigrate create table name=legacy_customer primaryKey=customer_code# Good
wheels dbmigrate create table name=user
wheels dbmigrate create table name=product
# Avoid
wheels dbmigrate create table name=users
wheels dbmigrate create table name=products// In the generated migration file
t = createTable(name="orders", force=false, id=true, primaryKey="id");
t.integer(columnNames="customer_id");
t.decimal(columnNames="total", precision=10, scale=2);
t.string(columnNames="status", default="pending");
t.timestamps();
t.create();component extends="wheels.migrator.Migration" {
function up() {
transaction {
t = createTable(name="tableName", force=false, id=true, primaryKey="id");
// Add your columns here:
t.string(columnName="name");
t.integer(columnName="age");
t.boolean(columnName="active", default=true);
t.text(columnName="description");
// MySQL only: use size parameter for larger text fields
t.text(columnName="content", size="mediumtext"); // 16MB
t.text(columnName="largeContent", size="longtext"); // 4GB
t.timestamps();
t.create();
}
}
function down() {
transaction {
dropTable("tableName");
}
}
}wheels plugin outdated [--format=<format>]format
No
string
table, json
table
Output format for outdated plugin list
wheels plugin outdated===========================================================
Checking for Plugin Updates
===========================================================
bcrypt [OUTDATED] 0.0.3 -> 0.0.4
shortcodes [OK] v0.0.4
wheels-test [OK] v1.0.0
===========================================================
Found 1 outdated plugin:
Plugin Current Latest
-----------------------------------------------
bcrypt 0.0.3 0.0.4
-----------------------------------------------------------
Commands:
wheels plugin update bcrypt===========================================================
Checking for Plugin Updates
===========================================================
bcrypt [OK] v0.0.4
shortcodes [OK] v0.0.4
wheels-test [OK] v1.0.0
===========================================================
[OK] All plugins are up to date!===========================================================
Checking for Plugin Updates
===========================================================
bcrypt [OK] v0.0.4
problematic-plugin [ERROR] Could not check version
shortcodes [OK] v0.0.4
===========================================================
[OK] All plugins are up to date!
Could not check 1 plugin:
- problematic-pluginwheels plugin outdated===========================================================
Checking for Plugin Updates
===========================================================
bcrypt [OUTDATED] 0.0.3 -> 0.0.4
shortcodes [OUTDATED] 0.0.3 -> 0.0.4
wheels-test [OK] v1.0.0
===========================================================
Found 2 outdated plugins:
Plugin Current Latest
-----------------------------------------------
bcrypt 0.0.3 0.0.4
shortcodes 0.0.3 0.0.4
-----------------------------------------------------------
Commands:
Update all outdated plugins:
wheels plugin update:all
Update specific plugin:
wheels plugin update <plugin-name>wheels plugin outdated --format=json{
"outdated": [
{
"name": "bcrypt",
"slug": "cfwheels-bcrypt",
"currentVersion": "0.0.3",
"latestVersion": "0.0.4"
},
{
"name": "shortcodes",
"slug": "cfwheels-shortcodes",
"currentVersion": "0.0.3",
"latestVersion": "0.0.4"
}
],
"count": 2,
"errors": []
}wheels plugin update bcryptwheels plugin update:all===========================================================
Checking for Plugin Updates
===========================================================
No plugins installed in /plugins folder
Install plugins with: wheels plugin install <plugin-name>Wheels supports BoxLang 1.x, providing developers with a modern, high-performance CFML runtime. You can run BoxLang applications using either CommandBox or BoxLang Mini-Server.
Java 21 or higher installed on your system
Wheels application (generated or existing)
BoxLang runtime (see installation options below)
CommandBox provides the easiest and most feature-rich way to run BoxLang applications with Wheels.
# Install BoxLang engine in CommandBox
box install boxlang
# Start server with BoxLang
box server start cfengine=boxlang
# Or specify specific BoxLang version (optional)
box server start [email protected]BoxLang requires specific modules for full Wheels compatibility. These dependencies should be added to your box.json file:
{
"dependencies": {
"bx-compat-cfml": "^1.27.0+35",
"bx-csrf": "^1.2.0+3",
"bx-esapi": "^1.6.0+9",
"bx-image": "^1.0.1",
"bx-wddx":"^1.5.0+8",
"bx-mysql": "^1.0.1+7"
}
}# Install all dependencies from box.json
box install
# Or install specific BoxLang modules individually
box install bx-compat-cfml
box install bx-csrf
box install bx-esapi
box install bx-image
box install bx-wddx
box install bx-mysqlbx-compat-cfml - CFML compatibility layer for BoxLang
bx-csrf - Cross-Site Request Forgery protection
bx-esapi - Enterprise Security API for input validation
bx-image - Image manipulation functionality
bx-wddx - Web Distributed Data eXchange (WDDX) conversion
bx-mysql - MySQL database driver
For other databases supported by Wheels, install the corresponding BoxLang modules:
Microsoft SQL Server: box install bx-mssql
PostGreSQL Server: box install bx-postgresql
Oracle Server: box install bx-oracle
For any additional functionality or database drivers not included in the core dependencies:
Browse ForgeBox: Visit forgebox.io
Search for BoxLang modules: Look for modules with bx- prefix
Copy install command: Each module page provides the exact box install command
Install the module: Run the command in your project directory
Example: For Microsoft SQL Server support, visit the bx-mssql module page on ForgeBox and copy the installation command.
Create a server.json file in your application root to persist BoxLang settings:
{
"name":"wheels",
"web":{
"host":"localhost",
"http":{
"port":3000
},
"webroot":"public",
"rewrites":{
"enable":true,
"config":"public/urlrewrite.xml"
}
},
"app": {
"cfengine": "boxlang"
}
}# Generate new Wheels app (if needed)
wheels g app myapp --engine=boxlang
# Navigate to app directory
cd myapp
# Install BoxLang dependencies
box install
# Start BoxLang development server
server start cfengine=boxlang
# Access your application at http://localhost:8080BoxLang Mini-Server provides a lightweight, standalone option perfect for minimal setups or specific deployment scenarios.
BoxLang Mini-Server can be downloaded directly from the official BoxLang releases. The latest version is fully compatible with Wheels.
Download the latest BoxLang Mini-Server:
# Download the latest BoxLang Mini-Server JAR file
curl -LO https://downloads.ortussolutions.com/ortussolutions/boxlang-runtimes/boxlang-miniserver/boxlang-miniserver-latest.jarDownload BoxLang Mini-Server Package (optional, for additional files)
# Download complete package with additional files
curl -LO https://downloads.ortussolutions.com/ortussolutions/boxlang-runtimes/boxlang-miniserver/boxlang-miniserver-latest.zip
unzip boxlang-miniserver.zipPrepare Your Application Structure
your-wheels-app/
├── config/ # Configuration files
├── app/ # Controllers, models, views
├── public/ # Web-accessible files
│ ├── index.bxm # BoxLang entry point (required)
│ ├── stylesheets/
│ ├── javascripts/
│ └── images/
└── vendor/wheels/ # Wheels framework filesSetup BoxLang Entry Point
Create an index.bxm file in your public/ folder with the following content:
<bx:script>
// BoxLang rewrite handler for Wheels
// This file handles URL rewriting for BoxLang compatibility
// Get the requested URI
requestURI = cgi.request_uri ?: "";
// Remove query string for pattern matching
if (find("?", requestURI)) {
requestURI = listFirst(requestURI, "?");
}
// Handle requests that come through /index.bxm/path - redirect to clean URL
if (find("/index.bxm/", requestURI)) {
cleanPath = replace(requestURI, "/index.bxm", "");
queryString = cgi.query_string ?: "";
redirectURL = cleanPath;
if (len(queryString)) {
redirectURL &= "?" & queryString;
}
bx:header name="Location" value=redirectURL;
bx:header statusCode=301;
bx:abort;
}
// Static paths that should not be rewritten (based on urlrewrite.xml)
staticPaths = "cf_script,flex2gateway,jrunscripts,CFIDE,lucee,cfformgateway,cffileservlet,files,images,javascripts,miscellaneous,stylesheets,wheels/public/assets";
specialFiles = "robots.txt,favicon.ico,sitemap.xml,index.cfm";
// Check if this is a static path or special file
isStatic = false;
if (len(requestURI) && requestURI != "/") {
cleanPath = right(requestURI, len(requestURI) - 1); // Remove leading slash
// Check special files first
fileName = listLast(requestURI, "/");
if (listFindNoCase(specialFiles, fileName)) {
isStatic = true;
}
// Check static paths
if (!isStatic) {
for (staticPath in listToArray(staticPaths)) {
if (left(cleanPath, len(staticPath)) == staticPath) {
isStatic = true;
break;
}
}
}
// Check file extensions for static files
if (!isStatic && listLen(cleanPath, ".") > 1) {
extension = listLast(cleanPath, ".");
staticExtensions = "js,css,png,jpg,jpeg,gif,ico,pdf,zip,txt,xml,json";
if (listFindNoCase(staticExtensions, extension)) {
isStatic = true;
}
}
}
// If it's a static file, let it pass through
if (isStatic) {
bx:header statusCode=404;
writeOutput("File not found");
bx:abort;
}
// Set up CGI variables for clean URL generation
if (len(requestURI) && requestURI != "/") {
cgi.path_info = requestURI;
}
// Override script name for clean URL generation
cgi.script_name = "/index.cfm";
// Clean up request URI
if (find("/index.bxm", cgi.request_uri ?: "")) {
cgi.request_uri = replace(cgi.request_uri, "/index.bxm", "");
}
// Update request scope for Wheels compatibility
if (structKeyExists(request, "cgi")) {
request.cgi.script_name = "/index.cfm";
if (structKeyExists(request.cgi, "request_uri") && find("/index.bxm", request.cgi.request_uri)) {
request.cgi.request_uri = replace(request.cgi.request_uri, "/index.bxm", "");
}
}
</bx:script>
<!--- Include the main Wheels bootstrap file --->
<bx:include template="index.cfm" />This file serves as the BoxLang-specific entry point that handles URL rewriting and bootstraps your Wheels application.
java -jar /path/to/boxlang-miniserver-1.6.0.jar \
--webroot /path/to/your/app/public \
--rewritejava -jar /path/to/boxlang-miniserver-1.6.0.jar \
--webroot /path/to/your/app/public \
--host 127.0.0.1 \
--port 8080 \
--rewrite \
--debugIf using the Wheels base template structure:
java -jar /path/to/boxlang-miniserver-1.6.0.jar \
--webroot /path/to/your/app/templates/base/src/public \
--rewrite \
--port 8080--webroot
Document root directory (required)
None
--host
IP address to bind to
0.0.0.0
--port
Port number
8080
--rewrite
Enable URL rewriting (recommended for Wheels)
false
--debug
Enable debug mode
false
--config
Path to configuration file
None
--libs
Additional library paths
None
You can read the further details from the boxlang mini-server documentation
Missing BoxLang Dependencies (CommandBox)
Problem: Functions or features not working, missing module errors
Solution: Ensure all required BoxLang modules are installed: box install
Check: Verify box.json contains all required bx-* dependencies
Missing index.bxm File (Mini-Server)
Problem: Server returns 404 or directory listing
Solution: Create index.bxm in your public/ folder using the complete file content provided above in the Setup steps
URL Routing Not Working
Problem: Routes return 404 errors
Solution: Always include the --rewrite flag when starting Mini-Server
Version Compatibility Issues
Problem: Unexpected errors or features not working
Solution: Verify you're using a recent version of BoxLang 1.x
Path Resolution Problems
Problem: Files not found or incorrect paths
Solution: Use absolute paths to avoid directory resolution issues
# Verify server is responding
curl http://localhost:8080
# Test Wheels is loading
curl http://localhost:8080/wheels
# Check specific routes
curl http://localhost:8080/say/helloWelcome to the comprehensive documentation for the Wheels CLI - a powerful command-line interface for the Wheels framework.
Wheels CLI is a CommandBox module that provides a comprehensive set of tools for developing Wheels applications. It offers:
Code Generation - Quickly scaffold models, controllers, views, and complete CRUD operations
Database Migrations - Manage database schema changes with version control
Testing Tools - Run tests, generate coverage reports, and use watch mode
Development Tools - File watching, automatic reloading, and development servers
Code Analysis - Security scanning, performance analysis, and code quality checks
Plugin Management - Install and manage Wheels plugins
Environment Management - Switch between development, testing, and production
Complete reference for all CLI commands organized by category:
Core Commands - Essential commands like init, reload.
Code Generation - Generate applications, models, controllers, views
Database Commands - Complete database management and migrations
Testing Commands - Run tests and generate coverage
Configuration - Manage application settings
Get up and running with Wheels CLI in minutes. Learn how to:
Install Wheels CLI
Create your first application
Generate CRUD scaffolding
Run tests and migrations
Service Architecture - Understand the CLI's architecture
Creating Custom Commands - Extend the CLI with your own commands
Template System - Customize code generation templates
Testing Guide - Write and run tests effectively
Migration Guide - Database migration best practices
Security Guide - Security scanning and hardening
Performance Guide - Optimization techniques
Configuration Options - All available configuration settings
Template Variables - Variables available in templates
Exit Codes - Understanding command exit codes
Environment Variables - Environment configuration
Generate complete applications or individual components:
# Create new application
wheels new blog
# Generate complete CRUD scaffolding
wheels scaffold name=post properties=title:string,content:text,published:boolean
# Generate individual components
wheels generate model user
wheels generate controller users --rest
wheels generate view users indexComplete database lifecycle management:
# Database operations
wheels db create # Create database
wheels db setup # Create + migrate + seed
wheels db reset # Drop + recreate + migrate + seed
wheels db shell # Interactive database shell
wheels db dump # Backup database
wheels db restore backup.sql # Restore from backup
# Migrations
wheels dbmigrate create table posts
wheels dbmigrate latest
wheels db status # Check migration status
wheels db rollback # Rollback migrationsComprehensive testing support:
# Run all tests
wheels test run
# Advanced testing with TestBox CLI
wheels test:all # Run all tests
wheels test:unit # Run unit tests only
wheels test:integration # Run integration tests only
wheels test:watch # Watch mode
wheels test:coverage # Generate coverage reportsEnhance your development workflow:
# Reload application
wheels reload development
# Analyze code
wheels analyze code
wheels security scanInstall CommandBox (if not already installed):
# macOS/Linux
curl -fsSl https://downloads.ortussolutions.com/debs/gpg | sudo apt-key add -
or
brew install commandbox
# Windows
choco install commandboxInstall Wheels CLI:
box install wheels-cliCreate Your First App:
wheels new myapp
cd myapp
box server startExplore Commands:
wheels --help
wheels generate --help
wheels dbmigrate --help3.0.x
2.5+
5.0+
Lucee 5.3+, Adobe 2018+
2.0.x
2.0-2.4
4.0+
Lucee 5.2+, Adobe 2016+
Documentation: https://wheels.dev/docs
Slack: CFML Slack - #wheels channel
We welcome contributions! See our Contributing Guide for details on:
Reporting issues
Suggesting features
Submitting pull requests
Creating custom commands
Modernized service architecture
Enhanced testing capabilities with watch mode
Security scanning and performance optimization
Plugin and environment management
Improved code generation with more options
Better error handling and user feedback
Comprehensive documentation
All Commands - Complete command reference
Quick Start - Get started in minutes
Creating Commands - Extend the CLI
Service Architecture - Technical deep dive
Testing Guide - Testing best practices
Wheels CLI is open source software licensed under the Apache License 2.0. See LICENSE for details.
Ready to get started? Head to the Quick Start Guide or explore the Command Reference.
Manage application dependencies using box.json.
wheels deps <action> [name] [options]The wheels deps command provides a streamlined interface for managing your Wheels application's dependencies through box.json. It integrates with CommandBox's package management system while providing Wheels-specific conveniences.
action
Required - Action to perform: list, install, update, remove, report
None
name
Package name (required for install/update/remove actions)
None
version
Specific version to install (optional, for install action only)
Latest version
--dev
Install as development dependency (install action only)
false
Display all dependencies from box.json with their installation status.
wheels deps listOutput shows:
Package name
Version specification
Type (Production/Development)
Installation status
Example output:
Dependencies:
cbvalidation @ ^4.6.0+28 (Production) - Not Installed
wirebox @ ^7.4.2+24 (Production) - Not Installed
shortcodes @ ^0.0.4 (Production) - Not Installed
Dev Dependencies:
testbox @ ^6.4.0+17 (Development) - InstalledInstall a new dependency and add it to box.json.
wheels deps install <name>
wheels deps install <name> <version>
wheels deps install <name> --devExamples:
# Install latest version as production dependency
wheels deps install cbvalidation
# Install specific version
wheels deps install cbvalidation 3.0.0
# Install as development dependency (saves to devDependencies)
wheels deps install testbox --devImportant: The --dev flag uses CommandBox's --saveDev flag internally, ensuring packages are correctly saved to the devDependencies section of box.json.
Update an existing dependency to the latest version allowed by its version specification.
wheels deps update <name>Example:
wheels deps update wireboxThe command will:
Check if the dependency exists in box.json
Determine if it's a production or dev dependency
Update to the latest compatible version
Show version change information
Remove a dependency from both box.json and the file system.
wheels deps remove <name>Example:
wheels deps remove oldpackageNote: Remove action will ask for confirmation before proceeding.
Generate a comprehensive dependency report with outdated package check.
wheels deps reportThe report includes:
Project information (name, version)
Wheels version
CFML engine details
All dependencies with installation status
Development dependencies
Installed modules information
Outdated package check
Export to JSON file
Example output:
Dependency Report:
Generated: 2025-09-19 11:38:44
Wheels Version: 3.0.0-SNAPSHOT
CFML Engine: Lucee 5.4.6.9
Dependencies:
cbvalidation @ ^4.6.0+28 - Installed: No
shortcodes @ ^0.0.4 - Installed: No
wirebox @ ^7.4.2+24 - Installed: No
Dev Dependencies:
testbox @ ^6.4.0+17 - Installed: Yes
Checking for outdated packages...
┌────────────────┬───────────┬──────────┬──────────┬─────────────────────┐
│ Package │ Installed │ Update │ Latest │ Location │
├────────────────┼───────────┼──────────┼──────────┼─────────────────────┤
│ testbox@^6.4.. │ 6.4.0+17 │ 6.4.0+17 │ 6.4.0+17 │ /testbox │
└────────────────┴───────────┴──────────┴──────────┴─────────────────────┘
There are no outdated dependencies!
Full report exported to: dependency-report-20250919-113851.jsonThe wheels deps commands delegate to CommandBox's package management system:
install uses box install with --saveDev flag for dev dependencies
update uses box update
remove uses box uninstall
report uses box outdated
This ensures compatibility with the broader CFML package ecosystem.
The command manages two dependency sections in box.json:
{
"dependencies": {
"wheels-core": "^3.0.0",
"wirebox": "^7"
}
}{
"devDependencies": {
"testbox": "^6",
"commandbox-cfformat": "*"
}
}The command checks for installed packages using the installPaths defined in box.json to determine actual installation locations. This ensures accurate detection regardless of where packages are installed:
Primary: Checks the exact path specified in box.json → installPaths
Fallback: Checks standard locations like /modules directory
Simple names: wirebox
Namespaced: forgebox:wirebox
Versioned: [email protected]
"installPaths": {
"testbox": "testbox/",
"wirebox": "wirebox/",
"cbvalidation": "modules/cbvalidation/",
"shortcodes": "plugins/Shortcodes/"
}The installation status reflects the physical presence of packages on the filesystem, not just their listing in box.json dependencies.
Common scenarios:
No box.json: Prompts to run box init
Package not found: Shows available dependencies
Update failures: Shows current and attempted versions
Network issues: Displays CommandBox error messages
Initialize First: Run box init before managing dependencies
Use Version Constraints: Specify version ranges for stability
Separate Dev Dependencies: Use --dev for test/build tools
Regular Updates: Run wheels deps report to check for outdated packages
Commit box.json: Always version control your dependency specifications
Dependencies are installed to the /modules directory
The command respects CommandBox's dependency resolution
Version specifications follow npm-style semver patterns
Dev dependencies are not installed in production environments
box install - CommandBox package installation
box.json - Package descriptor documentation
wheels init - Initialize a Wheels application
wheels plugins - Manage Wheels CLI plugins
A comprehensive guide to the Wheels directory structure...
Understanding the Wheels directory structure is essential whether you're building applications or contributing to the framework itself.
Wheels uses different directory layouts depending on your role:
Application Development: A streamlined structure optimized for building applications, available through ForgeBox or the CLI.
Framework Development: A comprehensive monorepo structure that includes development tools, tests, and documentation for framework contributors.
Both structures serve specific purposes, and understanding each will help you work more effectively with Wheels.
When you create a new Wheels application using wheels new or download from ForgeBox, you'll work with this focused project structure:
app/
controllers/
Controller.cfc
events/
global/
migrator/
migrations/
models/
Model.cfc
plugins/
views/
config/
public/
files/
images/
javascripts/
stylesheets/
miscellaneous/
Application.cfc
urlrewrite.xml
Application.cfc
index.cfm
tests/
TestBox/
vendorapp/controllers/ - Contains your controller files with a base Controller.cfc file already present. Place shared controller methods in Controller.cfc since all controllers inherit from this base class.
app/models/ - Houses your model files, typically one per database table. The existing Model.cfc file serves as the base class for all models and should contain shared model functionality.
app/views/ - Stores your view templates, organized by controller (e.g., views for the Users controller go in app/views/users/). This is where you prepare content for your users.
app/events/ - Contains event handlers that respond to ColdFusion application events, providing a cleaner alternative to placing code directly in Application.cfc.
app/global/ - Holds globally accessible functions available throughout your application.
plugins/ - Contains downloaded Wheels plugins that extend your application's functionality.
config/ - All configuration changes should be made here. Set environments, routes, and other application settings. Individual setting files in subdirectories can override main configuration.
public/files/ - Files intended for delivery to users via the sendFile() function should be placed here. Also serves as general file storage.
public/images/ - Recommended location for image assets. While not required, Wheels functions involving images assume this conventional location.
public/javascripts/ - Recommended location for JavaScript files.
public/stylesheets/ - Recommended location for CSS files.
public/miscellaneous/ - Special directory for code that must run completely outside the framework. Contains an empty Application.cfc that prevents Wheels involvement. Ideal for Flash AMF binding or <cfajaxproxy> connections to CFCs.
app/migrator/migrations - Database migration CFC files.
tests/ - Location for your application's unit tests.
public/urlrewrite.xml - Required for Tomcat/Tuckey or CommandBox URL rewriting.
public/Application.cfc and public/index.cfm - Framework bootstrap files. Do not modify these files.
Contributors working on the Wheels framework itself will encounter this comprehensive repository structure:
cli/
core/
src/
wheels/
design_docs/
docs/
examples/
templates/
base/
src/
app/
config/
public/
tests/
vendor/
.env
box.json
server.json
test-artifacts/
tests/
tools/
.cfformat.json
.editorconfig
CFConfig.json
CHANGELOG.md
compose.ymlcli/ - Source code for command-line interface tools including generators and database migration utilities.
core/src/wheels/ - The core Wheels framework code. This is the actual framework that gets distributed. When new versions are released, this directory often contains all necessary updates.
design_docs/ - Architecture documentation, design decisions, and planning materials explaining the framework's structural choices.
docs/ - Official documentation in Markdown format, synchronized with the public website at wheels.dev.
examples/ - Sample applications demonstrating various Wheels features, useful for testing framework changes in realistic scenarios.
/ - The exact application template structure that developers receive when creating new projects. This mirrors the application development structure described above and includes essential configuration files like box.json, server.json, and .env.
vendor/ - Third-party dependencies and packages used by the framework development environment. Contains libraries managed through CommandBox/ForgeBox package management.
test-artifacts/ - Files generated during test suite execution, typically excluded from version control.
tests/ - Complete TestBox test suite for framework validation and regression testing.
tools/ - Build scripts, Docker configurations, and development utilities for maintaining the framework.
Configuration files (.cfformat.json, .editorconfig, CFConfig.json) maintain consistent development standards across contributor environments.
When working on applications, your primary focus areas are:
Configuration: config/ directory for all settings
Application Logic: app/controllers/, app/models/, app/migrator/migrations, app/views/, and app/events/
Static Assets: public/files/, public/images/, public/javascripts/, and public/stylesheets/
Extensions: plugins/ for third-party functionality
The / folder in the framework repository becomes the root directory of every new Wheels application. When contributing to the framework:
Work primarily in core/src/wheels/ for framework code
Update docs/ for documentation changes
Test changes using applications in / or examples/
Use tests/ for framework testing
You can add additional directories to your application structure. When doing so:
Include a blank Application.cfc file in custom directories to prevent Wheels from processing requests in those locations
Follow the established naming conventions for consistency
Consider whether new directories belong in public/ (web-accessible) or app/ (application logic)
Environment Setup - Use the provided compose.yml file to test changes across multiple CFML engines, ensuring broad compatibility.
Testing Requirements - Execute the complete test suite located in /tests before submitting pull requests to prevent regressions.
Code Standards - Follow the formatting rules defined in .cfformat.json. Most development environments can automatically apply these standards.
Documentation Updates - Update relevant documentation in /docs when adding features or modifying existing behavior.
CLI Development - When working on command-line tools, ensure corresponding documentation updates in /docs/command-line-tools.
Configuration First - Begin development by setting up routes, environments, and database connections in the config/ directory.
MVC Architecture - Organize code according to the Model-View-Controller pattern that the directory structure supports:
Controllers handle requests and coordinate between models and views
Models manage data and business logic
Views present information to users
Asset Organization - Use the conventional public/ subdirectories for different asset types. This ensures Wheels functions work as expected and maintains project organization.
Plugin Integration - Evaluate existing plugins in plugins/ before developing custom solutions.
Database Management - Use the migration system (wheels db migrate) rather than manual SQL scripts for database schema changes.
Package Management - Use CommandBox and the box.json file to manage dependencies. The vendor/ directory contains installed packages and should be excluded from version control.
Local Development - Configure your development environment using server.json for server settings and .env for environment variables. Never commit sensitive data in .env files to version control.
Testing Strategy - Implement unit tests in tests/ to ensure application reliability.
This directory structure reflects years of framework development and community feedback. Each directory serves a specific purpose that supports either application development or framework contribution. The clear separation between public assets, application logic, and configuration ensures maintainable and scalable Wheels applications.
The Wheels CLI provides commands for managing static assets in your application, including compilation, optimization, and cleanup of JavaScript, CSS, and image files.
wheels assets precompile
wheels assets clean
wheels assets clobberPrepares your assets for production deployment by minifying and optimizing them.
# Basic precompilation (defaults to production)
wheels assets precompile
# Force recompilation of all assets
wheels assets precompile --force
# Target specific environments (with aliases)
wheels assets precompile --environment=production # Full minification
wheels assets precompile --environment=prod # Alias for production
wheels assets precompile --environment=staging # Light minification
wheels assets precompile --environment=stage # Alias for staging
wheels assets precompile --environment=testing # Light minification
wheels assets precompile --environment=test # Alias for testing
wheels assets precompile --environment=maintenance # Light minification
wheels assets precompile --environment=development # No minification
wheels assets precompile --environment=dev # Alias for developmentThe command applies different levels of asset optimization based on the target environment:
Production (production, prod): Full minification - Maximum compression, removes all comments and whitespace, optimizes code structure
Staging (staging, stage): Light minification - Removes comments and excessive whitespace but preserves some formatting for debugging
Testing (testing, test): Light minification - Same as staging, optimized for testing environments
Maintenance (maintenance): Light minification - Minimal processing for maintenance mode deployments
Development (development, dev): No minification - Preserves original formatting and comments for debugging
Minifies JavaScript files: Removes comments, whitespace, and unnecessary characters (level depends on environment)
Minifies CSS files: Removes comments, whitespace, and optimizes CSS rules (level depends on environment)
Generates cache-busted filenames: Adds MD5 hashes to filenames (e.g., application-a1b2c3d4.min.js)
Creates manifest.json: Maps original filenames to compiled versions
Processes images: Copies images with cache-busted names
Output location: Stores all compiled assets in /public/assets/compiled/
{
"application.js": "application-a1b2c3d4.min.js",
"admin.js": "admin-b2c3d4e5.min.js",
"styles.css": "styles-c3d4e5f6.min.css",
"admin.css": "admin-d4e5f6g7.min.css",
"logo.png": "logo-e5f6g7h8.png",
"banner.jpg": "banner-f6g7h8i9.jpg"
}Removes old compiled assets while keeping recent versions for rollback capability.
# Clean old assets (keeps 3 most recent versions by default)
wheels assets clean
# Keep 5 versions of each asset
wheels assets clean --keep=5
# Preview what would be deleted without actually deleting
wheels assets clean --dryRunIdentifies old versions: Finds all compiled assets with hash fingerprints
Keeps recent versions: Retains the specified number of most recent versions (default: 3)
Removes old files: Deletes older versions to free disk space
Updates manifest: Ensures manifest.json remains current
Dry run option: Preview deletions without making changes
Completely removes all compiled assets and the manifest file.
# Remove all compiled assets (with confirmation prompt)
wheels assets clobber
# Skip confirmation prompt
wheels assets clobber --forceDeletes compiled directory: Removes /public/assets/compiled/ and all contents
Removes manifest: Deletes the manifest.json file
Confirmation prompt: Asks for confirmation unless --force is used
Complete cleanup: Useful for fresh starts or troubleshooting
Before deployment:
# Compile assets for production
wheels assets precompile --environment=production
# Clean old versions to save space
wheels assets clean --keep=3After deployment verification:
# If rollback needed, previous versions are still available
# If deployment successful, further cleanup can be done
wheels assets clean --keep=2During development, you typically don't need compiled assets:
# Remove all compiled assets in development
wheels assets clobber --force
# Precompile only when testing production builds
wheels assets precompile --environment=developmentExample CI/CD pipeline step:
# .github/workflows/deploy.yml
- name: Compile Assets
run: |
wheels assets precompile --environment=production
wheels assets clean --keep=3After running wheels assets precompile:
/public/
/assets/
/compiled/
manifest.json
application-a1b2c3d4.min.js
styles-e5f6g7h8.min.css
logo-i9j0k1l2.png
/javascripts/
application.js (original)
/stylesheets/
styles.css (original)
/images/
logo.png (original)Configure asset handling in your Wheels application:
// config/settings.cfm
// Enable asset fingerprinting in production
set(useAssetFingerprinting = true);
// Set asset cache duration (in minutes)
set(assetsCacheMinutes = 1440); // 24 hours
// Define assets path
set(assetsPath = "/assets");# Force recompilation
wheels assets precompile --force
# Verify manifest exists and is current
cat public/assets/compiled/manifest.json# Check space used by compiled assets
du -sh public/assets/compiled/
# Aggressive cleanup - keep only 1 version
wheels assets clean --keep=1
# Or remove everything and recompile
wheels assets clobber --force
wheels assets precompile# Ensure assets were compiled for the correct environment
wheels assets precompile --environment=production
# Check that manifest.json exists
ls -la public/assets/compiled/manifest.jsonBackup: Always backup assets before running clobber in production
Version Control: Don't commit compiled assets to version control
Deployment: Run precompile as part of your deployment process
Performance: Compiled assets significantly improve load times
Cache Busting: Hash fingerprints ensure browsers load updated assets


This command works correctly without options (parameters). Option support is under development and will be available soon.
Create a new Wheels application from templates.
This command supports multiple parameter formats:
Positional parameters: wheels generate app blog (most common)
Named parameters: name=value (e.g., name=blog, template=HelloWorld)
Flag parameters: --flag equals flag=true (e.g., --useBootstrap equals useBootstrap=true)
Parameter Mixing Rules:
✅ ALLOWED:
All positional: wheels generate app blog
All positional + flags: wheels generate app blog --useBootstrap --init
All named: name=blog template=HelloWorld --useBootstrap
❌ NOT ALLOWED:
Positional + named: wheels generate app blog name=myapp (causes error)
Recommendation: Use positional for name/template, flags for options: wheels generate app blog --useBootstrap --init
The wheels generate app command creates a new Wheels application with a complete directory structure, configuration files, and optionally sample code. It supports multiple templates for different starting points.
Backend Edition template
Complete MVC structure
Sample code and configuration
H2 database setup by default
Simple "Hello World" example
One controller and view
Great for learning
Dynamic content example
Database interaction
Form handling
Static pages example
Layout system
Navigation structure
H2 is setup by default (--setupH2=true)
No external database needed
Perfect for development
Auto-configured datasource
To disable: --setupH2=false
Create application:
Configure in CommandBox:
Navigate to directory
Install dependencies
Start server
Open browser
Create custom templates in ~/.commandbox/cfml/modules/wheels-cli/templates/apps/:
Use descriptive application names
Choose appropriate template for project type
Set secure reload password for production
Configure datasource before starting
Run tests after generation
Directory exists: Use --force or choose different name
Template not found: Check available templates with wheels info
Datasource errors: Configure database connection
Port conflicts: Change port in server.json
- Initialize existing application
- Interactive app creation
- Generate CRUD scaffolding
Analyzes application performance, identifying bottlenecks and optimization opportunities in your Wheels application.
The analyze performance command monitors your Wheels application to identify performance bottlenecks and provide optimization recommendations. It tracks metrics in real-time and provides both console output and optional HTML reports.
Request Performance: Response times, slow requests, controller/action patterns
Database Queries: Query execution times, slow queries, query patterns
Memory Usage: Memory consumption, peak usage, memory trends
View Rendering: Template rendering times (when target includes views)
Overall Health: Performance score and grade (A-F)
The analyzer assigns a performance grade based on collected metrics:
A (90-100): Excellent performance
B (80-89): Good performance
C (70-79): Acceptable performance
D (60-69): Poor performance, optimization needed
F (0-59): Critical performance issues
Monitor all metrics for 30 seconds:
Analyze for 2 minutes:
Monitor only database queries:
Monitor only memory consumption:
Set slow request threshold to 200ms:
Attempt to collect real metrics (if available):
Create a detailed HTML report with charts:
Full analysis with all options:
The HTML report includes:
Performance Dashboard: Visual metrics with color-coded indicators
Performance Grade: Large visual display of grade and score
Charts:
Response time trends over monitoring period
Memory usage over time
Detailed Tables:
Top 10 slow requests with timing
Top 10 slow queries with execution times
Recommendations: Context-aware optimization suggestions
Reports are saved to: reports/performance-[timestamp].html
Generates realistic performance patterns
Useful for testing and demonstration
Provides consistent baseline metrics
When enabled, attempts to:
Access ColdFusion's metrics service
Hook into Wheels debug information
Collect real request/response times
Capture actual query execution data
Falls back to simulation if real data unavailable
Default thresholds used for categorization:
Slow Requests: > 100ms (configurable via --threshold)
Slow Queries: > 50ms (fixed)
High Memory: > 500MB
Response Time Categories:
Fast: < 100ms
Moderate: 100-300ms
Slow: > 300ms
The analyzer provides context-aware recommendations based on:
High Average Response Time (>200ms): Suggests caching strategies
Slow Queries Detected: Recommends indexing and query optimization
High Memory Usage (>500MB): Suggests memory optimization
Multiple Slow Requests: Recommends async processing and lazy loading
Exit codes for automation:
0: Performance acceptable
1: Performance issues detected (slow requests or queries found)
Example Jenkins integration:
Run regularly in staging environment
Compare metrics over time to track improvements
Use HTML reports for stakeholder communication
Set appropriate thresholds based on application requirements
Baseline First: Establish performance baselines before optimization
Target Specific Areas: Use --target to focus on suspected bottlenecks
Realistic Load: Run during typical usage patterns for accurate results
Profile Mode: Enable --profile when real metrics are needed
Regular Monitoring: Schedule regular performance checks
Track Trends: Save reports to track performance over time
Simulated Data: Default mode uses simulated data; enable --profile for real metrics
Single Instance: Monitors single application instance only
External Services: Doesn't track external API or service calls
Browser Performance: Server-side only, doesn't measure client-side performance
Ensure application has debug mode available
Check ColdFusion administrator settings
Verify appropriate permissions for metrics access
Normal for JVM applications
Monitor trends rather than absolute values
Consider JVM heap settings
Adjust threshold to match application expectations
Check if application is under unusual load
Verify database connection pool settings
Generate a migration file for adding columns to an existing database table.
Alias: wheels db create column
This command supports multiple parameter formats:
Named parameters: name=value (e.g., name=users, dataType=string, columnName=email)
Flag parameters: --flag equals flag=true (e.g., --allowNull=false)
Flag with value: --flag=value (same as named parameters)
Parameter Mixing Rules:
✅ ALLOWED:
All named: name=users dataType=string columnName=email
Named parameters only (no positional support)
❌ NOT ALLOWED:
Positional parameters: This command does not support positional parameters
Recommendation: Use named parameters for all values: name=users dataType=string columnName=email allowNull=false
The dbmigrate create column command generates a migration file that adds a column to an existing database table. It supports standard column types and various options for column configuration.
string - VARCHAR(255)
text - TEXT/CLOB
integer - INTEGER
biginteger - BIGINT
float - FLOAT
decimal - DECIMAL
boolean - BOOLEAN/BIT
date - DATE
time - TIME
datetime - DATETIME/TIMESTAMP
timestamp - TIMESTAMP
binary - BLOB/BINARY
The generated migration file will be named with a timestamp and description:
Example:
For the command:
Generates:
Add preference column to user table:
Add tracking column to any table:
Add decimal columns for pricing:
For existing tables with data, make new columns nullable or provide defaults:
Choose the right column type for your data:
This command creates one column at a time:
Think through column requirements before creating:
Data type and size
Null constraints
Default values
Index requirements
Add foreign key columns with appropriate types:
For special column types, use blank migrations:
This command adds columns, not modifies them:
The migration includes automatic rollback with removeColumn()
Column order in down() is reversed for proper rollback
Always test migrations with data in development
Consider the impact on existing queries and code
- Create new tables
- Create custom migrations
- Remove tables
- Run migrations
- Rollback migrations
List all available environments for your Wheels application.
The wheels env list command displays all configured environments in your Wheels application. It shows environment details, current active environment, and configuration status.
OK Valid - Configuration is valid and working
Active - Currently active environment
WARN Invalid - Configuration errors
When using --check:
Configuration file exists
Syntax is valid
Database connection works
Required settings present
Development: Local development
Testing: Automated testing
Staging: Pre-production
Production: Live environment
User-defined environments
Special purpose configs
Client-specific setups
When using --verbose, shows:
Configuration:
Config file path
Last modified date
File size
Database:
Database name
Datasource name
Settings:
Debug mode
Cache settings
Custom configurations
Check /config/ directory
Verify environment.cfm exists
Run wheels env setup to create
Check configuration syntax
Verify database credentials
Test database connection
Check WHEELS_ENV variable
Verify environment.cfm logic
Set environment explicitly
Regular Checks: Validate environments periodically
Documentation: Keep environment purposes clear
Consistency: Use consistent naming
Cleanup: Remove unused environments
Security: Don't expose production details
Current environment marked with asterisk (*)
Invalid environments shown but marked
Verbose mode may expose sensitive data
JSON format useful for automation
- Environment management overview
- Setup new environment
- Switch environments
- List configuration
Install a Wheels plugin from ForgeBox into the /plugins folder.
This command supports multiple parameter formats:
Positional parameters: wheels plugins install cfwheels-bcrypt (plugin name)
Named parameters: name=value (e.g., name=cfwheels-bcrypt, version=1.0.0)
Flag parameters: --flag equals flag=true (e.g., --dev equals dev=true)
Flag with value: --flag=value (e.g., --version=1.0.0)
Parameter Mixing Rules:
✅ ALLOWED:
Positional: wheels plugins install cfwheels-bcrypt
Positional + flags: wheels plugins install cfwheels-bcrypt --version=1.0.0
All named: name=cfwheels-bcrypt version=1.0.0
❌ NOT ALLOWED:
Positional + named for same param: wheels plugins install cfwheels-bcrypt name=other
Recommendation: Use positional for plugin name, flags for options: wheels plugins install cfwheels-bcrypt --version=1.0.0
The plugins install command installs cfwheels-plugins type packages from ForgeBox into your application's /plugins folder. The command validates that the package is a valid cfwheels-plugin before installation.
Installs only cfwheels-plugins type packages
Validates package type before installation
Automatically places plugins in /plugins folder
Supports specific version installation
Beautiful, color-coded output
Helpful error messages
The command ensures that only packages with type cfwheels-plugins can be installed. This prevents accidental installation of non-plugin packages.
Output:
Output:
The command will find and install cfwheels-bcrypt from ForgeBox.
Output:
Output:
Display Header: Shows plugin name and target version
Package Validation: Verifies the package is type cfwheels-plugins
Download: Uses CommandBox's PackageService to download from ForgeBox
Installation: CommandBox installs the package
Directory Move: If installed to wrong location, moves to /plugins folder
Verification: Confirms installation success
Display Results: Shows success message with helpful next steps
The command uses PluginService which:
Calls ForgeBox API to check package type
Uses packageService.installPackage() to download and install
Checks common installation paths (/modules/, root)
Moves plugin to /plugins/ folder if needed
Returns success/failure status
The command only supports installing from ForgeBox:
The following sources are NOT supported:
❌ GitHub repositories
❌ Direct URLs
❌ Local ZIP files
❌ Local directories
To install plugins from these sources, use CommandBox's native install command and manually move to /plugins folder.
Solution: Verify the package type on ForgeBox is cfwheels-plugins
Solution: Check available plugins with wheels plugin list --available
Solution: Check internet connection and ForgeBox status
Only installs cfwheels-plugins type packages from ForgeBox
Plugins are installed to /plugins folder
The --dev parameter is accepted but not currently used
Package type validation prevents installation of incorrect packages
If a plugin is already installed, it will be overwritten
After installation, use wheels plugin info <name> to view details
Restart your application to activate the new plugin
The command automatically handles directory placement
- List installed plugins
- View plugin details
- Update a plugin
- Remove a plugin
The wheels get environment command displays the current environment setting for your Wheels application. It automatically detects which environment your application is configured to run in (development, staging, production, etc.) and shows where this configuration is coming from.
This command takes no parameters.
This will output something like:
The command checks for environment configuration in the following order of precedence:
.env file - Looks for WHEELS_ENV variable first, then Environment variable
System environment variable - Checks for WHEELS_ENV first, then Environment system variable
Default - Falls back to development if no configuration is found
The first valid configuration found is used and reported, along with which specific variable name was found.
The command first looks for a WHEELS_ENV variable in your application's .env file:
If WHEELS_ENV is not found, it then checks for Environment:
The regex pattern used ensures it correctly reads the value while ignoring:
Comments after the value (anything after #)
Trailing whitespace
Lines that are commented out
If not found in .env, it checks for system-level environment variables in the same order:
If no configuration is found anywhere, it defaults to development.
The command checks for two different variable names in this specific order:
WHEELS_ENV in .env file
Environment in .env file
WHEELS_ENV system environment variable
Environment system environment variable
Default to development
The command will show an error if:
It's not run from a Wheels application directory
There's an error reading configuration files
File permissions prevent reading configuration
Use WHEELS_ENV - Prefer WHEELS_ENV over Environment for clarity and consistency
Consistent Configuration - Use one primary method for setting environment across your team
Environment-Specific Files - Consider using .env.production, .env.development files with the merge command
Don't Commit Production Settings - Keep production .env files out of version control
Document Your Setup - Document which configuration method and variable name your team uses
Verify Before Deployment - Always run this command to verify environment before deploying
Understanding precedence is important when multiple configurations exist:
If both WHEELS_ENV and Environment are set in .env, only WHEELS_ENV will be used.
If you're migrating from a system that uses different environment variable names:
You can migrate gradually since the command checks both:
Leave existing Environment variables in place
Start using WHEELS_ENV for new deployments
The command will prefer WHEELS_ENV when both exist
This command works well with other Wheels CLI commands:
The command must be run from your Wheels application root directory
Environment values are case-sensitive (development ≠ Development)
Comments in .env files are properly ignored using #
Whitespace around values is automatically trimmed
The command clearly shows which variable name was found
WHEELS_ENV takes precedence over Environment when both exist
If changing environment variables doesn't seem to work:
Run wheels get environment to see which source and variable is being used
Remember .env file takes precedence over system variables
Remember WHEELS_ENV takes precedence over Environment
Restart your CommandBox server after changes
Check for typos in variable names
If the wrong environment is being detected:
Check if both WHEELS_ENV and Environment are set
Remember WHEELS_ENV has higher priority
Use the output to see exactly which variable is being read
If you get permission errors:
Ensure you have read access to .env files
Check that you're in the correct directory
Verify file ownership and permissions
If you're getting the default development when you expect a different value:
Check for typos in configuration files (it's WHEELS_ENV not WHEEL_ENV)
Ensure .env file is in the application root
Verify system environment variables are properly exported
Check that values don't have quotes unless intended
The wheels env merge command allows you to merge multiple environment configuration files (.env files) into a single consolidated file. This is particularly useful when working with different environments (development, staging, production) or when you have base configurations that need to be combined with environment-specific overrides.
This merges .env.defaults and .env.local into .env.merge
This merges the files and saves the result as .env
Combines base configuration with production-specific settings
Merges multiple files in the specified order (unlimited number of source files supported)
Shows what the merged result would look like without creating a file
Files are processed in the order they are specified on the command line. Later files take precedence over earlier ones when there are conflicting variable names.
Properties format (standard .env format):
JSON format:
Empty lines and comments (lines starting with #) are skipped in properties files
Values can contain = signs (everything after the first = is considered the value)
Leading and trailing whitespace is trimmed from keys and values
The command automatically detects whether a file is JSON or properties format
When the same variable exists in multiple files:
The value from the last processed file wins
Conflicts are tracked and reported with details showing:
The variable name
The original value and source file
The new value and source file
You'll see a summary showing which values were overridden
The merged output file is automatically organized:
Variables are grouped by prefix (e.g., DATABASE_*, API_*)
Groups are sorted alphabetically
Variables within groups are sorted alphabetically
Includes generated header comments with:
Generation timestamp
Source information
Section headers for each variable group
When using --dryRun or viewing output, sensitive values are automatically masked:
Variables containing password, secret, key, or token (case-insensitive) show as ***MASKED***
The actual values are still written to the output file (only display is masked)
Each variable shows its source file for traceability
The command will stop and show an error if:
Source files don't exist
Less than two source files are provided
Output file cannot be written (permissions, disk space, etc.)
File read operations fail
Default output filename is .env.merge (not .env.merged)
Multiple files supported - You can merge any number of files (not just 2 or 3)
Option format - Use double dashes for options: --output, --dryRun
Value preservation - Values containing = signs are properly preserved
Comment handling - Comments in source files are not preserved in the merged output
Use descriptive file names that indicate their purpose (.env.base, .env.production, .env.local)
Order files by precedence - place base/default files first, overrides last
Use dry-run first to preview results before committing to a merge
Keep sensitive data in local files that aren't committed to version control
Document your merge strategy in your project's README
Backup important configurations before merging
Review conflicts - Pay attention to the conflicts report to ensure expected overrides
The merged file includes helpful comments showing when it was generated
Variables are automatically organized by prefix for better readability
Use the --dryRun option to understand what changes will be made
The command validates all source files exist before starting the merge process
You can merge as many files as needed in a single command
The source file information is preserved during dry runs for debugging
wheels analyze performance [--target=<target>] [--duration=<seconds>] [--report] [--threshold=<ms>] [--profile]--target
Analysis target: all, controller, view, query, memory
all
--duration
Duration to run analysis in seconds (1-300)
30
--report
Generate HTML performance report with charts
false
--threshold
Performance threshold in milliseconds for slow requests
100
--profile
Enable profiling mode for real metrics (when available)
false
wheels analyze performancewheels analyze performance --duration=120wheels analyze performance --target=querywheels analyze performance --target=memorywheels analyze performance --threshold=200wheels analyze performance --profilewheels analyze performance --reportwheels analyze performance --target=all --duration=60 --threshold=200 --profile --reportAnalyzing application performance...
Starting performance monitoring for 30 seconds...
Target: all
Threshold: 100ms
[====================] 100% Complete!
==================================================
PERFORMANCE ANALYSIS COMPLETE
==================================================
Data Source: SIMULATED (Enable --profile for real data)
Request Performance
--------------------------------------------------
Requests Analyzed: 42
Average Response Time: 156ms
Slowest Request: 891ms
Fastest Request: 23ms
Slow Requests (>100ms): 18
Database Performance
--------------------------------------------------
Queries Executed: 42
Average Query Time: 28ms
Slow Queries (>50ms): 8
Memory Usage
--------------------------------------------------
Average Memory: 193MB
Peak Memory: 205MB
Top Slow Requests:
--------------------------------------------------
1. reports.index() - 891ms
2. users.create() - 645ms
3. products.update() - 523ms
4. orders.index() - 412ms
5. dashboard.show() - 387ms
Top Slow Queries:
--------------------------------------------------
1. SELECT * FROM orders WHERE id = ? - 187ms
2. UPDATE products WHERE id = ? - 156ms
3. SELECT * FROM users WHERE id = ? - 98ms
==================================================
Performance Grade: B (83/100)
==================================================
Performance Recommendations:
--------------------------------------------------
* Implement caching strategies for frequently accessed data
* Optimize database queries and add appropriate indexes
* Enable query result caching in production
* Minimize database round trips
* Use connection pooling for database connectionsstage('Performance Check') {
steps {
sh 'wheels analyze performance --duration=60 --threshold=200'
}
}wheels get environmentwheels get envwheels get environmentCurrent Environment:
development
Configured in: .env file (WHEELS_ENV)# .env file - Primary variable
WHEELS_ENV=production
DATABASE_HOST=localhost# .env file - Alternative variable
Environment=staging
DATABASE_HOST=localhost# Linux/Mac - Primary variable
export WHEELS_ENV=staging
# Windows - Primary variable
set WHEELS_ENV=staging
# Or using the alternative variable
export Environment=productionCurrent Environment:
production
Configured in: .env file (WHEELS_ENV)Current Environment:
staging
Configured in: .env file (Environment)Current Environment:
staging
Configured in: System environment variable (WHEELS_ENV)Current Environment:
production
Configured in: System environment variable (Environment)Current Environment:
development
Configured in: Using default# Check environment before starting server
wheels get environment
commandbox server start# Verify which configuration source and variable is being used
wheels get environment
# Check each source manually
cat .env | grep -E "WHEELS_ENV|Environment"
echo $WHEELS_ENV
echo $Environment# In deployment script
wheels get environment
if [ $? -eq 0 ]; then
echo "Environment configured successfully"
fi# If migrating from a system that uses "Environment" variable
# Both will work without changes:
# Old: Environment=production
# New: WHEELS_ENV=production
wheels get environmentError: This command must be run from a Wheels application directoryError reading environment: [specific error message].env file - WHEELS_ENV (highest priority)
↓
.env file - Environment
↓
System variable - WHEELS_ENV
↓
System variable - Environment
↓
Default: development (lowest priority)# Old configuration
Environment=production
# New configuration (both work)
WHEELS_ENV=production
# The command will detect either one
wheels get environment# Check environment, then run migrations
wheels get environment
wheels db migrate
# Verify environment before running tests
wheels get environment
wheels test
# Check environment, then start server
wheels get environment
commandbox server startwheels env merge <source1> <source2> [source3...] [options]source1
string
Yes
First source .env file to merge
source2
string
Yes
Second source .env file to merge
source3, source4, ...
string
No
Additional source .env files to merge (unlimited)
--output
string
No
Output file name (default: .env.merge)
--dryRun
flag
No
Show what would be merged without actually writing the file
wheels env merge .env.defaults .env.localwheels env merge .env.defaults .env.local --output=.envwheels env merge source1=.env source2=.env.production --output=.env.mergedwheels env merge source1=base.env source2=common.env source3=dev.env source4=local.env --output=.env.developmentwheels env merge base.env override.env --dryRunDATABASE_HOST=localhost
DATABASE_PORT=5432
API_KEY=your-secret-key{
"DATABASE_HOST": "localhost",
"DATABASE_PORT": "5432",
"API_KEY": "your-secret-key"
}# Start with base configuration
wheels env merge source1=.env.base source2=.env.development --output=.env
# Add local overrides
wheels env merge source1=.env source2=.env.local --output=.env# Merge base, environment, and local configs
wheels env merge source1=.env.base source2=.env.staging source3=.env.local --output=.env# Create production configuration
wheels env merge source1=.env.base source2=.env.production --output=.env.prod
# Preview staging configuration
wheels env merge source1=.env.base source2=.env.staging --dryRun# Check what the final configuration looks like
wheels env merge source1=.env.defaults source2=.env.current --dryRunMerging environment files:
1. .env.defaults
2. .env.local
3. .env.override
Merged 3 files into .env.merge
Total variables: 15
Conflicts resolved (later files take precedence):
DATABASE_HOST: 'db.example.com' (.env.defaults) → 'localhost' (.env.local)
DEBUG_MODE: 'false' (.env.defaults) → 'true' (.env.override)Merged result (DRY RUN):
DATABASE Variables:
DATABASE_HOST = localhost (from .env.local)
DATABASE_NAME = myapp (from .env.defaults)
DATABASE_PASSWORD = ***MASKED*** (from .env.local)
DATABASE_PORT = 5432 (from .env.defaults)
API Variables:
API_BASE_URL = https://api.example.com (from .env.defaults)
API_KEY = ***MASKED*** (from .env.local)
API_TOKEN = ***MASKED*** (from .env.override)
Other Variables:
APP_NAME = MyApplication (from .env.defaults)
DEBUG_MODE = true (from .env.override)# Merged Environment Configuration
# Generated by wheels env merge command
# Date: 2024-12-15 14:30:45
# API Configuration
API_BASE_URL=https://api.example.com
API_KEY=actual-key-value
API_TOKEN=actual-token-value
# DATABASE Configuration
DATABASE_HOST=localhost
DATABASE_NAME=myapp
DATABASE_PASSWORD=actual-password
DATABASE_PORT=5432
# Other Configuration
APP_NAME=MyApplication
DEBUG_MODE=truewheels generate app [name] [template] [directory] [options]
wheels g app [name] [template] [directory] [options]
wheels new [name] [template] [directory] [options]name
Application name
MyApp
template
Template to use
wheels-base-template@BE
directory
Target directory
./{name}
reloadPassword
Set reload password
'' (empty)
datasourceName
Database datasource name
App name
cfmlEngine
CFML engine (lucee/adobe/boxlang)
lucee
--useBootstrap
Include Bootstrap CSS
false
--setupH2
Setup H2 embedded database
true
--init
Initialize as CommandBox package
false
--force
Overwrite existing directory
false
wheels generate app myappwheels generate app myapp HelloWorldwheels generate app myapp HelloDynamicwheels generate app myapp HelloPages# Positional (recommended)
wheels generate app blog
# OR all named
wheels g app name=blog# Positional (recommended)
wheels generate app api HelloDynamic
# OR all named
wheels g app name=api template=HelloDynamic# Positional + named (recommended)
wheels generate app myapp --directory=./projects/
# OR all named
wheels g app name=myapp directory=./projects/# Positional + flag (recommended)
wheels generate app portfolio --useBootstrap
# OR all named
wheels g app name=portfolio useBootstrap=true# Positional + flag (recommended)
wheels generate app demo --setupH2
# OR all named
wheels g app name=demo setupH2=true# Positional + flags (recommended)
wheels generate app enterprise --template=HelloDynamic --directory=./apps/ \
--reloadPassword=secret \
--datasourceName=enterprise_db \
--cfmlEngine=adobe \
--useBootstrap \
--setupH2
# OR all named
wheels g app name=enterprise template=HelloDynamic directory=./apps/ \
reloadPassword=secret \
datasourceName=enterprise_db \
cfmlEngine=adobe \
useBootstrap=true \
setupH2=truemyapp/
├── .wheels-cli.json # CLI configuration
├── box.json # Dependencies
├── server.json # Server configuration
├── Application.cfc # Application settings
├── config/
│ ├── app.cfm # App configuration
│ ├── routes.cfm # URL routes
│ └── settings.cfm # Framework settings
├── controllers/
│ └── Main.cfc # Default controller
├── models/
├── views/
│ ├── layout.cfm # Default layout
│ └── main/
│ └── index.cfm # Home page
├── public/
│ ├── stylesheets/
│ ├── javascripts/
│ └── images/
├── tests/
└── wheels/ # Framework files{
"name": "myapp",
"version": "1.0.0",
"dependencies": {
"wheels": "^2.5.0"
}
}{
"web": {
"http": {
"port": 3000
}
},
"app": {
"cfengine": "lucee5"
}
}{
"name": "myapp",
"version": "1.0.0",
"framework": "wheels",
"reload": "wheels"
}wheels generate app myappwheels generate app myapp datasourceName=myapp_db --setupH2=falseserver set app.datasources.myapp_db={...}cd myappbox installbox server starthttp://localhost:3000mytemplate/
├── config/
├── controllers/
├── models/
├── views/
└── template.jsonwheels dbmigrate create column name=<table_name> dataType=<type> columnName=<column> [options]name
string
Yes
-
The name of the database table to modify
dataType
string
Yes
-
The column type to add
columnName
string
Yes
-
The column name to add
default
any
No
-
The default value to set for the column
allowNull
boolean
No
true
Should the column allow nulls
limit
number
No
-
The character limit of the column
precision
number
No
-
The precision of the numeric column
scale
number
No
-
The scale of the numeric column
[timestamp]_create_column_[columnname]_in_[tablename]_table.cfc20240125160000_create_column_email_in_user_table.cfcwheels dbmigrate create column name=user dataType=string columnName=emailwheels dbmigrate create column name=user dataType=boolean columnName=is_active default=truewheels dbmigrate create column name=user dataType=string columnName=bio allowNull=true limit=500wheels dbmigrate create column name=product dataType=decimal columnName=price precision=10 scale=2wheels dbmigrate create column name=user dataType=string columnName=phone allowNull=truecomponent extends="wheels.migrator.Migration" hint="create column phone in user table" {
function up() {
transaction {
addColumn(table="user", columnType="string", columnName="phone", allowNull=true);
}
}
function down() {
transaction {
removeColumn(table="user", column="phone");
}
}
}# Create separate migrations for each column
wheels dbmigrate create column name=user dataType=boolean columnName=newsletter_subscribed default=true
wheels dbmigrate create column name=user dataType=string columnName=theme_preference default="light"wheels dbmigrate create column name=product dataType=integer columnName=last_modified_by allowNull=true
wheels dbmigrate create column name=product dataType=datetime columnName=last_modified_at allowNull=truewheels dbmigrate create column name=product dataType=decimal columnName=price precision=10 scale=2 default=0
wheels dbmigrate create column name=product dataType=decimal columnName=cost precision=10 scale=2# Good - nullable
wheels dbmigrate create column name=user dataType=text columnName=bio allowNull=true
# Good - with default
wheels dbmigrate create column name=user dataType=string columnName=status default="active"
# Bad - will fail if table has data (not nullable, no default)
wheels dbmigrate create column name=user dataType=string columnName=required_field --allowNull=false# For short text
wheels dbmigrate create column name=user dataType=string columnName=username limit=50
# For long text
wheels dbmigrate create column name=post dataType=text columnName=content
# For money
wheels dbmigrate create column name=invoice dataType=decimal columnName=amount precision=10 scale=2# Create separate migrations for related columns
wheels dbmigrate create column name=customer dataType=string columnName=address_line1
wheels dbmigrate create column name=customer dataType=string columnName=city
wheels dbmigrate create column name=customer dataType=string columnName=state limit=2# Add foreign key column
wheels dbmigrate create column name=order dataType=integer columnName=customer_id
# Then create index in separate migration
wheels dbmigrate create blank name=add_order_customer_id_index# Create blank migration for custom column types
wheels dbmigrate create blank name=add_user_preferences_json
# Then manually add the column with custom SQL# This will fail if table has data
wheels dbmigrate create column name=user dataType=string columnName=required_field --allowNull=false
# Do this instead
wheels dbmigrate create column name=user dataType=string columnName=required_field default="pending"# Wrong - trying to change existing column type
wheels dbmigrate create column name=user dataType=integer columnName=age
# Right - use blank migration for modifications
wheels dbmigrate create blank name=change_user_age_to_integerwheels env list [options]--format
Output format (table, json, yaml)
table
--verbose
Show detailed configuration
false
--check
Validate environment configurations
false
--filter
Filter by environment type
All
--sort
Sort by (name, type, modified)
name
--help
Show help information
wheels env listwheels env list --verbosewheels env list --format=jsonwheels env list --checkwheels env list --filter=productionAvailable Environments
=====================
NAME TYPE DATABASE STATUS
development * Development wheels_dev OK Valid
testing Testing wheels_test OK Valid
staging Staging wheels_staging OK Valid
production Production wheels_prod OK Valid
qa Custom wheels_qa WARN Invalid
* = Current environmentAvailable Environments
=====================
development * [Active]
Type: Development
Database: wheels_dev
Datasource: wheels_development
Debug: Enabled
Cache: Disabled
Config: /config/development/settings.cfm
Modified: 2024-01-10 14:23:45
testing
Type: Testing
Database: wheels_test
Datasource: wheels_testing
Debug: Enabled
Cache: Disabled
Config: /config/testing/settings.cfm
Modified: 2024-01-08 09:15:22
staging
Type: Staging
Database: wheels_staging
Datasource: wheels_staging
Debug: Partial
Cache: Enabled
Config: /config/staging/settings.cfm
Modified: 2024-01-12 16:45:00{
"environments": [
{
"name": "development",
"type": "Development",
"active": true,
"database": "wheels_dev",
"datasource": "wheels_development",
"debug": true,
"cache": false,
"configPath": "/config/development/settings.cfm",
"lastModified": "2024-01-10T14:23:45Z",
"status": "valid"
},
{
"name": "production",
"type": "Production",
"active": false,
"database": "wheels_prod",
"datasource": "wheels_production",
"debug": false,
"cache": true,
"configPath": "/config/production/settings.cfm",
"lastModified": "2024-01-12T16:45:00Z",
"status": "valid"
}
],
"current": "development",
"total": 5
}# Production environments only
wheels env list --filter=production
# Development environments
wheels env list --filter=development# Valid environments only
wheels env list --filter=valid
# Environments with issues
wheels env list --filter=issues# Environments containing "prod"
wheels env list --filter="*prod*"
# Can also be written as
wheels env list --filter=*prod*wheels env list --sort=namewheels env list --sort=typewheels env list --sort=modifiedwheels plugins install <name> [--dev] [--version=<version>]name
Yes
string
Plugin name or slug from ForgeBox
dev
No
boolean
Install as development dependency (not used)
version
No
string
Specific version to install (default: latest)
wheels plugins install cfwheels-bcrypt===========================================================
Installing Plugin
===========================================================
Plugin: cfwheels-bcrypt
Version: latest
[CommandBox installation output...]
===========================================================
[OK] Plugin installed successfully!
Bcrypt encryption support for Wheels
Commands:
wheels plugin list View all installed plugins
wheels plugin info cfwheels-bcrypt View plugin detailswheels plugins install cfwheels-shortcodes --version=0.0.3===========================================================
Installing Plugin
===========================================================
Plugin: cfwheels-shortcodes
Version: 0.0.3
[CommandBox installation output...]
===========================================================
[OK] Plugin installed successfully!
Shortcode support for Wheels content
Commands:
wheels plugin list View all installed plugins
wheels plugin info cfwheels-shortcodes View plugin detailswheels plugins install bcryptwheels plugins install commandbox-migrations===========================================================
Installing Plugin
===========================================================
Plugin: commandbox-migrations
Version: latest
===========================================================
[ERROR] Failed to install plugin
Error: Only cfwheels-plugins can be installed via this command
Possible solutions:
- Verify the plugin name is correct
- Check if the plugin exists on ForgeBox:
wheels plugin list --available
- Ensure the plugin type is 'cfwheels-plugins'wheels plugins install nonexistent-plugin===========================================================
Installing Plugin
===========================================================
Plugin: nonexistent-plugin
Version: latest
===========================================================
[ERROR] Failed to install plugin
Error: Plugin not found on ForgeBox
Possible solutions:
- Verify the plugin name is correct
- Check if the plugin exists on ForgeBox:
wheels plugin list --available
- Ensure the plugin type is 'cfwheels-plugins'# By slug
wheels plugins install cfwheels-bcrypt
# By name (auto-finds slug)
wheels plugins install bcrypt
# Specific version
wheels plugins install cfwheels-bcrypt --version=0.0.4[ERROR] Failed to install plugin
Error: Only cfwheels-plugins can be installed via this command[ERROR] Failed to install plugin
Error: Plugin not found on ForgeBox[ERROR] Failed to install plugin
Error: Could not connect to ForgeBoxAnalyzes code quality in your Wheels application, checking for best practices, potential issues, and code standards compliance.
wheels analyze code [--path=<path>] [--fix] [--format=<format>] [--severity=<severity>] [--report] [--verbose]This command supports multiple parameter formats:
Named parameters: name=value (e.g., path=app/controllers, format=json)
Flag parameters: --flag equals flag=true (e.g., --fix equals fix=true)
Flag with value: --flag=value equals flag=value (e.g., --path=app/models)
Parameter Mixing Rules:
✅ ALLOWED:
All flags: wheels analyze code --fix --report --verbose
Flags with values: wheels analyze code --path=app/models --format=json
Named + flags: path=app/controllers format=json --fix
❌ NOT ALLOWED:
Positional parameters: This command does not support positional parameters
Recommendation: Use flag syntax for consistency: wheels analyze code --path=app/models --fix --format=json
--path
Path to analyze (directory or file)
app
--fix
Attempt to fix issues automatically
false
--format
Output format: console, json, junit
console
--severity
Minimum severity level: info, warning, error
warning
--report
Generate HTML report
false
--verbose
Show detailed progress during analysis
false
The analyze code command performs comprehensive code quality analysis on your Wheels application. It automatically excludes framework files and focuses only on your application code.
Code Complexity: Cyclomatic complexity and function length metrics
Code Style: Line length, indentation, trailing spaces, tabs vs spaces
Security Issues: SQL injection risks, hardcoded credentials, evaluate() usage
Performance: N+1 queries, missing query caching, SELECT * usage
Best Practices: Variable scoping, output attributes, code organization
Wheels Conventions: Controller/Model naming, validations, filters
Code Smells: Long parameter lists, nested loops, TODO comments
Duplicate Code: Detection of similar code blocks (30+ lines by default)
Deprecated Functions: Outdated Wheels function usage
The analyzer assigns a health score (0-100) and letter grade (A-F) based on:
A (90-100): Excellent code quality
B (80-89): Good code quality with minor issues
C (70-79): Acceptable code quality, needs improvement
D (60-69): Poor code quality, significant refactoring needed
F (0-59): Critical issues, immediate attention required
Analyzes all code in the app/ directory by default:
wheels analyze code# Flag syntax (recommended)
wheels analyze code --path=app/controllers
# OR named
wheels analyze code path=app/controllers# Flag syntax (recommended)
wheels analyze code --path=app/models/User.cfc
# OR named
wheels analyze code path=app/models/User.cfcAutomatically fixes issues like trailing spaces, tabs, and missing var scoping:
wheels analyze code --fixCreates a detailed HTML report with visualizations:
wheels analyze code --reportReports are saved to reports/code-analysis-[timestamp].html
wheels analyze code --format=jsonwheels analyze code --format=junitwheels analyze code --severity=errorwheels analyze code --verbosewheels analyze code --path=app/models --fix --report --verbose==================================================
CODE QUALITY REPORT
==================================================
Grade: B (85/100)
Good code quality with minor issues
==================================================
Code Metrics
--------------------------------------------------
Files Analyzed: 42
Total Lines: 3,567
Functions: 156
Avg Complexity: 4
Duplicate Blocks: 3
Code Smells: 7
Deprecated Calls: 2
Issue Summary
--------------------------------------------------
Errors: 2 (Critical issues requiring immediate attention)
Warnings: 12 (Issues that should be addressed)
Info: 28 (Suggestions for improvement)
[Additional details for each file...]Structured JSON with all metrics, issues, and file details for programmatic processing.
XML format compatible with CI/CD tools like Jenkins, GitLab CI, and GitHub Actions.
Create a .wheelscheck file in your project root to customize rules:
{
"rules": {
"max-line-length": 120,
"indent-size": 4,
"max-function-length": 50,
"max-function-complexity": 10,
"max-file-length": 500,
"duplicate-threshold": 30,
"naming-convention": "camelCase"
},
"features": {
"duplicateDetection": true,
"complexityAnalysis": true,
"wheelsConventions": true,
"codeSmells": true
},
"exclude": [
"custom/path/to/exclude/",
"generated/"
]
}The analyzer automatically excludes:
Wheels framework files (vendor/wheels/, wheels/)
Third-party dependencies (vendor/, node_modules/)
Test frameworks (testbox/, tests/)
Build artifacts (build/, dist/)
Version control (.git/, .svn/)
System directories (WEB-INF/, CFIDE/)
Generated files (*.min.js, *.min.css)
The following issues can be automatically fixed with the --fix flag:
Trailing whitespace
Tab characters (converted to spaces)
Missing var scoping in functions
Missing output attribute on components
- name: Code Analysis
run: |
wheels analyze code --format=junit --severity=errorcode_quality:
script:
- wheels analyze code --format=json > code-quality.json
artifacts:
reports:
codequality: code-quality.jsonstage('Code Analysis') {
steps {
sh 'wheels analyze code --format=junit'
junit 'code-analysis-results.xml'
}
}Small projects (< 100 files): Analysis completes in seconds
Medium projects (100-500 files): 30-60 seconds typical
Large projects (500+ files): Several minutes, use --verbose to track progress
HTML report generation adds 5-30 seconds depending on project size
0: Success, no errors found
1: Analysis completed with errors found
2: Analysis failed (invalid path, configuration error)
Run analysis regularly during development to catch issues early
Use --fix for quick cleanup before commits
Include analysis in pre-commit hooks or CI pipelines
Start with --severity=error and gradually include warnings
Review the HTML report for visual insights into code quality
Use the grade as a benchmark to track improvement over time
Focus on fixing high-complexity functions first for maximum impact
Ensure you're in a Wheels application root directory
Check that the app/ directory exists
Verify path permissions
Use --path to analyze specific directories
Add frequently changing directories to exclude list
Consider splitting analysis across multiple runs
Some issues require manual intervention
Check file permissions for write access
Review the specific fix recommendations in the output
The wheels env set command allows you to set or update environment variables in .env files. This command provides a quick and safe way to modify environment configuration files directly from the command line, supporting both creation of new variables and updating of existing ones.
wheels env set KEY=VALUE [KEY2=VALUE2 ...] [--file=filename]This command supports multiple parameter formats:
Named parameters: KEY=VALUE (e.g., DB_HOST=localhost, API_KEY=secret)
Flag parameters: --flag=value (e.g., --file=.env.production)
Parameter Mixing Rules:
✅ ALLOWED:
Named KEY=VALUE pairs: wheels env set DB_HOST=localhost DB_PORT=3306
Named + file flag: wheels env set DB_HOST=localhost --file=.env.production
Multiple variables: wheels env set KEY1=value1 KEY2=value2 KEY3=value3
❌ NOT ALLOWED:
Positional parameters: This command does not support positional parameters
Recommendation: Use KEY=VALUE format with optional --file flag: wheels env set DB_HOST=localhost --file=.env.production
KEY=VALUE
KEY=VALUE
Yes
One or more environment variable assignments in KEY=VALUE format
--file
string
No
The .env file to update (default: .env)
wheels env set DB_HOST=localhostSets DB_HOST to localhost in the .env file
wheels env set DB_PORT=3306 DB_NAME=myapp DB_USER=rootSets multiple database-related variables in a single command
wheels env set --file=.env.production API_KEY=secretUpdates the .env.production file instead of the default .env
wheels env set DATABASE_URL="mysql://user:pass@localhost:3306/db"
wheels env set API_ENDPOINT=https://api.example.com/v1Sets variables with complex values including special characters
The command intelligently handles different scenarios:
Existing File: Updates or adds variables to the existing file
Non-existent File: Creates a new file with the specified variables
Format Detection: Automatically detects and preserves the file format (properties or JSON)
DATABASE_HOST=localhost
DATABASE_PORT=3306
API_KEY=your-secret-key{
"DATABASE_HOST": "localhost",
"DATABASE_PORT": "3306",
"API_KEY": "your-secret-key"
}Existing Variables: Overwrites the current value with the new one
New Variables: Appends to the end of the file (for properties format)
Comments: Preserves existing comments and empty lines
Line Format: Maintains the original file structure and formatting
Trailing Commas: Automatically removes trailing commas from values
Equal Signs: Values can contain = signs (everything after the first = is the value)
Whitespace: Trims leading and trailing whitespace from keys and values
Special Characters: Properly handles URLs, connection strings, and other complex values
When displaying updated variables, the command automatically masks sensitive values:
Variables containing password, secret, key, or token (case-insensitive)
Masked values appear as ***MASKED*** in the output
Actual values are still written correctly to the file
The command checks if your .env file is listed in .gitignore:
Displays a warning if the file is not ignored
Recommends adding it to prevent committing secrets
Only checks when working with files containing .env in the name
After successful execution, the command displays:
Confirmation message with the filename
List of all updated/added variables
Masked display for sensitive values
Git security warning (if applicable)
Environment variables updated in .env:
DB_HOST = localhost
DB_PORT = 3306
DB_PASSWORD = ***MASKED***
API_KEY = ***MASKED***
Warning: .env is not in .gitignore!
Add it to .gitignore to prevent committing secrets.# Create a new .env file with basic configuration
wheels env set APP_NAME=MyApp APP_ENV=development DEBUG=true# Set all database variables at once
wheels env set DB_HOST=localhost DB_PORT=5432 DB_NAME=myapp DB_USER=appuser DB_PASSWORD=secret# Configure API endpoints and keys
wheels env set API_BASE_URL=https://api.example.com API_KEY=abc123 API_TIMEOUT=30# Development settings
wheels env set --file=.env.development DEBUG=true LOG_LEVEL=debug
# Production settings
wheels env set --file=.env.production DEBUG=false LOG_LEVEL=error# Change database host from localhost to production server
wheels env set DB_HOST=db.production.com
# Update multiple values
wheels env set APP_ENV=production DEBUG=falseWhen creating a new file, the command adds:
Header comment indicating it was generated by the command
Timestamp comment (optional)
All specified variables
Example of a newly created file:
# Wheels Environment Configuration
# Generated by wheels env set command
APP_NAME=MyApplication
APP_ENV=development
DEBUG=trueThe command will display an error and stop if:
No KEY=VALUE pairs are provided
File write permissions are insufficient
The file path is invalid
File system errors occur (disk full, etc.)
# No arguments provided
Error: No key=value pairs provided. Usage: wheels env set KEY=VALUE
# File write failure
Error: Failed to update .env file: [specific error message]Use quotes for complex values containing spaces or special characters:
wheels env set CONNECTION_STRING="Server=localhost;Database=myapp;User=root"Update multiple related variables together to maintain consistency:
wheels env set DB_HOST=newhost DB_PORT=3306 DB_NAME=newdbKeep sensitive values in separate files not tracked by version control:
wheels env set --file=.env.local API_SECRET=very-secret-keyAlways check .gitignore to ensure sensitive files are not committed:
echo ".env" >> .gitignore
echo ".env.local" >> .gitignoreUse environment-specific files for different deployments:
wheels env set --file=.env.production APP_ENV=production
wheels env set --file=.env.staging APP_ENV=stagingUpdate multiple variables from different categories in one command:
wheels env set \
APP_NAME=MyApp \
DB_HOST=localhost \
DB_PORT=5432 \
CACHE_DRIVER=redis \
MAIL_HOST=smtp.example.com# Switch to production settings
wheels env set APP_ENV=production DEBUG=false LOG_LEVEL=error
# Switch back to development
wheels env set APP_ENV=development DEBUG=true LOG_LEVEL=debug# Create a template file for new projects
wheels env set --file=.env.example \
APP_NAME=YourAppName \
APP_ENV=development \
DB_HOST=localhost \
DB_PORT=3306 \
DB_NAME=your_database \
DB_USER=your_user \
DB_PASSWORD=your_passwordFile Format Preservation: The command preserves the original format (JSON or properties)
Comment Preservation: Existing comments and empty lines are maintained
Atomic Updates: All variables are updated in a single operation
No Validation: The command doesn't validate variable values
Case Sensitive: Variable names are case-sensitive
Overwrite Behavior: Existing values are always overwritten
Trailing Comma Removal: Automatically cleans trailing commas from values
Never commit .env files containing real credentials
Use .env.example files as templates with dummy values
Keep production secrets in secure vaults or CI/CD systems
Rotate credentials regularly using this command
Review git history before pushing to ensure no secrets were committed
Search for available Wheels plugins on ForgeBox.
wheels plugin search [query] [--format=<format>] [--orderBy=<field>]This command supports multiple parameter formats:
Positional parameters: wheels plugin search bcrypt (search query)
Named parameters: query=value (e.g., query=auth, format=json)
Flag parameters: --flag=value (e.g., --format=json, --orderBy=downloads)
Parameter Mixing Rules:
✅ ALLOWED:
Positional: wheels plugin search bcrypt
Positional + flags: wheels plugin search auth --format=json
All named: query=bcrypt format=json orderBy=downloads
Named + flags: query=auth --format=json
❌ NOT ALLOWED:
Positional + named for same param: wheels plugin search bcrypt query=other
Recommendation: Use positional for query, flags for options: wheels plugin search auth --format=json --orderBy=downloads
query
No
string
-
(empty)
Search term to filter plugins
format
No
string
table, json
table
Output format for the results
orderBy
No
string
name, downloads, updated
downloads
Sort results by specified field
The plugin search command searches ForgeBox for available cfwheels-plugins type packages. You can search for all plugins or filter by keywords. Results can be sorted by name, downloads, or last updated date.
Searches only cfwheels-plugins type packages
Filters results by search term
Multiple sort options
Color-coded, formatted output
JSON export support
Dynamic column widths
wheels plugin searchOutput:
===========================================================
Searching ForgeBox for Wheels Plugins
===========================================================
Found 5 plugins:
Name Version Downloads Description
-------------------------------------------------------------------------------
cfwheels-bcrypt 1.0.2 4393 CFWheels 2.x plugin helper meth...
shortcodes 0.0.4 189 Shortcodes Plugin for CFWheels
cfwheels-authenticateThis 2.0.0 523 Adds bCrypt authentication helpe...
cfwheels-jwt 2.1.0 412 CFWheels plugin for encoding and...
cfwheels-htmx-plugin 1.0.0 678 HTMX Plugin for CFWheels
-----------------------------------------------------------
Commands:
wheels plugin install <name> Install a plugin
wheels plugin info <name> View plugin detailswheels plugin search bcryptOutput:
===========================================================
Searching ForgeBox for Wheels Plugins
===========================================================
Search term: bcrypt
Found 1 plugin:
Name Version Downloads Description
-----------------------------------------------------------------------
cfwheels-bcrypt 1.0.2 4393 CFWheels 2.x plugin helper meth...
-----------------------------------------------------------
Commands:
wheels plugin install <name> Install a plugin
wheels plugin info <name> View plugin detailswheels plugin search nonexistentOutput:
===========================================================
Searching ForgeBox for Wheels Plugins
===========================================================
Search term: nonexistent
No plugins found matching 'nonexistent'
Try:
wheels plugin search <different-keyword>
wheels plugin list --availablewheels plugin search --orderBy=nameResults will be sorted alphabetically by plugin name.
wheels plugin search --orderBy=updatedResults will be sorted by most recently updated plugins first.
wheels plugin search --format=jsonOutput:
{
"plugins": [
{
"name": "CFWheels bCrypt",
"slug": "cfwheels-bcrypt",
"version": "1.0.2",
"description": "CFWheels 2.x plugin helper methods for the bCrypt Java Lib",
"author": "neokoenig",
"downloads": 4393,
"updateDate": "2022-05-30T02:09:07+00:00"
},
{
"name": "Shortcodes",
"slug": "shortcodes",
"version": "0.0.4",
"description": "Shortcodes Plugin for CFWheels",
"author": "neokoenig",
"downloads": 189,
"updateDate": "2017-05-16T09:03:02+00:00"
}
],
"count": 2,
"query": ""
}Execute ForgeBox Command: Runs forgebox show type=cfwheels-plugins to get all plugins
Parse Output: Scans the formatted output for lines containing Slug: "plugin-slug"
Extract Slugs: Uses regex to extract slug values from quoted strings
Filter by Query: If search term provided, only processes slugs containing that term
Fetch Details: For each matching slug, calls forgebox.getEntry(slug) to get:
Plugin title and description
Latest version (from latestVersion.version)
Author username (from user.username)
Download count (from hits)
Last updated date
Sort Results: Sorts plugins by specified order (downloads, name, or updated date)
Format Output: Displays in table or JSON format with dynamic column widths
Sorts by number of downloads, most popular first. Best for finding widely-used plugins.
Sorts alphabetically by plugin name. Best for browsing all available plugins.
Sorts by last update date, most recent first. Best for finding actively maintained plugins.
Broad Search: Start with general terms like "auth" or "cache"
Case Insensitive: Search is case-insensitive
Partial Matching: Matches plugins containing the search term anywhere in the slug
Popular First: Default sort shows most downloaded plugins first
Empty Query: Run without query to see all available plugins
Color-coded columns (cyan names, green versions, yellow downloads)
Dynamic column widths based on content
Truncated descriptions with ellipsis
Clear section headers and dividers
Helpful command suggestions
Structured data for programmatic use
Includes plugin count
Includes search query
Complete plugin information
After finding plugins:
# View detailed information
wheels plugin info cfwheels-bcrypt
# Install directly
wheels plugin install cfwheels-bcrypt
# List installed plugins
wheels plugin listFetches all cfwheels-plugins from ForgeBox
Filters results client-side
Queries detailed info for each matching plugin
May take a few seconds for large result sets
Results are not cached (always fresh)
If ForgeBox cannot be reached:
[ERROR] Error searching for plugins
Error: Connection timeoutIf no plugins of type cfwheels-plugins exist:
No plugins found
Try:
wheels plugin search <different-keyword>
wheels plugin list --availableOnly searches cfwheels-plugins type packages
Requires internet connection to query ForgeBox
Search is performed against plugin slugs
Results include version, downloads, and description
Dynamic table formatting adjusts to content
Some plugins may not have complete metadata
Plugins without valid metadata are skipped
wheels plugin info - View detailed plugin information
wheels plugin install - Install a plugin
wheels plugin list - List installed plugins
wheels plugin outdated - Check for plugin updates
Update a Wheels plugin to the latest or a specified version from ForgeBox.
wheels plugin update <name> [--version=<version>] [--force]name
Yes
string
Name, slug, or folder name of the plugin to update
version
No
string
Specific version to update to (default: latest)
force
No
boolean
Force update even if already at latest version
The plugin update command updates an installed Wheels plugin from the /plugins folder. It checks ForgeBox for the latest version, compares with the installed version, and performs the update if needed.
Smart version checking (prevents unnecessary updates)
Flexible plugin matching (by name, slug, or folder name)
Real-time version queries from ForgeBox
Clean removal and reinstallation process
Helpful success and error messages
Beautiful, color-coded output
wheels plugin update bcryptOutput:
===========================================================
Updating Plugin: bcrypt
===========================================================
Plugin: bcrypt
Current version: 0.0.3
Latest version: 0.0.4
Target version: 0.0.4
===========================================================
Removing old version...
Installing new version...
[CommandBox installation output...]
===========================================================
[OK] Plugin 'bcrypt' updated successfully!
Commands:
wheels plugin info bcrypt View plugin details
wheels plugin list View all installed pluginswheels plugin update bcryptOutput:
===========================================================
Updating Plugin: bcrypt
===========================================================
Plugin: bcrypt
Current version: 0.0.4
Latest version: 0.0.4
===========================================================
[OK] Plugin is already at the latest version (0.0.4)
Use --force to reinstall anyway:
wheels plugin update bcrypt --forcewheels plugin update bcrypt --version=0.0.3Output:
===========================================================
Updating Plugin: bcrypt
===========================================================
Plugin: bcrypt
Current version: 0.0.4
Latest version: 0.0.4
Target version: 0.0.3
===========================================================
Removing old version...
Installing new version...
[CommandBox installation output...]
===========================================================
[OK] Plugin 'bcrypt' updated successfully!
Commands:
wheels plugin info bcrypt View plugin details
wheels plugin list View all installed pluginswheels plugin update bcrypt --forceOutput:
===========================================================
Updating Plugin: bcrypt
===========================================================
Plugin: bcrypt
Current version: 0.0.4
Latest version: 0.0.4
Target version: 0.0.4
===========================================================
Removing old version...
Installing new version...
[CommandBox installation output...]
===========================================================
[OK] Plugin 'bcrypt' updated successfully!
Commands:
wheels plugin info bcrypt View plugin details
wheels plugin list View all installed pluginswheels plugin update nonexistent-pluginOutput:
===========================================================
Updating Plugin: nonexistent-plugin
===========================================================
[ERROR] Plugin not found
Plugin 'nonexistent-plugin' is not installed
Install this plugin:
wheels plugin install nonexistent-pluginwheels plugin update bcryptOutput (network error):
===========================================================
Updating Plugin: bcrypt
===========================================================
Plugin: bcrypt
Current version: 0.0.4
Error checking ForgeBox: Connection timeout
Unable to verify if update is needed
Options:
- Specify a version:
wheels plugin update bcrypt --version=x.x.x
- Force reinstall:
wheels plugin update bcrypt --forceFind Plugin: Searches /plugins folder for matching plugin by name, slug, or folder name
Read Metadata: Extracts current version and slug from plugin's box.json
Query ForgeBox: Uses forgebox show command to get latest version
Version Comparison: Cleans and compares versions to check if update needed
Skip if Up-to-Date: Exits early if already at target version (unless --force)
Remove Old Version: Deletes the plugin folder
Install New Version: Uses PackageService to download and install from ForgeBox
Verify Location: Moves plugin to /plugins if installed elsewhere
Confirm Success: Displays success message with helpful commands
The command uses a smart matching algorithm to find plugins by:
Folder name (e.g., bcrypt)
Slug from box.json (e.g., cfwheels-bcrypt)
Name from box.json (e.g., bcrypt, CFWheels Bcrypt)
Normalized variations (strips cfwheels- and wheels- prefixes)
This means you can update using any of these:
wheels plugin update bcrypt
wheels plugin update cfwheels-bcrypt
wheels plugin update "CFWheels Bcrypt"The command cleans versions before comparison:
Removes all non-numeric characters except dots
Example: v0.0.4 becomes 0.0.4
Example: 0.0.4-beta becomes 0.0.4
Compares cleaned strings for equality
If versions match, the plugin is considered up-to-date.
Use --force to reinstall even when already up-to-date. Useful for:
Recovering from corrupted installations
Forcing cache refresh
Testing installation process
Reinstalling after manual modifications
[ERROR] Plugins directory not found
Plugin 'bcrypt' is not installed[ERROR] Plugin not found
Plugin 'bcrypt' is not installed
Install this plugin:
wheels plugin install bcryptError checking ForgeBox: [error message]
Unable to verify if update is needed
Options:
- Specify a version:
wheels plugin update bcrypt --version=x.x.x
- Force reinstall:
wheels plugin update bcrypt --force[ERROR] Error updating plugin
Error: [error message]Check First: Use wheels plugin outdated to see which plugins need updates
Update One at a Time: Test each plugin update individually
Read Release Notes: Check ForgeBox for breaking changes
Test in Development: Update in dev environment before production
Keep Backups: Commit your code before updating plugins
Only updates plugins from /plugins folder
Only works with cfwheels-plugins type packages
Removes old version completely before installing new version
Uses ForgeBox slug for installation to ensure correct package
Requires internet connection to query ForgeBox
Version check is performed in real-time (not cached)
After update, plugin may require application reload
If installation fails, the old version is already removed (no automatic rollback)
The command uses CommandBox's PackageService for downloading
wheels plugin update:all - Update all plugins
wheels plugin outdated - Check for outdated plugins
wheels plugin info - View plugin details
wheels plugin list - List installed plugins
Get up and running with Wheels CLI in minutes. Learn installation, creating your first application, and common development tasks.
Get up and running with Wheels CLI in minutes.
CommandBox 5.0+
Java 8+
Database (MySQL, PostgreSQL, SQL Server, or H2)
# macOS/Linux
curl -fsSl https://downloads.ortussolutions.com/debs/gpg | sudo apt-key add -
echo "deb https://downloads.ortussolutions.com/debs/noarch /" | sudo tee -a /etc/apt/sources.list.d/commandbox.list
sudo apt-get update && sudo apt-get install commandbox
# Windows (PowerShell as Admin)
iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
choco install commandboxbox install wheels-cliwheels new blog
cd blogThis creates a new Wheels application with:
Complete directory structure
Configuration files
Sample code
Edit /config/settings.cfm:
<cfset set(dataSourceName="blog_development")>Or use H2 embedded database:
wheels new blog --setupH2Create the database:
# If using external database (MySQL, PostgreSQL, etc.)
wheels db createbox server startVisit http://localhost:3000
Let's create a blog post feature:
wheels generate scaffold name=post properties=title:string,content:text,published:booleanThis generates:
Model with validations
Controller with CRUD actions
Views for all actions
Database migration
Test files
wheels dbmigrate latestEdit /config/routes.cfm:
<cfscript>
// Add this line
resources("posts");
</cfscript>wheels reloadVisit http://localhost:3000/posts
You now have a fully functional blog post management system!
# Run all tests
wheels test run
# Watch mode
wheels test run --watch
# Specific tests
wheels test run tests/models/PostTest.cfcLet's add comments to posts:
# Generate comment model
wheels generate model comment --properties="author:string,content:text,postId:integer" \
--belongsTo="post"
# Update post model
wheels generate property post comments --has-many
# Generate comments controller
wheels generate controller comments --rest
# Run migration
wheels dbmigrate latest# Generate user model
wheels scaffold name=user properties=email:string,password:string,admin:boolean
# Generate session controller
wheels generate controller sessions new,create,delete
# Run migrations
wheels dbmigrate latest# Generate API resource
wheels generate api-resource product --properties="name:string,price:decimal"
# Or convert existing to API
wheels generate controller api/posts --api# Generate specific views
wheels generate view posts featured
wheels generate view users profile
# Add layouts
echo '<cfoutput><!DOCTYPE html>...</cfoutput>' > views/layout.cfmAlways use migrations for database changes:
# Create tables
wheels dbmigrate create table products
# Add columns
wheels dbmigrate create column products featured
# Create indexes
wheels dbmigrate create blank add_index_to_productsGenerate tests for your code:
# After creating a model
wheels generate test model post
# After creating a controller
wheels generate test controller posts# Development
wheels reload development
# Testing
wheels reload testing
# Production
wheels reload productiongit init
git add .
git commit -m "Initial Wheels application"Add to .gitignore:
/db/sql/
/logs/
/temp/
.envtail -f logs/wheels.logIn /config/settings.cfm:
<cfset set(showDebugInformation=true)>Port already in use:
box server start port=3001Database connection failed:
# Check datasource
box server info
box server showMigration failed:
# Check status
wheels db status
# Run specific migration
wheels dbmigrate exec 20240120000000
# Or rollback and try again
wheels db rollbackNeed to reset database:
# Complete reset (careful - destroys all data!)
wheels db reset --forceAccess database directly:
# CLI shell
wheels db shell
# Web console (H2 only)
wheels db shell --webRead the Guides:
Explore Commands:
wheels --help
wheels generate --help
wheels dbmigrate --help
Join the Community:
CFML Slack #wheels channel
Here's a complete blog setup:
# Create application
wheels new myblog --setupH2
cd myblog
# Generate blog structure
wheels scaffold post title:string,slug:string,content:text,publishedAt:datetime
wheels scaffold author name:string,email:string,bio:text
wheels generate model comment author:string,email:string,content:text,postId:integer \
--belongsTo=post
# Update associations
wheels generate property post authorId:integer --belongsTo=author
wheels generate property post comments --has-many
wheels generate property author posts --has-many
# Add routes
echo '<cfset resources("posts")>' >> config/routes.cfm
echo '<cfset resources("authors")>' >> config/routes.cfm
# Setup and seed database
wheels db setup --seed-count=10
# Start development
wheels server start
# Visit http://localhost:3000/postsYou now have a working blog with posts, authors, and comments!
The wheels config dump command exports your Wheels application configuration settings for inspection, backup, or migration purposes. It can display configurations in multiple formats and optionally mask sensitive values for security.
wheels config dumpThis displays the current environment's configuration in a formatted table in the console.
wheels config dump [environment] [--format=<type>] [--output=<file>] [--noMask]environment
No
The environment to dump (development, testing, production)
Auto-detects from WHEELS_ENV or defaults to "development"
--format=<type>
Output format for the configuration
table, json, env, cfml
Console: table
File: json
--output=<file>
File path to save the configuration
Any valid file path
None (displays to console)
--noMask
Don't mask sensitive values (passwords, keys, tokens, etc.)
Flag (true/false)
false (sensitive values are masked)
--formatSpecifies the output format for the configuration dump:
table: Formatted tables organized by category (best for console viewing)
json: Structured JSON format (best for programmatic use and file storage)
env: Environment variables format (.env compatible)
cfml: Wheels set() statements format
--outputWhen specified, saves the configuration to a file instead of displaying to console. If no format is explicitly specified with --output, JSON format is automatically used for better file compatibility.
--noMask⚠️ Security Warning: This option exposes sensitive configuration data including passwords, API keys, and tokens. Only use when absolutely necessary and ensure the output is stored securely.
Displays configuration in organized categories with formatted tables:
DATABASE Settings
CACHING Settings
SECURITY Settings
ENVIRONMENT Settings
OTHER Settings
Exports configuration as structured JSON:
{
"datasource": "myapp",
"cacheQueries": false,
"environment": "development",
"_environment": {
"WHEELS_ENV": "development"
}
}Exports as environment variables compatible with .env files:
## Application Settings
DATASOURCE=myapp
CACHE_QUERIES=false
ENVIRONMENT=developmentExports as Wheels set() statements:
// Wheels Configuration Export
set(datasource = "myapp");
set(cacheQueries = false);
set(environment = "development");# View current configuration
wheels config dump
# View production configuration
wheels config dump production# Backup as JSON (default for files)
wheels config dump --output=config-backup.json
# Backup with timestamp
wheels config dump --output="backup/config-$(date +%Y%m%d).json"# Export different environments
wheels config dump development --output=dev-config.json
wheels config dump production --output=prod-config.json
# Then compare using diff tools
diff dev-config.json prod-config.json# Create .env file template
wheels config dump --format=env --output=.env.template
# Create environment-specific files
wheels config dump production --format=env --output=.env.production# Export from source server (masked)
wheels config dump --output=config-export.json
# Export with sensitive data (be careful!)
wheels config dump --noMask --output=config-complete.json# Create settings file for another environment
wheels config dump --format=cfml --output=config/staging/settings.cfmBy default, the command masks sensitive values containing these keywords:
password
secret
key
token
apikey / api_key
private
credential
auth
passphrase
salt
pwd
Masked values appear as: ***MASKED***
Only use --noMask when:
You need complete configuration for migration
Output is being saved to a secure location
You're in a development environment
Never commit unmasked configuration files to version control!
The command loads configuration from multiple sources in order:
Base Settings: /config/settings.cfm
Environment Settings: /config/[environment]/settings.cfm
Environment Variables: .env file (if exists)
Environment detection checks (in order):
.env file: WHEELS_ENV variable
System environment: WHEELS_ENV variable
System environment: ENVIRONMENT variable
Default: development
When using --output:
No format specified: Automatically uses JSON format
Format specified: Uses the specified format
Notification: Displays which format was used in the success message
# These save as JSON
wheels config dump --output=config.json
wheels config dump --output=settings.txt
# This saves as ENV format
wheels config dump --format=env --output=settings.env
# This explicitly saves as table format
wheels config dump --format=table --output=config.txt# View current configuration
wheels config dump
# View testing environment configuration
wheels config dump testing
# Export as JSON to console
wheels config dump --format=json# Complete production backup (unmasked)
wheels config dump production --noMask --output=prod-complete.json
# Generate environment file for Docker
wheels config dump --format=env --output=docker/.env
# Create CFML settings for new environment
wheels config dump --format=cfml --output=config/custom/settings.cfm
# Quick masked backup with date
wheels config dump --output="backups/config-$(date +%Y%m%d-%H%M%S).json"# CI/CD configuration validation
wheels config dump --format=json | jq '.datasource'
# Environment variable generation for deployment
wheels config dump production --format=env --output=/tmp/app.envError: "No settings.cfm file found in config directory" Solution: Ensure you're running the command from your Wheels application root directory
Error: "Invalid format: [format]. Valid formats are: table, json, env, cfml" Solution: Use one of the supported formats: table, json, env, or cfml
Error: "Failed to write file" Solution: Ensure you have write permissions for the specified output directory
Issue: Always shows "development" environment Solution: Set the WHEELS_ENV environment variable or create a .env file with WHEELS_ENV=production
Regular Backups: Schedule regular configuration exports as part of your backup strategy
Version Control: Store masked configuration exports in version control for tracking changes
Environment Validation: Use dumps to verify configuration changes before deployment
Security First: Always use masked output unless absolutely necessary
Documentation: Keep exported configurations with deployment documentation
For issues or questions about the wheels config dump command:
Check the Wheels documentation
Verify your Wheels and CommandBox versions are compatible
Ensure proper file permissions and paths
Review the command output for specific error messages


Add properties to existing model files with database migrations and view updates.
CommandBox supports multiple parameter formats:
Named parameters: name=value (e.g., name=User, columnName=email)
Flag parameters: --flag equals flag=true (e.g., --allowNull equals allowNull=true)
Flag with value: --flag=value equals flag=value (e.g., --dataType=boolean)
Note: Flag syntax (--flag) avoids positional/named parameter conflicts and is recommended for boolean options.
The wheels generate property command generates a database migration to add a property to an existing model and scaffolds it into _form.cfm and show.cfm views.
Creates a string property called firstName on the User model.
Creates a boolean property with default value of 0 (false).
Creates a datetime property on the User model.
Creates a decimal property with 10 total digits and 2 decimal places.
Creates a required string property with maximum 50 characters.
The wheels generate property command performs these actions:
Creates Database Migration: Generates a migration file to add the column to the database
Updates Form View: Adds the property to _form.cfm if it exists
Updates Index View: Adds the property to index.cfm table if it exists
Updates Show View: Adds the property to show.cfm if it exists
Offers Migration: Prompts to run the migration immediately
File: app/migrator/migrations/[timestamp]_add_[columnName]_to_[tableName].cfc
When views exist, the command adds the new property to them:
Form View (_form.cfm): Adds appropriate input field
Index View (index.cfm): Adds column to table
Show View (show.cfm): Adds property display
When adding boolean properties without a default value, the command automatically sets default=0:
The command checks if the corresponding model file exists and asks for confirmation if it doesn't:
Column names are automatically converted to lowercase in migrations (following Wheels conventions):
Validation: Checks if model file exists (optional)
Migration: Creates database migration using wheels dbmigrate create column
Form Update: Adds form field to _form.cfm (if exists)
Index Update: Adds column to index.cfm table (if exists)
Show Update: Adds property display to show.cfm (if exists)
Migration Prompt: Asks if you want to run migration immediately
Check that view files exist (_form.cfm, index.cfm, show.cfm)
View files must be in /app/views/[modelPlural]/ directory
Ensure model name matches existing table
Check that column doesn't already exist
Verify database permissions
Boolean properties automatically get default=0 if no default specified
Use --default=1 for true default values
Run migrations immediately when prompted to keep database in sync
Use semantic names for properties (firstName, not fname)
Set appropriate defaults for boolean and numeric fields
Consider null constraints based on business logic
Add one property at a time for better change tracking
- Generate models
- Create columns
- Generate tests
Update all installed Wheels plugins to their latest versions from ForgeBox.
The plugin update:all command checks all installed plugins in the /plugins folder against ForgeBox and updates any outdated plugins to their latest versions. It provides a clear summary of what will be updated and handles each plugin update sequentially.
Checks all plugins for updates in one command
Color-coded status indicators for each plugin
Detailed update summary
dryRun mode to preview changes
Individual plugin update tracking
Helpful error reporting
Output (with updates available):
Output:
Output:
Output:
Output:
Display Header: Shows command is checking for updates
Plugin Discovery: Scans /plugins folder for installed plugins
Version Checking: Queries ForgeBox for each plugin's latest version
Status Display: Shows color-coded status for each plugin
Update List: Displays table of plugins that need updating
Sequential Updates: Updates each plugin one at a time
Progress Tracking: Shows success/failure for each update
Summary Report: Displays final update statistics
Helpful Commands: Suggests next steps
During checking, each plugin displays:
[OUTDATED] (yellow) - Update available, will be updated
[OK] (green) - Already at latest version
[ERROR] (red) - Could not check version
During updates:
[UPDATE] (yellow) - Plugin will be updated
[OK] (green) - Update completed successfully
[ERROR] (red) - Update failed
Use --dryRun to preview updates without actually performing them. This is useful for:
Checking what would be updated before committing
Testing in CI/CD pipelines
Reviewing changes before production updates
Planning maintenance windows
The dryRun mode:
Checks all plugins for updates
Shows what would be updated
Does NOT download or install anything
Provides command to perform actual updates
The command updates plugins sequentially:
One plugin at a time (safer than parallel)
Continues updating even if one fails
Tracks success/failure for each plugin
Provides detailed summary at the end
Plugins where version cannot be checked are listed separately and skipped for updates.
If a plugin update fails:
The failure is tracked
Other updates continue
Error is reported in summary
Plugin can be updated individually later
If ForgeBox cannot be reached:
Regular Updates: Run weekly or monthly to stay current
Test First: Always test updates in development before production
Use dryRun: Preview updates with --dryRun before applying
Read Release Notes: Check ForgeBox for breaking changes
Commit First: Commit your code before updating plugins
Update Individually: For critical plugins, use wheels plugin update <name>
Only updates plugins from /plugins folder
Only works with cfwheels-plugins type packages
Updates are performed sequentially (not in parallel)
Each update is independent - failures don't affect other updates
Requires internet connection to query ForgeBox and download updates
Version checks are performed in real-time (not cached)
Progress is shown for each plugin update
After updates, plugins may require application reload
Failed updates can be retried individually with wheels plugin update <name>
The command does NOT update plugins that are already at latest version
- Update a single plugin
- Check for outdated plugins
- List installed plugins
- View plugin details
The wheels env show command displays environment variables from .env files in your Wheels project. This command provides a convenient way to view your application's configuration, with intelligent grouping, security masking, and multiple output formats. It helps you understand what environment variables are available and how they're organized.
Displays all environment variables from .env in a grouped, readable table format
Shows only the DB_HOST variable and its value
Outputs all variables as formatted JSON
Displays variables from .env.production instead of .env
The table format groups variables by prefix and displays them in an organized, readable way:
Clean JSON output suitable for processing or integration:
Variables are automatically grouped by prefix for better organization:
DB_* variables (database configuration)
API_* variables (API settings)
WHEELS_* variables (framework settings)
Other Variables (ungrouped items)
Sensitive values are automatically masked when displayed:
Variables containing password → ********
Variables containing secret → ********
Variables containing key → ********
The actual values remain unchanged in your files - only the display is masked.
The command automatically handles quoted values:
Double quotes: KEY="value with spaces"
Single quotes: KEY='another value'
Quotes are stripped from displayed values
The command ensures you're in a valid Wheels project:
If the specified .env file doesn't exist, you'll see helpful guidance:
When requesting a specific key that doesn't exist:
The command provides helpful tips on how to use the variables in your Wheels application:
Grouped display makes it easy to understand related configurations
Security masking protects sensitive data during demos or screen sharing
JSON output is perfect for automation and integration scripts
Helpful error messages guide you when files are missing or keys don't exist
Project validation ensures you're running the command in the right location
Multiple file support lets you easily compare different environment configurations
The wheels get settings command displays the current Wheels application settings for your environment. It shows all configuration settings that are active, including defaults and any custom overrides from your configuration files. You can view all settings or filter to see specific ones.
settingName - Optional setting name or pattern to filter results. Can be a partial match.
Shows all active settings for the current environment
Shows only the cacheQueries setting
Shows all settings containing "cache" in their name (e.g., cacheQueries, cachePages, cacheImages)
The command resolves settings in the same order as Wheels:
Default Wheels Settings - Built-in framework defaults
Application Settings - From /config/settings.cfm
Environment Settings - From /config/[environment]/settings.cfm
Each level overrides the previous one, with environment-specific settings having the highest priority.
The command automatically detects the current environment using the same logic as wheels get environment:
Checks .env file for WHEELS_ENV
Checks system environment variable
Checks server.json
Defaults to development
The command parses set() function calls in your settings files:
cacheActions - Cache action output
cacheQueries - Cache database query results
cachePages - Cache entire page output
cachePartials - Cache partial/template output
cacheImages - Cache generated images
cacheRoutes - Cache routing configuration
cacheModelConfig - Cache model configurations
cacheControllerConfig - Cache controller configurations
cacheViewConfig - Cache view configurations
cacheDatabaseSchema - Cache database schema information
cacheFileChecking - Check for file changes when caching
dataSourceName - Primary datasource name
useExpandedColumnAliases - Use expanded column aliases in queries
useTimestampsOnDeletedColumn - Add timestamps to soft-deleted records
migratorTableName - Table name for migration versions
showDebugInformation - Display debug information
showErrorInformation - Display error details
sendEmailOnError - Send email notifications on errors
errorEmailAddress - Email address for error notifications
errorEmailServer - SMTP server for error emails
errorEmailSubject - Subject line for error emails
includeErrorInEmailSubject - Include error details in email subject
URLRewriting - URL rewriting mode (none, partial, full)
overwritePlugins - Allow plugin overwrites
deletePluginDirectories - Delete plugin directories on uninstall
loadIncompatiblePlugins - Load plugins with version mismatches
The command correctly interprets different data types:
Arrays and structs are displayed with summary information:
The command will show an error if:
Not run from a Wheels application directory
Settings files cannot be read
Settings files contain syntax errors
Environment-Specific Configs - Keep environment-specific settings in separate files (/config/[environment]/settings.cfm)
Document Custom Settings - Comment your custom settings in the configuration files
Use Consistent Naming - Follow Wheels naming conventions for custom settings
Verify Before Deployment - Always check settings for the target environment before deploying
Sensitive Data - Keep sensitive settings (API keys, passwords) in environment variables or .env files
Works well with other Wheels CLI commands:
Setting names are case-insensitive when filtering
The filter matches any part of the setting name
Settings are displayed in alphabetical order
Boolean values display as true or false
Complex values (arrays, structs) show a summary
The command shows which environment's settings are being displayed
Check which environment is active: wheels get environment
Verify the settings file exists in the correct location
Check for syntax errors in your settings files
Ensure settings use the correct set() function syntax
If expected settings are missing:
Verify they're defined using set() function
Check file paths: /config/settings.cfm and /config/[environment]/settings.cfm
Ensure no syntax errors prevent parsing
Remember the filter is case-insensitive
The filter matches any part of the setting name
Use more specific terms for precise filtering
The command parses settings files statically and may not capture all dynamic settings
Complex CFML expressions in settings may not be fully evaluated
Settings defined outside of set() function calls may not be detected
Runtime settings modifications are not reflected
Complete reference for all Wheels CLI commands organized by category.
Essential commands for managing your Wheels application.
wheels init - Bootstrap existing app for CLI
wheels info - Display version information
wheels reload - Reload application
wheels deps - Manage dependencies
wheels destroy [type] [name] - Remove generated code
Commands for generating application code and resources.
wheels generate app (alias: wheels new) - Create new application
wheels generate app-wizard - Interactive app creation
wheels generate controller (alias: wheels g controller) - Generate controller
wheels generate model (alias: wheels g model) - Generate model
wheels generate view (alias: wheels g view) - Generate view
wheels generate property - Add model property
wheels generate route - Generate route
wheels generate test - Generate tests
wheels generate snippets - Code snippets
wheels generate scaffold - Complete CRUD
Common options across generators:
--force - Overwrite existing files
--help - Show command help
Commands for managing database schema and migrations.
wheels db create - Create database
wheels db drop - Drop database
wheels dbmigrate info - Show migration status
wheels dbmigrate latest - Run all pending migrations
wheels dbmigrate up - Run next migration
wheels dbmigrate down - Rollback last migration
wheels dbmigrate reset - Reset all migrations
wheels dbmigrate exec [version] - Run specific migration
wheels dbmigrate create blank [name] - Create empty migration
wheels dbmigrate create table [name] - Create table migration
wheels dbmigrate create column [table] [column] - Add column migration
wheels dbmigrate remove table [name] - Drop table migration
Commands for running and managing tests.
wheels test run - Run tests
wheels test all - Run all tests
wheels test coverage - Run coverage tests
wheels test integration - Run integration tests
wheels test unit - Run unit tests
wheels test watch - Rerun tests on any change
Commands for managing development environments and application context.
wheels env setup [name] - Setup environment
wheels env list - List environments
wheels env merge - Merge env files
wheels env set - Set env variable
wheels env show - Show env variables
Commands for analyzing code quality and patterns.
wheels analyze code - Analyze code quality
wheels analyze performance - Performance analysis
wheels analyze security - Security analysis
Commands for Docker container management and deployment.
wheels docker init - Initialize Docker configuration
wheels docker deploy - Deploy using Docker
Many commands have shorter aliases:
Creating a new feature:
Starting development:
Deployment preparation:
wheels env show [options]--key
Show a specific environment variable by key name
-
--format
Output format: table or json
table
--file
Specific .env file to read
.env
wheels env showwheels env show --key=DB_HOSTwheels env show --format=jsonwheels env show --file=.env.production# View development variables
wheels env show --file=.env.development
# View production variables
wheels env show --file=.env.production# Check database configuration
wheels env show --key=DB_NAME
wheels env show --key=DB_HOST
# Check API settings
wheels env show --key=API_KEYEnvironment Variables Viewer
Environment Variables from .env:
╔════════╤══════════════════════════╤═══════════════════════════╗
║ Source │ Variable │ Value ║
╠════════╪══════════════════════════╪═══════════════════════════╣
║ .env │ DB_HOST │ localhost ║
╟────────┼──────────────────────────┼───────────────────────────╢
║ .env │ DB_NAME │ myapp ║
╟────────┼──────────────────────────┼───────────────────────────╢
║ .env │ DB_PASSWORD │ ******** ║
╟────────┼──────────────────────────┼───────────────────────────╢
║ .env │ DB_PORT │ 3306 ║
╟────────┼──────────────────────────┼───────────────────────────╢
║ .env │ DB_USER │ wheels ║
╟────────┼──────────────────────────┼───────────────────────────╢
║ .env │ API_BASE_URL │ https://api.example.com ║
╟────────┼──────────────────────────┼───────────────────────────╢
║ .env │ API_KEY │ ******** ║
╟────────┼──────────────────────────┼───────────────────────────╢
║ .env │ API_TIMEOUT │ 30 ║
╟────────┼──────────────────────────┼───────────────────────────╢
║ .env │ WHEELS_ENV │ development ║
╟────────┼──────────────────────────┼───────────────────────────╢
║ .env │ WHEELS_RELOAD_PASSWORD │ ******** ║
╟────────┼──────────────────────────┼───────────────────────────╢
║ .env │ APP_NAME │ My Application ║
╟────────┼──────────────────────────┼───────────────────────────╢
║ .env │ DEBUG_MODE │ true ║
╟────────┼──────────────────────────┼───────────────────────────╢
║ .env │ PORT │ 3000 ║
╚════════╧══════════════════════════╧═══════════════════════════╝
Tip: Access these in your app with application.env['KEY_NAME']
Or use them in config files: set(dataSourceName=application.env['DB_NAME'])
Wheels automatically loads .env on application start{
"API_BASE_URL": "https://api.example.com",
"API_KEY": "********",
"APP_NAME": "My Application",
"DB_HOST": "localhost",
"DB_NAME": "myapp",
"DB_PASSWORD": "********",
"DB_PORT": "3306",
"DEBUG_MODE": "true",
"WHEELS_ENV": "development"
}## Database Configuration
DB_HOST=localhost
DB_PORT=3306
DB_NAME=myapp
DB_USER=wheels
DB_PASSWORD="secret123"
## Application Settings
WHEELS_ENV=development
DEBUG_MODE=true{
"DB_HOST": "localhost",
"DB_PORT": "3306",
"DB_NAME": "myapp",
"WHEELS_ENV": "development",
"DEBUG_MODE": "true"
}This command must be run from a Wheels project root directoryNo .env file found in project root
Create a .env file with key=value pairs, for example:
## Database Configuration
DB_HOST=localhost
DB_PORT=3306
DB_NAME=myapp
DB_USER=wheels
DB_PASSWORD=secret
## Application Settings
WHEELS_ENV=development
WHEELS_RELOAD_PASSWORD=mypasswordEnvironment variable 'MISSING_KEY' not found
Available keys in .env:
- API_KEY
- DB_HOST
- DB_NAME
- DEBUG_MODE
- WHEELS_ENV# Review all current settings
wheels env show
# Check what's different between environments
wheels env show --file=.env.development
wheels env show --file=.env.production# Check if a specific variable is set
wheels env show --key=DB_HOST
# Verify API configuration
wheels env show --key=API_BASE_URL
wheels env show --key=API_KEY# Verify development setup
wheels env show --file=.env.development
# Check staging configuration
wheels env show --file=.env.staging# Generate configuration documentation
wheels env show --format=json > docs/environment-config.json
# Create environment template
wheels env show --file=.env.example<!-- In your Wheels application -->
<cfset dataSource = application.env['DB_NAME']>
<cfset apiKey = application.env['API_KEY']>
<cfset debugMode = application.env['DEBUG_MODE']>
<!-- In config files -->
<cfset set(dataSourceName=application.env['DB_NAME'])>
<cfset set(URLRewriting=application.env['URL_REWRITING'])># Regularly review your environment configuration
wheels env show# Always verify environment-specific settings
wheels env show --file=.env.production --key=WHEELS_ENV
wheels env show --file=.env.development --key=DEBUG_MODE# Check that sensitive values are properly set
wheels env show --key=API_KEY
wheels env show --key=DB_PASSWORD# Generate configuration documentation
wheels env show --format=json > config-docs.json# When debugging configuration issues:
# 1. Check if variable exists
wheels env show --key=PROBLEMATIC_VAR
# 2. Review all variables for typos
wheels env show
# 3. Compare against working environment
wheels env show --file=.env.working# View current config, then update if needed
wheels env show --key=DB_HOST
wheels env set DB_HOST=newhost.com
# Check merged configuration
wheels env merge .env.base .env.local --dry-run
wheels env show --file=.env.merged# In deployment scripts
wheels env show --file=.env.production --format=json | jq '.DB_HOST'# Quick environment check during development
wheels env show --key=WHEELS_ENV
wheels env show --key=DEBUG_MODEwheels get settings [settingName]wheels get settingswheels get settings cacheQuerieswheels get settings cache// config/settings.cfm
set(dataSourceName="myapp_db");
set(cacheQueries=true);
set(errorEmailAddress="[email protected]");Wheels Settings (development environment):
allowConcurrentRequestScope: false
cacheActions: false
cacheCullInterval: 5
cacheCullPercentage: 10
cacheDatabaseSchema: false
cacheFileChecking: false
cacheImages: false
cacheModelConfig: false
cachePages: false
cachePartials: false
cacheQueries: false
cacheRoutes: false
dataSourceName: myapp_db
errorEmailAddress: [email protected]
showDebugInformation: true
showErrorInformation: true
URLRewriting: partial
Total settings: 17wheels get settings cacheWheels Settings (development environment):
cacheActions: false
cacheCullInterval: 5
cacheCullPercentage: 10
cacheDatabaseSchema: false
cacheFileChecking: false
cacheImages: false
cacheModelConfig: false
cachePages: false
cachePartials: false
cacheQueries: true
cacheRoutes: false
Total settings: 11wheels get settings dataSourceNameWheels Settings (production environment):
dataSourceName: production_db
Total settings: 1wheels get settings nonexistentNo settings found matching 'nonexistent'# Check development settings
WHEELS_ENV=development wheels get settings cache
# Check production settings
WHEELS_ENV=production wheels get settings cachewheels get settings dataSourceNamewheels get settings cache# See all current settings
wheels get settings
# Check specific problematic setting
wheels get settings showDebugInformation# Verify production settings
WHEELS_ENV=production wheels get settings// config/settings.cfm
set(dataSourceName="myapp");
set(URLRewriting="partial");
set(errorEmailAddress="[email protected]");// config/production/settings.cfm
set(cacheQueries=true);
set(cachePages=true);
set(cachePartials=true);
set(showDebugInformation=false);
set(showErrorInformation=false);
set(sendEmailOnError=true);// config/development/settings.cfm
set(cacheQueries=false);
set(showDebugInformation=true);
set(showErrorInformation=true);
set(sendEmailOnError=false);set(cacheQueries=true);
set(showDebugInformation=false);set(cacheCullInterval=5);
set(cacheCullPercentage=10);set(dataSourceName="myapp_db");
set(errorEmailAddress="[email protected]");complexSetting: [array with 5 items]
structSetting: {3 items}Error: This command must be run from a Wheels application directoryError reading settings: [specific error message]
Details: [additional error details if available]# Check environment, then settings
wheels get environment
wheels get settings
# Verify cache settings before clearing cache
wheels get settings cache
wheels clear cache
# Check database settings before running migrations
wheels get settings dataSourceName
wheels db migratewheels generate property name=<modelName> columnName=<propertyName> [options]
#Can also be used as:
wheels g property name=<modelName> columnName=<propertyName> [options]name
Model name (table name)
Required
columnName
Name of column to add
Required
dataType
Type of column
biginteger, binary, boolean, date, datetime, decimal, float, integer, string, text, time, timestamp, uuid
string
default
Default value for column
Any valid default value
""
allowNull
Whether to allow null values
true, false
true
limit
Character or integer size limit
Numeric value
0
precision
Precision for decimal columns
Numeric value
0
scale
Scale for decimal columns
Numeric value
0
biginteger
BIGINT
Large integer values
binary
BLOB
Binary data
boolean
BOOLEAN
Boolean (true/false) values
date
DATE
Date only
datetime
DATETIME
Date and time
decimal
DECIMAL
Decimal numbers with precision/scale
float
FLOAT
Floating point numbers
integer
INTEGER
Integer values
string
VARCHAR
Variable character strings
text
TEXT
Long text content
time
TIME
Time only
timestamp
TIMESTAMP
Timestamp values
uuid
VARCHAR(35)
UUID/GUID strings
wheels generate property name=User columnName=firstNamewheels generate property name=User columnName=isActive --dataType=boolean --default=0wheels generate property name=User columnName=lastLoggedIn --dataType=datetimewheels generate property name=Product columnName=price --dataType=decimal --precision=10 --scale=2wheels generate property name=User columnName=username --dataType=string --limit=50 --allowNull=falsecomponent extends="wheels.migrator.Migration" {
function up() {
transaction {
addColumn(
table = "users",
columnName = "firstName",
columnType = "string",
limit = 255,
allowNull = true
);
}
}
function down() {
transaction {
removeColumn(table = "users", columnName = "firstName");
}
}
}<!-- Added to _form.cfm -->
#textField(objectName="user", property="firstName")#<!-- Added to table header -->
<th>First Name</th>
<!-- Added to table body -->
<td>#user.firstName#</td><!-- Added to show.cfm -->
<p><strong>First Name:</strong> #user.firstName#</p>wheels generate property name=User columnName=isActive --dataType=boolean
# Automatically gets default=0Hold On! We couldn't find a corresponding Model at /app/models/User.cfc:
are you sure you wish to add the property 'email' to users? [y/n]# Input: columnName=firstName
# Migration: columnName=firstnamewheels plugin update:all [--dryRun]dryRun
No
boolean
Preview updates without actually installing them
wheels plugin update:all===========================================================
Checking for Plugin Updates
===========================================================
bcrypt [OUTDATED] 0.0.3 -> 0.0.4
shortcodes [OUTDATED] 0.0.3 -> 0.0.4
wheels-test [OK] v1.0.0
===========================================================
Found 2 outdated plugins
Updating Plugins:
Plugin Current Latest Status
---------------------------------------------------------------
bcrypt 0.0.3 0.0.4 [UPDATE]
shortcodes 0.0.3 0.0.4 [UPDATE]
-----------------------------------------------------------
Updating bcrypt from 0.0.3 to 0.0.4...
[CommandBox installation output...]
[OK] bcrypt updated successfully
Updating shortcodes from 0.0.3 to 0.0.4...
[CommandBox installation output...]
[OK] shortcodes updated successfully
===========================================================
Update Summary
===========================================================
[OK] 2 plugins updated successfully
Updated plugins:
- bcrypt (0.0.3 -> 0.0.4)
- shortcodes (0.0.3 -> 0.0.4)
Commands:
wheels plugin list View all installed plugins
wheels plugin outdated Check for more updateswheels plugin update:all===========================================================
Checking for Plugin Updates
===========================================================
bcrypt [OK] v0.0.4
shortcodes [OK] v0.0.4
wheels-test [OK] v1.0.0
===========================================================
[OK] All plugins are already up to date!
No updates required.
Commands:
wheels plugin list View all installed plugins
wheels plugin outdated Check for updateswheels plugin update:all --dryRun===========================================================
Checking for Plugin Updates (DRY RUN)
===========================================================
bcrypt [OUTDATED] 0.0.3 -> 0.0.4
shortcodes [OUTDATED] 0.0.3 -> 0.0.4
wheels-test [OK] v1.0.0
===========================================================
Found 2 outdated plugins
Would Update:
Plugin Current Latest
---------------------------------------------------------------
bcrypt 0.0.3 0.0.4
shortcodes 0.0.3 0.0.4
-----------------------------------------------------------
[DRY RUN] No updates performed
To perform these updates:
wheels plugin update:allwheels plugin update:all===========================================================
Checking for Plugin Updates
===========================================================
bcrypt [OUTDATED] 0.0.3 -> 0.0.4
problematic-plugin [ERROR] Could not check version
shortcodes [OK] v0.0.4
===========================================================
Found 1 outdated plugin
Updating Plugins:
Plugin Current Latest Status
---------------------------------------------------------------
bcrypt 0.0.3 0.0.4 [UPDATE]
-----------------------------------------------------------
Updating bcrypt from 0.0.3 to 0.0.4...
[CommandBox installation output...]
[OK] bcrypt updated successfully
===========================================================
Update Summary
===========================================================
[OK] 1 plugin updated successfully
Updated plugins:
- bcrypt (0.0.3 -> 0.0.4)
Could not check 1 plugin:
- problematic-plugin
Commands:
wheels plugin list View all installed plugins
wheels plugin outdated Check for more updateswheels plugin update:all===========================================================
Checking for Plugin Updates
===========================================================
No plugins installed in /plugins folder
Install plugins with: wheels plugin install <plugin-name>wheels plugin update:all --dryRun===========================================================
Checking for Plugin Updates
===========================================================
bcrypt [ERROR] Could not check version
shortcodes [ERROR] Could not check version
===========================================================
[ERROR] Unable to check for updates
Could not check 2 plugins due to network issues.
Please check your internet connection and try again.# See which plugins are outdated
wheels plugin outdated
# Update all outdated plugins
wheels plugin update:all# Update all plugins except one
wheels plugin update:all --dryRun # See what would update
wheels plugin update plugin1 # Update individually
wheels plugin update plugin2wheels generate app [name]
Create new application
wheels generate scaffold [name]
Generate complete CRUD
wheels dbmigrate latest
Run database migrations
wheels test run
Run application tests
wheels reload
Reload application
wheels g controller users # Same as: wheels generate controller users
wheels g model user # Same as: wheels generate model user
wheels new myapp # Same as: wheels generate app myappwheels generate scaffold name=product properties=name:string,price:decimal
wheels dbmigrate latest
wheels test runwheels reload # Reload the application
wheels test run # Run testswheels test run
wheels analyze security
wheels analyze performance
wheels dbmigrate infoWHEELS_ENV
Environment mode
development
WHEELS_DATASOURCE
Database name
From config
WHEELS_RELOAD_PASSWORD
Reload password
From config
0
Success
1
General error
2
Invalid arguments
3
File not found
4
Permission denied
5
Database error





The wheels config check command validates your Wheels application configuration settings across different environments. It performs comprehensive checks on security settings, database configuration, environment-specific settings, and more, helping you identify potential issues before deployment.
wheels config check [environment] [--verbose] [--fix]environment
string
No
The environment to check (development, testing, production). If not specified, detects from current configuration
--verbose
flag
No
Show detailed validation information including fix suggestions
--fix
flag
No
Attempt to automatically fix certain issues
wheels config checkwheels config check productionwheels config check --verbosewheels config check --fixwheels config check production --verbose --fixThe command performs validation across multiple configuration areas:
Verifies existence of config/settings.cfm
Checks for environment-specific settings files
Validates file structure and syntax
Datasource Configuration: Ensures a datasource is configured
Core Settings: Validates essential Wheels configuration parameters
Sensitive Values: Detects hardcoded passwords, API keys, tokens
Debug Mode: Ensures debug is disabled in production
Error Emails: Checks error notification setup for production
Reload Password: Validates reload password strength
SSL/HTTPS: Verifies SSL enforcement in production
Session Security: Checks session timeout settings
Error Information: Ensures error details are hidden in production
Datasource Validity: Verifies datasource exists and is accessible
Migration Settings: Checks auto-migration configuration
Connection Settings: Validates database connection parameters
Environment Directory: Checks for environment-specific config directories
Caching Configuration: Validates cache settings for production
Performance Settings: Reviews optimization configurations
File Existence: Checks for .env file presence
File Permissions: Validates security permissions
Git Ignore: Ensures .env is in .gitignore
Environment Variables: Verifies WHEELS_ENV or Environment variable
SSL Enforcement: Validates forceSSL setting
Session Management: Reviews session timeout configuration
Error Handling: Ensures proper error information hiding
Cache Settings: Verifies all caching is enabled
The command provides real-time status as it performs checks:
========================================
Configuration Validation
Environment: development
========================================
Checking configuration files... [OK]
Checking required settings... [OK]
Checking security configuration... [WARNING]
Checking database configuration... [OK]
Checking environment-specific settings... [WARNING]
Checking .env file configuration... [FAILED]
========================================[OK] - Check passed successfully
[WARNING] - Non-critical issues found
[FAILED] - Critical errors detected
[FIXED] - Issue was automatically fixed
[ERRORS] (2):
- Missing config/settings.cfm file
- Datasource 'myapp_db' not found[WARNINGS] (3):
- Possible hardcoded sensitive value in 'apiKey'
- No environment-specific config directory for 'production'
- Automatic database migration is enabled[FIXED] Issues:
- Created sample .env file
- Added .env to .gitignore[PASSED] Configuration validation successful!
All checks completed successfully.Or with issues:
[FAILED] Configuration check failed
Found: 2 errors, 3 warnings
Tip: Run with --verbose flag for detailed fix suggestionsWhen using --verbose, each issue includes detailed fix suggestions:
wheels config check --verboseOutput:
[ERRORS] (1):
- Debug mode is enabled in production
--> Fix: Set showDebugInformation = false in config/production/settings.cfmThe --fix flag attempts to automatically resolve certain issues:
Create sample .env file if missing
Add .env to .gitignore
Create basic configuration templates
Set default secure values
wheels config check --fixOutput:
[FIXED] Issues:
- Created sample .env file
- Added .env to .gitignoreThe command detects the current environment in the following priority:
Command parameter (if specified)
WHEELS_ENV in .env file
Environment in .env file
WHEELS_ENV system environment variable
Environment system environment variable
Default to 'development'
# Check production configuration before deployment
wheels config check production --verbose# Ensure development environment is properly configured
wheels config check development --fix# In your CI/CD script
wheels config check production
if [ $? -ne 0 ]; then
echo "Configuration validation failed!"
exit 1
fi# Check for security issues across all environments
wheels config check development --verbose
wheels config check testing --verbose
wheels config check production --verboseThe command returns different exit codes for scripting:
0 - All checks passed (may have warnings)
1 - One or more errors found
Run Before Deployment - Always validate production configuration before deploying
Use in CI/CD - Include configuration checks in your automated pipelines
Regular Audits - Periodically check all environments for security issues
Fix Warnings - While warnings don't fail the check, addressing them improves security and performance
Version Control - After using --fix, review and commit the changes
Environment-Specific Configs - Create separate configuration directories for each environment
Error: This command must be run from a Wheels application directorySolution: Run the command from your Wheels application root directory
Error reading configuration: [error message]Solution: Check file permissions and ensure configuration files are valid CFML
Datasource 'myapp_db' not foundSolution: Configure the datasource in your CFML administrator or Application.cfc
.env file has overly permissive permissionsSolution: Run chmod 600 .env to restrict file permissions
<cfscript>
// Application settings
set(dataSourceName = application.env["DB_NAME"]);
set(reloadPassword = application.env["RELOAD_PASSWORD"]);
// Security settings
set(showDebugInformation = false);
set(showErrorInformation = false);
// Performance settings
set(cacheQueries = true);
set(cachePartials = true);
</cfscript><cfscript>
// Production overrides
set(showDebugInformation = false);
set(sendEmailOnError = true);
set(errorEmailAddress = "[email protected]");
set(forceSSL = true);
// Enable all caching
set(cacheControllerConfig = true);
set(cacheDatabaseSchema = true);
set(cacheFileChecking = true);
set(cacheImages = true);
set(cacheModelConfig = true);
set(cachePartials = true);
set(cacheQueries = true);
set(cacheRoutes = true);
</cfscript># Wheels Environment Configuration
WHEELS_ENV=development
# Database Configuration
DB_HOST=localhost
DB_PORT=3306
DB_NAME=myapp_development
DB_USER=dbuser
DB_PASSWORD=secure_password
# Application Settings
RELOAD_PASSWORD=change_this_password
SECRET_KEY=your_secret_key_here
API_KEY=your_api_keywheels get environment - Display current environment setting
wheels env merge - Merge environment configurations
wheels db create - Create database for current environment
wheels test - Run tests in current environment
Never commit .env files - Always keep .env in .gitignore
Use environment variables - Don't hardcode sensitive values
Restrict file permissions - Set appropriate permissions on configuration files
Different passwords per environment - Use unique credentials for each environment
Enable SSL in production - Always force SSL for production environments
Hide error details - Never show debug information in production
Run with --verbose first to understand all issues before using --fix
Create environment-specific directories even if empty for better organization
Use the command as part of your deployment checklist
Keep configuration files well-commented for team members
Regularly update and review security settings
Use strong, unique reload passwords for each environment
Document any custom configuration requirements for your team
Learn how to manage configuration in Wheels using environment variables, settings files, and CLI tools for configuration management.
This guide covers configuration management in Wheels, including working with environment variables, settings files, and the CLI tools for managing configuration.
Wheels provides flexible configuration management through:
Environment-specific settings files
Environment variables (.env files)
CLI commands for configuration management
Security best practices
Wheels supports two formats for .env files:
# Database Configuration
DB_HOST=localhost
DB_PORT=3306
DB_NAME=myapp_development
DB_USER=wheels
DB_PASSWORD=secretpassword
# Application Settings
WHEELS_ENV=development
RELOAD_PASSWORD=myreloadpassword
SECRET_KEY=a1b2c3d4e5f6g7h8i9j0
# Feature Flags
DEBUG_MODE=true
CACHE_ENABLED=false{
"DB_HOST": "localhost",
"DB_PORT": 3306,
"DB_NAME": "myapp_development",
"DB_USER": "wheels",
"DB_PASSWORD": "secretpassword",
"WHEELS_ENV": "development",
"DEBUG_MODE": true
}Wheels automatically loads environment-specific .env files:
.env - Base configuration (always loaded first)
.env.{environment} - Environment-specific overrides (loaded second)
Example structure:
myapp/
├── .env # Base configuration
├── .env.development # Development overrides
├── .env.testing # Testing overrides
├── .env.production # Production settings
└── .gitignore # MUST exclude .env files!Use ${VAR} syntax to reference other variables:
# .env
APP_NAME=MyWheelsApp
APP_ENV=development
# Database URLs with interpolation
DB_HOST=localhost
DB_PORT=3306
DB_NAME=${APP_NAME}_${APP_ENV}
DB_URL=mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}
# API endpoints
API_BASE_URL=https://api.example.com
API_USERS_URL=${API_BASE_URL}/users
API_ORDERS_URL=${API_BASE_URL}/ordersWheels automatically casts certain values:
# Booleans (cast to true/false)
DEBUG_MODE=true
CACHE_ENABLED=false
# Numbers (cast to numeric values)
MAX_CONNECTIONS=100
CACHE_TTL=3600
REQUEST_TIMEOUT=30
# Strings (remain as strings)
APP_NAME=MyApp
API_KEY=abc123def456Access environment variables in your settings files:
// config/settings.cfm
// Using application.env struct
set(dataSourceName = application.env['DB_NAME']);
set(dataSourceUserName = application.env['DB_USER']);
set(dataSourcePassword = application.env['DB_PASSWORD']);
// With defaults
set(cacheQueries = application.env['CACHE_ENABLED'] ?: false);
set(reloadPassword = application.env['RELOAD_PASSWORD'] ?: 'defaultpassword');
// Environment-specific logic
if (application.env['WHEELS_ENV'] == 'production') {
set(showDebugInformation = false);
set(cacheFileChecking = true);
}config/
├── settings.cfm # Base configuration
├── development/
│ └── settings.cfm # Development overrides
├── testing/
│ └── settings.cfm # Testing overrides
├── production/
│ └── settings.cfm # Production overrides
└── maintenance/
└── settings.cfm # Maintenance mode settingsSettings are loaded in this order (later overrides earlier):
Framework defaults
config/settings.cfm
config/{environment}/settings.cfm
Plugin settings
Runtime set() calls
// config/settings.cfm
set(dataSourceName = application.env['DB_NAME'] ?: 'wheelstutorial');
set(dataSourceUserName = application.env['DB_USER'] ?: 'root');
set(dataSourcePassword = application.env['DB_PASSWORD'] ?: '');// config/production/settings.cfm
// Production optimizations
set(cacheQueries = true);
set(cachePartials = true);
set(cachePages = true);
set(cacheActions = true);
set(cacheImages = true);
set(cacheModelConfig = true);
set(cacheControllerConfig = true);
set(cacheRoutes = true);
set(cacheDatabaseSchema = true);
set(cacheFileChecking = false);
// Security
set(showDebugInformation = false);
set(showErrorInformation = false);
// Error handling
set(sendEmailOnError = true);
set(errorEmailAddress = application.env['ERROR_EMAIL']);Export current configuration:
# View current config (masked)
wheels config dump
# Export production config as JSON
wheels config dump production --format=json --output=prod-config.json
# Export as .env format
wheels config dump --format=env --output=config.env
# Export without masking (careful!)
wheels config dump --no-maskValidate configuration for issues:
# Basic check
wheels config check
# Check production with fixes
wheels config check production --fix
# Verbose output with suggestions
wheels config check --verboseCommon checks performed:
Missing required settings
Hardcoded sensitive values
Production security settings
Database configuration
Caching optimizations
Find differences between environments:
# Full comparison
wheels config diff development production
# Show only differences
wheels config diff development production --changes-only
# Export as JSON
wheels config diff testing production --format=json > diff.jsonGenerate secure secrets:
# Generate and display
wheels secret
wheels secret --type=base64 --length=48
# Save to .env
wheels secret --save-to-env=SECRET_KEY
wheels secret --type=uuid --save-to-env=API_KEY
# Multiple secrets
wheels secret --save-to-env=SESSION_SECRET
wheels secret --save-to-env=CSRF_TOKEN
wheels secret --save-to-env=ENCRYPTION_KEY# Set single variable
wheels env set DB_HOST=localhost
# Set multiple
wheels env set DB_HOST=localhost DB_PORT=3306 DB_NAME=myapp
# Update production file
wheels env set --file=.env.production API_URL=https://api.example.com# Basic validation
wheels env validate
# Check required variables
wheels env validate --required=DB_HOST,DB_USER,DB_PASSWORD
# Validate production
wheels env validate --file=.env.production --verbose# Merge for deployment
wheels env merge .env .env.production --output=.env.deployed
# Preview merge
wheels env merge .env.defaults .env.local --dry-runAlways add .env files to .gitignore:
# Environment files
.env
.env.*
!.env.example
!.env.defaultsGenerate cryptographically secure values:
# For session secrets
wheels secret --type=hex --length=64 --save-to-env=SESSION_SECRET
# For API keys
wheels secret --type=base64 --length=48 --save-to-env=API_SECRET
# For passwords
wheels secret --type=alphanumeric --length=32Run checks before deployment:
# Full production check
wheels config check production --verbose
# Required checks
wheels env validate --file=.env.production \
--required=DB_HOST,DB_USER,DB_PASSWORD,SECRET_KEY,RELOAD_PASSWORDAlways use masking in logs/output:
// In your code
writeOutput("Database: #application.env['DB_NAME']#");
writeOutput("Password: ***MASKED***"); // Never output passwordsUse different secrets per environment:
# .env.development
SECRET_KEY=dev_secret_key_only_for_local
# .env.production
SECRET_KEY=prod_0a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0# 1. Create base config
wheels env set WHEELS_ENV=development DB_NAME=myapp_dev
# 2. Create production config
wheels env set --file=.env.production \
WHEELS_ENV=production \
DB_NAME=myapp_prod \
DB_HOST=prod.database.com
# 3. Create testing config
wheels env set --file=.env.testing \
WHEELS_ENV=testing \
DB_NAME=myapp_test
# 4. Validate all
wheels env validate
wheels env validate --file=.env.production
wheels env validate --file=.env.testing# 1. Merge configs for deployment
wheels env merge .env.defaults .env.production --output=.env.deploy
# 2. Validate merged config
wheels env validate --file=.env.deploy \
--required=DB_HOST,DB_USER,DB_PASSWORD,SECRET_KEY
# 3. Check security
wheels config check production --verbose# 1. Copy example file
cp .env.example .env
# 2. Set local values
wheels env set DB_HOST=localhost DB_USER=root DB_PASSWORD=
# 3. Generate secrets
wheels secret --save-to-env=SECRET_KEY
wheels secret --save-to-env=RELOAD_PASSWORD
# 4. Validate
wheels env validateCheck file exists and is readable
Verify format (properties vs JSON)
Check for syntax errors:
wheels env validateCheck WHEELS_ENV variable:
wheels env show --key=WHEELS_ENVVerify environment detection order:
.env file WHEELS_ENV
System environment WHEELS_ENV
Default to 'development'
Ensure variables are defined before use
Check syntax: ${VAR_NAME}
Maximum 10 interpolation passes (prevent loops)
Run security check:
wheels config check --verboseMove hardcoded values to .env
Use application.env references
Use .env files for all environment-specific values
Never commit secrets - use .gitignore
Generate strong secrets with wheels secret
Validate configuration before deployment
Use interpolation to reduce duplication
Environment-specific files for overrides
Check security regularly with CLI tools
Document required variables in .env.example
Mask sensitive values in output
Test configuration changes in development first
Create a new database based on your datasource configuration.
The wheels db create command creates a new database using the connection information from your configured datasource. If the datasource doesn't exist, the command offers an interactive wizard to create it for you, supporting MySQL, PostgreSQL, SQL Server, Oracle, and H2 databases.
Automatic .env file reading: Reads actual database credentials from .env.{environment} files using generic DB_* variable names
Interactive datasource creation: Prompts for credentials when datasource doesn't exist
Environment validation: Checks if environment exists before prompting for credentials
Smart error handling: Single, clear error messages without duplication
Post-creation setup: Automatically creates environment files and writes datasource to app.cfm after successful database creation
Examples:
Create database using default datasource:
Create database for development:
Create database for testing:
Create database with specific name:
Drop existing database and recreate:
If the specified datasource doesn't exist, the command will prompt you to create it interactively:
The datasource will be saved to both /config/app.cfm and CFConfig.json.
Creates database with UTF8MB4 character set
Uses utf8mb4_unicode_ci collation
Connects to information_schema system database
Supports MySQL 5.x, MySQL 8.0+, and MariaDB drivers
Default port: 3306
Creates database with UTF8 encoding
Uses en_US.UTF-8 locale settings
Terminates active connections before dropping (when using --force)
Connects to postgres system database
Default port: 5432
Creates database with default settings
Connects to master system database
Supports Microsoft SQL Server JDBC driver
Default port: 1433
Creates a USER/schema (Oracle's equivalent of a database)
Grants CONNECT and RESOURCE privileges automatically
Connects using SID (e.g., FREE, ORCL, XE)
Supports Oracle 12c+ with Container Database (CDB) architecture
Uses _ORACLE_SCRIPT session variable for non-C## users
Important: Database names cannot contain hyphens (use underscores)
Default port: 1521
Default SID: FREE (Oracle XE)
Embedded database - no server required
Database file created automatically on first connection
Only prompts for database name and optional credentials
No host/port configuration needed
Ideal for development and testing
The command provides real-time, formatted output showing each step:
⚠️ Note: This command depends on configuration values. Please verify your database configuration before executing it.
Datasource Configuration: The datasource can be configured in /config/app.cfm or created interactively
Database Privileges: The database user must have CREATE DATABASE privileges (CREATE USER for Oracle)
Network Access: The database server must be accessible
JDBC Drivers: Appropriate JDBC drivers must be available in the classpath
If you see "Driver not found" error for Oracle, you need to manually install the Oracle JDBC driver:
Steps to install Oracle JDBC driver:
Download the driver from Oracle's official website:
Visit:
Download the appropriate ojdbc JAR file (e.g., ojdbc11.jar or ojdbc8.jar)
Place the JAR file in CommandBox's JRE library directory:
Common paths:
Windows: C:\Program Files\CommandBox\jre\lib\
Mac/Linux: /usr/local/lib/CommandBox/jre/lib/
Restart CommandBox completely:
Important: Close ALL CommandBox instances
Don't just run reload - fully exit and restart CommandBox
This ensures the JDBC driver is properly loaded into the classpath
Verify installation:
You should see: [OK] Driver found: oracle.jdbc.OracleDriver
Note: Other database drivers (MySQL, PostgreSQL, MSSQL, H2) are typically included with CommandBox/Lucee by default.
No datasource was specified and none could be found in your Wheels configuration. Use the datasource= parameter or set dataSourceName in settings.
The specified datasource doesn't exist in your server configuration. The command will prompt you to create it interactively.
Oracle JDBC driver is not installed in CommandBox.
Fix: Follow the instructions above.
Quick steps:
Download ojdbc11.jar from
Place it in [CommandBox path]/jre/lib/
Close ALL CommandBox instances and restart (don't just reload)
Try the command again
The database already exists. Use --force flag to drop and recreate it:
The database user doesn't have permission to create databases. Grant CREATE privileges to the user.
Common causes:
Database server is not running
Wrong server/port configuration
Invalid credentials
Network/firewall issues
For PostgreSQL: pg_hba.conf authentication issues
For Oracle: TNS listener not running or incorrect SID
Database name contains invalid characters. Oracle usernames can only contain letters, numbers, and underscores.
Fix: Use underscores instead of hyphens:
Oracle Container Database (CDB) requires common users to start with C## prefix, or the connecting user needs privileges to set the _ORACLE_SCRIPT session variable.
Fix: Either use a C## prefixed name or grant additional privileges:
Attempting to drop an Oracle system user (SYS, SYSTEM, etc.). Choose a different database name.
Fix: Use a non-system username:
The command intelligently detects datasource configuration from multiple sources:
.env.{environment} file (highest priority - NEW!)
Reads actual credential values using generic DB_* variable names
Example: DB_HOST=localhost, DB_USER=sa, DB_PASSWORD=MyPass123!
Solves the issue where app.cfm contains unresolved placeholders like ##this.env.DB_HOST##
Datasource definitions in /config/app.cfm
Falls back to parsing connection strings if .env file doesn't exist
Maintains backward compatibility
Environment-specific settings: /config/[environment]/settings.cfm
Detects datasource name from set(dataSourceName="...")
General settings: /config/settings.cfm
Global datasource configuration
Database driver type (MySQL, PostgreSQL, MSSQL, Oracle, H2)
Connection details:
Host and port
Database name
Username and password
Oracle SID (if applicable)
All database types now use consistent DB_* variable names in .env files:
This makes it easy to switch database types without changing variable names.
- Drop an existing database
- Create and setup database
- Run migrations after creating database
The wheels env validate command validates the format and content of .env files in your Wheels project. This command helps ensure your environment configuration is properly formatted, contains required variables, and follows best practices. It's essential for catching configuration errors before deployment and maintaining consistent environment setups across different environments.
Validates the .env file in your project root
Validates the .env.production file
Validates that specific required variables are present
Shows detailed information about all variables found
Ensures production environment has all critical variables
Combines required variable checking with detailed output
The command validates several aspects of your .env file format:
Properties format: Standard KEY=VALUE pairs
JSON format: Valid JSON structure
Automatic detection based on content
Missing equals sign: KEY VALUE (invalid)
Empty key names: =value (invalid)
Valid key=value format: KEY=value (valid)
Valid characters: Letters, numbers, underscores only
Standard format: UPPER_SNAKE_CASE recommended
Non-standard warnings: Mixed case, special characters
Ensures specified variables are present and have values
Identifies when the same key appears multiple times:
Identifies common placeholder values in sensitive variables:
your_password
your_secret
change_me
xxx
TODO
Solution: Add equals sign: DB_HOST=localhost
Solution: Provide a key name: MY_KEY=value
Solution: Fix JSON syntax or convert to properties format
Solution: Add the missing variable: API_KEY=your-api-key
Solution: Provide a value: DB_PASSWORD=your-password
Recommendation: Use DB_HOST instead of dbHost
Recommendation: Replace placeholder with actual value
Recommendation: Remove duplicate or rename one key
The command returns different exit codes for automation:
0: Validation passed (may have warnings)
1: Validation failed (has errors)
This makes it perfect for use in scripts and CI/CD pipelines:
Use the validation to catch placeholder values in sensitive variables
Regularly validate that required security-related variables are present
Use --verbose mode carefully in CI/CD (sensitive values are masked but logs might be visible)
Validate environment files before committing changes
Use different required variable lists for different environments
Set up validation as part of your local development setup script
Include validation in project setup documentation
Use validation to ensure consistent environment setup across team members
Define standard required variables for your project type
Integrate validation into deployment pipelines
Use exit codes to fail deployments when validation errors occur
Set up regular validation checks for configuration drift detection
Initialize a new CFWheels plugin in the /plugins directory.
The plugin init command creates a new CFWheels plugin following the standard CFWheels plugin structure. The plugin is created directly in your application's /plugins directory and includes all necessary files to get started.
Creates plugin in /plugins directory
Follows CFWheels plugin conventions
Includes mixin="global" for framework-wide availability
Generates documentation files
Includes test suite
Ready for ForgeBox publishing
Output:
The command creates the following structure in /plugins/pluginName/:
Key Features:
mixin="global" makes functions available everywhere in Wheels
Functions documented with Wheels doc format [section: Plugins]
Version tracking via this.version
Edit /plugins/myHelper/myHelper.cfc and add your plugin methods:
Edit index.cfm and README.md with usage examples and function descriptions.
Then in your Wheels application:
Edit /plugins/myHelper/tests/myHelperTest.cfc:
Run tests:
Common CFWheels plugin categories:
Data Helpers - String manipulation, date formatting, validation
Authentication - User authentication, session management, encryption
API Tools - REST helpers, JSON formatting, API clients
Database - Query helpers, soft delete, auditing
UI Components - Form helpers, tables, pagination
Email - Email formatting, templates, sending
Caching - Cache management, warming, invalidation
Testing - Test helpers, fixtures, mocking
Naming Convention: Always prefix with wheels- (automatic)
Function Naming: Use clear, descriptive names
Documentation: Document all public functions with Wheels format
Testing: Include comprehensive test coverage
Versioning: Follow semantic versioning (MAJOR.MINOR.PATCH)
Dependencies: Minimize external dependencies
Compatibility: Test with supported Wheels versions
Wheels scans /plugins directory on startup
Each plugin's main CFC is instantiated
With mixin="global", functions become available in:
Controllers
Models
Views
Other plugins
Call wheels reload to reload plugins after changes
Solution: Choose a different name or remove the existing plugin first.
The command must be run from within a Wheels application directory.
Plugin is created directly in /plugins directory
Plugin name automatically prefixed with wheels- if not present
Folder name uses simple plugin name (without wheels- prefix)
Use mixin="global" to make functions available everywhere
Restart or reload Wheels after creating plugin
Plugin functions documented with [section: Plugins] format
Type must be cfwheels-plugins for ForgeBox categorization
- Install plugins from ForgeBox
- List installed plugins
- Reload application
- Full plugin development guide
wheels env validate [options]--file
Name of the .env file to validate
.env
--required
Comma-separated list of required keys that must be present
empty
--verbose
Show detailed validation information including all variables
false
wheels env validatewheels env validate --file=.env.productionwheels env validate --required=DB_HOST,DB_USER,DB_PASSWORDwheels env validate --verbosewheels env validate --file=.env.production --required=DB_HOST,DB_NAME,DB_USER,DB_PASSWORD,API_KEY# Validate all environment files
wheels env validate --file=.env.development
wheels env validate --file=.env.staging
wheels env validate --file=.env.production# Development requirements
wheels env validate --file=.env.development --required=DB_HOST,WHEELS_ENV
# Production requirements
wheels env validate --file=.env.production --required=DB_HOST,DB_NAME,DB_USER,DB_PASSWORD,API_KEY,WHEELS_ENVwheels env validate --file=.env.production --required=DB_HOST,API_KEY --verbosewheels env validate --required=DB_HOST,API_KEYLine 15: Duplicate key: 'DB_HOST' (previous value will be overwritten)Validating: .env
Summary:
Total variables: 12
Validation passed with no issues!Validating: .env.development
Warnings:
Line 5: Non-standard key name: 'dbHost' (should contain only letters, numbers, and underscores)
Line 12: Placeholder value detected for 'API_KEY'
Summary:
Total variables: 8
Validation passed with 2 warningsValidating: .env.production
Errors found:
Line 3: Invalid format (missing '='): DB_HOST localhost
Required key missing: 'API_KEY'
Line 8: Empty key name
Warnings:
Line 10: Duplicate key: 'DB_PORT' (previous value will be overwritten)
Summary:
Total variables: 6
Validation failed with 3 errorsValidating: .env
Summary:
Total variables: 10
Environment Variables:
DB:
DB_HOST = localhost
DB_NAME = myapp
DB_PASSWORD = ***MASKED***
DB_PORT = 3306
DB_USER = admin
API:
API_BASE_URL = https://api.example.com
API_KEY = ***MASKED***
API_TIMEOUT = 30
Other:
APP_NAME = My Application
WHEELS_ENV = development
Validation passed with no issues!Error: Line 5: Invalid format (missing '='): DB_HOST localhostError: Line 8: Empty key nameError: Invalid JSON format: Unexpected character at position 15Error: Required key missing: 'API_KEY'Warning: Required key has empty value: 'DB_PASSWORD'Warning: Line 3: Non-standard key name: 'dbHost' (should contain only letters, numbers, and underscores)Warning: Line 7: Placeholder value detected for 'API_KEY'Warning: Line 12: Duplicate key: 'DB_PORT' (previous value will be overwritten)# Validate production config before deployment
wheels env validate --file=.env.production --required=DB_HOST,DB_NAME,DB_USER,DB_PASSWORD,API_KEY
# Check staging environment
wheels env validate --file=.env.staging --required=DB_HOST,API_KEY# Quick validation during development
wheels env validate
# Detailed check when debugging
wheels env validate --verbose# In your deployment pipeline
wheels env validate --file=.env.production --required=DB_HOST,API_KEY
if [ $? -ne 0 ]; then
echo "Environment validation failed!"
exit 1
fi# Validate new team member's setup
wheels env validate --required=DB_HOST,WHEELS_ENV,API_KEY
# Check if all environments are properly configured
for env in development staging production; do
wheels env validate --file=.env.$env
done# Regular audit of all environment files
wheels env validate --file=.env.development --verbose
wheels env validate --file=.env.production --verbose# Add to your development routine
wheels env validate# Define different requirements for different environments
wheels env validate --file=.env.development --required=DB_HOST,WHEELS_ENV
wheels env validate --file=.env.production --required=DB_HOST,DB_NAME,DB_USER,DB_PASSWORD,API_KEY# Add to git pre-commit hooks
#!/bin/sh
wheels env validate --file=.env.example# In CI/CD pipeline
wheels env validate --file=.env.production --required=DB_HOST,API_KEY# Generate configuration documentation
wheels env validate --verbose > config-validation-report.txt# Set variables then validate
wheels env set DB_HOST=localhost DB_PORT=3306
wheels env validate --required=DB_HOST,DB_PORT# Merge files then validate result
wheels env merge .env.base .env.local --output=.env.merged
wheels env validate --file=.env.merged --required=DB_HOST,API_KEY# Validate then show configuration
wheels env validate --verbose
wheels env showif wheels env validate --file=.env.production; then
echo "Production config is valid"
# Continue with deployment
else
echo "Production config has errors"
exit 1
fiwheels db create [--datasource=<name>] [--environment=<env>] [--database=<dbname>] [--force]datasource
string
Current datasource
Specify which datasource to use. If not provided, uses the default datasource from your Wheels configuration.
environment
string
Current environment
Specify the environment to use. Defaults to the current environment (development if not set).
database
string
wheels_dev
Specify the database name to create. Note for Oracle: Database names cannot contain hyphens. Use underscores instead (e.g., myapp_dev not myapp-dev).
force
boolean
false
Drop the existing database if it already exists and recreate it. Without this flag, the command will error if the database already exists.
# Use specific datasource
wheels db create --datasource=myapp_dev
# Specify environment
wheels db create --environment=testing
# Custom database name
wheels db create --database=myapp_production
# Force recreation
wheels db create --forcewheels db createwheels db create datasource=myapp_devwheels db create datasource=myapp_test --environment=testingwheels db create --database=myapp_v2wheels db create --forceDatasource 'myapp_dev' not found in server configuration.
Would you like to create this datasource now? [y/n]: y
=== Interactive Datasource Creation ===
Select database type:
1. MySQL
2. PostgreSQL
3. MSSQL
4. Oracle
5. H2
Select database type [1-5]: 1
Selected: MySQL
Enter connection details:
Host [localhost]:
Port [3306]:
Database name [wheels_dev]: myapp_dev
Username [root]:
Password: ****
Review datasource configuration:
Datasource Name: myapp_dev
Database Type: MySQL
Host: localhost
Port: 3306
Database: myapp_dev
Username: root
Connection String: jdbc:mysql://localhost:3306/myapp_dev
Create this datasource? [y/n]: y==================================================================
Database Creation Process
==================================================================
Datasource: myapp_dev
Environment: development
------------------------------------------------------------------
Database Type: MySQL
Database Name: myapp_development
------------------------------------------------------------------
>> Initializing MySQL database creation...
[OK] Driver found: com.mysql.cj.jdbc.Driver
[OK] Connected successfully to MySQL server!
>> Checking if database exists...
>> Creating MySQL database 'myapp_development'...
[OK] Database 'myapp_development' created successfully!
>> Verifying database creation...
[OK] Database 'myapp_development' verified successfully!
------------------------------------------------------------------
[OK] MySQL database creation completed successfully![CommandBox installed path]/jre/lib/wheels db create datasource=myOracleDSwheels db create --force# Wrong
wheels db create --database=my-app-dev
# Correct
wheels db create --database=my_app_devwheels db create --database=C##MYAPPwheels db create --database=myapp_dev --forceDB_TYPE=mssql # Database type
DB_HOST=localhost # Host (not MSSQL_HOST)
DB_PORT=1433 # Port (not MSSQL_PORT)
DB_DATABASE=wheels_dev # Database name (not MSSQL_DATABASE)
DB_USER=sa # Username (not MSSQL_USER)
DB_PASSWORD=Pass123! # Password (not MSSQL_PASSWORD)
DB_DATASOURCE=wheels_devwheels plugin init <name> [--author=<name>] [--description=<text>] [--version=<version>] [--license=<type>]name
Yes
string
-
-
Plugin name (will be prefixed with 'wheels-')
author
No
string
-
""
Plugin author name
description
No
string
-
""
Plugin description
version
No
string
-
"1.0.0"
Initial version number
license
No
string
MIT, Apache-2.0, GPL-3.0, BSD-3-Clause, ISC, Proprietary
MIT
License type
wheels plugin init my-helper===========================================================
Initializing Wheels Plugin: wheels-my-helper
===========================================================
Creating plugin in /plugins/myHelper/...
===========================================================
[OK] Plugin created successfully in /plugins/myHelper/
Files Created:
myHelper.cfc Main plugin component
index.cfm Documentation page
box.json Package metadata
README.md Project documentation
Next Steps:
1. Edit myHelper.cfc to add your plugin functions
2. Update index.cfm and README.md with usage examples
3. Test: wheels reload (then call your functions)
4. Publish: box login && box publishwheels plugin init authentication \
--author="Jane Smith" \
--description="Authentication and authorization system" \
--version="0.1.0" \
--license=MITwheels plugin init api-tools --author="DevTeam"plugins/
└── myHelper/
├── box.json Package configuration
├── myHelper.cfc Main plugin component (mixin="global")
├── index.cfm Plugin documentation page
├── README.md Project documentation
├── .gitignore Git ignore file
└── tests/ Test suite
└── myHelperTest.cfc TestBox testscomponent hint="wheels-my-helper" output="false" mixin="global" {
public function init() {
this.version = "1.0.0";
return this;
}
/**
* Example function - Add your plugin methods here
*
* [section: Plugins]
* [category: myHelper]
*
* @param1 Description of parameter
*/
public function myHelperExample(required string param1) {
// Your plugin logic here
return arguments.param1;
}
}<h1>wheels-my-helper</h1>
<p>Plugin description</p>
<h3>Installation</h3>
<pre>
wheels plugin install wheels-my-helper
</pre>
<h3>Usage</h3>
<h4>Example Function</h4>
<pre>
// Call the example function
result = myHelperExample("test");
</pre>{
"name": "wheels-my-helper",
"version": "1.0.0",
"author": "Your Name",
"slug": "wheels-my-helper",
"type": "cfwheels-plugins",
"keywords": "cfwheels,wheels,plugin",
"homepage": "",
"shortDescription": "Plugin description",
"private": false
}wheels plugin init my-helper --author="Your Name"public function formatCurrency(required numeric amount) {
return dollarFormat(arguments.amount);
}
public function slugify(required string text) {
return lCase(reReplace(arguments.text, "[^a-zA-Z0-9]+", "-", "all"));
}wheels reload// Your functions are now available everywhere
formatted = formatCurrency(1234.56); // Returns "$1,234.56"
slug = slugify("My Blog Post"); // Returns "my-blog-post"it("should format currency correctly", function() {
var plugin = createObject("component", "myHelper").init();
var result = plugin.formatCurrency(1234.56);
expect(result).toInclude("1,234");
});box testbox runcd plugins/myHelper
git init
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/username/wheels-my-helper.git
git push -u origin main
box login
box publish[ERROR] Plugin already exists
Plugin 'myHelper' already exists in /plugins folderInteractive wizard for creating a new Wheels application with guided setup.
wheels generate app-wizard [options]
#Can also be used as:
wheels g app-wizard [options]
wheels new [options]CommandBox supports multiple parameter formats:
Named parameters: name=value (e.g., name=MyApp, template=wheels-base-template@BE)
Flag parameters: --flag equals flag=true (e.g., --expert equals expert=true)
Flag with value: --flag=value equals flag=value (e.g., --skipInstall=true)
Note: Flag syntax (--flag) avoids positional/named parameter conflicts and is recommended for boolean options.
The wheels generate app-wizard command provides an interactive, step-by-step wizard for creating a new Wheels application. It guides you through configuration options with helpful prompts, making it ideal for beginners or when you want to explore available options.
name
Application name (optional - will prompt if not provided)
Prompted
template
App template to use
ForgeBox endpoint or slug
wheels-base-template@BE
directory
Directory to create app in
Valid directory path
{current directory}/{name}
reloadPassword
Reload password for the app
Any string
changeMe
datasourceName
Database datasource name
Valid datasource name
{app name}
cfmlEngine
CFML engine for server.json
lucee, adobe, lucee6, lucee5, adobe2023, etc.
lucee
useBootstrap
Add Bootstrap to the app
true, false
false
setupH2
Setup H2 database for development
true, false
true
init
Initialize directory as a package
true, false
false
force
Force installation into non-empty directory
true, false
false
nonInteractive
Run without prompts using defaults
true, false
false
expert
Show advanced configuration options
true, false
false
skipInstall
Skip dependency installation after creation
true, false
false
Please enter a name for your application: MyWheelsAppValidates name format (alphanumeric, underscores, hyphens)
Checks for reserved names
Creates directory with this name
Which Wheels Template shall we use?
❯ 3.0.x - Wheels Base Template - Bleeding Edge
2.5.x - Wheels Base Template - Stable Release
Wheels Template - HTMX - Alpine.js - Simple.css
Wheels Starter App
Wheels - TodoMVC - HTMX - Demo App
Enter a custom template endpointPlease enter a 'reload' password for your application: changeMeUsed for ?reload=true&password=xxx functionality
Secures application reload via URL
Please enter a datasource name if different from MyWheelsApp: MyWheelsAppSets datasource name in configuration files
You'll configure the actual datasource in your CFML server admin
Please select your preferred CFML engine?
❯ Lucee (Latest)
Adobe ColdFusion (Latest)
Lucee 6.x
Lucee 5.x
Adobe ColdFusion 2023
Adobe ColdFusion 2021
Adobe ColdFusion 2018
Enter a custom engine endpointH2 Java embedded SQL database for development? [y,n]Only shown when using Lucee engine
Only asked if skipInstall=false
Sets up embedded H2 database for development
========= Dependencies ======================
Configure dependencies and plugins for your application.
Would you like us to setup some default Bootstrap settings? [y/n]Only shown if skipInstall=false
Configures Bootstrap CSS framework
Additional dependencies may be added here
Finally, shall we initialize your application as a package
by creating a box.json file? [y,n]Creates box.json for ForgeBox package management
Always asked regardless of skipInstall setting
========= Expert Mode: Advanced Configuration ==========
Configure advanced options for your application.
Custom server port (leave empty for default 8080): 8080
Custom JVM settings (e.g. -Xmx512m -Xms256m):
Setup custom environment configurations (dev, staging, production)? [y,n]
Enable advanced routing features (nested resources, constraints)? [y,n]
Custom plugin repositories (comma-separated URLs):
Build tool integration? [None/Apache Ant/Gradle/Maven/NPM Scripts]+-----------------------------------------------------------------------------------+
| Great! Think we're all good to go. We're going to create a Wheels application for |
| you with the following parameters. |
+-----------------------+-----------------------------------------------------------+
| Template | wheels-base-template@BE |
| Application Name | MyWheelsApp |
| Install Directory | D:\path\to\MyWheelsApp |
| Reload Password | changeMe |
| Datasource Name | MyWheelsApp |
| CF Engine | lucee |
| Setup H2 Database | true | (if applicable)
| Setup Bootstrap | false | (if applicable)
| Initialize as Package | true |
| Force Installation | false |
| Skip Dependency Install | false |
+-----------------------+-----------------------------------------------------------+
Sound good? [y/n]The skipInstall parameter significantly changes the wizard experience:
✅ Asks about H2 database setup (if using Lucee)
✅ Asks about Bootstrap dependencies
✅ Shows "Dependencies" section
✅ Includes dependency settings in summary table
✅ Installs dependencies after app creation
❌ Skips H2 database question (even with Lucee)
❌ Skips Bootstrap dependency question
✅ Shows "Dependencies Skipped" message with explanation
✅ Excludes dependency settings from summary table
✅ Still asks about package initialization (box.json creation)
✅ Still asks about core configuration (name, template, passwords, etc.)
❌ Skips dependency installation after app creation
========= Dependencies Skipped ================
Dependency installation is disabled (skipInstall=true).
Dependencies like Bootstrap and H2 database will not be configured or installed.wheels generate app-wizardRuns full interactive wizard with all prompts.
wheels generate app-wizard name=MyAppSkips name prompt, asks for other configuration.
wheels generate app-wizard --skipInstallRuns wizard but skips H2 database and Bootstrap questions.
wheels generate app-wizard --expertIncludes advanced configuration options like custom ports and JVM settings.
wheels generate app-wizard --nonInteractiveUses all defaults, no prompts. Creates app immediately.
wheels generate app-wizard name=MyApp template=wheels-base-template@BE --skipInstall --expert --forcePre-configured with expert mode and skipped dependencies.
When --expert is enabled, additional configuration options are available:
Custom server port: Override default port 8080
JVM settings: Custom memory and performance settings
Custom environment configurations: Setup dev, staging, production configs
Advanced routing features: Enable nested resources and route constraints
Custom plugin repositories: Additional ForgeBox endpoints
Build tool integration: Ant, Gradle, Maven, or NPM integration
Use --nonInteractive to bypass all prompts:
Name: MyWheelsApp
Template: wheels-base-template@BE
Reload Password: changeMe
Datasource Name: Same as app name
CFML Engine: lucee
Directory: {current directory}/{name}
wheels generate app-wizard --nonInteractive name=CustomApp template=wheels-starter-app --cfmlEngine=adobeAfter successful creation:
Model generation complete!
Next steps:
1. cd MyWheelsApp
2. Review generated configuration files
3. Configure your datasource in CFML server admin
4. box server start (to start development server)
5. Visit http://localhost:8080
Additional commands:
- wheels generate model User name:string,email:string
- wheels generate controller Users
- wheels dbmigrate up (run database migrations)
- wheels test run (run tests)Must start with a letter
Can contain letters, numbers, underscores, and hyphens
Cannot contain spaces or special characters
Cannot exceed 50 characters
Cannot be a reserved name (con, prn, aux, nul, wheels, etc.)
Must be a valid file system path
Parent directory must exist and be writable
Will warn if target directory is not empty (unless --force used)
Invalid application name:
'123app' is not valid. Application name must start with a letter.
Please try again: MyAppDirectory not empty:
Target directory is not empty. Use --force to overwrite, or choose a different location.Missing dependencies:
Warning: Some dependencies could not be installed.
Run 'box install' in your application directory to install them manually.Use descriptive names: Choose clear, project-specific application names
Review configuration: Check the summary table before confirming
Consider skipInstall: Use --skipInstall for custom dependency management
Expert mode for production: Use --expert for production-ready configurations
Save time with non-interactive: Use --nonInteractive in automated scripts
Template selection: Choose templates that match your project requirements
wheels generate app-wizard name=MyAPI template=wheels-base-template@BE --skipInstall --expertwheels generate app-wizard name=MyWebApp --useBootstrap --setupH2wheels generate app-wizard --nonInteractive name=TeamProject template=wheels-starter-app --cfmlEngine=lucee --forcewheels generate app-wizard --nonInteractive --skipInstall name=BuildApp template=wheels-base-template@BECheck terminal compatibility
Try --nonInteractive mode
Ensure adequate system resources
Verify internet connection for template downloads
Check CommandBox version compatibility
Try --skipInstall and install dependencies manually
Check file permissions in target directory
Review generated server.json file
Verify datasource configuration in CFML admin
Check application settings in /config/app.cfm
wheels generate app - Non-interactive app generation
wheels generate controller - Generate controllers
wheels generate model - Generate models
wheels scaffold - Generate complete CRUD
The wheels config diff command compares configuration settings and environment variables between two environments. It helps identify differences in both Wheels settings files and environment-specific .env files, making it easier to understand configuration variations across development, testing, and production environments.
wheels config diff <env1> <env2> [--changesOnly] [--format=<format>] [--env] [--settings]env1
string
Yes
First environment to compare (e.g., development, testing, production)
env2
string
Yes
Second environment to compare
--changesOnly
flag
No
Only show differences, hide identical values
--format
string
No
Output format: table (default) or json
--env
flag
No
Compare only environment variables
--settings
flag
No
Compare only Wheels settings
The command can operate in three modes:
Both (Default) - Compares both settings and environment variables
Environment Variables Only - Use --env flag
Settings Only - Use --settings flag
# Compare everything between development and production
wheels config diff development production# Show only the differences, hide identical values
wheels config diff development production --changesOnly# Compare only .env files between environments
wheels config diff development production --env# Compare only Wheels settings files
wheels config diff development production --settings# Output comparison as JSON for parsing
wheels config diff development production --format=jsonThe command compares:
Base settings from config/settings.cfm
Environment-specific overrides from config/{environment}/settings.cfm
All set() function calls are parsed and compared
The command compares:
Environment-specific files: .env.{environment}
Falls back to .env for development environment if .env.development doesn't exist
All KEY=VALUE pairs are parsed with proper handling of:
Comments (lines starting with #)
Inline comments
Quoted values (single or double quotes)
Whitespace trimming
config/
├── settings.cfm # Base settings
├── development/
│ └── settings.cfm # Development overrides
├── testing/
│ └── settings.cfm # Testing overrides
└── production/
└── settings.cfm # Production overridesproject_root/
├── .env # Base/development environment variables
├── .env.development # Development-specific variables
├── .env.testing # Testing-specific variables
└── .env.production # Production-specific variablesThe table output is organized into clear sections:
========================================
Configuration Comparison: development vs production
========================================
[SETTINGS CONFIGURATION]
Different Values:
┌──────────────────────┬────────────┬────────────┐
│ Setting │ development│ production │
├──────────────────────┼────────────┼────────────┤
│ showDebugInformation │ true │ false │
│ cacheQueries │ false │ true │
└──────────────────────┴────────────┴────────────┘
Only in development:
┌─────────────────┬──────────┐
│ Setting │ Value │
├─────────────────┼──────────┤
│ debugPlugin │ true │
└─────────────────┴──────────┘
Only in production:
┌─────────────────┬──────────┐
│ Setting │ Value │
├─────────────────┼──────────┤
│ forceSSL │ true │
└─────────────────┴──────────┘
[ENVIRONMENT VARIABLES]
Different Values:
┌──────────────┬────────────────┬────────────────┐
│ Variable │ development │ production │
├──────────────┼────────────────┼────────────────┤
│ DB_NAME │ app_dev │ app_prod │
│ DEBUG_MODE │ true │ false │
└──────────────┴────────────────┴────────────────┘
========================================
SUMMARY
========================================
Settings:
Total: 25
Identical: 20
Different: 2
Unique: 3
Environment Variables:
Total: 15
Identical: 10
Different: 2
Unique: 3
Overall:
Total configurations: 40
Identical: 30
Different: 4
Unique: 6
Similarity: 75%{
"env1": "development",
"env2": "production",
"comparisons": {
"settings": {
"identical": [...],
"different": [...],
"onlyInFirst": [...],
"onlyInSecond": [...]
},
"env": {
"identical": [...],
"different": [...],
"onlyInFirst": [...],
"onlyInSecond": [...]
}
},
"summary": {
"settings": {
"totalSettings": 25,
"identical": 20,
"different": 2,
"onlyInFirst": 1,
"onlyInSecond": 2
},
"env": {
"totalVariables": 15,
"identical": 10,
"different": 2,
"onlyInFirst": 1,
"onlyInSecond": 2
},
"overall": {
"total": 40,
"identical": 30,
"different": 4,
"unique": 6,
"similarity": 75
}
}
}The command automatically masks sensitive values containing these keywords:
password
secret
key
token
apikey/api_key
private
credential
auth
passphrase
salt
Masked values appear as ***MASKED*** in the output.
┌──────────────┬────────────────┬────────────────┐
│ Variable │ development │ production │
├──────────────┼────────────────┼────────────────┤
│ DB_PASSWORD │ ***MASKED*** │ ***MASKED*** │
│ API_KEY │ ***MASKED*** │ ***MASKED*** │
└──────────────┴────────────────┴────────────────┘# Verify configuration differences before deploying to production
wheels config diff testing production --changesOnly# Check if development and testing have similar configurations
wheels config diff development testing# Review all security-related settings between environments
wheels config diff development production --settings | grep -i "debug\|error\|ssl"# In your deployment script
wheels config diff staging production --format=json > config-diff.json
# Parse JSON to validate critical settings match expectations# Ensure all required environment variables exist in production
wheels config diff development production --env --changesOnly# Get a quick overview of configuration similarity
wheels config diff development testing | grep "Similarity:"wheels config diff development productionShows all differences and similarities between development and production configurations.
wheels config diff testing production --changesOnlyShows only the differences, useful for quick reviews.
wheels config diff development staging --env --changesOnlyShows only environment variable differences between development and staging.
wheels config diff development production --format=json | jq '.summary.overall.similarity'Outputs similarity percentage for automated checks.
wheels config diff development production --settings --changesOnlyValidates only Wheels settings differences.
Warning: Settings for environment 'staging' not found!The command continues with available data and shows warnings for missing files.
Warning: No settings.cfm file found in config directoryThe command will still compare environment variables if available.
Error: Cannot compare an environment to itselfYou must specify two different environments.
Error: Invalid format: xml. Valid formats are: table, jsonOnly table and json formats are supported.
Regular Comparisons - Run comparisons before each deployment to catch unintended changes
Use changesOnly for Reviews - Focus on differences during code reviews
Automate with JSON - Use JSON output in CI/CD pipelines for automated validation
Document Differences - Keep a record of intentional differences between environments
Security First - Always review security-related settings (debug, error handling, SSL)
Version Control - Track both settings files and environment files in version control (except sensitive .env files)
Use --changesOnly to quickly identify configuration drift
Pipe JSON output to jq for advanced filtering and processing
Create aliases for common comparisons (e.g., alias cfgdiff='wheels config diff')
Review the similarity percentage as a quick health check
Use the command before and after configuration changes to verify impact
Combine with wheels config check for comprehensive configuration validation
<cfscript>
// Production-specific settings
set(showDebugInformation = false);
set(showErrorInformation = false);
set(sendEmailOnError = true);
set(errorEmailAddress = "[email protected]");
set(cacheQueries = true);
set(forceSSL = true);
</cfscript># Production Environment Variables
WHEELS_ENV=production
# Database Configuration
DB_HOST=prod.database.com
DB_PORT=3306
DB_NAME=app_production
DB_USER=prod_user
DB_PASSWORD=secure_password_here
# Application Settings
DEBUG_MODE=false
LOG_LEVEL=error
SESSION_TIMEOUT=30
# API Keys
API_KEY=prod_api_key_here
SECRET_KEY=prod_secret_keyEnsure files exist in the correct locations
Check file permissions (must be readable)
Verify file naming conventions (.env.{environment})
Check for syntax errors in settings.cfm files
Ensure .env files use proper KEY=VALUE format
Remove any special characters that might cause parsing errors
If using --env or --settings, ensure you're not filtering out the data you want
Check that both environments have the respective files
Large configuration files may take time to parse
Consider using --changesOnly to reduce output
Use JSON format for faster processing in scripts
wheels config check - Validate configuration for issues
wheels get environment - Display current environment
wheels env merge - Merge environment configurations
wheels env set - Set environment variables
The wheels config diff command is an essential tool for managing multi-environment Wheels applications. It provides comprehensive comparison capabilities for both application settings and environment variables, helping teams maintain consistency and catch configuration drift before it causes issues in production.
Run TestBox tests for your Wheels application using the TestBox CLI integration.
Note: This command replaces the deprecated
wheels testcommand.
box install testbox-cli --globalwheels test run [spec] [options]This command supports multiple parameter formats:
Named parameters: name=value (e.g., format=json, filter="User")
Flag parameters: --flag equals flag=true (e.g., --coverage equals coverage=true)
Flag with value: --flag=value equals flag=value (e.g., --format=json)
Parameter Mixing Rules:
✅ ALLOWED:
All named: wheels test run format=json verbose=true
All flags: wheels test run --verbose --coverage
Named + flags: wheels test run format=json --coverage
❌ NOT ALLOWED:
Positional + named: Not applicable for this command (no positional parameters)
Recommendation: Use named parameters for specific values, flags for boolean options: wheels test run format=json --coverage
The wheels test run command executes your application's TestBox test suite with support, filtering, and various output formats. This is the primary command for running your application tests (as opposed to framework tests).
type
string
app
Type of tests to run
format
string
txt
Test output format: txt, junit, json
bundles
string
-
The path or list of paths of the spec bundle CFCs to run and test ONLY
directory
string
-
The directory to use to discover test bundles and specs to test
recurse
boolean
true
Recurse the directory mapping or not
verbose
boolean
true
Display extra details including passing and skipped tests
servername
string
-
Server name for test execution
filter
string
-
Filter tests by pattern or name
labels
string
-
The list of labels that a suite or spec must have in order to execute
coverage
boolean
false
Enable code coverage with FusionReactor
wheels test run# Named parameter (recommended for string values)
wheels test run filter="User"
wheels test run filter="test_user_validation"# Named parameter (recommended)
wheels test run bundles="tests.models"
wheels test run bundles="tests.models,tests.controllers"# Named parameter (recommended)
wheels test run labels="unit"
wheels test run labels="critical,auth"# Flag (recommended for boolean)
wheels test run --coverage
# OR named
wheels test run coverage=true# Named (recommended)
wheels test run format=json
wheels test run format=junit
# OR flag with value
wheels test run --format=json# Named parameters (recommended)
wheels test run directory="tests/specs"
wheels test run directory="tests/specs/unit" recurse=false# Flags + named (recommended)
wheels test run --verbose --coverage format=txt
# OR all named
wheels test run verbose=true coverage=true format=txt# Named (recommended)
wheels test run type=core
wheels test run type=appStandard test directory layout:
/tests/
├── Application.cfc # Test configuration
├── models/ # Model tests
│ ├── UserTest.cfc
│ └── ProductTest.cfc
├── controllers/ # Controller tests
│ ├── UsersTest.cfc
│ └── ProductsTest.cfc
├── views/ # View tests
├── integration/ # Integration tests
└── helpers/ # Test helperscomponent extends="wheels.Testbox" {
function run() {
describe("User Model", function() {
beforeEach(function() {
// Reset test data
application.wirebox.getInstance("User").deleteAll();
});
it("validates required fields", function() {
var user = model("User").new();
expect(user.valid()).toBeFalse();
expect(user.errors).toHaveKey("email");
expect(user.errors).toHaveKey("username");
});
it("saves with valid data", function() {
var user = model("User").new(
email="[email protected]",
username="testuser",
password="secret123"
);
expect(user.save()).toBeTrue();
expect(user.id).toBeGT(0);
});
it("prevents duplicate emails", function() {
var user1 = model("User").create(
email="[email protected]",
username="user1"
);
var user2 = model("User").new(
email="[email protected]",
username="user2"
);
expect(user2.valid()).toBeFalse();
expect(user2.errors.email).toContain("already exists");
});
});
}
}component extends="wheels.Testbox" {
function run() {
describe("Products Controller", function() {
it("lists all products", function() {
// Create test data
var product = model("Product").create(name="Test Product");
// Make request
var event = execute(
event="products.index",
renderResults=true
);
// Assert response
expect(event.getRenderedContent()).toInclude("Test Product");
expect(event.getValue("products")).toBeArray();
});
it("requires auth for create", function() {
var event = execute(
event="products.create",
renderResults=false
);
expect(event.getValue("relocate_URI")).toBe("/login");
});
});
}
}component {
this.name = "WheelsTestingSuite" & Hash(GetCurrentTemplatePath());
// Use test datasource
this.datasources["wheelstestdb"] = {
url = "jdbc:h2:mem:wheelstestdb;MODE=MySQL"
};
this.datasource = "wheelstestdb";
// Test settings
this.testbox = {
testBundles = "tests",
recurse = true,
format = "simple",
labels = "",
options = {}
};
}wheels test run format=txtPlain txt output
Good for CI systems
No colors
wheels test run format=json√ tests.specs.functions.Example (3 ms)
[Passed: 1] [Failed: 0] [Errors: 0] [Skipped: 0] [Suites/Specs: 1/1]
√ Tests that DummyTest
√ is Returning True (1 ms)
╔═════════════════════════════════════════════════════════════════════╗
║ Passed ║ Failed ║ Errored ║ Skipped ║ Bundles ║ Suites ║ Specs ║
╠═════════════════════════════════════════════════════════════════════╣
║ 1 ║ 0 ║ 0 ║ 0 ║ 1 ║ 1 ║ 1 ║
╚═════════════════════════════════════════════════════════════════════╝wheels test run format=junitJUnit XML format
For CI integration
Jenkins compatible
# Run only model tests
wheels test run bundles=tests.models
# Run multiple bundles
wheels test run bundles=tests.models,tests.controllerscomponent extends="wheels.Testbox" labels="label title"# Run only critical tests
wheels test run labels="label title"
# Run auth OR api tests
wheels test run labels=auth,api# Run tests matching pattern
wheels test run filter="user"
wheels test run filter="validate*"Benefits:
Faster execution
Better CPU utilization
Finds concurrency issues
Generate coverage reports:
wheels test run --coverage coverageOutputDir=coverage/View report:
open coverage/index.htmlCreate reusable test utilities:
// /tests/helpers/TestHelper.cfc
component {
function createTestUser(struct overrides={}) {
var defaults = {
email: "test#CreateUUID()#@example.com",
username: "user#CreateUUID()#",
password: "testpass123"
};
return model("User").create(
argumentCollection = defaults.append(arguments.overrides)
);
}
function loginAs(required user) {
session.userId = arguments.user.id;
session.isAuthenticated = true;
}
}function beforeAll() {
transaction action="begin";
}
function afterAll() {
transaction action="rollback";
}function beforeEach() {
queryExecute("DELETE FROM users");
queryExecute("DELETE FROM products");
}function loadFixtures() {
var users = deserializeJSON(
fileRead("/tests/fixtures/users.json")
);
for (var userData in users) {
model("User").create(userData);
}
}- name: Run tests
run: |
wheels test run format=junit outputFile=test-results.xml
- name: Upload results
uses: actions/upload-artifact@v4
with:
name: test-results
path: test-results.xml#!/bin/bash
# .git/hooks/pre-commit
echo "Running tests..."
wheels test run labels=unit
if [ $? -ne 0 ]; then
echo "Tests failed. Commit aborted."
exit 1
fi# Increase memory
box server set jvm.heapSize=1024
box server restartUse beforeEach/afterEach
Reset global state
Use transactions
Avoid time-dependent tests
Mock external services
Use fixed test data
wheels test - Run framework tests
wheels test coverage - Generate coverage
wheels test debug - Debug tests
wheels generate test - Generate test files
Switch to a different environment in your Wheels application.
wheels env switch [name] [options]The wheels env switch command changes the active environment for your Wheels application by updating the wheels_env variable in the .env file. It validates the environment configuration, optionally creates backups, and can restart the application server.
name
Target environment name
Required
--check
Validate before switching
true
--restart
Restart application after switch
false
--backup
Backup current environment
false
--force
Force switch even with issues
false
--quiet
Suppress output
false
wheels env switch stagingwheels env switch production --restartwheels env switch testing --forcewheels env switch production --backupwheels env switch development --quietwheels env switch production --backup --restart --checkValidates Target Environment (if --check is enabled):
Checks for .env.[environment] file
Checks for config/[environment]/settings.cfm file
Validates environment configuration
Creates Backup (if --backup is enabled):
Backs up current .env file
Backs up server.json if it exists
Creates timestamped backup files
Updates Configuration:
Updates or creates wheels_env variable in .env
Falls back to environment variable if wheels_env doesn't exist
Updates server.json profile if file exists
Restarts Application (if --restart is enabled):
Stops and starts CommandBox server if server.json exists
Falls back to wheels reload command
Environment Switch
==================================================
Current Environment: development
Target Environment: staging
Validating target environment... [OK]
Creating backup... [OK]
Backup saved: .env.backup-20240115-103045
Switching environment... [OK]
Updated environment variable... [OK]
==================================================
[SUCCESS] Environment switched successfully!
Environment Details:
- Environment: staging
- Database: wheels_staging
- Debug Mode: Enabled
- Cache: Partial
IMPORTANT:
- Restart your application server for changes to take effect
- Run 'wheels reload' if using Wheels development server
- Or use 'wheels env switch staging --restart' next timeThe command updates or creates the wheels_env variable:
Before:
wheels_env=development
# or
environment=developmentAfter:
wheels_env=stagingIf no environment variable exists, it adds:
wheels_env=stagingThe validation checks for required environment files:
Environment Configuration File:
Checks: .env.[environment]
Location: Project root
Wheels Settings File:
Checks: config/[environment]/settings.cfm
Location: Project config directory
Valid: Both files exist
Valid with warning: One file exists
Invalid: Neither file exists (unless --force is used)
Validates the target environment before switching:
# With validation (default)
wheels env switch production
# Skip validation
wheels env switch production --no-checkCreates timestamped backups before switching:
wheels env switch production --backup
# Creates: .env.backup-20240115-103045
# Creates: server.json.backup-20240115-103045 (if exists)Automatically restarts the application:
wheels env switch production --restartBypasses validation and confirmation prompts:
# Force switch even if validation fails
wheels env switch production --force
# Combine with other options
wheels env switch production --force --no-checkMinimal output for scripting:
wheels env switch production --quiet
# Output: Environment switched to productionWhen switching to production from another environment:
Displays warning about production implications
Requires confirmation (unless --force or --quiet)
Shows warnings about debug mode, caching, and error handling
Warning message:
WARNING: Switching to PRODUCTION environment
This will:
- Disable debug mode
- Enable full caching
- Hide detailed error messages
Are you sure you want to continue? (yes/no):If the switch fails:
Displays error message
Provides troubleshooting suggestions
Sets exit code 1 for scripting
Example error output:
[X] Failed to switch environment
Error: Environment 'invalid' is not configured
Suggestions:
- Check if you have write permissions for .env file
- Ensure the environment name is valid
- Try running with administrator/sudo privileges if needed
- Use --force to bypass validation checksThe --backup option creates timestamped backup files:
.env.backup-YYYYMMDD-HHMMSS - Backup of current .env file
server.json.backup-YYYYMMDD-HHMMSS - Backup of server.json (if exists)
If you need to restore from a backup:
# Restore .env file
cp .env.backup-20240115-103045 .env
# Restore server.json
cp server.json.backup-20240115-103045 server.json
# Reload application
wheels reloadWhen using --restart, the command will:
If server.json exists:
Stop CommandBox server
Start CommandBox server
If server.json doesn't exist:
Execute wheels reload command
If --restart is not used, you need to manually restart:
# CommandBox server
server restart
# Or Wheels development server
wheels reloadThe command reads additional configuration from .env.[environment] files if they exist:
database - Database connection name
debug - Debug mode (true/false)
cache - Cache configuration
Example .env.production:
database=wheels_production
debug=false
cache=full- name: Switch to staging
run: |
wheels env switch staging --check
wheels test run
wheels deploy exec staging#!/bin/bash
# deploy.sh
# Switch environment with backup
wheels env switch $1 --backup
# Run migrations
wheels dbmigrate latest
# Clear caches
wheels cache clear
# Verify environment
wheels env current# Switch to testing environment quietly
wheels env switch testing --quiet --force
# Run tests
wheels test run
# Switch back to development
wheels env switch development --quietCheck validation errors in output
Verify .env.[environment] file exists
Verify config/[environment]/settings.cfm exists
Use --force to bypass validation
Ensure server was restarted
Check .env file for correct wheels_env value
Review application logs for errors
Manually restart services if needed
Check write permissions for .env file
Run with appropriate privileges
Ensure backup directory is writable
Warning appears if only one configuration file exists
Environment may work but might not be fully configured
Check both .env.[environment] and config/[environment]/settings.cfm
Always validate: Keep --check enabled for production switches
Create backups: Use --backup for critical environment changes
Test first: Switch in staging before production
Use --restart: Automatically restart to apply changes immediately
Document changes: Log environment switches in deployment notes
Production switches require explicit confirmation
Backup files contain sensitive configuration
.env files should be in .gitignore
Use --quiet mode carefully in automated scripts
Review environment-specific configurations regularly
The command modifies the .env file in place
Creates wheels_env variable if it doesn't exist
Falls back to updating environment variable if found
Some changes require application restart to take effect
Database connections may need to be reset after switching
Cached data should be cleared after environment switch
0 - Success
1 - Failure (validation error, write error, or user cancellation)
wheels env - Environment management overview
wheels env list - List available environments
wheels env setup - Setup new environments
wheels env current - Show current environment
wheels reload - Reload application
Generate view files for controllers.
wheels generate view [objectName] [name] [template]
wheels g view [objectName] [name] [template]This command supports multiple parameter formats:
Positional parameters: wheels generate view user show (most common for this command)
Named parameters: objectName=value name=value (e.g., objectName=user name=show)
Flag parameters: --flag equals flag=true (e.g., --force equals force=true)
Parameter Mixing Rules:
✅ ALLOWED:
All positional: wheels generate view user show
All positional + flags: wheels generate view user show --force
All named: objectName=user name=show template=crud/show
❌ NOT ALLOWED:
Positional + named: wheels generate view user name=show (causes error)
Recommendation: Use positional parameters for objectName and name, flags for options: wheels generate view user show --force
The wheels generate view command creates view files for controllers. It can generate individual views using templates or create blank view files.
objectName
View path folder (e.g., user)
Required
name
Name of the file to create (e.g., edit)
Required
template
Optional template to use
--force
Overwrite existing code
false
Available templates:
crud/_form - Form partial for new/edit views
crud/edit - Edit form view
crud/index - List/index view
crud/new - New form view
crud/show - Show/detail view
# Positional (recommended)
wheels generate view user show
# OR all named
wheels g view objectName=user name=showCreates: /views/users/show.cfm with empty content
# Positional with template (recommended)
wheels generate view user show crud/show
# OR all named
wheels g view objectName=user name=show template=crud/showCreates: /views/users/show.cfm using the show template
# Positional (recommended)
wheels generate view user edit crud/editCreates: /views/users/edit.cfm using the edit template
# Positional (recommended)
wheels generate view user _form crud/_formCreates: /views/users/_form.cfm using the form partial template
# Positional (recommended)
wheels generate view product index crud/indexCreates: /views/products/index.cfm using the index template
# Positional + flag (recommended)
wheels generate view user show --force
# OR all named
wheels g view objectName=user name=show force=trueOverwrites existing /views/users/show.cfm
<!--- View file created by wheels generate view ---><h1>Products</h1>
<p>
#linkTo(text="New Product", action="new", class="btn btn-primary")#
</p>
<cfif products.recordCount>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<cfoutput query="products">
<tr>
<td>#products.id#</td>
<td>#products.name#</td>
<td>#dateFormat(products.createdAt, "mm/dd/yyyy")#</td>
<td>
#linkTo(text="View", action="show", key=products.id, class="btn btn-sm btn-info")#
#linkTo(text="Edit", action="edit", key=products.id, class="btn btn-sm btn-warning")#
#linkTo(text="Delete", action="delete", key=products.id, method="delete", confirm="Are you sure?", class="btn btn-sm btn-danger")#
</td>
</tr>
</cfoutput>
</tbody>
</table>
<cfelse>
<p class="alert alert-info">No products found.</p>
</cfif><h1>New Product</h1>
#includePartial("/products/form")##startFormTag(action=formAction)#
<cfif product.hasErrors()>
<div class="alert alert-danger">
<h4>Please correct the following errors:</h4>
#errorMessagesFor("product")#
</div>
</cfif>
<div class="form-group">
#textFieldTag(name="product[name]", value=product.name, label="Name", class="form-control")#
</div>
<div class="form-group">
#textAreaTag(name="product[description]", value=product.description, label="Description", class="form-control", rows=5)#
</div>
<div class="form-group">
#numberFieldTag(name="product[price]", value=product.price, label="Price", class="form-control", step="0.01")#
</div>
<div class="form-group">
#selectTag(name="product[categoryId]", options=categories, selected=product.categoryId, label="Category", class="form-control", includeBlank="-- Select Category --")#
</div>
<div class="form-group">
#checkBoxTag(name="product[isActive]", checked=product.isActive, label="Active", value=1)#
</div>
<div class="form-actions">
#submitTag(value=submitLabel, class="btn btn-primary")#
#linkTo(text="Cancel", action="index", class="btn btn-secondary")#
</div>
#endFormTag()#<h1>Product Details</h1>
<div class="card">
<div class="card-body">
<h2 class="card-title">#product.name#</h2>
<dl class="row">
<dt class="col-sm-3">Description</dt>
<dd class="col-sm-9">#product.description#</dd>
<dt class="col-sm-3">Price</dt>
<dd class="col-sm-9">#dollarFormat(product.price)#</dd>
<dt class="col-sm-3">Category</dt>
<dd class="col-sm-9">#product.category.name#</dd>
<dt class="col-sm-3">Status</dt>
<dd class="col-sm-9">
<cfif product.isActive>
<span class="badge badge-success">Active</span>
<cfelse>
<span class="badge badge-secondary">Inactive</span>
</cfif>
</dd>
<dt class="col-sm-3">Created</dt>
<dd class="col-sm-9">#dateTimeFormat(product.createdAt, "mmm dd, yyyy h:nn tt")#</dd>
<dt class="col-sm-3">Updated</dt>
<dd class="col-sm-9">#dateTimeFormat(product.updatedAt, "mmm dd, yyyy h:nn tt")#</dd>
</dl>
</div>
<div class="card-footer">
#linkTo(text="Edit", action="edit", key=product.id, class="btn btn-primary")#
#linkTo(text="Delete", action="delete", key=product.id, method="delete", confirm="Are you sure?", class="btn btn-danger")#
#linkTo(text="Back to List", action="index", class="btn btn-secondary")#
</div>
</div>default
Standard HTML structure
General purpose
bootstrap
Bootstrap 5 components
Modern web apps
tailwind
Tailwind CSS classes
Utility-first design
ajax
AJAX-enabled views
Dynamic updates
mobile
Mobile-optimized
Responsive design
print
Print-friendly layout
Reports
email
Email template
Notifications
Templates are located in:
~/.commandbox/cfml/modules/wheels-cli/templates/views/
├── default/
│ ├── index.cfm
│ ├── show.cfm
│ ├── new.cfm
│ ├── edit.cfm
│ └── _form.cfm
├── bootstrap/
└── custom/Partials start with underscore:
_form.cfm - Form partial
_item.cfm - List item partial
_sidebar.cfm - Sidebar partial
wheels generate view shared header,footer,navigation --partial<!--- In layout or view --->
#includePartial("/shared/header")#
#includePartial("/products/form", product=product)#
#includePartial(partial="item", query=products)#<!--- Generated view assumes layout wrapper --->
<h1>Page Title</h1>
<p>Content here</p>wheels generate view products standalone --layout=false<!DOCTYPE html>
<html>
<head>
<title>Standalone View</title>
</head>
<body>
<h1>Products</h1>
<!-- Complete HTML structure -->
</body>
</html>wheels generate view products index --format=htmlCreates: /views/products/index.html
wheels generate view emails welcome --format=txtCreates: /views/emails/welcome.txt
wheels generate view products search --template=ajax<cfif isAjax()>
<!--- Return just the content --->
<cfoutput query="products">
<div class="search-result">
<h3>#products.name#</h3>
<p>#products.description#</p>
</div>
</cfoutput>
<cfelse>
<!--- Include full page structure --->
<div id="search-results">
<cfinclude template="_results.cfm">
</div>
</cfif>#startFormTag(action="create", method="post", class="needs-validation")#
#textField(objectName="product", property="name", label="Product Name", class="form-control", required=true)#
#textArea(objectName="product", property="description", label="Description", class="form-control", rows=5)#
#select(objectName="product", property="categoryId", options=categories, label="Category", class="form-control")#
#submitTag(value="Save Product", class="btn btn-primary")#
#endFormTag()##startFormTag(action="upload", multipart=true)#
#fileFieldTag(name="productImage", label="Product Image", accept="image/*", class="form-control")#
#submitTag(value="Upload", class="btn btn-primary")#
#endFormTag()#wheels generate view products index --template=mobile<div class="container-fluid">
<div class="row">
<div class="col-12">
<h1 class="h3">Products</h1>
</div>
</div>
<div class="row">
<cfoutput query="products">
<div class="col-12 col-md-6 col-lg-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">#products.name#</h5>
<p class="card-text">#products.description#</p>
#linkTo(text="View", action="show", key=products.id, class="btn btn-primary btn-sm")#
</div>
</div>
</div>
</cfoutput>
</div>
</div>wheels generate view products index --locale=esCreates: /views/products/index_es.cfm
<h1>#l("products.title")#</h1>
<p>#l("products.description")#</p>
#linkTo(text=l("buttons.new"), action="new", class="btn btn-primary")#wheels generate view products index
wheels generate test view products/indexcomponent extends="wheels.Test" {
function test_index_displays_products() {
products = model("Product").findAll(maxRows=5);
result = renderView(view="/products/index", products=products, layout=false);
assert(Find("<h1>Products</h1>", result));
assert(Find("New Product", result));
assertEquals(products.recordCount, ListLen(result, "<tr>") - 1);
}
}<cfcache action="cache" timespan="#CreateTimeSpan(0,1,0,0)#">
<!--- Expensive view content --->
#includePartial("products/list", products=products)#
</cfcache><div class="products-container" data-lazy-load="/products/more">
<!--- Initial content --->
</div>
<script>
// Implement lazy loading
</script>Keep views simple and focused on presentation
Use partials for reusable components
Move complex logic to helpers or controllers
Follow naming conventions consistently
Use semantic HTML markup
Include accessibility attributes
Optimize for performance with caching
Test views with various data states
<cfif products.recordCount>
<!--- Show products --->
<cfelse>
<div class="empty-state">
<h2>No products found</h2>
<p>Get started by adding your first product.</p>
#linkTo(text="Add Product", action="new", class="btn btn-primary")#
</div>
</cfif><div class="loading-spinner" style="display: none;">
<i class="fa fa-spinner fa-spin"></i> Loading...
</div><cfif structKeyExists(variables, "error")>
<div class="alert alert-danger">
<strong>Error:</strong> #error.message#
</div>
</cfif>wheels generate controller - Generate controllers
wheels scaffold - Generate complete CRUD
wheels generate test - Generate view tests
Understanding the Wheels CLI service layer architecture.
The Wheels CLI uses a service-oriented architecture that separates concerns and provides reusable functionality across commands. This architecture makes the CLI more maintainable, testable, and extensible.
┌─────────────────┐
│ Commands │ ← User interacts with
├─────────────────┤
│ Services │ ← Business logic
├─────────────────┤
│ Models │ ← Data and utilities
├─────────────────┤
│ Templates │ ← Code generation
└─────────────────┘/commands/wheels/)Commands are the user-facing interface:
// commands/wheels/generate/model.cfc
component extends="wheels-cli.models.BaseCommand" {
property name="codeGenerationService" inject="CodeGenerationService@wheels-cli";
property name="migrationService" inject="MigrationService@wheels-cli";
function run(
required string name,
boolean migration = true,
string properties = "",
boolean force = false
) {
// Delegate to services
var result = codeGenerationService.generateModel(argumentCollection=arguments);
if (arguments.migration) {
migrationService.createTableMigration(arguments.name, arguments.properties);
}
print.greenLine("✓ Model generated successfully");
}
}/models/)Services contain reusable business logic:
// models/CodeGenerationService.cfc
component accessors="true" singleton {
property name="templateService" inject="TemplateService@wheels-cli";
property name="fileService" inject="FileService@wheels-cli";
function generateModel(
required string name,
string properties = "",
struct associations = {},
boolean force = false
) {
var modelName = helpers.capitalize(arguments.name);
var template = templateService.getTemplate("model");
var content = templateService.populateTemplate(template, {
modelName: modelName,
properties: parseProperties(arguments.properties),
associations: arguments.associations
});
return fileService.writeFile(
path = "/models/#modelName#.cfc",
content = content,
force = arguments.force
);
}
}/models/BaseCommand.cfc)All commands extend from BaseCommand:
component extends="commandbox.system.BaseCommand" {
property name="print" inject="print";
property name="fileSystemUtil" inject="FileSystem";
function init() {
// Common initialization
return this;
}
// Common helper methods
function ensureDirectoryExists(required string path) {
if (!directoryExists(arguments.path)) {
directoryCreate(arguments.path, true);
}
}
function resolvePath(required string path) {
return fileSystemUtil.resolvePath(arguments.path);
}
}Manages code generation templates with override system:
Template Loading: Searches app snippets first, then CLI templates
Variable Substitution: Replaces placeholders with actual values
Custom Template Support: Apps can override any CLI template
Path Resolution: app/snippets/ overrides /cli/templates/
Dynamic Content: Generates form fields, validations, relationships
Key features:
Template hierarchy allows project customization
Preserves markers for future CLI additions
Supports conditional logic in templates
Handles both simple and complex placeholders
See Template System Guide for detailed documentation.
Handles database migrations:
Generate migration files
Track migration status
Execute migrations
Testing functionality:
Run TestBox tests
Generate coverage reports
Watch mode support
Centralized code generation:
Generate models, controllers, views
Handle associations
Manage validations
Code analysis tools:
Complexity analysis
Code style checking
Dependency analysis
Security scanning:
SQL injection detection
XSS vulnerability scanning
Hardcoded credential detection
Performance optimization:
Cache analysis
Query optimization
Asset optimization
Plugin management:
Install/remove plugins
Version management
Dependency resolution
Environment management:
Environment switching
Configuration management
Docker/Vagrant support
Services use WireBox for dependency injection:
// In ModuleConfig.cfc
function configure() {
// Service mappings
binder.map("TemplateService@wheels-cli")
.to("wheels.cli.models.TemplateService")
.asSingleton();
binder.map("MigrationService@wheels-cli")
.to("wheels.cli.models.MigrationService")
.asSingleton();
}// models/MyNewService.cfc
component accessors="true" singleton {
// Inject dependencies
property name="fileService" inject="FileService@wheels-cli";
property name="print" inject="print";
function init() {
return this;
}
function doSomething(required string input) {
// Service logic here
return processInput(arguments.input);
}
private function processInput(required string input) {
// Private helper methods
return arguments.input.trim();
}
}In /ModuleConfig.cfc:
binder.map("MyNewService@wheels-cli")
.to("wheels.cli.models.MyNewService")
.asSingleton();component extends="wheels-cli.models.BaseCommand" {
property name="myNewService" inject="MyNewService@wheels-cli";
function run(required string input) {
var result = myNewService.doSomething(arguments.input);
print.line(result);
}
}Most services are singletons:
component singleton {
// Shared instance across commands
}For creating multiple instances:
component {
function createGenerator(required string type) {
switch(arguments.type) {
case "model":
return new ModelGenerator();
case "controller":
return new ControllerGenerator();
}
}
}For different implementations:
component {
function setStrategy(required component strategy) {
variables.strategy = arguments.strategy;
}
function execute() {
return variables.strategy.execute();
}
}component extends="wheels.Testbox" {
function beforeAll() {
// Create service instance
variables.templateService = createMock("wheels.cli.models.TemplateService");
}
function run() {
describe("TemplateService", function() {
it("loads templates correctly", function() {
var template = templateService.getTemplate("model");
expect(template).toBeString();
expect(template).toInclude("component extends=""Model""");
});
it("substitutes variables", function() {
var result = templateService.populateTemplate(
"Hello {{name}}",
{name: "World"}
);
expect(result).toBe("Hello World");
});
});
}
}function beforeAll() {
// Create mock
mockFileService = createEmptyMock("FileService");
// Define behavior
mockFileService.$("writeFile").$results(true);
// Inject mock
templateService.$property(
propertyName = "fileService",
mock = mockFileService
);
}Each service should have one clear purpose:
// Good: Focused service
component name="ValidationService" {
function validateModel() {}
function validateController() {}
}
// Bad: Mixed responsibilities
component name="UtilityService" {
function validateModel() {}
function sendEmail() {}
function generatePDF() {}
}Always inject dependencies:
// Good: Injected dependency
property name="fileService" inject="FileService@wheels-cli";
// Bad: Direct instantiation
variables.fileService = new FileService();Services should handle errors gracefully:
function generateFile(required string path) {
try {
// Attempt operation
fileWrite(arguments.path, content);
return {success: true};
} catch (any e) {
// Log error
logError(e);
// Return error info
return {
success: false,
error: e.message
};
}
}Services should be configurable:
component {
property name="settings" inject="coldbox:modulesettings:wheels-cli";
function getTimeout() {
return variables.settings.timeout ?: 30;
}
}Services can emit events:
// In service
announce("wheels-cli:modelGenerated", {model: modelName});
// In listener
function onModelGenerated(event, data) {
// React to event
}Services can call each other:
component {
property name="validationService" inject="ValidationService@wheels-cli";
function generateModel() {
// Validate first
if (!validationService.isValidModelName(name)) {
throw("Invalid model name");
}
// Continue generation
}
}// models/plugins/MyServicePlugin.cfc
component implements="IServicePlugin" {
function enhance(required component service) {
// Add new method
arguments.service.myNewMethod = function() {
return "Enhanced!";
};
}
}component extends="BaseService" {
property name="decoratedService";
function init(required component service) {
variables.decoratedService = arguments.service;
return this;
}
function doSomething() {
// Add behavior
log("Calling doSomething");
// Delegate
return variables.decoratedService.doSomething();
}
}Load services only when needed:
function getTemplateService() {
if (!structKeyExists(variables, "templateService")) {
variables.templateService = getInstance("TemplateService@wheels-cli");
}
return variables.templateService;
}Cache expensive operations:
component {
property name="cache" default={};
function getTemplate(required string name) {
if (!structKeyExists(variables.cache, arguments.name)) {
variables.cache[arguments.name] = loadTemplate(arguments.name);
}
return variables.cache[arguments.name];
}
}Use async for long-running tasks:
function analyzeLargeCodebase() {
thread name="analysis-#createUUID()#" {
// Long running analysis
}
}component {
property name="log" inject="logbox:logger:{this}";
function doSomething() {
log.debug("Starting doSomething with args: #serializeJSON(arguments)#");
// ... logic ...
log.debug("Completed doSomething");
}
}# In CommandBox REPL
repl> getInstance("TemplateService@wheels-cli")
repl> getMetadata(getInstance("TemplateService@wheels-cli"))Planned service architecture improvements:
Service Mesh: Inter-service communication layer
Service Discovery: Dynamic service registration
Circuit Breakers: Fault tolerance patterns
Service Metrics: Performance monitoring
API Gateway: Unified service access
A quick tutorial that demonstrates how quickly you can get database connectivity up and running with Wheels.
Wheels's built in model provides your application with some simple and powerful functionality for interacting with databases. To get started, you will make some simple configurations, call some functions within your controllers, and that's it. Best yet, you will rarely ever need to write SQL code to get those redundant CRUD tasks out of the way.
We'll learn by building part of a sample user management application. This tutorial will teach you the basics of setting up a resource that interacts with the Wheels ORM.
By default, Wheels will connect to a data source wheels-dev. To change this default behavior, open the file at /config/settings.cfm. In a fresh install of Wheels, you'll see the follwing code:
These lines provide Wheels with the necessary information about the data source, URL rewriting, and reload password for your application, and include the appropriate values. This may include values for dataSourceName, dataSourceUserName, and dataSourcePassword. More on URL rewriting and reload password later.
In Wheels applications, you typically configure which datasource to use in /config/settings.cfm.
The actual datasource definitions should be added in /config/app.cfm.
Wheels will look for the datasource defined in settings.cfm and match it against what you’ve defined in app.cfm or through your CFML engine’s administrator.
You can manage datasources through the Administrator interface of your CFML engine:
Access your CFML administrator (Adobe ColdFusion Administrator or Lucee Server/Web Administrator).
Navigate to the Datasource section.
Create a new datasource with the exact same name as the one you’ve set in settings.cfm (dataSourceName).
Provide connection details (JDBC driver, connection string, username, password).
Configure optional features like connection pooling and validation queries.
This method lets you manage database connectivity centrally in the engine’s admin console.
/config/app.cfm (Lucee & BoxLang)You can also define datasources programmatically in your Wheels application using the this.datasources struct inside /config/app.cfm.
This approach works in Lucee and BoxLang without needing Administrator access.
Regardless of which method you choose, ensure that the datasource name and configuration in your Wheels configuration matches exactly with the datasource created in your CFML engine.
Wheels supports MySQL, SQL Server, PostgreSQL, Oracle and H2. It doesn't matter which DBMS you use for this tutorial; we will all be writing the same CFML code to interact with the database. Wheels does everything behind the scenes that needs to be done to work with each DBMS.
That said, here's a quick look at a table that you'll need in your database, named users:
Note a couple things about this users table:
The table name is plural.
The table has an auto-incrementing primary key named id.
These are database used by Wheels. This framework strongly encourages that everyone follow convention over configuration. That way everyone is doing things mostly the same way, leading to less maintenance and training headaches down the road.
Fortunately, there are ways of going outside of these conventions when you really need it. But let's learn the conventional way first. Sometimes you need to learn the rules before you can know how to break them.
Next, open the file at /config/routes.cfm. You will see contents similar to this:
We are going to create a section of our application for listing, creating, updating, and deleting user records. In Wheels routing, this requires a plural resource, which we'll name users.
Because a users resource is more specific than the "generic" routes provided by Wheels, we'll list it first in the chain of mapper method calls:
This will create URL endpoints for creating, reading, updating, and deleting user records:
Name is referenced in your code to tell Wheels where to point forms and links.
Method is the HTTP verb that Wheels listens for to match up the request.
URL Path is the URL that Wheels listens for to match up the request.
First, let's create a simple form for adding a new user to the users table. To do this, we will use Wheels's form helper functions. Wheels includes a whole range of functions that simplifies all of the tasks that you need to display forms and communicate errors to the user.
Now create a new file in /app/views/users called new.cfm. This will contain the view code for our simple form.
Next, add these lines of code to the new file:
What we've done here is use form helpers to generate all of the form fields necessary for creating a new user in our database. It may feel a little strange using functions to generate form elements, but it will soon become clear why we're doing this. Trust us on this one… you'll love it!
To generate the form tag's action attribute, the function takes parameters similar to the function that we introduced in the Beginner Tutorial: Hello World tutorial. We can pass in controller, action, key, and other route- and parameter-defined URLs just like we do with .
To end the form, we use the function. Easy enough.
The and helpers are similar. As you probably guessed, they create <input> elements with type="text" and type="password", respectively. And the function creates an <input type="submit" /> element.
One thing you'll notice is the and functions accept arguments called objectName and property. As it turns out, this particular view code will throw an error because these functions are expecting an object named user. Let's fix that.
All of the form helper calls in our view specify an objectName argument with a reference to a variable named user. That means that we need to supply our view code with an object called user. Because the controller is responsible for providing the views with data, we'll set it there.
Create a new ColdFusion component at /app/controllers/Users.cfc.
As it turns out, our controller needs to provide the view with a blank user object (whose instance variable will also be called user in this case). In our new action, we will use the function to generate a new instance of the user model.
To get a blank set of properties in the model, we'll also call the generated model's method.
Wheels will automatically know that we're talking about the users database table when we instantiate a user model. The convention: database tables are plural and their corresponding Wheels models are singular.
Why is our model name singular instead of plural? When we're talking about a single record in the users database, we represent that with an individual model object. So the users table contains many user objects. It just works better in conversation.
Now when we run the URL at http://localhost:8080/users/new, we'll see the form with the fields that we defined.
The HTML generated by your application will look something like this:
So far we have a fairly well-formed, accessible form, without writing a bunch of repetitive markup.
Next, we'll code the create action in the controller to handle the form submission and save the new user to the database.
A basic way of doing this is using the model object's method:
Because we used the objectName argument in the fields of our form, we can access the user data as a struct in the params struct.
There are more things that we can do in the create action to handle validation, but let's keep it simple in this tutorial.
Notice that our create action above redirects the user to the users index route using the function. We'll use this action to list all users in the system with "Edit" links. We'll also provide a link to the "New User" form that we just coded.
First, let's get the data that the listing needs. Create an action named index in the users controller like so:
This call to the model's method will return a query object of all users in the system. By using the method's order argument, we're also telling the database to order the records by username.
In the view at /app/views/users/index.cfm, it's as simple as looping through the query and outputting the data
We'll now show another cool aspect of form helpers by creating a screen for editing users.
You probably noticed in the code listed above that we'll have an action for editing a single users record. We used the form helper function to add an "Edit" button to the form. This action expects a key as well.
Because in the form helper function we specified the parameter key, Wheels adds this parameter into the URL when generating the route.
Wheels will automatically add the provided 'key' from the URL to the params struct in the controllers edit() function.
Given the provided key, we'll have the action load the appropriate user object to pass on to the view:
The view at /app/views/users/edit.cfm looks almost exactly the same as the view for creating a user:
But an interesting thing happens. Because the form fields are bound to the user object via the form helpers' objectName arguments, the form will automatically provide default values based on the object's properties.
With the user model populated, we'll end up seeing code similar to this:
Pretty cool, huh?
There's a lot of repetition in the new and edit forms. You'd imagine that we could factor out most of this code into a single view file. To keep this tutorial from becoming a book, we'll just continue on knowing that this could be better.
Now we'll create the update action. This will be similar to the create action, except it will be updating the user object:
To update the user, simply call its method with the user struct passed from the form via params. It's that simple.
After the update, we'll add a success message and send the end user back to the edit form in case they want to make more changes.
Notice in our listing above that we have a delete action. Here's what it would look like:
We simply load the user using the model's method and then call the object's method. That's all there is to it.
We've shown you quite a few of the basics in getting a simple user database up and running. We hope that this has whet your appetite to see some of the power packed into the Wheels framework. There's plenty more.
Be sure to read on to some of the related chapters listed below to learn more about working with Wheels's ORM.
Instructions for upgrading Wheels applications
Wheels follows Semantic Versioning ():
Major releases (e.g., 2.x.x → 3.x.x) may include breaking changes and require code adjustments.
Minor releases (e.g., 3.0.x → 3.1.x) may add new features in a backwards-compatible way.
Patch releases (e.g., 3.0.1 → 3.0.2) focus on bug fixes.
Upgrading an existing application involves more than swapping the wheels folder — it often requires project restructuring, dependency updates, and code modifications.
1. Backup First
Create a full backup of your application code and database.
Commit all changes if using version control.
2. Read All Relevant Upgrade Notes
If skipping versions, review the upgrade notes for every version in between.
Some changes require manual adjustments outside the wheels folder.
3. Use a Safe Environment
Test upgrades in a local or staging environment before production.
4. Upgrade Steps
Follow the version-specific changes listed below.
Replace/update the wheels folder with the version you are upgrading to.
Apply all required file moves, renames, and deletions.
Install new dependencies (box install if using CommandBox).
Update code for removed or renamed functions.
Check plugins for compatibility.
5. Test Thoroughly
Run your test suite.
Fix routing, mappings, or missing dependency issues.
6. Deploy to Production
Clear caches and restart the CFML engine after deployment.
This guide walks you through upgrading an existing Wheels application to version 3.0.0. It covers compatibility changes, the new folder structure, and the CommandBox (box.json) dependency management process for the vendor/ folder.
No longer supported: Adobe ColdFusion 2016 and earlier.
Supported engines:
Adobe ColdFusion 2018+
Lucee 5+
BoxLang
Wheels 3.x adopts a more modern and organized structure. Update your application as follows:
Move your app code into a new app/ directory at the root:
Keep the config/ folder at the root.
Keep the db/ folder at the root.
Keep the public/ folder at the root. Move all static assets into public/:
Inside public/, replace the following with the latest versions from Wheels 3.x:
Wheels 3.x manages its core files and related libraries via CommandBox. This means no manually downloading or replacing vendor files.
box.jsonAt your project root, define dependencies and where they should be installed:
Explanation:
dependencies: Lists the packages your app needs, with version ranges.
installPaths: Ensures each dependency installs into a specific folder inside vendor/.
Run:
This will:
Download wheels-core, wirebox, and testbox
Place them into the correct vendor/ subfolders based on installPaths
Ensure Application.cfc includes mappings for the new locations:
Mappings must point to the correct folders inside vendor/ so Wheels can find its core files and libraries.
Before (Wheels 2.x)
After (Wheels 3.x)
Run box install to ensure all dependencies are fetched.
Review and update Application.cfc mappings.
Test your application thoroughly.
Check the Wheels 3.x release notes for any breaking changes affecting your code.
Commit the new structure and box.json so the setup is reproducible for all team members.
No manual vendor updates — dependencies are versioned and reproducible.
Easier upgrades — box update replaces manual downloads.
Consistent environments — the same box.json ensures all developers run the same versions.
Replace the wheels folder with the new one from the 2.3.0 download.
Replace the wheels folder with the new one from the 2.2.0 download.
Replace the wheels folder with the new one from the 2.1.0 download.
Rename any instances of findLast() to findLastOne()
Create /events/onabort.cfm to support the onAbort method
As always, the first step is to replace the wheels folder with the new one from the 2.0 download.
Other major changes required to upgrade your application are listed in the following sections.
Wheels 2.0 requires one of these CFML engines:
Lucee 4.5.5.006 + / 5.2.1.9+
Adobe ColdFusion 10.0.23 / 11.0.12+ / 2016.0.4+
We've updated our minimum requirements to match officially supported versions from the vendors. (For example, Adobe discontinued support for ColdFusion 10 in May 2017, which causes it to be exposed to security exploits in the future. We've included it in 2.0 but it may be discontinued in a future version)
The events/functions.cfm file has been moved to global/functions.cfm.
The models/Model.cfc file should extend wheels.Model instead of Wheels (models/Wheels.cfc can be deleted).
The controllers/Controller.cfc file should extend wheels.Controller instead of Wheels(controllers/Wheels.cfc can be deleted).
The init function of controllers and models must be renamed to config.
The global setting modelRequireInit has been renamed to modelRequireConfig.
The global setting cacheControllerInitialization has been renamed to cacheControllerConfig.
The global setting cacheModelInitialization has been renamed to cacheModelConfig.
The global setting clearServerCache has been renamed to clearTemplateCache.
The updateProperties() method has been removed, use update() instead.
JavaScript arguments like confirm and disable have been removed from the link and form helper functions (use the and plugins to reinstate the old behavior).
The renderPage function has been renamed to renderView
includePartial() now requires the partial and query arguments to be set (if using a query)
The function has been removed in Wheels 2.0 in favor of a new routing API. See the chapter for information about the new RESTful routing system.
A limited version of the "wildcard" route ([controller]/[action]/[key]) is available as [controller]/[action]) if you use the new mapper method:
By default, this is limited to GET requests for security reasons.
It is strongly recommended that you enable Wheels 2.0's built-in CSRF protection.
For many applications, you need to follow these steps:
In controllers/Controller.cfc, add a call to to the config method.
Add a call to the helper in your layouts' <head> sections.
Configure any AJAX calls that POST data to your application to pass the authenticityToken from the <meta>tags generated by as an X-CSRF-TOKEN HTTP header.
Update your route definitions to enforce HTTP verbs on actions that manipulate data (get, post, patch, delete, etc.)
Make sure that forms within the application are POSTing data to the actions that require post, patch, and delete verbs.
See documentation for the for more information.
Note: If you had previously installed the , you may remove it and rely on the functionality included in the Wheels 2 core.
If you have previously been using the dbmigrate plugin, you can now use the inbuilt version within the Wheels 2 core.
Database Migration files in /db/migrate/ should now be moved to /app/migrator/migrations/ and extend wheels.migrator.Migration, not plugins.dbmigrate.Migration which can be changed with a simple find and replace. Note: Oracle is not currently supported for Migrator.
Replace the wheels folder with the new one from the 1.4 download.
Replace URL rewriting rule files – i.e, .htaccess, web.config, IsapiRewrite.ini
In addition, if you're upgrading from an earlier version of Wheels, we recommend reviewing the instructions from earlier reference guides below.
If you are upgrading from Wheels 1.1.0 or newer, follow these steps:
Replace the wheels folder with the new one from the 1.3 download.
Replace the root root.cfm file with the new one from the 1.3 download.
Remove the <cfheader> calls from the following files:
events/onerror.cfm
events/onmaintenance.cfm
events/onmissingtemplate.cfm
In addition, if you're upgrading from an earlier version of Wheels, we recommend reviewing the instructions from earlier reference guides below.
Note: To accompany the newest 1.1.x releases, we've highlighted the changes that are affected by each release in this cycle.
If you are upgrading from Wheels 1.0 or newer, the easiest way to upgrade is to replace the wheels folder with the new one from the 1.1 download. If you are upgrading from an earlier version, we recommend reviewing the steps outlined in Upgrading to Wheels 1.0.
Note: To accompany the newest 1.1.x releases, we've highlighted the changes that are affected by each release in this cycle.
Be sure to review your plugins and their compatibility with your newly-updated version of Wheels. Some plugins may stop working, throw errors, or cause unexpected behavior in your application.
1.1: The minimum Adobe ColdFusion version required is now 8.0.1.
1.1: The minimum Railo version required is now 3.1.2.020.
1.1: The H2 database engine is now supported.
1.1: The .htaccess file has been changed. Be sure to copy over the new one from the new version 1.1 download and copy any addition changes that you may have also made to the original version.
1.1: By default, Wheels 1.1 will wrap database queries in transactions. This requires that your database engine supports transactions. For MySQL in particular, you can convert your MyISAM tables to InnoDB to be compatible with this new functionality. Otherwise, to turn off automatic transactions, place a call to set(transactionMode="none").
1.1: Binary data types are now supported.
Model Code
1.1: Validations will be applied to some model properties automatically. This may cause unintended behavior with your validations. To turn this setting off, call set(automaticValidations=false) in config/settings.cfm.
1.1: The class argument in hasOne(), hasMany(), and belongsTo() has been deprecated. Use the modelName argument instead.
1.1: afterFind() callbacks no longer require special logic to handle the setting of properties in objects and queries. (The "query way" works for both cases now.) Because arguments will always be passed in to the method, you can't rely on StructIsEmpty() to determine if you're dealing with an object or not. In the rare cases that you need to know, you can now call isInstance() or isClass() instead.
1.1: On create, a model will now set the updatedAt auto-timestamp to the same value as the createdAt timestamp. To override this behavior, call set(setUpdatedAtOnCreate=false) in config/settings.cfm.
View Code
1.1: Object form helpers (e.g. textField() and radioButton()) now automatically display a label based on the property name. If you left the label argument blank while using an earlier version of Wheels, some labels may start appearing automatically, leaving you with unintended results. To stop a label from appearing, use label=false instead.
1.1: The contentForLayout() helper to be used in your layout files has been deprecated. Use the includeContent() helper instead.
1.1: In production mode, query strings will automatically be added to the end of all asset URLs (which includes JavaScript includes, stylesheet links, and images). To turn off this setting, call set(assetQueryString=false) in config/settings.cfm.
1.1: stylesheetLinkTag() and javaScriptIncludeTag() now accept external URLs for the source/sources argument. If you manually typed out these tags in previous releases, you can now use these helpers instead.
1.1: flashMessages(), errorMessageOn(), and errorMessagesFor() now create camelCased class attributes instead (for example error-messages is now errorMessages). The same goes for the class attribute on the tag that wraps form elements with errors: it is now fieldWithErrors.
Controller Code
1.1.1: The if argument in all validation functions is now deprecated. Use the condition argument instead.
Our listing of steps to take while upgrading your Wheels application from earlier versions to 1.0.x.
Upgrading from an earlier version of 1.x? Then the upgrade path is simple. All you need to do is replace the wheels folder with the new wheels folder from the download.
The easiest way to upgrade is to setup an empty website, deploy a fresh copy of Wheels 1.0, and then transfer your application code to it. When transferring, please make note of the following changes and make the appropriate changes to your code.
Note: To accompany the newest 1.0 release, we've highlighted the changes that are affected by that release.
1.0: URL rewriting with IIS 7 is now supported.
1.0: URL rewriting in a sub folder on Apache is now supported.
ColdFusion 9 is now supported.
Oracle 10g or later is now supported.
PostgreSQL is now supported.
Railo 3.1 is now supported.
1.0: There is now an app.cfm file in the config folder. Use it to set variables that you'd normally set in Application.cfc (i.e., this.name, this.sessionManagement, this.customTagPaths, etc.)
1.0: There is now a web.config file in the root.
1.0: There is now a Wheels.cfc file in the models folder.
1.0: The Wheels.cfc file in the controllers folder has been updated.
1.0: The IsapiRewrite4.ini and .htaccess files in the root have both been updated.
The controller folder has been changed to controllers.
The model folder has been changed to models.
The view folder has been changed to views.
Rename all of your CFCs in models and controllers to UpperCamelCase. So controller.cfc will become Controller.cfc, adminUser.cfc will become AdminUser.cfc, and so on.
All images must now be stored in the images folder, files in the files folder, JavaScript files in the javascripts folder, and CSS files in the stylesheets folder off of the root.
deletedOn, updatedOn, and createdOn are no longer available as auto-generated fields. Please change the names to deletedAt, updatedAt, and createdAt instead to get similar functionality, and make sure that they are of type datetime, timestamp, or equivalent.
Config Code
1.0: The action of the default route (home) has changed to wheels. The way configuration settings are done has changed quite a bit. To change a Wheels application setting, use the new set() function with the name of the Wheels property to change. (For example, <cfset set(dataSourceName="mydatasource")>.) To see a list of available Wheels settings, refer to the Configuration and Defaults chapter. Model Code
1.0: The extends attribute in models/Model.cfc should now be Wheels.
findById() is now called findByKey(). Additionally, its id argument is now named key instead. For composite keys, this argument will accept a comma-delimited list.
When using a model's findByKey() or findOne() functions, the found property is no longer available. Instead, the functions return false if the record was not found.
A model's errorsOn() function now always returns an array, even if there are no errors on the field. When there are errors for the field, the array elements will contain a struct with name, fieldName, and message elements.
The way callbacks are created has changed. There is now a method for each callback event ( beforeValidation(), beforeValidationOnCreate(), etc.) that should be called from your model's init() method. These methods take a single argument: the method within your model that should be invoked during the callback event. See the chapter on Object Callbacks for an example.
View Code
1.0: The contents of the views/wheels folder have been changed.
The wrapLabel argument in form helpers is now replaced with labelPlacement. Valid values for labelPlacement are before, after, and around.
The first argument for includePartial() has changed from name to partial. If you're referring to it through a named argument, you'll need to replace all instances with partial.
The variable that keeps a counter of the current record when using includePartial() with a query has been renamed from currentRow to current.
There is now an included wheels view folder in views. Be sure to copy that into your existing Wheels application if you're upgrading.
The location of the default layout has changed. It is now stored at /views/layout.cfm. Now controller-specific layouts are stored in their respective view folder as layout.cfm. For example, a custom layout for www.domain.com/about would be stored at /views/about/layout.cfm.
In linkTo(), the id argument is now called key. It now accepts a comma-delimited list in the case of composite keys.
The linkTo() function also accepts an object for the key argument, in which case it will automatically extract the keys from it for use in the hyperlink.
The linkTo() function can be used only for controller-, action-, and route-driven links now. * The url argument has been removed, so now all static links should be coded using a standard "a" tag.
Controller Code
1.0: The extends attribute in controllers/Controller.cfc should now be Wheels. Multiple-word controllers and actions are now word-delimited by hyphens in the URL. For example, if your controller is called SiteAdmin and the action is called editLayout, the URL to access that action would be .
The default route for Wheels has changed from [controller]/[action]/[id] to [controller]/[action]/[key]. This is to support composite keys. The params.id value will now only be available as params.key.
You can now pass along composite keys in the URL. Delimit multiple keys with a comma. (If you want to use this feature, then you can't have a comma in the key value itself).
Learn how to extend Wheels CLI with your own custom commands.
Wheels CLI is built on CommandBox, making it easy to add custom commands. Commands can be simple scripts or complex operations using the service architecture.
Create a new file in /commands/wheels/:
CommandBox generates help from your code:
Create nested command structure:
Usage:
Get user input:
Show progress for long operations:
Use verbs for actions: generate, create, deploy
Use nouns for resources: model, controller, migration
Be consistent with existing commands
Create box.json:
<cfscript>
/*
Use this file to configure your application.
You can also use the environment specific files (e.g. /config/production/settings.cfm) to override settings set here.
Don't forget to issue a reload request (e.g. reload=true) after making changes.
See https://wheels.dev/3.0.0/guides/working-with-wheels/configuration-and-defaults for more info.
*/
/*
You can change the "wheels.dev" value from the two functions below to set your datasource.
You can change the the value for the "dataSourceName" to set a default datasource to be used throughout your application.
You can also change the value for the "coreTestDataSourceName" to set your testing datasource.
You can also uncomment the 2 "set" functions below them to set the username and password for the datasource.
*/
set(coreTestDataSourceName="wheels-dev");
set(dataSourceName="wheels-dev");
// set(dataSourceUserName="");
// set(dataSourcePassword="");
/*
If you comment out the following line, Wheels will try to determine the URL rewrite capabilities automatically.
The "URLRewriting" setting can bet set to "on", "partial" or "off".
To run with "partial" rewriting, the "cgi.path_info" variable needs to be supported by the web server.
To run with rewriting set to "on", you need to apply the necessary rewrite rules on the web server first.
*/
set(URLRewriting="On");
// Reload your application with ?reload=true&password=wheels.dev
set(reloadPassword="wheels-dev");
// CLI-Appends-Here
</cfscript>
set(dataSourceName="back2thefuture");
// set(dataSourceUserName="marty");
// set(dataSourcePassword="mcfly");component {
this.name = "WheelsApp";
// Define a datasource for use in settings.cfm
this.datasources["wheels-dev"] = {
class: "com.mysql.cj.jdbc.Driver",
connectionString: "yourConnectionString",
username: "yourUsername",
password: "yourPassword"
};
}id
int
auto increment
username
varchar(100)
varchar(255)
passwd
varchar(15)
mapper()
.wildcard()
.root(method = "get")
.end();mapper()
.resources("users")
.wildcard()
.root(method = "get")
.end();users
GET
/users
Lists users
newUsers
GET
/users/new
Display a form for creating a user record
users
POST
/users
Form posts a new user record to be created
editUser
GET
/users/[id]/edit
Displays a form for editing a user record
user
PATCH
/users/[id]
Form posts an existing user record to be updated
user
DELETE
/users/[id]
Deletes a user record
<cfoutput>
<h1>New User</h1>
#startFormTag(route="users")#
<div>
#textField(objectName="user", property="username", label="Username")#
</div>
<div>
#textField(objectName="user", property="email", label="Email")#
</div>
<div>
#passwordField(
objectName="user",
property="passwd",
label="Password"
)#
</div>
<div>#submitTag()#</div>
#endFormTag()#
</cfoutput>component extends="Controller" {
function config(){}
function new() {
user = model("user").new();
}
}<h1>New User</h1>
<form action="/users" method="post">
<div>
<label for="user-username">
Username
<input id="user-username" type="text" value="" name="user[username]">
</label>
</div>
<div>
<label for="user-email">
Email
<input id="user-email" type="text" value="" name="user[email]">
</label>
</div>
<div>
<label for="user-passwd">
Password
<input id="user-passwd" type="password" value="" name="user[passwd]">
</label>
</div>
<div><input value="Save changes" type="submit"></div>
</form>function create() {
user = model("user").create(params.user);
redirectTo(
route="users",
success="User created successfully."
);
}function index() {
users = model("user").findAll(order="username");
}<cfoutput>
<h1>Users</h1>
<p>#linkTo(text="New User", route="newUser")#</p>
<table>
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th colspan="2"></th>
</tr>
</thead>
<tbody>
<cfloop query="users">
<tr>
<td>
#EncodeForHtml(users.username)#
</td>
<td>
#EncodeForHtml(users.email)#
</td>
<td>
#linkTo(
text="Edit",
route="editUser",
key=users.id,
title="Edit #users.username#"
)#
</td>
<td>
#buttonTo(
text="Delete",
route="user",
key=users.id,
method="delete",
title="Delete #users.username#"
)#
</td>
</tr>
</cfloop>
</tbody>
</table>
</cfoutput>function edit() {
user = model("user").findByKey(params.key);
}<cfoutput>
<h1>Edit User #EncodeForHtml(user.username)#</h1>
#startFormTag(route="user", key=user.key(), method="patch")#
<div>
#textField(objectName="user", property="username", label="Username")#
</div>
<div>
#textField(objectName="user", property="email", label="Email")#
</div>
<div>
#passwordField(
objectName="user",
property="passwd",
label="Password"
)#
</div>
<div>#submitTag()#</div>
#endFormTag()#
</cfoutput><h1>Edit User Homer Simpson</h1>
<form action="/users/1" method="post">
<input type="hidden" name="_method" value="patch">
<div>
<input type="hidden" name="user[id]" value="15">
</div>
<div>
<label for="user-username">
Name
<input
id="user-username"
type="text"
value="Homer Simpson"
name="user[username]">
</label>
</div>
<div>
<label for="user-email">
Email
<input
id="user-email"
type="text"
value="[email protected]"
name="user[email]">
</label>
</div>
<div>
<label for="user-passwd">
Password
<input
id="user-passwd"
type="password"
value="donuts.mmm"
name="user[passwd]">
</label>
</div>
<div><input value="Save changes" type="submit"></div>
</form>function update() {
user = model("user").findByKey(params.key);
user.update(params.user);
redirectTo(
route="editUser",
key=user.id,
success="User updated successfully."
);
}function delete() {
user = model("user").findByKey(params.key);
user.delete();
redirectTo(
route="users",
success="User deleted successfully."
);
}app/controllers/
app/global/
app/migrator/
app/models/
app/views/
plugins/public/images/
public/files/
public/javascripts/
public/miscellaneous/
public/stylesheets/Application.cfc
index.cfm
urlrewrite.xml{
"name": "wheels-app",
"version": "1.0.0",
"dependencies": {
"wirebox": "^7.0.0",
"testbox": "^6.0.0",
"wheels-core": "^3.0.0-SNAPSHOT"
},
"installPaths": {
"wirebox": "vendor/wirebox/",
"testbox": "vendor/testbox/",
"wheels-core": "vendor/wheels/"
}
}box install/app
/config
/db
/public
/vendor
/wheels
/wirebox
/testbox
/tests/controllers
/global
/migrator
/models
/plugins
/views
/config
/db
/public
/vendor (manual files)
Application.cfc
index.cfm
urlrewrite.xml/app
/controllers
/global
/migrator
/models
/plugins
/views
/config
/db
/public
Application.cfc
index.cfm
urlrewrite.xml
/vendor
/wheels
/wirebox
/testbox
.env
box.json
server.jsonmapper()
.wildcard()
.end();// commands/wheels/hello.cfc
component extends="wheels-cli.models.BaseCommand" {
/**
* Say hello
*/
function run(string name = "World") {
print.line("Hello, #arguments.name#!");
}
}wheels hello
# Output: Hello, World!
wheels hello John
# Output: Hello, John!component extends="wheels-cli.models.BaseCommand" {
// Command metadata
property name="name" default="mycommand";
property name="description" default="Does something useful";
// Service injection
property name="myService" inject="MyService@wheels-cli";
/**
* Main command entry point
*
* @name Name of something
* @force Force overwrite
* @name.hint The name to use
* @force.hint Whether to force
*/
function run(
required string name,
boolean force = false
) {
// Command logic here
}
}wheels hello --help
NAME
wheels hello
SYNOPSIS
wheels hello [name]
DESCRIPTION
Say hello
ARGUMENTS
name = World
Name to greet// commands/wheels/deploy.cfc
component extends="wheels-cli.models.BaseCommand" {
function run() {
print.line("Usage: wheels deploy [staging|production]");
}
}
// commands/wheels/deploy/staging.cfc
component extends="wheels-cli.models.BaseCommand" {
function run() {
print.line("Deploying to staging...");
}
}
// commands/wheels/deploy/production.cfc
component extends="wheels-cli.models.BaseCommand" {
function run() {
print.line("Deploying to production...");
}
}wheels deploy staging
wheels deploy productioncomponent extends="wheels-cli.models.BaseCommand" {
function run() {
// Simple input
var name = ask("What's your name? ");
// Masked input (passwords)
var password = ask("Enter password: ", "*");
// Confirmation
if (confirm("Are you sure?")) {
print.line("Proceeding...");
}
// Multiple choice
var choice = multiselect()
.setQuestion("Select features to install:")
.setOptions([
"Authentication",
"API",
"Admin Panel",
"Blog"
])
.ask();
}
}component extends="wheels-cli.models.BaseCommand" {
function run() {
// Progress bar
var progressBar = progressBar.create(total=100);
for (var i = 1; i <= 100; i++) {
// Do work
sleep(50);
// Update progress
progressBar.update(
current = i,
message = "Processing item #i#"
);
}
progressBar.clear();
print.greenLine("✓ Complete!");
// Spinner
var spinner = progressSpinner.create();
spinner.start("Loading...");
// Do work
sleep(2000);
spinner.stop();
}
}component extends="wheels-cli.models.BaseCommand" {
property name="codeGenerationService" inject="CodeGenerationService@wheels-cli";
property name="templateService" inject="TemplateService@wheels-cli";
function run(required string name) {
// Use services
var template = templateService.getTemplate("custom");
var result = codeGenerationService.generateFromTemplate(
template = template,
data = {name: arguments.name}
);
print.greenLine("Generated: #result.path#");
}
}// models/CustomService.cfc
component singleton {
function processData(required struct data) {
// Service logic
return data;
}
}
// Register in ModuleConfig.cfc
binder.map("CustomService@wheels-cli")
.to("wheels.cli.models.CustomService")
.asSingleton();function run(required string file) {
var filePath = resolvePath(arguments.file);
if (!fileExists(filePath)) {
error("File not found: #filePath#");
}
var content = fileRead(filePath);
print.line(content);
}function run(required string name) {
var content = "Hello, #arguments.name#!";
var filePath = resolvePath("output.txt");
if (fileExists(filePath) && !confirm("Overwrite existing file?")) {
return;
}
fileWrite(filePath, content);
print.greenLine("✓ File created: #filePath#");
}function run(required string dir) {
// Create directory
ensureDirectoryExists(arguments.dir);
// List files
var files = directoryList(
path = resolvePath(arguments.dir),
recurse = true,
filter = "*.cfc"
);
for (var file in files) {
print.line(file);
}
}function run() {
// Basic colors
print.line("Normal text");
print.redLine("Error message");
print.greenLine("Success message");
print.yellowLine("Warning message");
print.blueLine("Info message");
// Bold
print.boldLine("Important!");
print.boldRedLine("Critical error!");
// Inline colors
print.line("This is #print.red('red')# and #print.green('green')#");
}function run() {
// Create table
print.table([
["Name", "Type", "Size"],
["users.cfc", "Model", "2KB"],
["posts.cfc", "Model", "3KB"],
["comments.cfc", "Model", "1KB"]
]);
// With headers
var data = queryNew("name,type,size", "varchar,varchar,varchar", [
["users.cfc", "Model", "2KB"],
["posts.cfc", "Model", "3KB"]
]);
print.table(
data = data,
headers = ["File Name", "Type", "File Size"]
);
}function run() {
print.tree([
{
label: "models",
children: [
{label: "User.cfc"},
{label: "Post.cfc"},
{label: "Comment.cfc"}
]
},
{
label: "controllers",
children: [
{label: "Users.cfc"},
{label: "Posts.cfc"}
]
}
]);
}function run(required string file) {
try {
var content = fileRead(arguments.file);
processFile(content);
print.greenLine("✓ Success");
} catch (any e) {
print.redLine("✗ Error: #e.message#");
if (arguments.verbose ?: false) {
print.line(e.detail);
print.line(e.stacktrace);
}
// Set exit code
return 1;
}
}function run(required string name) {
// Validation
if (!isValidName(arguments.name)) {
error("Invalid name. Names must be alphanumeric.");
}
// Warnings
if (hasSpecialChars(arguments.name)) {
print.yellowLine("Warning: Special characters detected");
}
// Success
print.greenLine("Name is valid");
}
private function error(required string message) {
print.redLine("#arguments.message#");
exit(1);
}// tests/commands/HelloTest.cfc
component extends="wheels.Testbox" {
function run() {
describe("Hello Command", function() {
it("greets with default name", function() {
var result = execute("wheels hello");
expect(result).toInclude("Hello, World!");
});
it("greets with custom name", function() {
var result = execute("wheels hello John");
expect(result).toInclude("Hello, John!");
});
});
}
private function execute(required string command) {
// Capture output
savecontent variable="local.output" {
shell.run(arguments.command);
}
return local.output;
}
}it("generates files correctly", function() {
// Run command
execute("wheels generate custom test");
// Verify files created
expect(fileExists("/custom/test.cfc")).toBeTrue();
// Verify content
var content = fileRead("/custom/test.cfc");
expect(content).toInclude("component");
// Cleanup
fileDelete("/custom/test.cfc");
});function run(required string name, string type = "default") {
// Validate required
if (!len(trim(arguments.name))) {
error("Name cannot be empty");
}
// Validate options
var validTypes = ["default", "custom", "advanced"];
if (!arrayFind(validTypes, arguments.type)) {
error("Invalid type. Must be one of: #arrayToList(validTypes)#");
}
}function run() {
print.line("Starting process...").toConsole();
// Show what's happening
print.indentedLine("→ Loading configuration");
var config = loadConfig();
print.indentedLine("→ Processing files");
var count = processFiles();
print.indentedLine("→ Saving results");
saveResults();
print.greenBoldLine("✓ Complete! Processed #count# files.");
}function run(required string name) {
var filePath = resolvePath("#arguments.name#.txt");
// Check if already exists
if (fileExists(filePath)) {
print.yellowLine("File already exists, skipping");
return;
}
// Create file
fileWrite(filePath, "content");
print.greenLine("✓ Created file");
}{
"name": "my-wheels-commands",
"version": "1.0.0",
"type": "commandbox-modules",
"dependencies": {
"wheels-cli": "^3.0.0"
}
}my-wheels-commands/
├── ModuleConfig.cfc
├── commands/
│ └── wheels/
│ └── mycommand.cfc
└── models/
└── MyService.cfcbox forgebox publish// commands/wheels/db/backup.cfc
component extends="wheels-cli.models.BaseCommand" {
property name="datasource" inject="coldbox:datasource";
function run(string file = "backup-#dateFormat(now(), 'yyyy-mm-dd')#.sql") {
print.line("Creating database backup...").toConsole();
var spinner = progressSpinner.create();
spinner.start("Backing up database");
try {
// Get database info
var dbInfo = getDatabaseInfo();
// Create backup
var backupPath = resolvePath(arguments.file);
createBackup(dbInfo, backupPath);
spinner.stop();
print.greenBoldLine("✓ Backup created: #backupPath#");
} catch (any e) {
spinner.stop();
print.redLine("✗ Backup failed: #e.message#");
return 1;
}
}
}// commands/wheels/quality.cfc
component extends="wheels-cli.models.BaseCommand" {
property name="analysisService" inject="AnalysisService@wheels-cli";
function run(string path = ".", boolean fix = false) {
var issues = analysisService.analyze(arguments.path);
if (arrayLen(issues)) {
print.redLine("Found #arrayLen(issues)# issues:");
for (var issue in issues) {
print.line("#issue.file#:#issue.line# - #issue.message#");
}
if (arguments.fix) {
print.line().line("Attempting fixes...");
var fixed = analysisService.fix(issues);
print.greenLine("Fixed #fixed# issues");
}
} else {
print.greenLine("✓ No issues found!");
}
}
}Learn how to manage database schema changes effectively using Wheels CLI migrations.
Database migrations provide version control for your database schema. They allow you to:
Track schema changes over time
Share database changes with your team
Deploy schema updates safely
Roll back changes if needed
Keep database and code in sync
A migration is a CFC file that describes a database change. Each migration has:
A timestamp-based version number
An up() method to apply changes
An optional down() method to reverse changes
Migrations are stored in /app/migrator/migrations/ with this naming convention:
[YYYYMMDDHHmmss]_[description].cfcExample:
20240125143022_create_users_table.cfc
20240125143523_add_email_to_users.cfc# Create blank migration
wheels dbmigrate create blank add_status_to_orders
# Create table migration
wheels dbmigrate create table products
# Add column migration
wheels dbmigrate create column users emailBasic migration template:
component extends="wheels.migrator.Migration" {
function up() {
transaction {
// Apply changes
}
}
function down() {
transaction {
// Reverse changes
}
}
}function up() {
transaction {
t = createTable("products");
// Primary key (auto-created as 'id' by default)
t.primaryKey("productId"); // Custom primary key
// Column types
t.string(columnNames="name", limit=100);
t.text(columnNames="description");
t.text(columnNames="content", size="mediumtext"); // MySQL only: mediumtext (16MB)
t.text(columnNames="longDescription", size="longtext"); // MySQL only: longtext (4GB)
t.integer(columnNames="quantity");
t.bigInteger(columnNames="views");
t.float(columnNames="weight");
t.decimal(columnNames="price", precision=10, scale=2);
t.boolean(columnNames="active", default=true);
t.date(columnNames="releaseDate");
t.datetime(columnNames="publishedAt");
t.timestamp(columnNames="lastModified");
t.time(columnNames="openingTime");
t.binary(columnNames="data");
t.uniqueidentifier(columnNames="uniqueId");
// Special columns
t.timestamps(); // Creates createdAt and updatedAt
t.references(referenceNames="user"); // Creates userId foreign key
// Create the table
t.create();
}
}function up() {
transaction {
t = createTable("products",
id=false, // Don't create auto-increment id
force=true, // Drop if exists
options="ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
);
// Composite primary key
t.primaryKey(["orderId", "productId"]);
t.create();
}
}function down() {
transaction {
dropTable("products");
}
}function up() {
transaction {
addColumn(
table="users",
column="phoneNumber",
type="string",
limit=20,
allowNull=true
);
// Multiple columns
t = changeTable("users");
t.string(columnNames="address");
t.string(columnNames="city");
t.string(columnNames="postalCode", limit=10);
t.update();
}
}function up() {
transaction {
changeColumn(
table="products",
column="price",
type="decimal",
precision=12,
scale=2,
allowNull=false,
default=0
);
}
}function up() {
transaction {
renameColumn(
table="users",
column="email_address",
newName="email"
);
}
}function up() {
transaction {
removeColumn(table="users", column="deprecated_field");
// Multiple columns
t = changeTable("products");
t.removeColumn("oldPrice");
t.removeColumn("legacyCode");
t.update();
}
}function up() {
transaction {
// Simple index
addIndex(table="users", columnNames="email");
// Unique index
addIndex(
table="users",
columnNames="username",
unique=true
);
// Composite index
addIndex(
table="products",
columnNames="category,status",
name="idx_category_status"
);
// In table creation
t = createTable("orders");
t.string(columnNames="orderNumber");
t.index("orderNumber", unique=true);
t.create();
}
}function down() {
transaction {
removeIndex(table="users", name="idx_users_email");
// Or by column
removeIndex(table="products", column="sku");
}
}function up() {
transaction {
// Simple foreign key
addForeignKey(
table="orders",
column="userId",
referenceTable="users",
referenceColumn="id"
);
// With options
addForeignKey(
table="orderItems",
column="orderId",
referenceTable="orders",
referenceColumn="id",
onDelete="CASCADE",
onUpdate="CASCADE"
);
// In table creation
t = createTable("posts");
t.references(referenceNames="user", onDelete="SET NULL");
t.references(referenceNames="category", foreignKey=true);
t.create();
}
}function down() {
transaction {
removeForeignKey(
table="orders",
name="fk_orders_users"
);
}
}function up() {
transaction {
// Single record
sql("
INSERT INTO roles (name, description, createdAt)
VALUES ('admin', 'Administrator', NOW())
");
// Multiple records
addRecord(table="permissions", name="users.create");
addRecord(table="permissions", name="users.read");
addRecord(table="permissions", name="users.update");
addRecord(table="permissions", name="users.delete");
}
}function up() {
transaction {
updateRecord(
table="products",
where="status IS NULL",
values={status: "active"}
);
// Complex updates
sql("
UPDATE users
SET fullName = CONCAT(firstName, ' ', lastName)
WHERE fullName IS NULL
");
}
}function down() {
transaction {
removeRecord(
table="roles",
where="name = 'temp_role'"
);
}
}function up() {
transaction {
// Check if column exists
if (!hasColumn("users", "avatar")) {
addColumn(table="users", column="avatar", type="string");
}
// Check if table exists
if (!hasTable("analytics")) {
t = createTable("analytics");
t.integer("views");
t.timestamps();
t.create();
}
// Database-specific
if (getDatabaseType() == "mysql") {
sql("ALTER TABLE users ENGINE=InnoDB");
}
}
}function up() {
transaction {
// Complex operations
sql("
CREATE VIEW active_products AS
SELECT * FROM products
WHERE active = 1 AND deletedAt IS NULL
");
// Stored procedures
sql("
CREATE PROCEDURE CleanupOldData()
BEGIN
DELETE FROM logs WHERE createdAt < DATE_SUB(NOW(), INTERVAL 90 DAY);
END
");
}
}function up() {
transaction {
// Always run
addColumn(table="users", column="lastLoginAt", type="datetime");
// Development only
if (getEnvironment() == "development") {
// Add test data
for (var i = 1; i <= 100; i++) {
addRecord(
table="users",
email="test#i#@example.com",
password="hashed_password"
);
}
}
}
}# Check migration status
wheels dbmigrate info
# Run all pending migrations
wheels dbmigrate latest
# Run next migration only
wheels dbmigrate up
# Rollback last migration
wheels dbmigrate down
# Run specific version
wheels dbmigrate exec 20240125143022
# Reset all migrations
wheels dbmigrate resetCreate migration
wheels dbmigrate create table ordersEdit migration file
// Edit /app/migrator/migrations/[timestamp]_create_orders_table.cfcTest migration
# Run migration
wheels dbmigrate latest
# Verify
wheels dbmigrate info
# Test rollback
wheels dbmigrate downCommit and share
git add db/migrate/
git commit -m "Add orders table migration"function up() {
transaction {
// All operations in transaction
// Rollback on any error
}
}function up() {
transaction {
addColumn(table="users", column="nickname", type="string");
}
}
function down() {
transaction {
removeColumn(table="users", column="nickname");
}
}# Good: Separate migrations
wheels dbmigrate create blank add_status_to_orders
wheels dbmigrate create blank add_priority_to_orders
# Bad: Multiple unrelated changes
wheels dbmigrate create blank update_orders_and_users# Test up
wheels dbmigrate latest
# Test down
wheels dbmigrate down
# Test up again
wheels dbmigrate up# Bad: Editing existing migration
# Good: Create new migration to fix issues
wheels dbmigrate create blank fix_orders_status_columnfunction up() {
transaction {
// Add nullable first
addColumn(table="users", column="role", type="string", allowNull=true);
// Set default values
updateRecord(table="users", where="1=1", values={role: "member"});
// Make non-nullable
changeColumn(table="users", column="role", allowNull=false);
}
}function up() {
transaction {
// Drop foreign keys first
removeForeignKey(table="posts", name="fk_posts_users");
// Rename table
renameTable(oldName="posts", newName="articles");
// Recreate foreign keys
addForeignKey(
table="articles",
column="userId",
referenceTable="users",
referenceColumn="id"
);
}
}function up() {
transaction {
// First migration: deprecate column
if (getEnvironment() != "production") {
announce("Column 'users.oldField' is deprecated and will be removed");
}
}
}
// Later migration (after code deployment)
function up() {
transaction {
removeColumn(table="users", column="oldField");
}
}# Check error
wheels dbmigrate info
# Fix migration file
# Retry
wheels dbmigrate latest-- Manually fix schema_migrations table
DELETE FROM schema_migrations WHERE version = '20240125143022';function up() {
// Increase timeout for large tables
setting requestTimeout="300";
transaction {
// Add index without locking (MySQL)
sql("ALTER TABLE large_table ADD INDEX idx_column (column)");
}
}#!/bin/bash
# Check for pending migrations
if wheels dbmigrate info | grep -q "pending"; then
echo "Pending migrations detected!"
wheels dbmigrate info
exit 1
fi# .github/workflows/deploy.yml
- name: Run migrations
run: |
wheels dbmigrate latest
wheels dbmigrate infowheels dbmigrate commands - Migration command reference
Database Schema - Schema import/export
Model Generation - Generate models with migrations
Testing Guide - Testing migrations
Generate test files for models, controllers, views, and other components using TestBox BDD syntax.
This command supports multiple parameter formats:
Positional parameters: wheels generate test model User (most common)
Named parameters: type=value target=value (e.g., type=model target=User)
Flag parameters: --flag equals flag=true (e.g., --crud equals crud=true)
Parameter Mixing Rules:
✅ ALLOWED:
All positional: wheels generate test model User
All positional + flags: wheels generate test model User --crud --factory
All named: type=model target=User crud=true
❌ NOT ALLOWED:
Positional + named: wheels generate test model target=User (causes error)
Recommendation: Use positional for type/target, flags for options: wheels generate test model User --crud --factory
The wheels generate test command creates test files for various components of your Wheels application using TestBox 5 BDD syntax. All generated tests use standard CFML cfhttp() for HTTP testing and proper Wheels model() syntax, ensuring compatibility and reliability.
The command generates different test structures based on the type:
Generate a basic model test with validation and association tests:
Output: tests/specs/models/UserSpec.cfc
Generated Code:
Generate a model test with create, read, update, delete operations:
Output: tests/specs/models/ProductSpec.cfc
Contains:
Basic validation tests
it("should create a new product") - Tests model().new() and save()
it("should find an existing product") - Tests findByKey()
it("should update an existing product") - Tests property updates and save()
it("should delete a product") - Tests delete() method
Sample CRUD Test:
Generate tests using model().create() for test data:
Output: tests/specs/models/OrderSpec.cfc
Generated Code:
Generate HTTP-based controller tests:
Output: tests/specs/controllers/UsersControllerSpec.cfc
Generated Code:
Generate full CRUD controller tests via HTTP:
Output: tests/specs/controllers/ProductsControllerSpec.cfc
Contains:
it("should list all products (index action)") - Tests GET /products
it("should display a specific product (show action)") - Tests GET /products/:id
it("should create a new product (create action)") - Tests POST /products
it("should update an existing product (update action)") - Tests PUT /products/:id
it("should delete a product") - Tests DELETE /products/:id
Sample Controller Test:
Generate view rendering tests:
Output: tests/specs/views/users/editViewSpec.cfc
Generated Code:
Generate unit tests for custom services/libraries:
Output: tests/specs/unit/OrderProcessorSpec.cfc
Generated Code:
Generate unit tests with MockBox mocking examples:
Output: tests/specs/unit/PaymentServiceSpec.cfc
Additional Mock Test:
Generate end-to-end workflow tests:
Output: tests/specs/integration/CheckoutFlowIntegrationSpec.cfc
Generated Code:
Generate API endpoint tests with JSON handling:
Output: tests/specs/integration/api/UsersAPISpec.cfc
Generated Code:
Overwrite existing test files without confirmation:
Effect: Overwrites tests/specs/models/UserSpec.cfc without prompting.
Create test and open in default editor:
Effect: Creates test file and opens it in your system's default .cfc file editor.
All generated tests include:
TestBox 5 BDD Syntax: Modern describe() and it() syntax
Proper Lifecycle Methods: beforeAll(), beforeEach(), afterEach() hooks
HTTP-Based Testing: Uses cfhttp() for controller, view, integration, and API tests
Model Testing: Uses model().new(), model().create(), and model().findByKey()
CRUD Operations: Complete create, read, update, delete test cases (with --crud)
Factory Pattern: Test data creation using model().create() (with --factory)
Mock Examples: MockBox stub examples (with --mock)
JSON Handling: serializeJSON() and deserializeJSON() for API tests
Placeholder Comments: Helpful comments guiding test implementation
Run the generated tests using the Wheels test command:
Fill in Test Attributes: Generated tests include // Add test attributes comments - replace with actual model attributes
Customize Assertions: Add specific assertions for your application's business logic
Use Factory Pattern: Use --factory flag for tests requiring multiple similar objects
Test Edge Cases: Add tests for empty values, null inputs, boundary conditions
Clean Up Test Data: Use afterEach() or transactions to clean up test data
Use Descriptive Test Names: Keep it() descriptions clear and specific
Ensure your model exists in /app/models/ before generating tests.
Verify your routes are configured correctly in /config/routes.cfm.
Add required attributes in the model().create() calls with valid test data.
- Run tests
- Testing documentation
- TestBox framework docs
The Wheels CLI provides advanced testing capabilities through integration with TestBox. These commands offer specialized test runners for different testing scenarios.
Runs all tests in your application using TestBox CLI.
Runs only unit tests located in the tests/specs/unit directory.
Runs only integration tests located in the tests/specs/integration directory.
Same as test:unit but defaults to tests/specs/integration directory.
Watches for file changes and automatically reruns tests.
Runs tests with code coverage analysis (requires FusionReactor).
The standard test directory structure for Wheels applications:
The --type parameter determines which test suite to run:
app (default): Runs tests in /wheels/app/tests route, uses tests/specs directory
core: Runs tests in /wheels/core/tests route, for framework tests
When you run test:unit or test:integration for the first time and the directories don't exist, sample test files are created automatically in the correct locations:
Unit tests: tests/specs/unit/SampleUnitTest.cfc
Integration tests: tests/specs/integration/SampleIntegrationTest.cfc
All test commands support multiple output formats via the --format parameter:
txt (default): Human-readable text output
json: JSON format for parsing and automation
junit: JUnit XML format for CI/CD integration
html: HTML format for browser viewing
Organize Tests by Type
Keep unit tests in tests/specs/unit/
Keep integration tests in tests/specs/integration/
Use subdirectories for better organization
Use Labels for Test Organization
Set Coverage Thresholds
Aim for at least 80% code coverage
Use --threshold to enforce minimum coverage
Watch Mode for TDD
Use test:watch during development
Keep tests running in a separate terminal
CI/CD Integration
Use --format=junit for CI systems
Generate coverage reports with --coverageReporter=xml
Code coverage requires FusionReactor 8.0+ to be installed and configured:
Install FusionReactor
Enable Code Coverage in FusionReactor settings
Restart your ColdFusion/Lucee server
Run wheels test:coverage
If you get an error about TestBox CLI not being installed:
Make sure your test files:
Are in the correct directory (tests/specs/ or subdirectories)
Have the .cfc extension
Extend wheels.Testbox
If coverage shows as disabled:
Verify FusionReactor is installed
Check that Code Coverage is enabled in FusionReactor settings
Ensure you've restarted the server after enabling coverage
The test commands use these routes:
App tests: http://localhost:port/wheels/app/tests
Core tests: http://localhost:port/wheels/core/tests
Ensure these routes are accessible and properly configured.
wheels test run - Modern test runner (not a TestBox wrapper)
box testbox run - Direct TestBox CLI usage
wheels g test - Generate test files
wheels test:all--type
string
app
Type of tests to run: (app, core)
--format
string
txt
Output format (txt, json, junit, html)
--coverage
boolean
false
Generate coverage report
--coverageReporter
string
-
Coverage reporter format (html, json, xml)
--coverageOutputDir
string
-
Directory for coverage output
--verbose
boolean
true
Display extra details including passing and skipped tests
--directory
string
tests/specs
The directory to use to discover test bundles and specs to test. Mutually exclusive with bundles. Example: directory=tests.specs
--recurse
boolean
true
Recurse the directory mapping or not
--bundles
string
-
The path or list of paths of the spec bundle CFCs to run and test ONLY
--labels
string
-
The list of labels that a suite or spec must have in order to execute
--excludes
string
-
The list of labels that a suite or spec must not have in order to execute
--filter
string
-
Test filter pattern
# Run all app tests
wheels test:all
# Run with JSON format
wheels test:all --format=json
# Run with coverage
wheels test:all --coverage --coverageReporter=html
# Filter tests by name
wheels test:all --filter=UserTest --verbose
# Run core tests
wheels test:all --type=core
# Run specific bundles
wheels test:all --bundles=tests.specs.unit.models,tests.specs.unit.controllerswheels test:unit--type
string
app
Type of tests to run: (app, core)
--format
string
txt
Output format (txt, json, junit, html)
--verbose
boolean
true
Display extra details including passing and skipped tests
--bundles
string
-
The path or list of paths of the spec bundle CFCs to run and test ONLY
--labels
string
-
The list of labels that a suite or spec must have in order to execute
--excludes
string
-
The list of labels that a suite or spec must not have in order to execute
--filter
string
-
Test filter pattern
--directory
string
tests/specs/unit
The directory to use to discover test bundles and specs to test. Mutually exclusive with bundles. Example: directory=tests.specs
# Run unit tests
wheels test:unit
# Run with JSON format
wheels test:unit --format=json
# Filter specific tests
wheels test:unit --filter=UserModelTest
# Run core unit tests
wheels test:unit --type=corewheels test:integration# Run integration tests
wheels test:integration
# Run specific workflow tests
wheels test:integration --filter=UserWorkflowTest
# With verbose output and JUnit format
wheels test:integration --verbose --format=junit
# Run integration tests
wheels test:integration --type=appwheels test:watch--type
string
app
Type of tests to run: (app, core)
--directory
string
tests/specs
The directory to use to discover test bundles and specs to test. Mutually exclusive with bundles. Example: directory=tests.specs
--format
string
txt
Output format (txt, json, junit, html)
--verbose
boolean
true
Display extra details including passing and skipped tests
--delay
integer
1000
Delay in milliseconds before rerunning tests
--bundles
string
-
The path or list of paths of the spec bundle CFCs to run and test ONLY
--labels
string
-
The list of labels that a suite or spec must have in order to execute
--excludes
string
-
The list of labels that a suite or spec must not have in order to execute
--filter
string
-
Test filter pattern
# Watch all tests
wheels test:watch
# Watch unit tests only
wheels test:watch --directory=tests/specs/unit
# Watch with custom delay and JSON format
wheels test:watch --delay=500 --format=jsonwheels test:coverage--type
string
app
Type of tests to run: (app, core)
--directory
string
tests/specs
The directory to use to discover test bundles and specs to test. Mutually exclusive with bundles. Example: directory=tests.specs
--format
string
txt
Output format (txt, json, junit, html)
--outputDir
string
tests/results/coverage
Directory to output the coverage report
--threshold
integer
-
Coverage percentage threshold (0-100)
--pathsToCapture
string
/app
Paths to capture for coverage
--whitelist
string
*.cfc
Whitelist paths for coverage
--blacklist
string
*Test.cfc,*Spec.cfc
Blacklist paths from coverage
--bundles
string
-
The path or list of paths of the spec bundle CFCs to run and test ONLY
--labels
string
-
The list of labels that a suite or spec must have in order to execute
--excludes
string
-
The list of labels that a suite or spec must not have in order to execute
--filter
string
-
Test filter pattern
--verbose
boolean
true
Display extra details including passing and skipped tests
# Basic coverage
wheels test:coverage
# With threshold and specific directory
wheels test:coverage --threshold=80 --directory=tests/specs/unit
# Coverage for specific paths
wheels test:coverage --pathsToCapture=/models,/controllers
# With JUnit output
wheels test:coverage --format=json --outputDir=coverage-reportstests/
├── specs/ # Main test directory (default for type=app)
│ ├── unit/ # Unit tests
│ │ ├── models/ # Model unit tests
│ │ ├── controllers/ # Controller unit tests
│ │ └── helpers/ # Helper unit tests
│ ├── integration/ # Integration tests
│ │ ├── workflows/ # User workflow tests
│ │ └── api/ # API integration tests
│ └── functions/ # Function-specific tests
└── results/ # Test results and reports
└── coverage/ # Coverage reportsit("should process payments", function() {
// test code
}).labels("critical", "payments");box install testbox-cli
box reloadwheels generate test [type] [target] [options]
wheels g test [type] [target] [options]type
Type of test: model, controller, view, unit, integration, api
Required
target
Name of the component/object to test
Required
--name
Name of the view (required for view tests)
""
--crud
Generate CRUD test methods (create, read, update, delete)
false
--mock
Generate mock/stub examples (for unit tests)
false
--factory
Generate factory examples using model().create() pattern
false
--force
Overwrite existing files without prompting
false
--open
Open the created file in default editor
false
model
/tests/specs/models/
Model validations, associations, callbacks, custom methods
Direct model instantiation
controller
/tests/specs/controllers/
Controller actions via HTTP requests
cfhttp() requests
view
/tests/specs/views/
View rendering via HTTP requests
cfhttp() requests
unit
/tests/specs/unit/
Service/library components with custom logic
Direct component instantiation
integration
/tests/specs/integration/
End-to-end workflow tests
cfhttp() requests
api
/tests/specs/integration/api/
API endpoints with JSON request/response
cfhttp() with JSON
# Positional (recommended)
wheels generate test model User
# OR all named
wheels g test type=model target=Usercomponent extends="wheels.Testbox" {
function run() {
describe("User Model", function() {
beforeEach(function() {
variables.user = model("User").new();
});
it("should validate required fields", function() {
expect(user.valid()).toBe(false);
// Add specific field validations here
});
it("should have expected associations", function() {
// Test your model associations here
// Example: expect(isObject(user)).toBe(true);
});
it("should test custom model methods", function() {
// Test custom model methods here
});
});
}
}# Positional + flag (recommended)
wheels generate test model Product --crud
# OR all named
wheels g test type=model target=Product crud=trueit("should create a new product", function() {
product.name = "Test Product";
expect(product.save()).toBe(true);
var newProduct = product;
expect(newProduct.id).toBeGT(0);
});wheels generate test model Order --crud --factorybeforeEach(function() {
// Factory pattern: create reusable test data with sensible defaults
variables.order = model("Order").new({
// Add default test attributes here
});
});
it("should create a new order", function() {
var newOrder = model("Order").create({
// Add test attributes
});
expect(newOrder.id).toBeGT(0);
});wheels generate test controller Userscomponent extends="wheels.Testbox" {
function beforeAll() {
variables.baseUrl = "http://localhost:8080";
}
function run() {
describe("Users Controller", function() {
it("should respond to index request", function() {
cfhttp(url = "#variables.baseUrl#/users", method = "GET", result = "response");
expect(response.status_code).toBe(200);
// Add more specific assertions for your controller actions
});
});
}
}wheels generate test controller Products --crudit("should list all products (index action)", function() {
cfhttp(url = "#variables.baseUrl#/products", method = "GET", result = "response");
expect(response.status_code).toBe(200);
expect(response.filecontent).toInclude("Products");
});
it("should create a new product (create action)", function() {
cfhttp(url = "#variables.baseUrl#/products", method = "POST", result = "response") {
cfhttpparam(type = "formfield", name = "product[name]", value = "Test Product");
// Add more form fields as needed
}
expect(response.status_code).toBe(302); // Redirect on success
});wheels generate test view users editcomponent extends="wheels.Testbox" {
function beforeAll() {
variables.baseUrl = "http://localhost:8080";
}
function run() {
describe("Users edit View", function() {
it("should render edit view without errors", function() {
// Test view rendering via HTTP request
cfhttp(url = "#variables.baseUrl#/users/edit", method = "GET", result = "response");
expect(response.status_code).toBe(200);
expect(response.filecontent).toInclude("Users");
});
it("should display required HTML elements", function() {
cfhttp(url = "#variables.baseUrl#/users/edit", method = "GET", result = "response");
// Add specific HTML element assertions
// expect(response.filecontent).toInclude("<form");
// expect(response.filecontent).toInclude("<input");
});
});
}
}wheels generate test unit OrderProcessorcomponent extends="wheels.Testbox" {
function run() {
describe("OrderProcessor Unit Tests", function() {
it("should test orderprocessor functionality", function() {
// Create your service/component to test
// var service = new app.lib.OrderProcessorService();
// Test your service methods here
// expect(service.someMethod()).toBe(expectedValue);
});
it("should handle edge cases", function() {
// Test edge cases like empty strings, null values, etc.
// expect(someFunction("")).toBe(expectedValue);
});
it("should handle errors gracefully", function() {
// Test error handling
// expect(function() {
// someFunction(invalidInput);
// }).toThrow();
});
});
}
}wheels generate test unit PaymentService --mockit("should work with mocked dependencies", function() {
// Example of using MockBox for mocking
// var mockDependency = createMock("app.lib.DependencyService");
// mockDependency.$("someMethod").$results("mocked value");
// Test with mocked dependency
});wheels generate test integration CheckoutFlow --crudcomponent extends="wheels.Testbox" {
function beforeAll() {
variables.baseUrl = "http://localhost:8080";
}
function run() {
describe("CheckoutFlow Integration Test", function() {
it("should complete the full checkoutflow workflow", function() {
// Test complete user journey using HTTP requests
// 1. Visit listing page
cfhttp(url = "#variables.baseUrl#/checkoutflows", method = "GET", result = "listResponse");
expect(listResponse.status_code).toBe(200);
// 2. Create new record
cfhttp(url = "#variables.baseUrl#/checkoutflows", method = "POST", result = "createResponse") {
cfhttpparam(type = "formfield", name = "checkoutflow[name]", value = "Integration Test");
}
expect(createResponse.status_code).toBe(302); // Redirect on success
// 3. Verify listing shows new record
cfhttp(url = "#variables.baseUrl#/checkoutflows", method = "GET", result = "verifyResponse");
expect(verifyResponse.filecontent).toInclude("Integration Test");
// 4. Add more workflow steps (update, delete, etc.)
});
it("should complete operations within acceptable time", function() {
var startTime = getTickCount();
cfhttp(url = "#variables.baseUrl#/checkoutflows", method = "GET", result = "response");
var endTime = getTickCount();
var executionTime = endTime - startTime;
expect(executionTime).toBeLT(5000, "Request should complete in under 5 seconds");
});
});
}
}wheels generate test api Users --crudcomponent extends="wheels.Testbox" {
function beforeAll() {
variables.apiUrl = "http://localhost:8080/api";
}
function run() {
describe("Users API", function() {
it("should return paginated users via GET", function() {
cfhttp(url = "#variables.apiUrl#/users", method = "GET", result = "response") {
cfhttpparam(type = "header", name = "Accept", value = "application/json");
// Add authentication header if needed
// cfhttpparam(type = "header", name = "Authorization", value = "Bearer TOKEN");
}
expect(response.status_code).toBe(200);
var jsonData = deserializeJSON(response.filecontent);
expect(jsonData).toHaveKey("data");
expect(isArray(jsonData.data)).toBe(true);
});
it("should create a new user via POST", function() {
var postData = {
name = "API Test User"
};
cfhttp(url = "#variables.apiUrl#/users", method = "POST", result = "response") {
cfhttpparam(type = "header", name = "Content-Type", value = "application/json");
cfhttpparam(type = "body", value = serializeJSON(postData));
}
expect(response.status_code).toBe(201);
var jsonData = deserializeJSON(response.filecontent);
expect(jsonData.data).toHaveKey("id");
});
it("should return 401 for unauthorized requests", function() {
// Test without authentication header
cfhttp(url = "#variables.apiUrl#/users", method = "GET", result = "response");
// expect(response.status_code).toBe(401);
// Add your authentication tests here
});
});
}
}wheels generate test model User --forcewheels generate test controller Products --crud --openit("should validate required fields", function() {
expect(user.valid()).toBe(false);
expect(user.errors).toHaveKey("email");
});it("should create a new user", function() {
var user = model("User").create({
email = "[email protected]",
firstName = "Test"
});
expect(user.id).toBeGT(0);
});it("should return 200 status", function() {
cfhttp(url = "#baseUrl#/users", method = "GET", result = "response");
expect(response.status_code).toBe(200);
});it("should return valid JSON", function() {
cfhttp(url = "#apiUrl#/users", method = "GET", result = "response") {
cfhttpparam(type = "header", name = "Accept", value = "application/json");
}
var data = deserializeJSON(response.filecontent);
expect(data).toHaveKey("data");
});# Run all tests
wheels test run
# Run specific test bundle
wheels test run --testBundles=ProductSpec
# Run with coverage
wheels test run --coverageGenerate a complete RESTful API controller with advanced features like pagination, filtering, sorting, and authentication.
wheels generate api-resource [name] [options]
wheels g api-resource [name] [options]The wheels generate api-resource command creates a production-ready RESTful API controller optimized for JSON APIs. It generates API-specific controllers with no view rendering logic, including optional features like pagination, filtering, sorting, authentication, and API versioning.
The generated controllers use provides("json") and renderWith() to return JSON responses with proper HTTP status codes for REST operations.
name
Resource name (singular or plural)
Required
--version
API version (v1, v2, etc.)
v1
--format
Response format (json, xml)
json
--auth
Include authentication
false
--pagination
Include pagination
true
--filtering
Include filtering
true
--sorting
Include sorting
true
--skipModel
Skip model generation
false
--skipMigration
Skip migration generation
false
--skipTests
Skip test generation
false
--namespace
API namespace
api
--docs
Generate API documentation template
false
--force
Overwrite existing files
false
This command supports multiple parameter formats:
Named parameters: name=value (e.g., name=products, version="v2")
Flag parameters: --flag equals flag=true (e.g., --auth equals auth=true)
Flag with value: --flag=value equals flag=value (e.g., --version=v2)
Parameter Mixing Rules:
✅ ALLOWED:
All positional: wheels generate api-resource products
All named: name=products version="v2" auth=true
Positional + flags: wheels generate api-resource products --auth --skipModel
❌ NOT ALLOWED:
Positional + named: wheels generate api-resource products version="v2" (causes error)
Recommendation: Use positional for name + flags for options: wheels generate api-resource products --auth --version=v2
Generate a simple API controller:
# Positional name only
wheels generate api-resource productsCreates:
/models/Product.cfc - Model file
/controllers/api/v1/Products.cfc - Versioned API controller with pagination, filtering, sorting
Generate a minimal API controller without optional features:
# Using named parameters (all named)
wheels g api-resource name=products pagination=false filtering=false sorting=false
# OR using flags (positional + flags) - RECOMMENDED
wheels g api-resource products --pagination=false --filtering=false --sorting=falseCreates a simple controller with only basic CRUD operations.
Generate API controller with authentication:
# Using flag
wheels generate api-resource products --authIncludes Bearer token authentication that requires Authorization header for create, update, delete actions.
Generate API controller with custom versioning:
# Using flags with values
wheels generate api-resource products --version=v2 --namespace=publicCreates /controllers/public/v2/Products.cfc
Generate only the controller (model already exists):
# Using flag
wheels generate api-resource products --skipModelGenerate everything with API documentation:
# Multiple flags
wheels generate api-resource products --auth --docsCreates:
/models/Product.cfc
/controllers/api/v1/Products.cfc with authentication
/app/docs/api/products.md - API documentation
wheels generate api-resource products --auth --pagination --filtering --sortingGenerates:
component extends="wheels.Controller" {
function config() {
provides("json");
filters(through="setJsonResponse");
filters(through="authenticate", except="index,show");
}
/**
* GET /products
* Supports: ?page=1&perPage=25&sort=name,-price&filter[name]=widget
*/
function index() {
local.page = params.page ?: 1;
local.perPage = params.perPage ?: 25;
local.options = {};
local.options.page = local.page;
local.options.perPage = local.perPage;
if (structKeyExists(params, "sort")) {
local.options.order = parseSort(params.sort);
}
if (structKeyExists(params, "filter")) {
local.options.where = parseFilter(params.filter);
}
local.products = model("Product").findAll(argumentCollection=local.options);
local.response = {
data = local.products,
meta = {
pagination = {
page = local.products.currentPage ?: local.page,
perPage = local.perPage,
total = local.products.totalRecords ?: 0,
pages = local.products.totalPages ?: 1
}
}
};
renderWith(data=local.response);
}
function show() { /* ... */ }
function create() { /* ... */ }
function update() { /* ... */ }
function delete() { /* ... */ }
// Helper methods for pagination, filtering, sorting, auth
private function authenticate() { /* ... */ }
private function parseSort(required string sort) { /* ... */ }
private function parseFilter(required struct filter) { /* ... */ }
}After generating your API resource, add routes to /config/routes.cfm:
// Add inside mapper() block
namespace(name="api", function() {
namespace(name="v1", function() {
resources(name="products", except="new,edit");
});
});Creates routes:
GET /api/v1/products
GET /api/v1/products/:key
POST /api/v1/products
PUT /api/v1/products/:key
DELETE /api/v1/products/:key
namespace(name="api", function() {
namespace(name="v2", function() {
resources(name="products", except="new,edit");
});
});If you used --namespace="":
resources(name="products", except="new,edit");When --pagination is enabled (default):
Request:
curl "http://localhost:8080/api/v1/products?page=2&perPage=10"Response:
{
"data": [ /* products */ ],
"meta": {
"pagination": {
"page": 2,
"perPage": 10,
"total": 100,
"pages": 10
}
}
}When --filtering is enabled (default):
Request:
curl "http://localhost:8080/api/v1/products?filter[name]=widget&filter[minPrice]=10"The generated controller includes a parseFilter() method with TODO comments for you to implement your filtering logic.
When --sorting is enabled (default):
Request:
# Sort by name ascending, then price descending
curl "http://localhost:8080/api/v1/products?sort=name,-price"The - prefix indicates descending order.
When --auth is enabled:
Request:
curl -X POST http://localhost:8080/api/v1/products \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{"product":{"name":"Widget"}}'The generated controller includes authentication methods that you need to implement with your actual token validation logic.
The generated controller uses proper REST HTTP status codes:
index
200 OK
-
show
200 OK
404 Not Found
create
201 Created
422 Unprocessable Entity
update
200 OK
404 Not Found, 422 Unprocessable Entity
delete
204 No Content
404 Not Found
auth failure
-
401 Unauthorized
# List products with pagination
curl "http://localhost:8080/api/v1/products?page=1&perPage=25"
# Get specific product
curl http://localhost:8080/api/v1/products/1
# Create product
curl -X POST http://localhost:8080/api/v1/products \
-H "Content-Type: application/json" \
-d '{"product":{"name":"Widget","price":29.99}}'
# Update product
curl -X PUT http://localhost:8080/api/v1/products/1 \
-H "Content-Type: application/json" \
-d '{"product":{"price":39.99}}'
# Delete product
curl -X DELETE http://localhost:8080/api/v1/products/1# Filter and sort
curl "http://localhost:8080/api/v1/products?filter[name]=widget&sort=-createdAt"
# Pagination with filters
curl "http://localhost:8080/api/v1/products?page=1&perPage=10&filter[minPrice]=20&sort=name"# With Bearer token
curl -X POST http://localhost:8080/api/v1/products \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"product":{"name":"Widget"}}'Success with Pagination (200 OK):
{
"data": [
{
"id": 1,
"name": "Widget",
"price": 29.99,
"createdAt": "2023-01-01T12:00:00Z",
"updatedAt": "2023-01-01T12:00:00Z"
}
],
"meta": {
"pagination": {
"page": 1,
"perPage": 25,
"total": 1,
"pages": 1
}
}
}Validation Error (422):
{
"error": "Validation failed",
"errors": [
{
"property": "name",
"message": "This field is required"
}
]
}Not Found (404):
{
"error": "Record not found"
}Unauthorized (401):
{
"error": "Unauthorized"
}Edit the generated parseFilter() method:
private function parseFilter(required struct filter) {
local.where = [];
local.params = {};
if (structKeyExists(arguments.filter, "name")) {
arrayAppend(local.where, "name LIKE :name");
local.params.name = "%#arguments.filter.name#%";
}
if (structKeyExists(arguments.filter, "minPrice")) {
arrayAppend(local.where, "price >= :minPrice");
local.params.minPrice = arguments.filter.minPrice;
}
if (structKeyExists(arguments.filter, "category")) {
arrayAppend(local.where, "category = :category");
local.params.category = arguments.filter.category;
}
return arrayLen(local.where) ? arrayToList(local.where, " AND ") : "";
}Edit the generated isValidToken() method:
private function isValidToken(required string token) {
// Example: Check against database
local.apiKey = model("ApiKey").findOne(where="token = :token", token=arguments.token);
if (isObject(local.apiKey) && local.apiKey.active) {
// Store user context in session/request
request.user = local.apiKey.user();
return true;
}
return false;
}Edit the parseSort() method:
private function parseSort(required string sort) {
local.allowedFields = ["id", "name", "price", "category", "createdAt", "updatedAt"];
// ... rest of method
}Use Versioning: Always version your APIs (--version=v1)
Enable Pagination: Prevent performance issues with large datasets
Add Authentication: Secure your API endpoints with --auth
Document Your API: Use --docs flag and keep documentation updated
Implement Filtering: Customize parseFilter() for your model fields
Whitelist Sort Fields: Only allow sorting on indexed fields
Use Proper Status Codes: 201 for creation, 204 for deletion
Return Error Details: Always include error messages for 4xx/5xx
Rate Limiting: Consider adding rate limiting for public APIs
CORS Headers: Add CORS support for browser-based clients
Feature
api-resource
controller --api
scaffold
Generates model
Optional
No
Yes
Generates views
No
No
Yes
Actions
REST only
REST only
Full CRUD
Format
Configurable
JSON only
HTML + JSON
Versioning
Yes
No
No
Pagination
Optional
No
No
Filtering
Optional
No
No
Sorting
Optional
No
No
Authentication
Optional
No
No
Best for
Production APIs
Simple APIs
Full-stack apps
wheels generate controller - Generate standard controllers
wheels generate model - Generate models
wheels generate scaffold - Generate full CRUD resources
Wheels REST Documentation - REST API best practices
Generate a model with properties, validations, and associations.
CommandBox supports multiple parameter formats:
Named parameters: name=value (e.g., name=User, properties=name:string,email:string)
Flag parameters: --flag equals flag=true (e.g., --migration equals migration=true)
Flag with value: --flag=value equals flag=value (e.g., --primaryKey=uuid)
Note: Flag syntax (--flag) avoids positional/named parameter conflicts and is recommended for boolean options.
The wheels generate model command creates a new model CFC file with optional properties, associations, and database migrations. Models represent database tables and contain business logic, validations, and relationships.
name: Cannot be empty, must be valid CFML component name (alphanumeric, starts with letter)
Valid property types for the properties parameter:
Must be singular (User, not Users)
Must be PascalCase (User, BlogPost)
Cannot contain spaces or special characters
Must be valid CFML component name
Relationship model names must follow model naming conventions
Models referenced in relationships should exist or be created
Comma-separated values cannot contain spaces around commas
Creates:
/models/User.cfc
Migration file (if enabled)
Common validation methods:
Lifecycle callbacks:
When --migration is enabled:
Empty name: name="" → Provide a valid model name
Plural names: name=Users → Use singular form: name=User
Invalid characters: name="Blog Post" → Use PascalCase: name=BlogPost
Lowercase: name=user → Use PascalCase: name=User
Missing colon: properties="name,email:string" → properties="name:string,email:string"
Invalid types: properties="name:varchar" → properties="name:string"
Extra spaces: properties="name: string" → properties="name:string"
Missing type: properties="name:" → properties="name:string"
Invalid boolean: --migration=yes → --migration=true or --migration=false
Mixed syntax: migration=true --force → --migration=true --force
Lowercase models: belongsTo="user" → belongsTo="User"
Extra spaces: belongsTo="User, Category" → belongsTo="User,Category"
Invalid separators: belongsTo="User;Category" → belongsTo="User,Category"
Naming: Use singular names (User, not Users)
Properties: Define all database columns with correct types
Validations: Add comprehensive validations in model code
Associations: Define all relationships using PascalCase
Callbacks: Use for automatic behaviors
Indexes: Add to migration for performance
Validation: Always validate parameters before running command
Generate model tests:
Check that name is not empty: name=User
Ensure PascalCase format: name=User (not name=user)
Use singular form: name=User (not name=Users)
Remove special characters: name=BlogPost (not name="Blog Post")
Check property format: properties="name:string,email:string"
Ensure valid property types (see Property Types table above)
Remove extra spaces: name:string (not name: string)
Use comma separators: name:string,email:string
Use PascalCase model names: belongsTo="User" (not belongsTo="user")
Remove spaces after commas: belongsTo="User,Category"
Ensure referenced models exist or will be created
Check --migration=false wasn't set
Ensure you have write permissions in the directory
Verify migration directory exists: /app/migrator/migrations/
Use --flag for flag=true: --force equals force=true
Use --flag=false for false values: --migration=false
Don't mix syntaxes: Use all flags or all named parameters
- Create migrations
- Add properties to existing models
- Generate controllers
- Generate complete CRUD
wheels generate model name=<modelName> [options]
#Can also be used as:
wheels g model name=<modelName> [options]name
Model name (singular)
Required
properties
Model properties (format: name:type,name2:type2)
Property format: name:type[,name2:type2] where type is valid column type
""
belongsTo
Parent model relationships (comma-separated)
Valid model names (PascalCase), comma-separated
""
hasMany
Child model relationships (comma-separated)
Valid model names (PascalCase), comma-separated
""
hasOne
One-to-one relationships (comma-separated)
Valid model names (PascalCase), comma-separated
""
primaryKey
Primary key column name(s)
Valid column name (alphanumeric, underscore)
id
tableName
Custom database table name
Valid table name (alphanumeric, underscore)
""
description
Model description
Any descriptive text
""
migration
Generate database migration
true, false
true
force
Overwrite existing files
true, false
false
string
VARCHAR(255)
Default string type
text
TEXT
For longer text content
integer
INTEGER
Whole numbers
biginteger
BIGINT
Large whole numbers
float
FLOAT
Decimal numbers
decimal
DECIMAL(10,2)
Precise decimal numbers
boolean
BOOLEAN
true/false values
date
DATE
Date values
datetime
DATETIME
Date and time values
timestamp
TIMESTAMP
Timestamp values
binary
BLOB
Binary data
uuid
VARCHAR(35)
UUID strings
wheels generate model name=Userwheels generate model name=User --properties="firstName:string,lastName:string,email:string,age:integer"wheels generate model name=Post --belongsTo="User" --hasMany="Comments"wheels generate model name=Setting --migration=falsewheels generate model name=Product --properties="name:string,price:decimal,stock:integer,active:boolean" --belongsTo="Category,Brand" --hasMany="Reviews,OrderItems"# Valid model name and properties
wheels generate model name=User --properties="firstName:string,lastName:string,email:string,age:integer"
# Valid relationships
wheels generate model name=Post --belongsTo="User,Category" --hasMany="Comments,Tags"
# Valid property types
wheels generate model name=Product --properties="name:string,description:text,price:decimal,inStock:boolean,createdAt:datetime"# Error: Model name cannot be empty
wheels generate model name=""
# Result: Invalid model name error
# Error: Model name should be singular
wheels generate model name=Users
# Result: Warning about plural name
# Error: Invalid characters
wheels generate model name="Blog Post"
# Result: Invalid model name error# Error: Invalid property type
wheels generate model name=User --properties="name:varchar,age:int"
# Result: Use 'string' instead of 'varchar', 'integer' instead of 'int'
# Error: Missing property type
wheels generate model name=User --properties="name,email:string"
# Result: Property format must be 'name:type'# Error: Invalid model name format
wheels generate model name=Post --belongsTo="user,blog_category"
# Result: Use PascalCase: 'User,BlogCategory'
# Error: Spaces in comma-separated values
wheels generate model name=Post --belongsTo="User, Category"
# Result: Remove spaces: 'User,Category'string
VARCHAR(255)
string
text
TEXT
string
integer
INTEGER
numeric
biginteger
BIGINT
numeric
float
FLOAT
numeric
decimal
DECIMAL(10,2)
numeric
boolean
BOOLEAN
boolean
date
DATE
date
datetime
DATETIME
date
timestamp
TIMESTAMP
date
binary
BLOB
binary
uuid
VARCHAR(35)
string
component extends="Model" {
function init() {
// Table name (optional if following conventions)
table("users");
// Validations
validatesPresenceOf("email");
validatesUniquenessOf("email");
validatesFormatOf("email", regex="^[^@]+@[^@]+\.[^@]+$");
// Callbacks
beforeCreate("setDefaultValues");
}
private function setDefaultValues() {
if (!StructKeyExists(this, "createdAt")) {
this.createdAt = Now();
}
}
}component extends="Model" {
function init() {
// Properties
property(name="firstName", label="First Name");
property(name="lastName", label="Last Name");
property(name="email", label="Email Address");
property(name="age", label="Age");
// Validations
validatesPresenceOf("firstName,lastName,email");
validatesUniquenessOf("email");
validatesFormatOf("email", regex="^[^@]+@[^@]+\.[^@]+$");
validatesNumericalityOf("age", onlyInteger=true, greaterThan=0, lessThan=150);
}
}component extends="Model" {
function init() {
// Associations
belongsTo("user");
hasMany("comments", dependent="deleteAll");
// Nested properties
nestedProperties(associations="comments", allowDelete=true);
// Validations
validatesPresenceOf("title,content,userId");
validatesLengthOf("title", maximum=255);
}
}// Presence
validatesPresenceOf("name,email");
// Uniqueness
validatesUniquenessOf("email,username");
// Format
validatesFormatOf("email", regex="^[^@]+@[^@]+\.[^@]+$");
validatesFormatOf("phone", regex="^\d{3}-\d{3}-\d{4}$");
// Length
validatesLengthOf("username", minimum=3, maximum=20);
validatesLengthOf("bio", maximum=500);
// Numerical
validatesNumericalityOf("age", onlyInteger=true, greaterThan=0);
validatesNumericalityOf("price", greaterThan=0);
// Inclusion/Exclusion
validatesInclusionOf("status", list="active,inactive,pending");
validatesExclusionOf("username", list="admin,root,system");
// Confirmation
validatesConfirmationOf("password");
// Custom
validate("customValidation");belongsTo("user");
belongsTo(name="author", modelName="user", foreignKey="authorId");hasMany("comments");
hasMany(name="posts", dependent="deleteAll", orderBy="createdAt DESC");hasOne("profile");
hasOne(name="address", dependent="delete");hasMany("categorizations");
hasMany(name="categories", through="categorizations");// Before callbacks
beforeCreate("method1,method2");
beforeUpdate("method3");
beforeSave("method4");
beforeDelete("method5");
beforeValidation("method6");
// After callbacks
afterCreate("method7");
afterUpdate("method8");
afterSave("method9");
afterDelete("method10");
afterValidation("method11");
afterFind("method12");
afterInitialization("method13");component extends="wheels.migrator.Migration" {
function up() {
transaction {
t = createTable("users");
t.string("firstName");
t.string("lastName");
t.string("email");
t.integer("age");
t.timestamps();
t.create();
addIndex(table="users", columnNames="email", unique=true);
}
}
function down() {
transaction {
dropTable("users");
}
}
}function init() {
softDeletes();
}function init() {
property(name="fullName", sql="firstName + ' ' + lastName");
}function scopeActive() {
return where("active = ?", [true]);
}
function scopeRecent(required numeric days=7) {
return where("createdAt >= ?", [DateAdd("d", -arguments.days, Now())]);
}function init() {
beforeCreate("setDefaults");
}
private function setDefaults() {
if (!StructKeyExists(this, "status")) {
this.status = "pending";
}
if (!StructKeyExists(this, "priority")) {
this.priority = 5;
}
}wheels generate model name=User --properties="email:string,name:string"
wheels generate test model name=UserGenerate route definitions for your Wheels application's /config/routes.cfm file.
wheels generate route [objectname]
#can also be used as:
wheels g route [objectname]
# HTTP method routes
wheels generate route --get="pattern,controller##action"
wheels generate route --post="pattern,controller##action"
wheels generate route --put="pattern,controller##action"
wheels generate route --patch="pattern,controller##action"
wheels generate route --delete="pattern,controller##action"
# Root route
wheels generate route --root="controller##action"
# Resources route (explicit)
wheels generate route --resources=true [objectname]The wheels generate route command helps you create route definitions in your Wheels application's /config/routes.cfm file. It supports individual HTTP method routes, RESTful resource routes, and root routes.
IMPORTANT: All HTTP method parameters must use the equals syntax: --get="pattern,handler" not --get pattern,handler
CommandBox supports multiple parameter formats:
Named parameters: name=value (e.g., objectname=products, get="pattern,handler")
Flag parameters: --flag equals flag=true (e.g., --resources equals resources=true)
Flag with value: --flag=value equals flag=value (e.g., --get="pattern,handler")
Note: Flag syntax (--flag) avoids positional/named parameter conflicts and is recommended for boolean options.
objectname
The name of the resource for resources route
Optional (required for resources routes)
--get
Create a GET route with pattern,handler format
--get="products/sale,products##sale"
--post
Create a POST route with pattern,handler format
--post="contact,contact##send"
--put
Create a PUT route with pattern,handler format
--put="users/[key],users##update"
--patch
Create a PATCH route with pattern,handler format
--patch="profiles,profiles##update"
--delete
Create a DELETE route with pattern,handler format
--delete="sessions,sessions##destroy"
--resources
Create a resources route (use with objectname)
--resources=true
false
--root
Create a root route with handler
--root="pages##home"
wheels generate route productsGenerates in /config/routes.cfm:
.resources("products")This creates all standard RESTful routes:
GET /products (index)
GET /products/new (new)
POST /products (create)
GET /products/[key] (show)
GET /products/[key]/edit (edit)
PUT/PATCH /products/[key] (update)
DELETE /products/[key] (delete)
wheels generate route --get="products/sale,products##sale"Generates:
.get(pattern="products/sale", to="products##sale")wheels generate route --post="api/users,api.users##create"Generates:
.post(pattern="api/users", to="api.users##create")wheels generate route --put="users/[key]/activate,users##activate"Generates:
.put(pattern="users/[key]/activate", to="users##activate")wheels generate route --delete="sessions,sessions##destroy"Generates:
.delete(pattern="sessions", to="sessions##destroy")wheels generate route --root="pages##home"Generates:
.root(to="pages##home", method="get")wheels generate route --resources=true usersGenerates:
.resources("users")Routes support dynamic segments using [key] notation:
wheels generate route --get="users/[key]/profile,users##profile"Generates:
.get(pattern="users/[key]/profile", to="users##profile")The [key] parameter will be available as params.key in your controller.
You can use any parameter name:
wheels generate route --get="posts/[year]/[month],posts##archive"Generates:
.get(pattern="posts/[year]/[month]", to="posts##archive")Parameters will be available as params.year and params.month in your controller.
If you omit the handler, the route will use standard controller/action mapping:
wheels generate route --get="products/search"Generates:
.get(pattern="products/search")This will map to the search action in the products controller.
All routes are added to /config/routes.cfm at the CLI marker position:
<cfscript>
mapper()
.resources("products") // Existing routes
.get(pattern="about", to="pages#about") // Existing routes
// CLI-Appends-Here // CLI adds new routes here
.wildcard() // Wildcard should stay last
.root(to="home#index") // Root route
.end();
</cfscript>The command automatically places new routes at the correct position before the wildcard route. Routes are processed in order, so specific routes must come before general ones.
Resource routes automatically create URL helpers:
<!--- For resources("products") --->
#linkTo(route="products", text="All Products")# <!-- /products -->
#linkTo(route="product", key=123, text="View")# <!-- /products/123 -->
#linkTo(route="newProduct", text="Add Product")# <!-- /products/new -->
#linkTo(route="editProduct", key=123, text="Edit")# <!-- /products/123/edit -->
#urlFor(route="products")# <!-- /products -->
#urlFor(route="product", key=123)# <!-- /products/123 -->For custom routes, you'll need to manually create route names in your routes.cfm:
<!--- Add name parameter manually in routes.cfm --->
.get(name="productSale", pattern="products/sale", to="products##sale")
<!--- Then use in views --->
#linkTo(route="productSale", text="Special Sale")#Building on CommandBox's parameter syntax, Wheels route generation supports:
wheels generate route --get="products/sale,products##sale"
wheels generate route --post="contact/send,contact##send"
wheels generate route --resources=true --objectname=users
wheels generate route --root="pages##home"wheels generate route products # objectname (resources route)
wheels g route users # Short alias with objectnamewheels generate route users --resources=true
wheels generate route --objectname=products --resources=trueFormat: --method="pattern,handler" or --method="pattern"
Methods: get, post, put, patch, delete
Separator: Comma (,) between pattern and handler
Quotes: Always use quotes around the value
Handler Format: controller##action (double hash required in CFML)
Format: --resources=true objectname or objectname --resources=true
Boolean: Must be explicit true or false
Objectname: Required when using resources flag
Format: --root="controller##action"
Handler: Required controller and action
Quotes: Always use quotes
# GET route with handler
wheels generate route --get="api/users,api##index"
# POST route with handler
wheels generate route --post="users/login,sessions##create"
# PUT route with handler
wheels generate route --put="profiles/[key],profiles##update"# Pattern-only routes (uses convention)
wheels generate route --get="products/search"
wheels generate route --post="newsletter/signup"# Resources flag (explicit true/false)
wheels generate route --resources=true products
wheels generate route --resources=false # Invalid - needs objectname# Objectname with resources flag
wheels generate route products --resources=true
wheels generate route --resources=true users
# Multiple routes in sequence
wheels generate route --get="login,sessions##new"
wheels generate route --post="login,sessions##create"
wheels generate route --delete="logout,sessions##destroy"❌ Missing equals sign:
wheels generate route --get products/sale,products##sale❌ Missing quotes:
wheels generate route --get=products/sale,products##sale❌ Single hash instead of double:
wheels generate route --get="products/sale,products#sale"❌ Missing objectname with resources:
wheels generate route --resources=true # No objectname✅ Correct formats:
wheels generate route --get="products/sale,products##sale"
wheels generate route products --resources
wheels generate route --root="pages##home"
wheels generate route users # Positional objectname# Single parameter
wheels generate route --get="users/[key],users##show"
# Multiple parameters
wheels generate route --get="posts/[year]/[month],posts##archive"
# Optional parameters (configure manually in routes.cfm)
wheels generate route --get="blog/[category]" # Add [category?] manually# RESTful API endpoints
wheels generate route --get="api/v1/users,api.v1.users##index"
wheels generate route --post="api/v1/users,api.v1.users##create"
wheels generate route --put="api/v1/users/[key],api.v1.users##update"
wheels generate route --delete="api/v1/users/[key],api.v1.users##destroy"# Admin controllers
wheels generate route --get="admin/dashboard,admin.dashboard##index"
wheels generate route --get="admin/users,admin.users##index"
# Module-based controllers
wheels generate route --get="shop/products,shop.products##index"
wheels generate route --post="shop/checkout,shop.checkout##process"Quoted Parameters: Preserve spaces and special characters
Equals Processing: Splits parameter name from value
Boolean Conversion: Converts "true"/"false" strings to boolean values
Array Processing: CommandBox processes space-separated values as arrays
reconstructArgs(): Processes CommandBox parameter format
Validation: Checks required parameters are present
Route Generation: Formats parameters for Wheels router syntax
File Injection: Places routes at correct position in routes.cfm
The command looks for // CLI-Appends-Here comment to place new routes. If not found, it tries different indentation levels:
// CLI-Appends-Here (3 tabs)
// CLI-Appends-Here (2 tabs)
// CLI-Appends-Here (1 tab)
// CLI-Appends-Here (no tabs)
After using the CLI, you may want to reorganize routes manually:
<cfscript>
mapper()
// Public pages first
.get(pattern="about", to="pages##about")
.get(pattern="contact", to="contact##index")
// Resources grouped together
.resources("products")
.resources("users")
// Authentication routes
.get(pattern="login", to="sessions##new")
.post(pattern="login", to="sessions##create")
// CLI generated routes will appear here
// CLI-Appends-Here
.wildcard() // Always keep wildcard last
.root(to="home##index", method="get")
.end();
</cfscript>Use equals syntax: Always use --get="pattern,handler" format
Resources for CRUD: Use resources route for full CRUD operations
Custom routes for special actions: Use HTTP method routes for non-CRUD actions
Check route order: Specific routes before general ones
Test after generation: Visit URLs to ensure routes work
Reload application: Use ?reload=true after route changes
Issue: "Missing argument" errors when using HTTP method parameters
❌ Incorrect:
wheels generate route --get products/sale,products##sale # Missing =
wheels generate route --post contact send # Missing quotes and =✅ Correct:
wheels generate route --get="products/sale,products##sale" # With = and quotes
wheels generate route --post="contact,sessions##create" # Proper formatSolution: Always use equals syntax with quotes for HTTP method parameters.
Issue: Template compilation errors with single hash (#) in handlers
❌ Incorrect:
wheels generate route --get="users,users#show" # Single # causes CFML errors✅ Correct:
wheels generate route --get="users,users##show" # Double ## for CFML escapingSolution: Always use double hash (##) in controller##action handlers.
Issue: Generated routes don't respond or show 404 errors
Possible Causes:
Application not reloaded after route changes
Route order conflicts (specific routes after wildcard)
Controller or action doesn't exist
Solutions:
# 1. Always reload after route changes
http://localhost:8080/?reload=true
# 2. Check route order in routes.cfm
# Ensure wildcard() comes AFTER specific routes
# 3. Verify controller exists
# For route: --get="products,products##sale"
# Need: /app/controllers/Products.cfc with sale() functionIssue: Complex patterns not parsed correctly
❌ Problematic:
# Spaces in patterns without quotes
wheels generate route --get=api/v1/users,api##index
# Special characters not escaped
wheels generate route --get="api-users,api#index"✅ Solutions:
# Always quote complex patterns
wheels generate route --get="api/v1/users,api##index"
# Use proper CFML escaping
wheels generate route --get="api-users,api##index"Issue: Resources flag not working or missing objectname
❌ Common Mistakes:
wheels generate route --resources=true # Missing objectname
wheels generate route --resources products # Missing =true
wheels generate route products --resource # Wrong flag name✅ Correct Usage:
wheels generate route --resources=true products # Explicit flag with objectname
wheels generate route products --resources=true # Alternative order
wheels generate route products # Default resources (implicit)Issue: Routes added in wrong location or break existing routes
Common Problems:
CLI marker // CLI-Appends-Here not found
Routes added after wildcard route
Malformed routes.cfm syntax
Solutions:
<!-- Ensure routes.cfm has proper structure -->
<cfscript>
mapper()
// Existing routes
.resources("products")
// CLI marker for new routes
// CLI-Appends-Here
// Wildcard MUST be last
.wildcard()
// Root route
.root(to="home##index", method="get")
.end(); // Don't forget .end()!
</cfscript>Before generating routes, verify:
# 1. Check current directory is Wheels app root
ls config/routes.cfm # Should exist
# 2. Verify routes.cfm has CLI marker
grep "CLI-Appends-Here" config/routes.cfm # Should find marker
# 3. Check routes.cfm syntax is valid
# Look for proper mapper() and .end() structureAfter generating routes, always:
# 1. Reload application
http://localhost:8080/?reload=true
# 2. Test route in browser
http://localhost:8080/your-new-route
# 3. Check debug footer for route information
# Look for your new route in the Routes section# Test different HTTP methods
curl -X GET http://localhost:8080/api/users
curl -X POST http://localhost:8080/api/users -d "name=test"
curl -X PUT http://localhost:8080/api/users/1 -d "name=updated"
curl -X DELETE http://localhost:8080/api/users/1"Please provide either an objectname for a resources route or specify a route type"
Cause: No parameters provided to command
Solution: Provide either objectname or HTTP method parameter
"key [TYPE] doesn't exist"
Cause: Internal processing error (rare)
Solution: Try simpler route first, then add complexity
"Template compilation error"
Cause: Single hash (#) in generated route
Solution: Check for double hash (##) in all handlers
"Route not found" (404 errors)
Cause: Route not added or application not reloaded
Solution: Check routes.cfm and reload application
# Always use consistent formatting
wheels generate route --get="pattern,handler" # ✅ Consistent
wheels generate route --get pattern,handler # ❌ Inconsistent# Plan route structure before generating
# 1. Resources routes first
wheels generate route products
wheels generate route users
# 2. Custom routes second
wheels generate route --get="search,search##index"
wheels generate route --post="contact,contact##send"
# 3. API routes with namespace pattern
wheels generate route --get="api/products,api.products##index"# Generate one route at a time
wheels generate route --get="test,test##index"
# Test it works
curl http://localhost:8080/test
# Then generate next route# Document custom routes in routes.cfm
.get(name="productSearch", pattern="products/search", to="products##search")
// Custom search endpoint for products - returns JSONIf you encounter issues not covered here:
Check the debug footer: Shows all registered routes
Verify controller exists: Match route handler to actual controller/action
Test with simple routes first: Basic patterns before complex ones
Check Wheels routing guide: For advanced routing features
Reload frequently: Always reload after route changes
wheels generate scaffold - Generate complete CRUD with routes
wheels generate controller - Generate controllers
wheels generate model - Generate models
Wheels Routing Guide - Complete routing documentation
Generate a controller with actions and optional views.
wheels generate controller name=<controllerName> [options]
#Can also be used as:
wheels g controller name=<controllerName> [options]This command supports multiple parameter formats:
Positional parameters: wheels generate controller Products (controller name)
Named parameters: name=value (e.g., name=Products, actions=index,show)
Flag parameters: --flag equals flag=true (e.g., --crud equals crud=true)
Flag with value: --flag=value equals flag=value (e.g., --actions=index,show)
Parameter Mixing Rules:
ALLOWED:
Positional: wheels generate controller Products
Positional + flags: wheels generate controller Products --crud --api
All named: name=Products actions=index,show
NOT ALLOWED:
Positional + named: wheels generate controller Products actions=index (causes error)
Recommendation: Use positional for controller name, flags for options: wheels generate controller Products --crud
Note: Flag syntax (--flag) avoids positional/named parameter conflicts and is recommended for boolean options.
Important: Use --actions (plural) not --action (singular) for custom action lists.
The wheels generate controller command creates a new controller CFC file with specified actions and optionally generates corresponding view files. It supports both traditional and RESTful controller patterns.
name
Name of the controller to create (usually plural)
Required
actions
Actions to generate (comma-delimited) - HIGHEST PRIORITY, overrides --crud
index
crud
Generate CRUD controller with actions (index, show, new, create, edit, update, delete) and scaffold-style views (index, show, new, edit, _form)
false
api
Generate API controller (no views generated, only JSON/XML endpoints)
false
noViews
Skip view generation (only generate controller)
false
description
Controller description comment
""
force
Overwrite existing files
false
Understanding how parameters interact is important for getting the expected results.
The controller generator creates views for ALL actions by default (unless you use --api or --noViews).
The command evaluates parameters in the following order:
PRIORITY 1 (HIGHEST): --actions
└─> If specified, generates ONLY the specified actions
└─> Overrides --crud and --api
└─> Example: --actions=dashboard,reports
PRIORITY 2: --crud
└─> If specified (and --actions not used), generates 7 CRUD actions:
- index, show, new, create, edit, update, delete
└─> Generates 5 scaffold-style views:
- index.cfm, show.cfm, new.cfm, edit.cfm, _form.cfm
PRIORITY 3: --api
└─> If specified (and --actions not used), generates 5 API actions:
- index, show, create, update, delete
└─> Implies --crud but excludes form actions (new, edit)
└─> Implies --noViews (no views generated)
PRIORITY 4 (DEFAULT): No flags
└─> Generates only 1 action: index
└─> Generates 1 view: index.cfmViews are generated for actions EXCEPT when:
--api flag is used (API controllers return JSON/XML, not HTML)
--noViews flag is used (explicitly skip view generation)
Views ARE generated by default for:
Controllers with --crud flag (5 scaffold-style views)
Controllers with custom --actions (1 view per action)
Controllers with no flags (1 view for index action)
wheels generate controller Products
# Result:
# Actions: index
# Views: index.cfmwheels generate controller Products --actions=dashboard,reports,export
# Result:
# Actions: dashboard, reports, export
# Views: dashboard.cfm, reports.cfm, export.cfmwheels generate controller Products --crud
# Result:
# Actions: index, show, new, create, edit, update, delete (7 actions)
# Views: index.cfm, show.cfm, new.cfm, edit.cfm, _form.cfm (5 views)
# Note: Same views as "wheels generate scaffold"wheels generate controller Products --actions=dashboard,export --noViews
# Result:
# Actions: dashboard, export
# Views: None (explicitly skipped)wheels generate controller Orders --api
# Result:
# Actions: index, show, create, update, delete (5 actions)
# Views: None (API returns JSON/XML)wheels generate controller Products --crud --noViews
# Result:
# Actions: index, show, new, create, edit, update, delete (7 actions)
# Views: None (explicitly skipped)wheels generate controller Products --actions=dashboard,reports --crud
# Result:
# Actions: dashboard, reports (--actions has priority)
# Views: dashboard.cfm, reports.cfm (views still created)
# Note: --crud is ignored when --actions is specifiedViews generated by default - Unless you use --api or --noViews, views are created for ALL actions
--actions overrides --crud - Custom actions take priority, but views are STILL generated
--api implies --crud and --noViews - API controllers get 5 actions, no views
--noViews skips all views - Use when you only need the controller file
--crud generates scaffold-style views - Same 5 views as wheels generate scaffold command
ACTION GENERATION:
├─ Has --actions? → Use those actions ONLY (highest priority)
├─ Has --api? → Generate 5 actions (index, show, create, update, delete)
├─ Has --crud? → Generate 7 actions (index, show, new, create, edit, update, delete)
└─ Default → Generate 1 action (index)
VIEW GENERATION:
├─ Has --api? → NO VIEWS (JSON/XML responses)
├─ Has --noViews? → NO VIEWS (explicitly skipped)
├─ Has --crud? → 5 VIEWS (index, show, new, edit, _form)
└─ Default → CREATE 1 VIEW PER ACTIONTraditional web app (scaffold-style)
--crud
7
5 (index, show, new, edit, _form)
REST API (JSON/XML)
--api
5
None
Single page controller
(no flags)
1 (index)
1 (index)
Custom actions with views
--actions=dashboard,export
2
2 (dashboard, export)
Controller only (no views)
--crud --noViews
7
None
Custom without views
--actions=api,process --noViews
2
None
Use --crud - Traditional web app with CRUD operations, forms, and .resources() routing (same as scaffold)
Use --api - REST API that returns JSON/XML (no views)
Use --actions - Custom actions with automatic view generation (highest priority)
Use --noViews - When you only need controller file (manual view management)
Use no flags - Simple controller with index action and view
Both generate RESTful controllers with different purposes:
Purpose
Traditional web application
API endpoints
Actions
7 (includes new, edit forms)
5 (no form actions)
Views
5 scaffold-style views
None
View Files
index, show, new, edit, _form
N/A
Routing
Use .resources() in routes.cfm
Use .resources() in routes.cfm
Response
HTML pages with forms
JSON/XML data
Use Case
User-facing web apps
Mobile apps, SPAs, integrations
Important: --crud generates the same views as wheels generate scaffold command, including the _form.cfm partial for form code reuse.
Examples:
# CRUD web app with scaffold-style views
wheels generate controller Products --crud
# Result: 7 actions + 5 views
# Use for: Traditional web applications with HTML forms
# REST API without views
wheels generate controller Products --api
# Result: 5 actions + 0 views
# Use for: Mobile apps, single-page apps, third-party integrationsUnderstanding how --actions overrides --crud:
# Example 1: CRUD flag generates 7 actions and 5 views
wheels generate controller Products --crud
# Result:
# Actions: index, show, new, create, edit, update, delete (7 actions)
# Views: index.cfm, show.cfm, new.cfm, edit.cfm, _form.cfm (5 views)
# Example 2: Actions flag OVERRIDES crud (HIGHEST PRIORITY)
wheels generate controller Products --crud --actions=index
# Result:
# Actions: index (only this action)
# Views: index.cfm (only this view)
# Note: --actions has HIGHEST PRIORITY, --crud is ignored
# Example 3: Multiple custom actions override crud
wheels generate controller Products --crud --actions=dashboard,reports
# Result:
# Actions: dashboard, reports (only these actions)
# Views: dashboard.cfm, reports.cfm (only these views)
# Note: --actions overrides --crud completelyCommon Mistake: Using --action (singular) instead of --actions (plural)
# WRONG - will not work
wheels generate controller Products --action=index
# CORRECT - use plural --actions
wheels generate controller Products --actions=index# Positional (recommended)
wheels generate controller Products
# OR named
wheels g controller name=ProductsCreates:
app/controllers/Products.cfc with index action
app/views/products/index.cfm view
# Positional + flag (recommended)
wheels generate controller Products --actions=dashboard,reports,export
# OR all named
wheels g controller name=Products actions=dashboard,reports,exportCreates:
app/controllers/Products.cfc with 3 custom actions
app/views/products/dashboard.cfm
app/views/products/reports.cfm
app/views/products/export.cfm
# Positional + flag (recommended)
wheels generate controller Products --crud
# OR all named
wheels g controller name=Products crud=trueCreates:
app/controllers/Products.cfc with 7 CRUD actions
5 scaffold-style views: index.cfm, show.cfm, new.cfm, edit.cfm, _form.cfm
Note: The _form.cfm partial is shared between new and edit views, just like in wheels generate scaffold.
# Positional + flag (recommended)
wheels generate controller Orders --api
# OR all named
wheels g controller name=Orders api=trueCreates:
app/controllers/Orders.cfc with 5 API actions
No views (API controllers don't need views)
# Positional + flag (recommended)
wheels generate controller Products --crud --noViews
# OR all named
wheels g controller name=Products rest=true noViews=trueCreates:
app/controllers/Products.cfc with 7 RESTful actions
No views (explicitly skipped with --noViews)
# Positional + flags (recommended)
wheels generate controller Reports --actions=dashboard,export --noViews
# OR all named
wheels g controller name=Reports actions=dashboard,export noViews=trueCreates:
app/controllers/Reports.cfc with 2 custom actions
No views (explicitly skipped)
component extends="Controller" {
/**
* Controller config settings
**/
function config() {
}
/**
* index action
*/
function index() {
// TODO: Implement index action
}
}/**
* Handles user management operations
*/
component extends="Controller" {
/**
* Controller config settings
**/
function config() {
}
/**
* index action
*/
function index() {
// TODO: Implement index action
}
}component extends="Controller" {
function config() {
verifies(except="index,new,create", params="key", paramsTypes="integer", handler="objectNotFound");
}
/**
* View all Products
**/
function index() {
products=model("product").findAll();
}
/**
* View Product
**/
function show() {
product=model("product").findByKey(params.key);
}
/**
* Add New Product
**/
function new() {
product=model("product").new();
}
/**
* Create Product
**/
function create() {
product=model("product").create(params.product);
if(product.hasErrors()){
renderView(action="new");
} else {
redirectTo(action="index", success="Product successfully created");
}
}
/**
* Edit Product
**/
function edit() {
product=model("product").findByKey(params.key);
}
/**
* Update Product
**/
function update() {
product=model("product").findByKey(params.key);
if(product.update(params.product)){
redirectTo(action="index", success="Product successfully updated");
} else {
renderView(action="edit");
}
}
/**
* Delete Product
**/
function delete() {
product=model("product").deleteByKey(params.key);
redirectTo(action="index", success="Product successfully deleted");
}
/**
* Redirect away if verifies fails, or if an object can't be found
**/
function objectNotFound() {
redirectTo(action="index", error="That Product wasn't found");
}
}/**
* API endpoint for order processing
*/
component extends="wheels.Controller" {
function init() {
provides("json");
filters(through="setJsonResponse");
}
/**
* GET /orders
* Returns a list of all orders
*/
function index() {
local.orders = model("order").findAll();
renderWith(data={ orders=local.orders });
}
/**
* GET /orders/:key
* Returns a specific order by ID
*/
function show() {
local.order = model("order").findByKey(params.key);
if (IsObject(local.order)) {
renderWith(data={ order=local.order });
} else {
renderWith(data={ error="Record not found" }, status=404);
}
}
/**
* POST /orders
* Creates a new order
*/
function create() {
local.order = model("order").new(params.order);
if (local.order.save()) {
renderWith(data={ order=local.order }, status=201);
} else {
renderWith(data={ error="Validation failed", errors=local.order.allErrors() }, status=422);
}
}
/**
* PUT /orders/:key
* Updates an existing order
*/
function update() {
local.order = model("order").findByKey(params.key);
if (IsObject(local.order)) {
local.order.update(params.order);
if (local.order.hasErrors()) {
renderWith(data={ error="Validation failed", errors=local.order.allErrors() }, status=422);
} else {
renderWith(data={ order=local.order });
}
} else {
renderWith(data={ error="Record not found" }, status=404);
}
}
/**
* DELETE /orders/:key
* Deletes a order
*/
function delete() {
local.order = model("order").findByKey(params.key);
if (IsObject(local.order)) {
local.order.delete();
renderWith(data={}, status=204);
} else {
renderWith(data={ error="Record not found" }, status=404);
}
}
/**
* Set Response to JSON
*/
private function setJsonResponse() {
params.format = "json";
}
}Views are automatically generated for non-API controllers:
<h1>Products</h1>
<p>#linkTo(text="New Product", action="new")#</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<cfloop query="products">
<tr>
<td>#products.name#</td>
<td>
#linkTo(text="Show", action="show", key=products.id)#
#linkTo(text="Edit", action="edit", key=products.id)#
#linkTo(text="Delete", action="delete", key=products.id, method="delete", confirm="Are you sure?")#
</td>
</tr>
</cfloop>
</tbody>
</table>Controller names: PascalCase, typically plural (Products, Users)
Action names: camelCase (index, show, createProduct)
File locations:
Controllers: /controllers/
Nested: /controllers/admin/Products.cfc
Views: /views/{controller}/
Add routes in /config/routes.cfm:
<cfscript>
mapper()
.get(name="products", to="products##index")
.get(name="product", to="products##show")
.post(name="products", to="products##create")
.wildcard()
.end();
</cfscript><cfscript>
mapper()
.resources("products")
.wildcard()
.end();
</cfscript>Generate tests alongside controllers:
wheels generate controller name=products --crud
wheels generate test controller name=productsUse plural names for resource controllers
Keep controllers focused on single resources
Use --crud for standard web app CRUD operations (with views and forms)
Use --api for API endpoints (JSON/XML, no views)
Use --actions when you need custom actions (HIGHEST PRIORITY - overrides --crud)
Implement proper error handling
Add authentication in config() method
Use filters for common functionality
function config() {
filters(through="authenticate", except="index,show");
}
private function authenticate() {
if (!session.isLoggedIn) {
redirectTo(controller="sessions", action="new");
}
}function index() {
products = model("Product").findAll(
page=params.page ?: 1,
perPage=25,
order="createdAt DESC"
);
}function index() {
if (StructKeyExists(params, "q")) {
products = model("Product").findAll(
where="name LIKE :search OR description LIKE :search",
params={search: "%#params.q#%"}
);
} else {
products = model("Product").findAll();
}
}wheels generate model - Generate models
wheels generate view - Generate views
wheels scaffold - Generate complete CRUD
wheels generate test - Generate controller tests
Comprehensive guide to testing in Wheels applications using the CLI.
Wheels CLI provides robust testing capabilities through TestBox integration, offering:
Unit and integration testing
BDD-style test writing
Watch mode for continuous testing
Code coverage reporting
Parallel test execution
Docker-based testing across multiple engines and databases
/tests/
├── Application.cfc # Test suite configuration
├── specs/ # Test specifications (alternative to folders below)
├── unit/ # Unit tests
│ ├── models/ # Model tests
│ ├── controllers/ # Controller tests
│ └── services/ # Service tests
├── integration/ # Integration tests
├── fixtures/ # Test data files
└── helpers/ # Test utilitiesFollow these conventions:
Model tests: UserTest.cfc or UserSpec.cfc
Controller tests: UsersControllerTest.cfc
Integration tests: UserFlowTest.cfc
// tests/unit/models/UserTest.cfc
component extends="wheels.Testbox" {
function run() {
describe("User Model", function() {
beforeEach(function() {
// Setup before each test
variables.user = model("User").new();
});
afterEach(function() {
// Cleanup after each test
});
it("validates email presence", function() {
variables.user.email = "";
expect(variables.user.valid()).toBeFalse();
expect(variables.user.errors).toHaveKey("email");
});
it("validates email format", function() {
variables.user.email = "invalid-email";
expect(variables.user.valid()).toBeFalse();
expect(variables.user.errors.email).toInclude("valid email");
});
});
}
}component extends="wheels.Testbox" {
function run() {
describe("Product Model", function() {
describe("Validations", function() {
it("requires a name", function() {
var product = model("Product").new();
expect(product.valid()).toBeFalse();
expect(product.errors).toHaveKey("name");
});
it("requires price to be positive", function() {
var product = model("Product").new(
name = "Test Product",
price = -10
);
expect(product.valid()).toBeFalse();
expect(product.errors.price).toInclude("greater than 0");
});
});
describe("Associations", function() {
it("has many reviews", function() {
var product = model("Product").findOne();
expect(product).toHaveKey("reviews");
expect(product.reviews()).toBeQuery();
});
});
describe("Scopes", function() {
it("filters active products", function() {
// Create test data
model("Product").create(name="Active", active=true);
model("Product").create(name="Inactive", active=false);
var activeProducts = model("Product").active().findAll();
expect(activeProducts.recordCount).toBe(1);
expect(activeProducts.name).toBe("Active");
});
});
});
}
}component extends="wheels.Testbox" {
function beforeAll() {
// Setup test request context
variables.mockController = prepareMock(createObject("component", "controllers.Products"));
}
function run() {
describe("Products Controller", function() {
describe("index action", function() {
it("returns all products", function() {
// Setup
var products = queryNew("id,name", "integer,varchar", [
[1, "Product 1"],
[2, "Product 2"]
]);
mockController.$("model").$args("Product").$returns(
mockModel.$("findAll").$returns(products)
);
// Execute
mockController.index();
// Assert
expect(mockController.products).toBe(products);
expect(mockController.products.recordCount).toBe(2);
});
});
describe("create action", function() {
it("creates product with valid data", function() {
// Setup params
mockController.params = {
product: {
name: "New Product",
price: 99.99
}
};
// Mock successful save
var mockProduct = createEmptyMock("models.Product");
mockProduct.$("save").$returns(true);
mockProduct.$("id", 123);
mockController.$("model").$args("Product").$returns(
createMock("models.Product").$("new").$returns(mockProduct)
);
// Execute
mockController.create();
// Assert
expect(mockController.flashMessages.success).toInclude("created successfully");
expect(mockController.redirectTo.action).toBe("show");
expect(mockController.redirectTo.key).toBe(123);
});
});
});
}
}component extends="wheels.Testbox" {
function run() {
describe("User Registration Flow", function() {
it("allows new user to register", function() {
// Visit registration page
var event = execute(event="users.new", renderResults=true);
expect(event.getRenderedContent()).toInclude("Register");
// Submit registration form
var event = execute(
event = "users.create",
eventArguments = {
user: {
email: "[email protected]",
password: "SecurePass123!",
passwordConfirmation: "SecurePass123!"
}
}
);
// Verify user created
var user = model("User").findOne(where="email='[email protected]'");
expect(user).toBeObject();
// Verify logged in
expect(session.userId).toBe(user.id);
// Verify redirect
expect(event.getValue("relocate_URI")).toBe("/dashboard");
});
});
}
}// tests/helpers/Factories.cfc
component {
function createUser(struct overrides = {}) {
var defaults = {
email: "user#createUUID()#@test.com",
password: "password123",
firstName: "Test",
lastName: "User"
};
defaults.append(arguments.overrides);
return model("User").create(defaults);
}
function createProduct(struct overrides = {}) {
var defaults = {
name: "Product #createUUID()#",
price: randRange(10, 100),
stock: randRange(0, 50)
};
defaults.append(arguments.overrides);
return model("Product").create(defaults);
}
}// tests/helpers/TestDatabase.cfc
component {
function setUp() {
// Start transaction
transaction action="begin";
}
function tearDown() {
// Rollback transaction
transaction action="rollback";
}
function clean() {
// Clean specific tables
queryExecute("DELETE FROM users WHERE email LIKE '%@test.com'");
queryExecute("DELETE FROM products WHERE name LIKE 'Test%'");
}
function loadFixtures(required string name) {
var fixtures = deserializeJSON(
fileRead("/tests/fixtures/#arguments.name#.json")
);
for (var record in fixtures) {
queryExecute(
"INSERT INTO #arguments.name# (#structKeyList(record)#)
VALUES (#structKeyList(record, ':')#)",
record
);
}
}
}# Run all tests
wheels test run
# Run specific test file
wheels test run tests/unit/models/UserTest.cfc
# Run tests in directory
wheels test run tests/unit/models/
# Run with specific reporter
wheels test run --reporter=json
wheels test run --reporter=junit --outputFile=results.xml# Watch for changes and rerun tests
wheels test run --watch
# Watch specific directory
wheels test run tests/models --watch
# Watch with custom debounce
wheels test run --watch --watchDelay=1000# Run by test bundles
wheels test run --bundles=models,controllers
# Run by labels
wheels test run --labels=critical
# Run by test name pattern
wheels test run --filter="user"
# Exclude patterns
wheels test run --excludes="slow,integration"# Generate HTML coverage report
wheels test coverage
# With custom output directory
wheels test coverage --outputDir=coverage-reports
# Include only specific paths
wheels test coverage --includes="models/,controllers/"In tests/Application.cfc:
this.coverage = {
enabled: true,
includes: ["models", "controllers"],
excludes: ["tests", "wheels"],
outputDir: expandPath("/tests/coverage/"),
reportFormats: ["html", "json"]
};// tests/Application.cfc
component {
this.name = "WheelsTestSuite" & Hash(GetCurrentTemplatePath());
// Test datasource
this.datasources["test"] = {
url: "jdbc:h2:mem:test;MODE=MySQL",
driver: "org.h2.Driver"
};
this.datasource = "test";
// TestBox settings
this.testbox = {
bundles: ["tests"],
recurse: true,
reporter: "simple",
reportpath: "/tests/results",
runner: ["tests/runner.cfm"],
labels: [],
options: {}
};
}# Set test environment
export WHEELS_ENV=testing
# Set test datasource
export WHEELS_TEST_DATASOURCE=myapp_test
# Enable verbose output
export TESTBOX_VERBOSE=truetests/
├── unit/ # Fast, isolated tests
│ ├── models/ # One file per model
│ └── services/ # Service layer tests
├── integration/ # Tests with dependencies
└── e2e/ # End-to-end testsdescribe("User Model", function() {
beforeEach(function() {
// Fresh instance for each test
variables.user = model("User").new();
// Clear caches
application.wheels.cache.queries = {};
});
afterEach(function() {
// Clean up test data
if (isDefined("variables.user.id")) {
variables.user.delete();
}
});
});// Good: Descriptive test names
it("validates email format with standard RFC 5322 regex", function() {
// test implementation
});
it("prevents duplicate email addresses case-insensitively", function() {
// test implementation
});
// Bad: Vague test names
it("works", function() {
// test implementation
});it("calculates order total with tax", function() {
// Arrange
var order = createOrder();
var item1 = createOrderItem(price: 100, quantity: 2);
var item2 = createOrderItem(price: 50, quantity: 1);
order.addItem(item1);
order.addItem(item2);
// Act
var total = order.calculateTotal(taxRate: 0.08);
// Assert
expect(total).toBe(270); // (200 + 50) * 1.08
});name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup CommandBox
uses: Ortus-Solutions/[email protected]
- name: Install dependencies
run: box install
- name: Run tests
run: |
box server start
wheels test run --reporter=junit --outputFile=test-results.xml
- name: Upload test results
uses: actions/upload-artifact@v4
with:
name: test-results
path: test-results.xml
- name: Generate coverage
run: wheels test coverage
- name: Upload coverage
uses: codecov/codecov-action@v1#!/bin/bash
# .git/hooks/pre-commit
echo "Running tests..."
wheels test run --labels=unit
if [ $? -ne 0 ]; then
echo "Tests failed! Commit aborted."
exit 1
fi
echo "Running linter..."
wheels analyze code
if [ $? -ne 0 ]; then
echo "Code quality check failed!"
exit 1
fiit("processes data correctly", function() {
var result = processData(testData);
// Debug output
debug(result);
writeDump(var=result, abort=false);
// Conditional debugging
if (request.debug ?: false) {
writeOutput("Result: #serializeJSON(result)#");
}
expect(result.status).toBe("success");
});# Run specific test with debugging
wheels test debug tests/unit/models/UserTest.cfc
# Enable verbose mode
wheels test run --verbose
# Show SQL queries
wheels test run --showSQLdescribe("Performance", function() {
it("handles 1000 concurrent users", function() {
var threads = [];
for (var i = 1; i <= 1000; i++) {
arrayAppend(threads, function() {
var result = model("Product").findAll();
return result.recordCount;
});
}
var start = getTickCount();
var results = parallel(threads);
var duration = getTickCount() - start;
expect(duration).toBeLT(5000); // Less than 5 seconds
expect(arrayLen(results)).toBe(1000);
});
});it("tests private method", function() {
var user = model("User").new();
// Use makePublic() for testing
makePublic(user, "privateMethod");
var result = user.privateMethod();
expect(result).toBe("expected");
});it("sends email on user creation", function() {
// Mock email service
var mockMailer = createEmptyMock("services.Mailer");
mockMailer.$("send").$returns(true);
// Inject mock
var user = model("User").new();
user.$property("mailer", mockMailer);
// Test
user.save();
// Verify
expect(mockMailer.$times("send")).toBe(1);
expect(mockMailer.$callLog().send[1].to).toBe(user.email);
});Wheels provides a comprehensive Docker environment for testing across multiple CFML engines and databases.
# Start the TestUI and all test containers
docker compose --profile all up -d
# Access the TestUI
open http://localhost:3000The modern TestUI provides:
Visual Test Runner: Run and monitor tests in real-time
Container Management: Start/stop containers directly from the UI
Multi-Engine Support: Test on Lucee 5/6 and Adobe ColdFusion 2018/2021/2023
Multi-Database Support: MySQL, PostgreSQL, SQL Server, H2, and Oracle
Pre-flight Checks: Ensures all services are running before tests
Test History: Track test results over time
The TestUI includes an API server that allows you to:
Click on any stopped engine or database to start it
Monitor container health and status
View real-time logs
No terminal required for basic operations
Use profiles to start specific combinations:
# Just the UI
docker compose --profile ui up -d
# Quick test setup (Lucee 5 + MySQL)
docker compose --profile quick-test up -d
# All Lucee engines
docker compose --profile lucee up -d
# All Adobe engines
docker compose --profile adobe up -d
# All databases
docker compose --profile db up -d# Using the CLI inside a container
docker exec -it wheels-lucee5-1 wheels test run
# Direct URL access
curl http://localhost:60005/wheels/core/tests?format=json&db=mysqlTest against different databases by using the db parameter:
# MySQL
wheels test run --db=mysql
# PostgreSQL
wheels test run --db=postgres
# SQL Server
wheels test run --db=sqlserver
# H2 (Lucee only)
wheels test run --db=h2
# Oracle
wheels test run --db=oraclewheels test run - Test execution command
wheels test coverage - Coverage generation
wheels generate test - Generate test files
TestBox Documentation - Complete TestBox guide
Docker Testing Guide - Detailed Docker testing documentation
Setup a new environment configuration for your Wheels application with comprehensive database, template, and configuration options.
The wheels env setup command creates and configures new environments for your Wheels application. It generates:
Environment-specific .env.[environment] files with database and server settings using generic DB_* variable names
Configuration files at config/[environment]/settings.cfm with Wheels settings
Template-specific files (Docker, Vagrant) if requested
Server.json updates for environment-specific configurations
Updates config/environment.cfm with the current environment setting
The command supports copying configurations from existing environments and allows full customization of database types, templates, and framework settings.
When setting up environments with non-H2 databases (MySQL, PostgreSQL, MSSQL, Oracle), if database credentials are not provided as command arguments, the command will interactively prompt you to enter:
Database host (default: localhost)
Database port (database-specific defaults)
Database username (default: varies by database type)
Database password (masked input)
Oracle SID (Oracle only)
This ensures you never use incorrect default credentials that could cause authentication failures.
Note: Always use named parameter syntax: environment=name to avoid parameter conflicts.
.env.[environment])Note: All database types now use generic DB_* variable names for portability and consistency.
For H2 database:
For MySQL database:
For Microsoft SQL Server:
config/[environment]/settings.cfm)--template=docker)Creates:
docker-compose.[environment].yml
Dockerfile (if not exists)
--template=vagrant)Creates:
Vagrantfile.[environment]
vagrant/provision-[environment].sh
Use Case: Development, testing, quick prototyping
Connection: No network port required (embedded)
Database Path: ./db/[database_name]
Default Credentials: username=sa, password=(empty)
Use Case: Production, staging environments
Default Port: 3306
Default Credentials: username=wheels, password=wheels_password
Use Case: Production, complex applications
Default Port: 5432
Default Credentials: username=wheels, password=wheels_password
Use Case: Enterprise environments
Default Port: 1433
Default Credentials: username=sa, password=Wheels_Pass123!
When using --base, the command copies configuration from an existing environment:
Database host, username, and password
Server configuration (port, CF engine)
Custom environment variables
Environment name
Database name (becomes wheels_[new_environment])
Database type, driver, and port (based on --dbtype)
Reload password (becomes wheels[new_environment])
development or dev - Local development
testing or test - Automated testing
staging - Pre-production testing
production or prod - Live environment
qa - Quality assurance
demo - Client demonstrations
Best for traditional server deployments:
Creates containerized environment:
Generated docker-compose.docker-dev.yml:
Creates VM-based environment:
The command provides environment-specific next steps:
Switch to environment: wheels env switch [environment]
Start server: box server start
Access application at: http://localhost:8080
Start Docker environment: docker-compose -f docker-compose.[environment].yml up
Access application at: http://localhost:8080
Stop environment: docker-compose -f docker-compose.[environment].yml down
Start Vagrant VM: vagrant up
Access application at: http://localhost:8080 or http://192.168.56.10:8080
SSH into VM: vagrant ssh
Stop VM: vagrant halt
Update config/environment.cfm to automatically detect environments:
Load .env.[environment] files in Application.cfc:
After creating an environment, validate the setup:
Environment already exists:
Base environment not found:
Database connection issues:
Verify database credentials in .env.[environment]
Check database server is running
Validate port configuration
Permission issues:
Ensure write permissions for config directory
Check file system permissions
Use consistent, descriptive names
Avoid spaces and special characters
Follow team conventions
Use separate databases per environment
Document database naming conventions
Implement proper backup strategies
Use strong, unique reload passwords
Never commit sensitive credentials
Use environment variables for secrets
Start with a solid base environment
Document environment purposes
Test configurations thoroughly
local: Traditional server deployments
docker: Containerized applications
vagrant: Isolated development VMs
Check syntax in generated settings.cfm
Verify file permissions in config directory
Review environment variable formats
Verify database server is running
Check credentials in .env.[environment]
Test connection manually
Review port configurations (remember H2 has no port)
Check config/environment.cfm logic
Verify server variables
Test detection rules manually
Enable debugging for detailed information
Disable caching for hot reload
Use H2 for fast setup and teardown
Disable debugging for performance
Enable all caching options
Use optimized database configurations
- List all environments
- Switch between environments
- Show current environment
wheels env setup environment=<name> [options]environment
Environment name (e.g., development, staging, production, testing)
Yes
--template
Deployment template type
local
local, docker, vagrant
--dbtype
Database type
h2
h2, mysql, postgres, mssql, oracle
--database
Custom database name
wheels_[environment]
Any valid database name
--datasource
ColdFusion datasource name
wheels_[environment]
Any valid datasource name
--host
Database host
localhost (or prompted)
Any valid hostname/IP
--port
Database port
Database-specific (or prompted)
Valid port number
--username
Database username
Database-specific (or prompted)
Any valid username
--password
Database password
(prompted if not provided)
Any string
--sid
Oracle SID (Oracle only)
ORCL (or prompted)
Any valid SID
--base
Base environment to copy from
(none)
Any existing environment name
--force
Overwrite existing environment
false
true, false
--debug
Enable debug settings
false
true, false
--cache
Enable cache settings
false
true, false
--reloadPassword
Custom reload password
wheels[environment]
Any string
--skipDatabase
Skip database creation
false
true, false
--help
Show detailed help information
false
-
# Create development environment with H2 database (default)
wheels env setup environment=development
# Create staging environment with MySQL (will prompt for credentials)
wheels env setup environment=staging --dbtype=mysql
# Create production environment with PostgreSQL and caching enabled
wheels env setup environment=production --dbtype=postgres --cache=true --debug=false
# Create environment with explicit credentials (no prompting)
wheels env setup environment=test --dbtype=mssql --host=localhost --port=1433 --username=sa --password=MyPassword123!# Running without credentials prompts interactively
wheels env setup environment=production --dbtype=mssql
# Output:
# Setting up production environment...
#
# Database credentials not provided for mssql database
# Would you like to enter database credentials now? [y/n] y
#
# Please provide database connection details:
#
# Database Host [localhost]: localhost
# Database Port [1433]: 1433
# Database Username [sa]: sa
# Database Password: ************
#
# Database credentials captured successfully!# Copy settings from development environment but use PostgreSQL
wheels env setup environment=testing --base=development --dbtype=postgres
# Create staging environment based on production settings
wheels env setup environment=staging --base=production --database=staging_db
# Create QA environment with custom settings
wheels env setup environment=qa --base=development --dbtype=mysql --debug=true --cache=false# Setup with custom database and datasource names
wheels env setup environment=integration --dbtype=mysql --database=wheels_integration_db --datasource=integration_ds
# Setup with specific reload password and debugging
wheels env setup environment=dev --debug=true --reloadPassword=mypassword123 --force
# Docker environment setup
wheels env setup environment=docker-dev --template=docker --dbtype=postgres --database=wheels_docker# Local development (default)
wheels env setup environment=dev --template=local --dbtype=h2
# Docker containerized environment
wheels env setup environment=docker-staging --template=docker --dbtype=mysql
# Vagrant VM environment
wheels env setup environment=vm-test --template=vagrant --dbtype=postgres## Wheels Environment: development
## Generated on: 2025-01-18 12:30:00
## Application Settings
WHEELS_ENV=development
WHEELS_RELOAD_PASSWORD=wheelsdevelopment
## Database Settings
DB_TYPE=h2
DB_HOST=
DB_PORT=
DB_DATABASE=./db/wheels_development
DB_USER=sa
DB_PASSWORD=
DB_DATASOURCE=wheels_development
## Server Settings
SERVER_PORT=8080
SERVER_CFENGINE=lucee5## Wheels Environment: production
## Generated on: 2025-01-18 12:30:00
## Application Settings
WHEELS_ENV=production
WHEELS_RELOAD_PASSWORD=wheelsproduction
## Database Settings
DB_TYPE=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=wheels_production
DB_USER=wheels
DB_PASSWORD=wheels_password
DB_DATASOURCE=wheels_production
## Server Settings
SERVER_PORT=8080
SERVER_CFENGINE=lucee5## Wheels Environment: staging
## Generated on: 2025-01-18 12:30:00
## Application Settings
WHEELS_ENV=staging
WHEELS_RELOAD_PASSWORD=wheelsstaging
## Database Settings
DB_TYPE=mssql
DB_HOST=localhost
DB_PORT=1433
DB_DATABASE=wheels_staging
DB_USER=sa
DB_PASSWORD=MySecurePassword123!
DB_DATASOURCE=wheels_staging
## Server Settings
SERVER_PORT=8080
SERVER_CFENGINE=lucee5<cfscript>
// Environment: production
// Generated: 2025-01-18 12:30:00
// Debug Mode: Disabled
// Cache Mode: Enabled
// Database settings
set(dataSourceName="wheels_production");
// Environment settings
set(environment="production");
// Debug settings - controlled by debug argument
set(showDebugInformation=false);
set(showErrorInformation=false);
// Caching settings - controlled by cache argument
set(cacheFileChecking=true);
set(cacheImages=true);
set(cacheModelInitialization=true);
set(cacheControllerInitialization=true);
set(cacheRoutes=true);
set(cacheActions=true);
set(cachePages=true);
set(cachePartials=true);
set(cacheQueries=true);
// Security
set(reloadPassword="wheelsproduction");
// URLs
set(urlRewriting="partial");
// Environment-specific settings
set(sendEmailOnError=true);
set(errorEmailAddress="[email protected]");
</cfscript>wheels env setup environment=dev --dbtype=h2 --database=my_dev_dbwheels env setup environment=prod --dbtype=mysql --database=wheels_productionwheels env setup environment=staging --dbtype=postgreswheels env setup environment=enterprise --dbtype=mssql# Copy from production but use H2 for testing
wheels env setup environment=test --base=production --dbtype=h2
# Copy from development but use different database name
wheels env setup environment=feature-branch --base=development --database=feature_test_dbwheels env setup environment=feature-auth --base=development
wheels env setup environment=performance-test --base=production --cache=false
wheels env setup environment=client-demo --base=stagingwheels env setup environment=prod --template=local --dbtype=mysqlwheels env setup environment=docker-dev --template=docker --dbtype=postgresversion: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- WHEELS_ENV=docker-dev
- DB_TYPE=postgres
- DB_HOST=db
- DB_PORT=5432
- DB_NAME=wheels
- DB_USER=wheels
- DB_PASSWORD=wheels_password
volumes:
- .:/app
depends_on:
- db
db:
image: postgres:14
ports:
- "5432:5432"
environment:
POSTGRES_DB=wheels
POSTGRES_USER=wheels
POSTGRES_PASSWORD=wheels_password
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:wheels env setup environment=vm-test --template=vagrant --dbtype=mysql<cfscript>
// Auto-detect environment based on server name
if (cgi.server_name contains "staging") {
this.env = "staging";
} else if (cgi.server_name contains "test") {
this.env = "testing";
} else if (cgi.server_name contains "demo") {
this.env = "demo";
} else if (cgi.server_name contains "localhost") {
this.env = "development";
} else {
this.env = "production";
}
</cfscript><cfscript>
component extends="wheels.Controller" {
function config() {
// Load environment-specific variables
var envFile = expandPath(".env." & get("environment"));
if (fileExists(envFile)) {
loadEnvironmentFile(envFile);
}
}
private function loadEnvironmentFile(filePath) {
var lines = fileRead(arguments.filePath).listToArray(chr(10));
for (var line in lines) {
if (len(trim(line)) && !line.startsWith("##")) {
var parts = line.listToArray("=");
if (arrayLen(parts) >= 2) {
var key = trim(parts[1]);
var value = trim(parts[2]);
// Set as system property or use in configuration
set(lCase(key), value);
}
}
}
}
}
</cfscript># List all environments to verify creation
wheels env list
# Switch to the new environment
wheels env switch [environment]
# Test database connection
wheels test run --type=core
# Check configuration
wheels env currentwheels env setup environment=staging --force# Check available environments
wheels env list
# Use correct base environment name
wheels env setup environment=test --base=development# Create testing environment for CI
wheels env setup environment=ci-test --base=production --dbtype=h2 --debug=false
# Create staging environment for deployment testing
wheels env setup environment=staging --base=production --dbtype=mysql --cache=true# Create feature-specific environment
wheels env setup environment=feature-login --base=development --database=login_feature_db
# Create A/B testing environments
wheels env setup environment=variant-a --base=production --database=variant_a_db
wheels env setup environment=variant-b --base=production --database=variant_b_dbThis command works correctly without options (parameters). Option support is under development and will be available soon.
Generate code snippets and boilerplate code for common patterns.
wheels generate snippets [pattern] [options]
wheels g snippets [pattern] [options]The wheels generate snippets command creates code snippets for common Wheels patterns and best practices. It provides ready-to-use code blocks that can be customized for your specific needs, helping you implement standard patterns quickly and consistently.
pattern
Snippet pattern to generate
Shows available patterns
--list
List all available snippets
false
--category
Filter by category
All categories
--output
Output format (console, file, clipboard)
console
--path
Output file path (required when output=file)
--customize
Interactive customization
false
--create
Create custom snippet template
--force
Overwrite existing files
false
wheels generate snippets --listOutput:
Available Snippets:
━━━━━━━━━━━━━━━━━━━
Authentication:
- login-form Login form with remember me
- auth-filter Authentication filter
- password-reset Password reset flow
- user-registration User registration with validation
Model Patterns:
- soft-delete Soft delete implementation
- audit-trail Audit trail with timestamps
- sluggable URL-friendly slugs
- versionable Version tracking
- searchable Full-text search
Controller Patterns:
- crud-actions Complete CRUD actions
- api-controller JSON API controller
- nested-resource Nested resource controller
- admin-controller Admin area controller
View Patterns:
- form-with-errors Form with error handling
- pagination-links Pagination navigation
- search-form Search form with filters
- ajax-form AJAX form submission
Database:
- migration-indexes Common index patterns
- seed-data Database seeding
- constraints Foreign key constraintswheels generate snippets login-formGenerates:
<!--- views/sessions/new.cfm --->
<h1>Login</h1>
#errorMessagesFor("user")#
#startFormTag(action="create", class="login-form")#
<div class="form-group">
#textField(
objectName="user",
property="email",
label="Email",
class="form-control",
placeholder="[email protected]",
required=true,
autofocus=true
)#
</div>
<div class="form-group">
#passwordField(
objectName="user",
property="password",
label="Password",
class="form-control",
required=true
)#
</div>
<div class="form-group">
#checkBox(
objectName="user",
property="rememberMe",
label="Remember me",
value="1"
)#
</div>
<div class="form-group">
#submitTag(value="Login", class="btn btn-primary")#
#linkTo(text="Forgot password?", route="forgotPassword", class="btn btn-link")#
</div>
#endFormTag()#
<!--- controllers/Sessions.cfc --->
component extends="Controller" {
function new() {
user = model("User").new();
}
function create() {
user = model("User").findOne(where="email='#params.user.email#'");
if (IsObject(user) && user.authenticate(params.user.password)) {
session.userId = user.id;
if (params.user.rememberMe == 1) {
cookie.rememberToken = user.generateRememberToken();
cookie.userId = encrypt(user.id, application.encryptionKey);
}
flashInsert(success="Welcome back, #user.firstName#!");
redirectTo(route="dashboard");
} else {
user = model("User").new(email=params.user.email);
flashInsert(error="Invalid email or password.");
renderView(action="new");
}
}
function delete() {
StructDelete(session, "userId");
StructDelete(cookie, "rememberToken");
StructDelete(cookie, "userId");
flashInsert(success="You have been logged out.");
redirectTo(route="home");
}
}wheels generate snippets auth-filterGenerates:
// In Controller.cfc or specific controller
function init() {
filters(through="authenticate", except="new,create");
filters(through="rememberUser", only="new");
}
private function authenticate() {
if (!isLoggedIn()) {
storeLocation();
flashInsert(notice="Please log in to continue.");
redirectTo(route="login");
}
}
private function isLoggedIn() {
return StructKeyExists(session, "userId") && session.userId > 0;
}
private function currentUser() {
if (!StructKeyExists(request, "currentUser")) {
if (isLoggedIn()) {
request.currentUser = model("User").findByKey(session.userId);
} else {
request.currentUser = false;
}
}
return request.currentUser;
}
private function rememberUser() {
if (!isLoggedIn() && StructKeyExists(cookie, "rememberToken")) {
user = model("User").findOne(
where="rememberToken='#cookie.rememberToken#' AND rememberTokenExpiresAt > NOW()"
);
if (IsObject(user)) {
session.userId = user.id;
user.updateRememberToken();
}
}
}
private function storeLocation() {
if (request.method == "GET") {
session.returnTo = request.cgi.path_info;
if (Len(request.cgi.query_string)) {
session.returnTo &= "?" & request.cgi.query_string;
}
}
}
private function redirectBackOrTo(required string route) {
if (StructKeyExists(session, "returnTo")) {
local.returnTo = session.returnTo;
StructDelete(session, "returnTo");
redirectTo(location=local.returnTo);
} else {
redirectTo(route=arguments.route);
}
}wheels generate snippets soft-deleteGenerates:
// In Model init()
property(name="deletedAt", sql="deleted_at");
// Soft delete callbacks
beforeDelete("softDelete");
afterFind("excludeDeleted");
// Default scope
function excludeDeleted() {
if (!StructKeyExists(arguments, "includeSoftDeleted") || !arguments.includeSoftDeleted) {
if (StructKeyExists(this, "deletedAt") && !IsNull(this.deletedAt)) {
return false; // Exclude from results
}
}
}
// Soft delete implementation
private function softDelete() {
this.deletedAt = Now();
this.save(validate=false, callbacks=false);
return false; // Prevent actual deletion
}
// Scopes
function active() {
return this.findAll(where="deleted_at IS NULL", argumentCollection=arguments);
}
function deleted() {
return this.findAll(where="deleted_at IS NOT NULL", argumentCollection=arguments);
}
function withDeleted() {
return this.findAll(includeSoftDeleted=true, argumentCollection=arguments);
}
// Restore method
function restore() {
this.deletedAt = "";
return this.save(validate=false);
}
// Permanent delete
function forceDelete() {
return this.delete(callbacks=false);
}wheels generate snippets audit-trail --customizeInteractive customization:
? Include user tracking? (Y/n) › Y
? Track IP address? (y/N) › Y
? Track changes in JSON? (Y/n) › YGenerates:
// models/AuditLog.cfc
component extends="Model" {
function init() {
belongsTo("user");
property(name="modelName", sql="model_name");
property(name="recordId", sql="record_id");
property(name="action", sql="action");
property(name="changes", sql="changes");
property(name="userId", sql="user_id");
property(name="ipAddress", sql="ip_address");
property(name="userAgent", sql="user_agent");
validatesPresenceOf("modelName,recordId,action");
}
}
// In audited model
function init() {
afterCreate("logCreate");
afterUpdate("logUpdate");
afterDelete("logDelete");
}
private function logCreate() {
createAuditLog("create", this.properties());
}
private function logUpdate() {
if (hasChanged()) {
createAuditLog("update", this.changedProperties());
}
}
private function logDelete() {
createAuditLog("delete", {id: this.id});
}
private function createAuditLog(required string action, required struct data) {
model("AuditLog").create({
modelName: ListLast(GetMetaData(this).name, "."),
recordId: this.id,
action: arguments.action,
changes: SerializeJSON(arguments.data),
userId: request.currentUser.id ?: "",
ipAddress: request.remoteAddress,
userAgent: request.userAgent
});
}
private function changedProperties() {
local.changes = {};
local.properties = this.properties();
for (local.key in local.properties) {
if (hasChanged(local.key)) {
local.changes[local.key] = {
from: this.changedFrom(local.key),
to: local.properties[local.key]
};
}
}
return local.changes;
}
// Audit log migration
component extends="wheels.migrator.Migration" {
function up() {
createTable(name="audit_logs") {
t.increments("id");
t.string(columnNames="model_name", allowNull=false);
t.integer(columnNames="record_id", allowNull=false);
t.string(columnNames="action", allowNull=false);
t.text(columnNames="changes");
t.integer(columnNames="user_id");
t.string(columnNames="ip_address");
t.string(columnNames="user_agent");
t.timestamps();
t.index(["model_name", "record_id"]);
t.index("user_id");
t.index("created_at");
};
}
function down() {
dropTable("audit_logs");
}
}wheels generate snippets crud-actionsGenerates complete CRUD controller with error handling, pagination, and filters.
wheels generate snippets api-controllerGenerates:
component extends="Controller" {
function init() {
provides("json");
filters(through="setApiHeaders");
filters(through="authenticateApi");
filters(through="logApiRequest", except="index,show");
}
private function setApiHeaders() {
header name="X-API-Version" value="1.0";
header name="X-RateLimit-Limit" value="1000";
header name="X-RateLimit-Remaining" value=getRateLimitRemaining();
}
private function authenticateApi() {
local.token = getAuthToken();
if (!Len(local.token)) {
renderUnauthorized("Missing authentication token");
}
request.apiUser = model("ApiKey").authenticate(local.token);
if (!IsObject(request.apiUser)) {
renderUnauthorized("Invalid authentication token");
}
}
private function getAuthToken() {
// Check Authorization header
if (StructKeyExists(getHttpRequestData().headers, "Authorization")) {
local.auth = getHttpRequestData().headers.Authorization;
if (Left(local.auth, 7) == "Bearer ") {
return Mid(local.auth, 8, Len(local.auth));
}
}
// Check X-API-Key header
if (StructKeyExists(getHttpRequestData().headers, "X-API-Key")) {
return getHttpRequestData().headers["X-API-Key"];
}
// Check query parameter
if (StructKeyExists(params, "api_key")) {
return params.api_key;
}
return "";
}
private function renderUnauthorized(required string message) {
renderWith(
data={
error: {
code: 401,
message: arguments.message
}
},
status=401
);
}
private function renderError(required string message, numeric status = 400) {
renderWith(
data={
error: {
code: arguments.status,
message: arguments.message
}
},
status=arguments.status
);
}
private function renderSuccess(required any data, numeric status = 200) {
renderWith(
data={
success: true,
data: arguments.data
},
status=arguments.status
);
}
private function renderPaginated(required any query) {
renderWith(
data={
success: true,
data: arguments.query,
pagination: {
page: arguments.query.currentPage,
perPage: arguments.query.perPage,
total: arguments.query.totalRecords,
pages: arguments.query.totalPages
}
}
);
}
}wheels generate snippets form-with-errorswheels generate snippets ajax-formGenerates:
<!--- View file --->
<div id="contact-form-container">
#startFormTag(
action="send",
id="contact-form",
class="ajax-form",
data={
remote: true,
method: "post",
success: "handleFormSuccess",
error: "handleFormError"
}
)#
<div class="form-messages" style="display: none;"></div>
<div class="form-group">
#textField(
name="name",
label="Name",
class="form-control",
required=true
)#
</div>
<div class="form-group">
#emailField(
name="email",
label="Email",
class="form-control",
required=true
)#
</div>
<div class="form-group">
#textArea(
name="message",
label="Message",
class="form-control",
rows=5,
required=true
)#
</div>
<div class="form-group">
#submitTag(
value="Send Message",
class="btn btn-primary",
data={loading: "Sending..."}
)#
</div>
#endFormTag()#
</div>
<script>
// AJAX form handler
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('contact-form');
form.addEventListener('submit', function(e) {
e.preventDefault();
const submitBtn = form.querySelector('[type="submit"]');
const originalText = submitBtn.value;
const loadingText = submitBtn.dataset.loading;
// Disable form
submitBtn.disabled = true;
submitBtn.value = loadingText;
// Send AJAX request
fetch(form.action, {
method: form.method,
body: new FormData(form),
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
handleFormSuccess(data);
} else {
handleFormError(data);
}
})
.catch(error => {
handleFormError({message: 'Network error. Please try again.'});
})
.finally(() => {
submitBtn.disabled = false;
submitBtn.value = originalText;
});
});
});
function handleFormSuccess(data) {
const form = document.getElementById('contact-form');
const messages = form.querySelector('.form-messages');
// Show success message
messages.className = 'form-messages alert alert-success';
messages.textContent = data.message || 'Message sent successfully!';
messages.style.display = 'block';
// Reset form
form.reset();
// Hide message after 5 seconds
setTimeout(() => {
messages.style.display = 'none';
}, 5000);
}
function handleFormError(data) {
const form = document.getElementById('contact-form');
const messages = form.querySelector('.form-messages');
// Show error message
messages.className = 'form-messages alert alert-danger';
messages.textContent = data.message || 'An error occurred. Please try again.';
messages.style.display = 'block';
// Show field errors
if (data.errors) {
Object.keys(data.errors).forEach(field => {
const input = form.querySelector(`[name="${field}"]`);
if (input) {
input.classList.add('is-invalid');
const error = document.createElement('div');
error.className = 'invalid-feedback';
error.textContent = data.errors[field].join(', ');
input.parentNode.appendChild(error);
}
});
}
}
</script>
<!--- Controller action --->
function send() {
contact = model("Contact").new(params);
if (contact.save()) {
if (isAjax()) {
renderWith(data={
success: true,
message: "Thank you! We'll be in touch soon."
});
} else {
flashInsert(success="Thank you! We'll be in touch soon.");
redirectTo(route="home");
}
} else {
if (isAjax()) {
renderWith(data={
success: false,
message: "Please correct the errors below.",
errors: contact.allErrors()
}, status=422);
} else {
renderView(action="new");
}
}
}wheels generate snippets migration-indexesGenerates common index patterns:
// Performance indexes
t.index("email"); // Single column
t.index(["last_name", "first_name"]); // Composite
t.index("created_at"); // Timestamp queries
// Unique constraints
t.index("email", unique=true);
t.index(["user_id", "role_id"], unique=true);
// Foreign key indexes
t.index("user_id");
t.index("category_id");
// Full-text search
t.index("title", type="fulltext");
t.index(["title", "content"], type="fulltext");
// Partial indexes (PostgreSQL)
t.index("email", where="deleted_at IS NULL");
// Expression indexes
t.index("LOWER(email)", name="idx_email_lower");wheels generate snippets seed-datawheels generate snippets --create=my-patternCreates template in ~/.wheels/snippets/my-pattern/:
my-pattern/
├── snippet.json
├── files/
│ ├── controller.cfc
│ ├── model.cfc
│ └── view.cfm
└── README.mdsnippet.json:
{
"name": "my-pattern",
"description": "Custom pattern description",
"category": "custom",
"author": "Your Name",
"version": "1.0.0",
"variables": [
{
"name": "modelName",
"prompt": "Model name?",
"default": "MyModel"
}
],
"files": [
{
"source": "files/controller.cfc",
"destination": "controllers/${controllerName}.cfc"
}
]
}wheels generate snippets login-form --output=clipboardwheels generate snippets api-controller --output=file --path=./controllers/Api.cfcwheels generate snippets --customizeReview generated code: Customize for your needs
Understand the patterns: Don't blindly copy
Keep snippets updated: Maintain with framework updates
Share useful patterns: Contribute back to community
Document customizations: Note changes made
Test generated code: Ensure it works in your context
Use consistent patterns: Across your application
wheels generate controller - Generate controllers
wheels generate model - Generate models
wheels scaffold - Generate complete resources
An overview of Wheels configuration and how it is used in your applications. Learn how to override a Wheels convention to make it your own.
We all love the "Convention over Configuration" motto of Wheels, but what about those two cases that pop into everyone's head? What if I want to develop in my own way? Or, What about an existing application that I need to port into Wheels? Gladly, that's what configuration and defaults are there for. Let's take a look at exactly how this is performed.
You will find configuration files in the config folder at the root of your Wheels application. In general, most of your settings will go in /config/settings.cfm.
You can also set values based on what environment you have set. For example, you can have different values for your settings depending on whether you're in development mode or production mode. See the chapter on Switching Environments for more details.
To change a Wheels application default, you generally use the set() function. With it, you can perform all sorts of tweaks to the framework's default behaviors.
Use the get() function to access the value of a Wheels application setting. Just pass it the name of the setting.
if (get("environment") == "production") {
// Do something for production environment
}In CFML's standard Application.cfc, you can normally set values for your application's properties in the thisscope. Wheels still provides these options to you in the file at config/app.cfm.
Here is an example of what can go in config/app.cfm:
this.name = "TheNextSiteToBeatTwitter";
this.sessionManagement = false;
this.customTagPaths = ListAppend(
this.customTagPaths,
ExpandPath("../customtags")
);Important: When your application starts, Wheels automatically loads .env files and makes their values available in this.env before config/app.cfm is executed. This means you can access environment variables directly in config/app.cfm using this.env.
Use this.env to access values from your .env file:
// Access environment variables using this.env
this.name = this.env["APP_NAME"] ?: "MyWheelsApp";
// Database configuration using environment variables
this.datasources["myapp"] = {
class: this.env["DB_CLASS"] ?: "org.h2.Driver",
connectionString: this.env["DB_CONNECTION_STRING"],
username: this.env["DB_USER"],
password: this.env["DB_PASSWORD"]
};
// Multiple datasources from environment variables
this.datasources["primary"] = {
class: "com.mysql.cj.jdbc.Driver",
connectionString: "jdbc:mysql://" & this.env["DB_HOST"] & ":" & this.env["DB_PORT"] & "/" & this.env["DB_NAME"],
username: this.env["DB_USER"],
password: this.env["DB_PASSWORD"]
};# Application Settings
APP_NAME=MyWheelsApp
WHEELS_ENV=development
# Database Configuration
DB_HOST=localhost
DB_PORT=3306
DB_NAME=myapp_dev
DB_USER=dbuser
DB_PASSWORD=secret123
DB_CLASS=com.mysql.cj.jdbc.Driver
DB_CONNECTION_STRING=jdbc:mysql://localhost:3306/myapp_dev?useSSL=falseYou can create environment-specific .env files:
.env - Default values for all environments
.env.development - Development-specific values
.env.production - Production-specific values
.env.testing - Testing-specific values
Wheels will automatically load the appropriate file (.env.[environment]) based the variable WHEELS_ENV defined in your current .env file.
Never commit sensitive credentials - Add .env to your .gitignore file
Use .env.example as a template - Commit a template with placeholder values
Use the null coalescing operator - Provide defaults: this.env["KEY"] ?: "default"
Document required variables - List all required environment variables in your README
# Copy this file to .env and fill in your values
# Never commit .env to version control!
APP_NAME=YourAppName
DB_HOST=localhost
DB_PORT=3306
DB_NAME=your_database
DB_USER=your_username
DB_PASSWORD=your_passwordThere are several types of configurations that you can perform in Wheels to override all those default behaviors. In Wheels, you can find all these configuration options:
Let's take a closer look at each of these options.
Not only are the environments useful for separating your production settings from your "under development" settings, but they are also opportunities for you to override settings that will only take effect in a specified environment.
The setting for the current environment can be found in config/environment.cfm and should look something like this:
set(environment="development");Full Listing of Environment Settings
environment
string
development
Environment to load. Set this value in config/environment.cfm. Valid values are development, testing, maintenance, and production.
reloadPassword
string
[empty string]
Password to require when reloading the Wheels application from the URL. Leave empty to require no password.
redirectAfterReload
boolean
Enabled in maintenance and production
Whether or not to redirect away from the current URL when it includes a reload request. This hinders accidentally exposing your application's reload URL and password in web analytics software, screenshots of the browser, etc.
ipExceptions
string
[empty string]
IP addresses that Wheels will ignore when the environment is set to maintenance. That way administrators can test the site while in maintenance mode, while the rest of users will see the message loaded in /app/events/onmaintenance.cfm.
allowEnvironmentSwitchViaUrl
boolean
true
Set to false to disable switching of environment configurations via URL. You can still reload the application, but switching environments themselves will be disabled.
Sometimes it is useful for our applications to "force" URL rewriting. By default, Wheels will try to determine what type of URL rewriting to perform and set it up for you. But you can force in or out this setting by using the example below:
set(urlRewriting="Off");The code above will tell Wheels to skip its automatic detection of the URL Rewriting capabilities and just set it as "Off".
You can also set it to "Partial" if you believe that your web server is capable of rewriting the URL as folders after index.cfm.
For more information, read the chapter about URL Rewriting.
Probably the most important configuration of them all. What is an application without a database to store all of its precious data?
The data source configuration is what tells Wheels which database to use for all of its models. (This can be overridden on a per-model basis, but that will be covered later.)
set(dataSourceName="yourDataSourceName");
set(dataSourceUserName="yourDataSourceUsername");
set(dataSourcePassword="yourDataSourcePassword");Recommended: Use environment variables for database credentials to keep sensitive information secure. You can access environment variables using this scope:
In config/app.cfm (recommended for datasource configuration):
// Use this.env to access .env file variables
this.datasources["myapp"] = {
class: this.env["DB_CLASS"],
connectionString: this.env["DB_CONNECTION_STRING"],
username: this.env["DB_USER"],
password: this.env["DB_PASSWORD"]
};See Using Environment Variables in config/app.cfm for more details on working with environment variables.
OK, here it's where the fun begins! Wheels includes a lot of functions to make your life as a CFML developer easier. A lot of those functions have sensible default argument values to minimize the amount of code that you need to write. And yes, you guessed it, Wheels lets you override those default argument values application-wide.
Let's look at a little of example:
set(functionName="findAll", perPage=20);That little line of code will make all calls to the findAll() method in Wheels return a maximum number of 20 record per page (if pagination is enabled for that findAll() call). How great is that? You don't need to set the perPage value for every single call to findAll() if you have a different requirement than the Wheels default of 10 records.
You'll generally want to configure how Wheels handles errors and debugging information based on your environment. For example, you probably won't want to expose CFML errors in your production environment, whereas you would want to see those errors in your development environment.
For example, let's say that we want to enable debugging information in our "development" environment temporarily:
// config/development/settings.cfm
set(showDebugInformation=false);Full Listing of Debugging and Error Settings
errorEmailServer
string
[empty string]
Server to use to send out error emails. When left blank, this defaults to settings in the ColdFusion Administrator (if set).
errorEmailAddress
string
[empty string]
Comma-delimited list of email address to send error notifications to. Only applies if sendEmailOnError is set to true.
errorEmailSubject
string
Error
Subject of email that gets sent to administrators on errors. Only applies if sendEmailOnError is set to true.
excludeFromErrorEmail
string
[empty string]
List of variables available in the scopes to exclude from the scope dumps included in error emails. Use this to keep sensitive information from being sent in plain text over email.
sendEmailOnError
boolean
Enabled in production environments that have a TLD like .com, .org, etc.
When set to true, Wheels will send an email to administrators whenever Wheels throws an error.
showDebugInformation
boolean
Enabled in development mode.
When set to true, Wheels will show debugging information in the footers of your pages.
showErrorInformation
boolean
Enabled in development, maintenance, and testing mode.
When set to false, Wheels will run and display code stored at /app/events/onerror.cfm instead of revealing CFML errors.
For more information, refer to the chapter about Switching Environments.
Wheels does a pretty good job at caching the framework and its output to speed up your application. But if personalization is key in your application, finer control over caching settings will become more important.
Let's say your application generates dynamic routes and you need it to check the routes on each request. This task will be as simple as this line of code:
set(cacheRoutes=false);Full Listing of Caching Settings
cacheActions
boolean
Enabled in maintenance, testing, and production
When set to true, Wheels will cache output generated by actions when specified (in a caches() call, for example).
cacheControllerConfig
boolean
Enabled in development, maintenance, testing, and production
When set to false, any changes you make to the config() function in the controller file will be picked up immediately.
cacheCullInterval
numeric
5
Number of minutes between each culling action. The reason the cache is not culled during each request is to keep performance as high as possible.
cacheCullPercentage
numeric
10
If you set this value to 10, then at most, 10% of expired items will be deleted from the cache.
cacheDatabaseSchema
boolean
Enabled in development, maintenance, testing, and production
When set to false, you can add a field to the database, and Wheels will pick that up right away.
cacheFileChecking
boolean
Enabled in development, maintenance, testing, and production
When set to true, Wheels will cache whether or not controller, helper, and layout files exist
cacheImages
boolean
Enabled in development, maintenance, testing, and production
When set to true, Wheels will cache general image information used in imageTag() like width and height.
cacheModelConfig
boolean
Enabled in development, maintenance, testing, and production
When set to false, any changes you make to the config() function in the model file will be picked up immediately.
cachePages
boolean
Enabled in maintenance, testing, and production
When set to true, Wheels will cache output generated by a view page when specified (in a renderView() call, for example).
cachePartials
boolean
Enabled in maintenance, testing, and production
When set to true, Wheels will cache output generated by partials when specified (in a includePartial() call for example).
cacheQueries
boolean
Enabled in maintenance, testing, and production
When set to true, Wheels will cache SQL queries when specified (in a findAll() call, for example).
clearQueryCacheOnReload
boolean
true
Set to true to clear any queries that Wheels has cached on application reload.
cacheRoutes
boolean
Enabled in development, maintenance, testing, and production
When set to true, Wheels will cache routes across all page views.
defaultCacheTime
numeric
60
Number of minutes an item should be cached when it has not been specifically set through one of the functions that perform the caching in Wheels (i.e., caches(), findAll(), includePartial(), etc.)
maximumItemsToCache
numeric
5000
Maximum number of items to store in Wheels's cache at one time. When the cache is full, items will be deleted automatically at a regular interval based on the other cache settings.
For more information, refer to the chapter on Caching.
The Wheels ORM provides many sensible conventions and defaults, but sometimes your data structure requires different column naming or behavior than what Wheels expects out of the box. Use these settings to change those naming conventions or behaviors across your entire application.
For example, if we wanted to prefix all of the database table names in our application with blog_ but didn't want to include that at the beginning of model names, we would do this:
set(tableNamePrefix="blog_");Now your post model will map to the blog_posts table, comment model will map to the blog_comments table, etc.
Full Listing of ORM Settings
afterFindCallbackLegacySupport
boolean
true
When this is set to false and you're implementing an afterFind() callback, you need to write the same logic for both the this scope (for objects) and arguments scope (for queries). Setting this to false makes both ways use the arguments scope so you don't need to duplicate logic. Note that the default is true for backwards compatibility.
automaticValidations
boolean
true
Set to false to stop Wheels from automatically running object validations based on column settings in your database.
setUpdatedAtOnCreate
boolean
true
Set to false to stop Wheels from populating the updatedAt timestamp with the createdAt timestamp's value.
softDeleteProperty
string
deletedAt
Name of database column that Wheels will look for in order to enforce soft deletes.
tableNamePrefix
string
[empty string]
String to prefix all database tables with so you don't need to define your model objects including it. Useful in environments that have table naming conventions like starting all table names with tbl
timeStampOnCreateProperty
string
createdAt
Name of database column that Wheels will look for in order to automatically store a "created at" time stamp when records are created.
timeStampOnUpdateProperty
string
updatedAt
Name of the database column that Wheels will look for in order to automatically store an "updated at" time stamp when records are updated.
transactionMode
string
commit
Use commit, rollback, or none to set default transaction handling for creates, updates and deletes.
useExpandedColumnAliases
boolean
false
When set to true, Wheels will always prepend children objects' names to columns included via findAll()'s include argument, even if there are no naming conflicts. For example, model("post").findAll(include="comment") in a fictitious blog application would yield these column names: id, title, authorId, body, createdAt, commentID, commentName, commentBody, commentCreatedAt, commentDeletedAt. When this setting is set to false, the returned column list would look like this: id, title, authorId, body, createdAt, commentID, name, commentBody, commentCreatedAt, deletedAt.
modelRequireConfig
boolean
false
Set to true to have Wheels throw an error when it can't find a config() method for a model. If you prefer to always use config() methods, this setting could save you some confusion when it appears that your configuration code isn't running due to misspelling "config" for example.
There are several settings that make plugin development more convenient. We recommend only changing these settings in development mode so there aren't any deployment issues in production, testing, and maintenancemodes. (At that point, your plugin should be properly packaged in a zip file.)
If you want to keep what's stored in a plugin's zip file from overwriting changes that you made in its expanded folder, set this in config/development/settings.cfm:
set(overwritePlugins=false);See the chapter on Installing and Using Plugins for more information.
deletePluginDirectories
boolean
true
When set to true, Wheels will remove subdirectories within the plugins folder that do not contain corresponding plugin zip files. Set to false to add convenience to the process for developing your own plugins.
loadIncompatiblePlugins
boolean
true
Set to false to stop Wheels from loading plugins whose supported versions do not match your current version of Wheels.
overwritePlugins
boolean
true
When set to true, Wheels will overwrite plugin files based on their source zip files on application reload. Setting this to false makes plugin development easier because you don't need to keep rezipping your plugin files every time you make a change.
showIncompatiblePlugins
boolean
false
When set to true, an incompatibility warning will be displayed for plugins that do not specify the current Wheels version.
Configure how Wheels handles linking to assets through view helpers like imageTag(), styleSheetLinkTag(), and javaScriptIncludeTag().
See the chapter about Date, Media, and Text Helpers for more information.
Full Listing of Asset Settings
assetQueryString
boolean
false in development mode, true in the other modes
Set to true to append a unique query string based on a time stamp to JavaScript, CSS, and image files included with the media view helpers. This helps force local browser caches to refresh when there is an update to your assets. This query string is updated when reloading your Wheels application. You can also hard code it by passing in a string.
assetPaths
struct
false
Pass false or a struct with up to 2 different keys to autopopulate the domains of your assets: http (required) and https. For example: {http="asset0.domain1.com,asset2.domain1.com,asset3.domain1.com", https="secure.domain1.com"}
Wheels includes a powerful routing system. Parts of it are configurable with the following settings.
See the chapters about Using Routes and Obfuscating URLs for more information about how this all works together.
Full Listing of Routing Settings
loadDefaultRoutes
boolean
true
Set to false to disable Wheels's default route patterns for controller/action/key, etc.
obfuscateUrls
boolean
false
Set to true to obfuscate primary keys in URLs.
Wheels has a multitude of view helpers for building links, forms, form elements, and more. Use these settings to configure global defaults for their behavior.
booleanAttributes
any
allowfullscreen, async, autofocus, autoplay, checked, compact, controls, declare, default, defaultchecked, defaultmuted, defaultselected, defer, disabled, draggable, enabled, formnovalidate, hidden, indeterminate, inert, ismap, itemscope, loop, multiple, muted, nohref, noresize, noshade, novalidate, nowrap, open, pauseonexit, readonly, required, reversed, scoped, seamless, selected, sortable, spellcheck, translate, truespeed, typemustmatch, visible
A list of HTML attributes that should be allowed to be set as boolean values when added to HTML tags (e.g. disabled instead of disabled="disabled"). You can also pass in true(all attributes will be boolean) or false (no boolean attributes allowed, like in XHTML).
Wheels includes built-in Cross-Site Request Forgery (CSRF) protection for form posts. Part of the CSRF protection involves storing an authenticity token in the session (default) or within an encrypted cookie. Most of the settings below are for when you've chosen to store the authenticity token within a cookie instead of the server's session store.
csrfStore
string
session
Which storage strategy to use for storing the CSRF authenticity token. Valid values are session or cookie. Choosing session requires no additional configuration. Choosing cookie for this requires additional configuration listed below.
csrfCookieEncryptionAlgorithm
string
AES
Encryption algorithm to use for encrypting the authenticity token cookie contents. This setting is ignored if you're using session storage. See your CF engine's documentation for the Encrypt() function for more information.
csrfCookieEncryptionSecretKey
string
Secret key used to encrypt the authenticity token cookie contents. This value must be configured to a string compatible with the csrfCookieEncryptionAlgorithmsetting if you're using cookie storage. This value is ignored if you're using session storage. See your CF engine's documentation for the Encrypt() function for more information.
csrfCookieEncryptionEncoding
string
Base64
Encoding to use to write the encrypted value to the cookie. This value is ignored if you're using session storage. See your CF engine's documentation for the Encrypt() function for more information.
csrfCookieName
string
_wheels_authenticity
The name of the cookie to be set to store CSRF token data. This value is ignored if you're using session storage.
csrfCookieDomain
string
Domain to set the cookie on. See your CF engine's documentation for cfcookie for more information.
csrfCookieEncodeValue
boolean
Whether or not to have CF encode the cookie. See your CF engine's documentation for cfcookie for more information.
csrfCookieHttpOnly
boolean
true
Whether or not they have CF set the cookie as HTTPOnly. See your CF engine's documentation for cfcookie for more information.
csrfCookiePath
string
/
Path to set the cookie on. See your CF engine's documentation for cfcookie for more information.
csrfCookiePreserveCase
boolean
Whether or not to preserve the case of the cookie's name. See your CF engine's documentation for cfcookie for more information.
csrfCookieSecure
boolean
Whether or not to only allow the cookie to be delivered over the HTTPS protocol. See your CF engine's documentation for cfcookie for more information.
Wheels includes built-in Cross-Origin Resource Sharing (CORS) which allows you to configure which cross-origin requests and methods are allowed. By default, this feature is turned off which will deny cross-origin requests at the browser level.
In this first version, the user can enable this feature, which will allow requests from all origins and all methods.
allowCorsRequests
boolean
false
disableEngineCheck
boolean
false
Set to true if you don't want Wheels to block you from using older CFML engines (such as ColdFusion 9, Railo etc).
enableMigratorComponent
boolean
true
Set to false to completely disable the migrator component which will prevent any Database migrations
enablePluginsComponent
boolean
true
Set to false to completely disable the plugins component which will prevent any plugin loading, and not load the entire plugins system
enablePublicComponent
boolean
true
Set to false to completely disable the public component which will disable the GUI even in development mode
autoMigrateDatabase
Boolean
false
Automatically runs available migration on applicationstart.
migratorTableName
String
c_o_r_e_migrator_versions
The name of the table that stores the versions migrated.
createMigratorTable
Boolean
true
Create the c_o_r_e_migrator_versions database table.
writeMigratorSQLFiles
Boolean
false
Writes the executed SQL to a .sql file in the /app/migrator/sql directory.
migratorObjectCase
String
lower
Specifies the case of created database object. Options are 'lower', 'upper' and 'none' (which uses the given value unmodified)
allowMigrationDown
Boolean
false (true in development mode)
Prevents 'down' migrations (rollbacks)
Generate complete CRUD scaffolding for a resource including model, controller, views, tests, and migration.
wheels generate scaffold name
#can also be used as:
wheels g scaffold name
# With properties and options
wheels generate scaffold Product --properties="name:string,price:decimal"
wheels generate scaffold Comment --belongsTo=Product --api=trueThe wheels scaffold command generates a complete CRUD (Create, Read, Update, Delete) implementation including model, controller, views, tests, and database migration. It's the fastest way to create a fully functional resource.
This command supports multiple parameter formats:
Positional parameters: wheels generate scaffold Product (resource name)
Named parameters: name=value (e.g., name=Product, properties="name:string,price:decimal")
Flag parameters: --flag equals flag=true (e.g., --api equals api=true)
Flag with value: --flag=value equals flag=value (e.g., --properties="name:string")
Parameter Mixing Rules:
✅ ALLOWED:
All positional: wheels generate scaffold Product
All named: name=Product properties="name:string"
Positional + flags: wheels generate scaffold Product --api --migrate
❌ NOT ALLOWED:
Positional + named: wheels generate scaffold Product properties="name:string" (causes error)
Recommendation: Use positional for resource name, flags for options: wheels generate scaffold Product --properties="name:string" --api --migrate
Note: Flag syntax (--flag) avoids positional/named parameter conflicts and is recommended for boolean options.
name
Resource name (singular)
Required
--properties
Model properties (format: name:type,name2:type2)
--properties="name:string,price:decimal"
--belongsTo
Parent model relationships (comma-separated)
--belongsTo=User,Category
--hasMany
Child model relationships (comma-separated)
--hasMany=orders,comments
--api
Generate API-only scaffold (no views)
--api=true or --api
false
--tests
Generate test files
--tests=false
true
--migrate
Run migrations after scaffolding
--migrate=true or --migrate
false
--force
Overwrite existing files
--force=true or --force
false
wheels generate scaffold Productwheels generate scaffold Product --properties="name:string,price:decimal,stock:integer"wheels scaffold Order --properties="total:decimal,status:string" \
--belongsTo=User --hasMany=orderItemswheels generate scaffold Product --api=true --properties="name:string,price:decimal"wheels generate scaffold Category --properties="name:string" --migrate=trueBuilding on CommandBox's parameter syntax, Wheels scaffold generation supports:
wheels generate scaffold Product # Resource name (required)
wheels g scaffold User # Short alias with resource namewheels generate scaffold Product --properties="name:string,price:decimal"
wheels generate scaffold Comment --belongsTo=Product --properties="content:text"
wheels generate scaffold User --hasMany=posts,comments --api=truewheels generate scaffold Order --properties="total:decimal" --belongsTo=User --migrate
wheels g scaffold Product --properties="name:string" --api --tests=false --forceFormat: Singular noun (e.g., Product, User, Comment)
Conventions: PascalCase recommended
Examples: Product, OrderItem, UserProfile
Format: --properties="name:type,name2:type2,name3:type3"
Types: string, text, integer, decimal, boolean, date, datetime, time
Separator: Comma (,) between properties
Quotes: Always use quotes around the entire properties string
belongsTo: --belongsTo=Model1,Model2 (comma-separated parent models)
hasMany: --hasMany=model1,model2 (comma-separated child models - lowercase plural)
Format: Model names in PascalCase for belongsTo, camelCase plural for hasMany
Short flags: --api, --migrate, --force (equals true)
Explicit: --api=true, --tests=false, --migrate=true
Default values: api=false, tests=true, migrate=false, force=false
# Basic properties
wheels generate scaffold Product --properties="name:string,description:text"
# Complex properties with various types
wheels generate scaffold Order --properties="total:decimal,status:string,orderDate:datetime,shipped:boolean"
# Properties with foreign keys (use belongsTo instead)
wheels generate scaffold Comment --properties="content:text,rating:integer" --belongsTo=Product,User# Single associations
wheels generate scaffold Post --belongsTo=User
wheels generate scaffold User --hasMany=posts
# Multiple associations
wheels generate scaffold Order --belongsTo=User,ShippingAddress --hasMany=orderItems,payments
wheels generate scaffold Product --hasMany=reviews,orderItems,images# API-only scaffold (no views)
wheels generate scaffold Product --api --properties="name:string,price:decimal"
# Skip tests generation
wheels generate scaffold Comment --tests=false --belongsTo=Post
# Force overwrite existing files
wheels generate scaffold User --force --properties="name:string,email:string"
# Auto-run migrations
wheels generate scaffold Category --migrate --properties="name:string,slug:string"# Complete e-commerce product
wheels generate scaffold Product \
--properties="name:string,description:text,price:decimal,inStock:boolean,sku:string" \
--belongsTo=Category \
--hasMany=orderItems,reviews \
--migrate
# Blog post with API
wheels generate scaffold Post \
--properties="title:string,content:text,published:boolean,publishedAt:datetime" \
--belongsTo=User \
--hasMany=comments \
--api=true \
--migrate
# User profile with relationships
wheels generate scaffold User \
--properties="firstName:string,lastName:string,email:string,active:boolean" \
--hasMany=posts,comments,orders \
--tests=true \
--migrate❌ Wrong property format:
wheels generate scaffold Product properties=name:string,price:decimal # Missing --
wheels generate scaffold Product --properties=name string price decimal # Wrong separator❌ Wrong association format:
wheels generate scaffold Comment belongs-to=Product # Wrong parameter name
wheels generate scaffold Order --belongsTo=user # Should be User (PascalCase)
wheels generate scaffold User --hasMany=Posts # Should be posts (camelCase plural)❌ Wrong boolean format:
wheels generate scaffold Product api=true # Missing --
wheels generate scaffold Product --api true # Should be --api=true or --api❌ Mixing positional and named parameters:
wheels generate scaffold Product properties="name:string" # Positional + named (ERROR)
wheels generate scaffold name=Product --api=true # Named + flag (inconsistent)✅ Correct formats:
wheels generate scaffold Product --properties="name:string,price:decimal" # Positional + flags
wheels generate scaffold Comment --belongsTo=Product # Positional + flags
wheels generate scaffold User --hasMany=posts,comments # Positional + flags
wheels generate scaffold Product --api=true # or just --api # Positional + flags
# OR all named parameters:
wheels generate scaffold name=Product properties="name:string" # All named# Different column types
wheels generate scaffold Event --properties="name:string,description:text,eventDate:date,startTime:time,duration:integer,price:decimal,active:boolean"
# Text fields for large content
wheels generate scaffold Article --properties="title:string,summary:text,content:text,publishedAt:datetime"
# Decimal fields with precision
wheels generate scaffold Product --properties="price:decimal,weight:decimal,dimensions:string"# Blog system
wheels generate scaffold User --hasMany=posts,comments
wheels generate scaffold Post --belongsTo=User --hasMany=comments
wheels generate scaffold Comment --belongsTo=User,Post
# E-commerce system
wheels generate scaffold Category --hasMany=products
wheels generate scaffold Product --belongsTo=Category --hasMany=orderItems,reviews
wheels generate scaffold Order --belongsTo=User --hasMany=orderItems
wheels generate scaffold OrderItem --belongsTo=Order,Product# API-only resources
wheels generate scaffold ApiUser --api --properties="name:string,email:string,apiKey:string"
wheels generate scaffold ApiToken --api --belongsTo=ApiUser --properties="token:string,expiresAt:datetime"
# Mobile app backend
wheels generate scaffold MobileUser --api --properties="deviceId:string,pushToken:string"
wheels generate scaffold Notification --api --belongsTo=MobileUser --properties="message:text,sent:boolean"Resource Name: First positional argument, converted to proper case variants
Properties: Parsed into individual property definitions with types
Associations: Split by comma and processed into relationship configurations
Boolean Flags: Converted to boolean values for scaffold options
Validation: Checked for required parameters and valid formats
reconstructArgs(): Processes CommandBox parameter format
validateScaffold(): Validates resource name and checks for conflicts
generateScaffold(): Coordinates generation of all components
Template Processing: Applies parameters to code generation templates
File Creation: Creates model, controller, views, tests, and migration
Model (/models/Product.cfc)
Properties and validations
Associations
Business logic
Controller (/controllers/Products.cfc)
All CRUD actions
Flash messages
Error handling
Views (/views/products/)
index.cfm - List all records
show.cfm - Display single record
new.cfm - New record form
edit.cfm - Edit record form
_form.cfm - Shared form partial
Migration (/app/migrator/migrations/[timestamp]_create_products.cfc)
Create table
Add indexes
Define columns
Tests (if enabled)
Model tests
Controller tests
Integration tests
Model - Same as standard
API Controller - JSON responses only
Migration - Same as standard
API Tests - JSON response tests
No Views - API doesn't need views
For wheels scaffold Product --properties="name:string,price:decimal,stock:integer":
/models/Product.cfccomponent extends="Model" {
function init() {
// Properties
property(name="name", label="Product Name");
property(name="price", label="Price");
property(name="stock", label="Stock Quantity");
// Validations
validatesPresenceOf("name,price,stock");
validatesUniquenessOf("name");
validatesNumericalityOf("price", greaterThan=0);
validatesNumericalityOf("stock", onlyInteger=true, greaterThanOrEqualTo=0);
}
}/controllers/Products.cfccomponent extends="Controller" {
function init() {
// Filters
}
function index() {
products = model("Product").findAll(order="name");
}
function show() {
product = model("Product").findByKey(params.key);
if (!IsObject(product)) {
flashInsert(error="Product not found.");
redirectTo(action="index");
}
}
function new() {
product = model("Product").new();
}
function create() {
product = model("Product").new(params.product);
if (product.save()) {
flashInsert(success="Product was created successfully.");
redirectTo(action="index");
} else {
flashInsert(error="There was an error creating the product.");
renderView(action="new");
}
}
function edit() {
product = model("Product").findByKey(params.key);
if (!IsObject(product)) {
flashInsert(error="Product not found.");
redirectTo(action="index");
}
}
function update() {
product = model("Product").findByKey(params.key);
if (IsObject(product) && product.update(params.product)) {
flashInsert(success="Product was updated successfully.");
redirectTo(action="index");
} else {
flashInsert(error="There was an error updating the product.");
renderView(action="edit");
}
}
function delete() {
product = model("Product").findByKey(params.key);
if (IsObject(product) && product.delete()) {
flashInsert(success="Product was deleted successfully.");
} else {
flashInsert(error="Product could not be deleted.");
}
redirectTo(action="index");
}
}/views/products/index.cfm<h1>Products</h1>
#flashMessages()#
<p>#linkTo(text="New Product", action="new", class="btn btn-primary")#</p>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Price</th>
<th>Stock</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<cfloop query="products">
<tr>
<td>#encodeForHtml(products.name)#</td>
<td>#dollarFormat(products.price)#</td>
<td>#products.stock#</td>
<td>
#linkTo(text="Show", action="show", key=products.id)#
#linkTo(text="Edit", action="edit", key=products.id)#
#linkTo(text="Delete", action="delete", key=products.id,
method="delete", confirm="Are you sure?")#
</td>
</tr>
</cfloop>
</tbody>
</table>/views/products/_form.cfm#errorMessagesFor("product")#
#textField(objectName="product", property="name", label="Product Name")#
#textField(objectName="product", property="price", label="Price")#
#textField(objectName="product", property="stock", label="Stock Quantity")#/app/migrator/migrations/[timestamp]_create_products.cfccomponent extends="wheels.migrator.Migration" {
function up() {
transaction {
t = createTable("products");
t.string("name");
t.decimal(columnNames="price", precision=10, scale=2);
t.integer("stock");
t.timestamps();
t.create();
addIndex(table="products", columnNames="name", unique=true);
}
}
function down() {
transaction {
dropTable("products");
}
}
}Add to /config/routes.cfm:
<cfset resources("products")>This creates all RESTful routes:
GET /products - index
GET /products/new - new
POST /products - create
GET /products/[key] - show
GET /products/[key]/edit - edit
PUT/PATCH /products/[key] - update
DELETE /products/[key] - delete
Run migration (if not using --migrate):
wheels dbmigrate latestAdd routes to /config/routes.cfm:
<cfset resources("products")>Restart application:
wheels reloadTest the scaffold:
Visit /products to see the index
Create, edit, and delete records
Run generated tests
In controller's index():
function index() {
if (StructKeyExists(params, "search")) {
products = model("Product").findAll(
where="name LIKE :search",
params={search: "%#params.search#%"}
);
} else {
products = model("Product").findAll();
}
}function index() {
products = model("Product").findAll(
page=params.page ?: 1,
perPage=20,
order="createdAt DESC"
);
}function init() {
filters(through="authenticate", except="index,show");
}The scaffold command uses templates to generate code. You can customize these templates to match your project's coding standards and markup preferences.
The CLI uses a template override system that allows you to customize the generated code:
CLI Templates - Default templates are located in the CLI module at /cli/templates/
App Templates - Custom templates in your application at /app/snippets/ override the CLI templates
This means you can modify the generated code structure by creating your own templates in the /app/snippets/ directory.
When generating code, the CLI looks for templates in this order:
First checks /app/snippets/[template-name]
Falls back to /cli/templates/[template-name] if not found in app
To customize scaffold output:
Copy the template you want to customize from /cli/templates/ to /app/snippets/
Modify the template to match your project's needs
Run scaffold - it will use your custom template
Example for customizing the form template:
# Create the crud directory in your app
mkdir -p app/snippets/crud
# Copy the form template
cp /path/to/wheels/cli/templates/crud/_form.txt app/snippets/crud/
# Edit the template to match your markup
# The CLI will now use your custom templateTemplates used by scaffold command:
crud/index.txt - Index/list view
crud/show.txt - Show single record view
crud/new.txt - New record form view
crud/edit.txt - Edit record form view
crud/_form.txt - Form partial shared by new/edit
ModelContent.txt - Model file structure
ControllerContent.txt - Controller file structure
Templates use placeholders that get replaced during generation:
|ObjectNameSingular| - Lowercase singular name (e.g., "product")
|ObjectNamePlural| - Lowercase plural name (e.g., "products")
|ObjectNameSingularC| - Capitalized singular name (e.g., "Product")
|ObjectNamePluralC| - Capitalized plural name (e.g., "Products")
|FormFields| - Generated form fields based on properties
<!--- CLI-Appends-Here ---> - Marker for future CLI additions
Issue: "Missing argument", "positional and named parameters", or parameter parsing errors
❌ Incorrect:
wheels generate scaffold Product properties=name:string,price:decimal # Missing --
wheels generate scaffold Comment belongs-to=Product # Wrong parameter name
wheels generate scaffold User api=true # Missing --
wheels generate scaffold Product properties="name:string" # Positional + named (ERROR)✅ Correct:
wheels generate scaffold Product --properties="name:string,price:decimal" # Positional + flags
wheels generate scaffold Comment --belongsTo=Product # Positional + flags
wheels generate scaffold User --api=true # Positional + flags
wheels generate scaffold name=Product properties="name:string" # All namedSolution:
Use --flag=value format with proper parameter names and quotes for complex values
Never mix positional and named parameters - use either all named (name=value) or positional with flags (Product --flag)
Issue: Relationships not generated correctly
❌ Common Mistakes:
wheels generate scaffold Comment --belongsTo=product # Lowercase, should be Product
wheels generate scaffold User --hasMany=Posts # Should be posts (lowercase plural)
wheels generate scaffold Order --belongsTo=user,User # Inconsistent case✅ Correct Usage:
wheels generate scaffold Comment --belongsTo=Product # PascalCase for belongsTo
wheels generate scaffold User --hasMany=posts # camelCase plural for hasMany
wheels generate scaffold Order --belongsTo=User # Consistent PascalCaseIssue: Properties not parsed or generated incorrectly
❌ Problematic:
# Missing quotes around properties
wheels generate scaffold Product --properties=name:string,price:decimal
# Wrong separator
wheels generate scaffold Product --properties="name string, price decimal"
# Invalid property types
wheels generate scaffold Product --properties="name:varchar,price:money"✅ Solutions:
# Always quote properties
wheels generate scaffold Product --properties="name:string,price:decimal"
# Use colon separator and comma between properties
wheels generate scaffold Product --properties="name:string,price:decimal,stock:integer"
# Use valid Wheels property types
wheels generate scaffold Product --properties="name:string,price:decimal,description:text,active:boolean"Issue: Files not created or partially generated
Possible Causes:
Insufficient permissions in target directories
Existing files blocking generation (use --force)
Invalid resource names
Template processing errors
Solutions:
# Check directory permissions
ls -la app/models app/controllers app/views
# Force overwrite existing files
wheels generate scaffold Product --force --properties="name:string"
# Use valid resource names (singular, PascalCase)
wheels generate scaffold ProductCategory # ✅ Good
wheels generate scaffold product-item # ❌ Invalid charactersIssue: Migrations not created or contain errors
Common Problems:
Properties with invalid SQL types
Association foreign keys missing
Migration syntax errors
Solutions:
# Check generated migration file
ls app/migrator/migrations/*create*
# Use valid property types that map to SQL
wheels generate scaffold Product --properties="name:string,price:decimal,inStock:boolean"
# For associations, foreign keys are auto-generated
wheels generate scaffold Comment --belongsTo=Product # Creates productId foreign keyIssue: Generated routes not working
Problem: Routes not added to routes.cfm or placed incorrectly
Solution:
<!-- Check routes.cfm for resources route -->
<cfscript>
mapper()
.resources("products") // Added by scaffold
// CLI-Appends-Here
.wildcard()
.root(to="home##index")
.end();
</cfscript>Before generating scaffolds:
# 1. Verify Wheels app directory
ls config/routes.cfm app/models app/controllers app/views
# 2. Check for naming conflicts
ls app/models/Product.cfc # Should not exist (or use --force)
ls app/controllers/Products.cfc
# 3. Plan your associations
# Know which models depend on othersAfter scaffolding:
# 1. Check all files were created
ls app/models/Product.cfc
ls app/controllers/Products.cfc
ls app/views/products/
ls app/migrator/migrations/*products*
# 2. Run migrations
wheels dbmigrate up
# 3. Test the scaffold
# Start server and visit /products
server start# 1. Run generated tests
wheels test
# 2. Manual testing
# Visit each CRUD action:
# GET /products (index)
# GET /products/new (new)
# POST /products (create)
# GET /products/1 (show)
# GET /products/1/edit (edit)
# PUT /products/1 (update)
# DELETE /products/1 (delete)"Cannot scaffold '[name]':"
Cause: Resource name validation failed or conflicts exist
Solution: Use singular, valid identifier name and check for existing files
"Scaffolding failed!"
Cause: Template processing or file creation error
Solution: Check file permissions and template syntax
"Missing argument name"
Cause: Parameter syntax error in command
Solution: Use proper --flag=value format
# Plan your scaffold before running
# 1. Resource name (singular, PascalCase)
# 2. Properties (all needed fields)
# 3. Associations (belongsTo and hasMany)
# 4. Options (api, migrate, tests)
# Example planning:
# Product: name:string, price:decimal, description:text, active:boolean
# Belongs to Category, has many OrderItems and Reviews
wheels generate scaffold Product \
--properties="name:string,price:decimal,description:text,active:boolean" \
--belongsTo=Category \
--hasMany=orderItems,reviews \
--migrate# Start simple, add complexity
# 1. Basic scaffold first
wheels generate scaffold Product --properties="name:string"
# 2. Add associations later if needed
# (modify generated files manually or re-scaffold with --force)# Customize templates before scaffolding
# 1. Copy templates to app/snippets/
mkdir -p app/snippets/crud
cp /path/to/cli/templates/crud/* app/snippets/crud/
# 2. Modify templates to match project style
# 3. Run scaffold - uses custom templatesProperties: Define all needed properties upfront
Associations: Include relationships in initial scaffold
Validation: Add custom validations after generation
Testing: Always generate and run tests
Routes: Use RESTful resources when possible
Security: Add authentication/authorization
Templates: Customize templates in /app/snippets/ to match your project standards
Planning: Design your data model before scaffolding
Incremental: Start simple, add complexity gradually
Scaffold generates everything at once:
# Scaffold does all of this:
wheels generate model product properties="name:string,price:decimal"
wheels generate controller products --rest
wheels generate view products index,show,new,edit,_form
wheels generate test model product
wheels generate test controller products
wheels dbmigrate create table productswheels generate model - Generate models
wheels generate controller - Generate controllers
wheels generate resource - Generate REST resources
wheels dbmigrate latest - Run migrations
Initialize Docker configuration for your Wheels application with support for multiple databases, CF engines, production mode, and Nginx reverse proxy.
wheels docker init [options]The wheels docker init command creates Docker configuration files for containerizing your Wheels application. It generates a Dockerfile, docker-compose.yml, .dockerignore, configures datasources in CFConfig.json, and optionally creates Nginx configuration for reverse proxy support.
--db
Database system to use
mysql
h2, mysql, postgres, mssql, oracle
--dbVersion
Database version to use
varies by db
Any valid version for the selected database
--cfengine
CFML engine to use
lucee
lucee, adobe
--cfVersion
CFML engine version
6
Any valid version for the selected engine
--port
Custom application port (overrides server.json)
from server.json or 8080
Any valid port number
--force
Overwrite existing Docker files without confirmation
false
true, false
--production
Generate production-ready configuration
false
true, false
--nginx
Include Nginx reverse proxy
false
true, false
Default Database Versions:
MySQL: 8.0
PostgreSQL: 15
MSSQL: 2019-latest
Oracle: latest (Oracle Database 23c Free)
H2: embedded (no version needed)
wheels docker initwheels docker init --db=postgreswheels docker init --db=postgres --dbVersion=13wheels docker init --cfengine=adobe --cfVersion=2023wheels docker init --db=h2wheels docker init --db=oraclewheels docker init --db=oracle --dbVersion=23-slimwheels docker init --port=3000wheels docker init --forcewheels docker init --productionwheels docker init --production --nginxwheels docker init --nginx --port=8080wheels docker init --db=postgres --dbVersion=15 --cfengine=lucee --cfVersion=6 --port=8888 --production --nginxChecks for existing files (unless --force is used):
Detects existing Dockerfile, docker-compose.yml, and .dockerignore
Prompts before overwriting existing Docker configuration
Lists all files that will be replaced
Allows cancellation to prevent accidental overwrites
Creates Dockerfile optimized for CFML applications:
Development mode (default):
Hot-reload enabled
Development tools installed (curl, nano)
Source code mounted for live updates
BOX_INSTALL=TRUE for automatic dependency installation
Production mode (--production):
Security hardened with non-root user (UID 1001)
box install --production for optimized dependencies
Health checks configured (30s interval, 10s timeout, 3 retries)
Production environment variables set
No source volume mounts
Optimized image size
Based on ortussolutions/commandbox:latest
Installs H2 extension for Lucee if H2 database selected
Exposes application port (custom, from server.json, or defaults to 8080)
Generates docker-compose.yml with:
Application service with port mapping or internal exposure
Database service (mysql, postgres, or mssql) if selected
Nginx reverse proxy service (if --nginx)
Environment variables for database connection
Volume mappings for data persistence
Development mode: Source code mounted for hot-reload, development command
Production mode: No source mounts, restart: always policies
Creates Nginx configuration (if --nginx):
Reverse proxy to application backend
Load balancing ready upstream configuration
WebSocket support with upgrade headers
Static asset caching with 1-day expiration
Custom upload size limits (100MB)
Health check endpoint at /health
Production mode only:
Security headers (X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy)
Gzip compression for text content types
Port configuration:
Development: Nginx on port 8080
Production: Nginx on port 80
Creates .dockerignore excluding:
.git and .gitignore
node_modules
.CommandBox
server.json
logs and *.log
tests
.env
Production mode only: README files, IDE configs (.vscode, .idea), .DS_Store
Updates CFConfig.json:
Configures wheels-dev datasource for selected database
Sets up proper JDBC drivers and connection strings:
MySQL: com.mysql.cj.jdbc.Driver
PostgreSQL: org.postgresql.Driver
MSSQL: com.microsoft.sqlserver.jdbc.SQLServerDriver
Oracle: oracle.jdbc.OracleDriver
Uses Docker service name db for host resolution
Configures appropriate ports and credentials
Updates server.json for Docker compatibility:
Sets web.host to 0.0.0.0 (required for Docker containers to accept external connections)
Sets openBrowser to false (Docker containers have no GUI)
Configures port from --port argument, existing server.json, or defaults to 8080
Sets CFEngine version (e.g., lucee@6 or adobe@2023)
Adds CFConfigFile reference if missing
FROM ortussolutions/commandbox:latest
## Install curl and nano
RUN apt-get update && apt-get install -y curl nano
## Clean up the image
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
## Copy application files
COPY . /app
WORKDIR /app
## Install Dependencies
ENV BOX_INSTALL TRUE
## Expose port
EXPOSE 8080
## Set Healthcheck URI
ENV HEALTHCHECK_URI "http://127.0.0.1:8080/"
## Start the application
CMD ["box", "server", "start", "--console", "--force"]FROM ortussolutions/commandbox:latest
## Install required packages
RUN apt-get update && apt-get install -y curl nano && \
apt-get clean && rm -rf /var/lib/apt/lists/*
## Copy application files
COPY . /app
WORKDIR /app
## Install Dependencies
RUN box install --production
## Production optimizations
ENV ENVIRONMENT production
ENV BOX_SERVER_PROFILE production
## Security: Run as non-root user
RUN useradd -m -u 1001 appuser && \
chown -R appuser:appuser /app
USER appuser
## Expose port
EXPOSE 8080
## Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://127.0.0.1:8080/ || exit 1
## Start the application
CMD ["box", "server", "start", "--console"]version: "3.8"
services:
app:
build: .
ports:
- "8080:8080"
environment:
ENVIRONMENT: development
DB_HOST: db
DB_PORT: 3306
DB_NAME: wheels
DB_USER: wheels
DB_PASSWORD: wheels
volumes:
- .:/app
- ../../../core/src/wheels:/app/vendor/wheels
- ../../../docs:/app/vendor/wheels/docs
- ../../../tests:/app/tests
command: sh -c "box install && box server start --console --force"
depends_on:
- db
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: wheels
MYSQL_DATABASE: wheels
MYSQL_USER: wheels
MYSQL_PASSWORD: wheels
ports:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql
volumes:
db_data:version: "3.8"
services:
app:
build: .
expose:
- 8080
environment:
ENVIRONMENT: production
DB_HOST: db
DB_PORT: 3306
DB_NAME: wheels
DB_USER: wheels
DB_PASSWORD: wheels
restart: always
depends_on:
- db
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
restart: always
depends_on:
- app
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: wheels
MYSQL_DATABASE: wheels
MYSQL_USER: wheels
MYSQL_PASSWORD: wheels
ports:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql
volumes:
db_data:events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Performance
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
upstream app_backend {
server app:8080;
}
server {
listen 80;
server_name _;
# Max upload size
client_max_body_size 100M;
# Security headers (production only)
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Gzip compression (production only)
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json;
location / {
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Static assets caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
proxy_pass http://app_backend;
proxy_cache_valid 200 1d;
expires 1d;
add_header Cache-Control "public, immutable";
}
# Health check endpoint
location /health {
access_log off;
proxy_pass http://app_backend;
}
}
}Image: mysql:8.0 (default)
Port: 3306
Credentials: wheels/wheels
Database: wheels
Root Password: wheels
Image: postgres:15 (default)
Port: 5432
Credentials: wheels/wheels
Database: wheels
Image: mcr.microsoft.com/mssql/server:2019-latest (default)
Port: 1433
Credentials: sa/Wheels123!
Database: wheels
Note: Requires EULA acceptance
Image: gvenzl/oracle-free:latest (default - Oracle Database 23c Free)
Port: 1521
Credentials: wheels/wheels
SID: FREE
Note: Uses lightweight Oracle Free container image
Available tags: latest, 23, 23-slim, 23-faststart, 23-slim-faststart
Embedded: No separate container needed
Extension: Automatically added to Lucee deployments via Dockerfile
Connection: Configured in CFConfig.json
Storage: Within application container filesystem
Source code mounted as volumes for hot-reload
Full development tools installed (curl, nano)
Debugging and verbose logging enabled
No restart policies
Direct port exposure (app accessible on configured port)
Development-friendly error messages
box install runs on container start with --force flag
Volume mounts for Wheels core, docs, and tests (framework development)
--production)No source code volume mounts - code baked into image
Security hardened Dockerfile with non-root user (UID 1001)
Automatic restart policies (restart: always)
Health checks configured (30s interval, 10s timeout, 60s start period, 3 retries)
Optimized image size with box install --production
Production environment variable set
Additional .dockerignore exclusions (docs, IDE configs)
BOX_SERVER_PROFILE=production environment
No command override in docker-compose (uses CMD from Dockerfile)
Source Volume Mount
✅ Yes
❌ No
Hot Reload
✅ Enabled
❌ Disabled
User
root
appuser (1001)
Restart Policy
none
always
Health Checks
❌ No
✅ Yes
Security Headers
❌ No
✅ Yes (with nginx)
Gzip Compression
❌ No
✅ Yes (with nginx)
Install Command
box install
box install --production
Image Size
Larger
Optimized
When using --nginx, an Nginx reverse proxy is configured between clients and your application.
Load balancing ready: Upstream configuration supports multiple app instances
SSL termination point: Add SSL certificates to nginx without app changes
Static asset caching: 1-day cache for images, CSS, JS
Security headers: Production mode adds security headers
Gzip compression: Production mode compresses responses
WebSocket support: Upgrade headers configured
Request buffering: Better handling of slow clients
Health checks: Dedicated /health endpoint
Development mode: Nginx listens on port 8080, app exposed internally
Production mode: Nginx listens on port 80, app exposed internally
App is only accessible through nginx when --nginx is used
The generated nginx.conf includes:
Upstream: app_backend pointing to app:8080 (Docker service)
Worker connections: 1024 concurrent connections
Client max body size: 100MB for file uploads
Proxy headers: Host, X-Real-IP, X-Forwarded-For, X-Forwarded-Proto
WebSocket support: HTTP/1.1 with Upgrade and Connection headers
Timeouts: 60s for connect, send, and read operations
Static caching: 1 day expiration for assets
Health check: /health endpoint with no access logging
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: no-referrer-when-downgradeEnabled for:
text/plain
text/css
text/xml
text/javascript
application/x-javascript
application/xml+rss
application/json
The command automatically updates your server.json with Docker-specific settings:
{
"name": "myapp",
"web": {
"host": "localhost",
"http": {
"port": "8080"
}
},
"openBrowser": true
}{
"name": "myapp",
"web": {
"host": "0.0.0.0",
"http": {
"port": "8080"
}
},
"openBrowser": false,
"CFConfigFile": "CFConfig.json",
"app": {
"cfengine": "lucee@6"
}
}web.host
0.0.0.0
Docker containers must bind to all interfaces to accept external connections. Using localhost or 127.0.0.1 prevents access from outside the container.
openBrowser
false
Docker containers run in headless mode with no GUI. Attempting to open a browser will fail and cause errors.
web.http.port
(from --port or existing)
Ensures the application port matches the Dockerfile EXPOSE and docker-compose port mapping.
CFConfigFile
CFConfig.json
Required for datasource configuration to work properly.
The port configuration follows this priority order:
--port command argument (highest priority)
Existing value in server.json
Default: 8080 (lowest priority)
If you specify --port=9000, the command will:
Update server.json with port 9000
Configure Dockerfile to EXPOSE 9000
Set docker-compose port mapping to 9000:9000
The following environment variables are configured in docker-compose.yml:
ENVIRONMENT
Application environment mode
development or production
BOX_SERVER_PROFILE
CommandBox server profile (production only)
production
DB_HOST
Database hostname (Docker service name)
db
DB_PORT
Database port
3306, 5432, 1433, 1521
DB_NAME
Database name
wheels
DB_USER
Database username
wheels or sa
DB_PASSWORD
Database password
wheels or Wheels123!
DB_SID
Oracle SID (Oracle only)
FREE
DB_TYPE
Database type (H2 only)
h2
After running wheels docker init, start your containers:
# Start in detached mode
docker-compose up -d
# Start with build (after code changes in production)
docker-compose up -d --build
# View logs
docker-compose logs -f
# View specific service logs
docker-compose logs -f app
docker-compose logs -f nginx
# Stop containers
docker-compose down
# Stop and remove volumes (WARNING: deletes database data)
docker-compose down -v
# Restart a specific service
docker-compose restart app
# Rebuild and restart
docker-compose up -d --build --force-recreateRequires Docker and Docker Compose installed
Use --force to skip confirmation prompts when overwriting existing files
server.json is automatically configured for Docker compatibility:
web.host changed to 0.0.0.0 (required for Docker networking)
openBrowser set to false (no GUI in containers)
web.http.port updated to match --port or existing value
CFConfigFile added if missing
CF engine version set (e.g., lucee@6)
Custom --port overrides the port from server.json
Port priority: --port argument > server.json > default (8080)
Database passwords are set to defaults suitable for development only
Production deployments MUST use secrets management and secure passwords
Production mode creates security-hardened configurations with non-root users
Nginx adds reverse proxy capabilities for load balancing, caching, and security
H2 database runs embedded within the application container
Volume mounts in development assume Wheels framework development structure
When using --nginx, the app is only exposed internally to nginx
CFConfig.json is updated with datasource configuration (skipped for H2)
Problem: Port already in use on host machine
Solutions:
Update the port in server.json before running wheels docker init
Use --port argument to specify a different port
Manually edit the ports in docker-compose.yml after generation
Stop the conflicting service: lsof -ti:8080 | xargs kill
Problem: Application cannot connect to database
Solutions:
Ensure the database container is fully started: docker-compose logs db
Check for initialization errors in database logs
Verify CFConfig.json has correct datasource configuration
Confirm the wheels-dev datasource name matches your application config
For MSSQL, ensure password meets complexity requirements
Wait for database to fully initialize (can take 30-60 seconds first time)
Problem: Permission denied errors in production mode
Solutions:
The generated files have 777 permissions for development convenience
Production Dockerfile runs as appuser (UID 1001)
Ensure all files are readable by UID 1001
Check volume mount permissions on host
Adjust file permissions: chmod -R 755 /path/to/app
Problem: 502 Bad Gateway or connection refused
Solutions:
Verify app service is running: docker-compose ps
Check app service logs: docker-compose logs app
Ensure app is listening on the correct port internally
Verify nginx config syntax: docker-compose exec nginx nginx -t
Check nginx logs: docker-compose logs nginx
Restart services: docker-compose restart app nginx
Problem: Still getting prompted despite using --force
Solutions:
Ensure correct syntax: wheels docker init --force (not --force=true)
Check for boolean parameter issues in command
Use the exact parameter name: --force
Problem: H2 extension not loading or database errors
Solutions:
Verify H2 extension was added to Dockerfile
Check Lucee admin for H2 extension installation
Ensure CFConfig.json doesn't have conflicting datasource config
Check application logs for H2 initialization errors
H2 works with Lucee only (not Adobe ColdFusion)
Problem: Cannot access the application at http://localhost:8080
Solutions:
Verify server.json has web.host set to 0.0.0.0 (not localhost or 127.0.0.1)
Check if port is already in use: lsof -ti:8080 (Unix) or netstat -ano | findstr :8080 (Windows)
Ensure docker-compose port mapping matches server.json port
Check container logs: docker-compose logs app
Restart containers: docker-compose restart app
If manually edited, run wheels docker init --force to regenerate proper configuration
Problem: Errors about browser opening or display issues
Solutions:
Verify server.json has openBrowser: false
Run wheels docker init --force to update server.json automatically
Manually set "openBrowser": false in server.json
Rebuild containers: docker-compose up -d --build
wheels docker deploy - Deploy using Docker
wheels deploy - General deployment commands
CommandBox Docker Images - Official CommandBox images
Docker Compose Documentation - Docker Compose reference
Nginx Documentation - Nginx configuration reference
A comprehensive guide to testing your Wheels application using TestBox 6...
This guide provides comprehensive instructions for testing your Wheels 3.0 application using TestBox 6. Wheels 3.0 now includes TestBox integration as an enabled option, moving beyond the legacy RocketUnit framework. TestBox is already included in your installation through box.json dependency management.
TestBox 6 is a next-generation testing framework for ColdFusion (CFML) based on BDD (Behavior Driven Development) and TDD (Test Driven Development), providing a clean, obvious syntax for writing tests. It serves as a comprehensive testing engine with multi-format output capabilities and database testing support.
For comprehensive TestBox documentation, refer to the official TestBox documentation.
BDD style or xUnit style testing
Testing life-cycle methods
MockBox integration for mocking and stubbing
Extensible reporters (JSON, XML, JUnit XML, Text, Console, TAP, HTML)
Asynchronous testing
Multi-suite capabilities
Test skipping and labels
Code coverage via FusionReactor
For a complete list of features, see the TestBox Features documentation.
Based on real Wheels 3.0 projects, your test structure should be organized as follows:
your-app/
├── app/
│ ├── controllers/
│ ├── models/
│ └── views/
├── config/
├── public/
├── tests/
│ ├── _assets/
│ ├── specs/
│ │ ├── controllers/
│ │ │ ├── ExampleControllerSpec.cfc
│ │ │ ├── PostControllerSpec.cfc
│ │ │ └── [Other Controller Tests]
│ │ └── functions/
│ │ └── ExampleSpec.cfc
│ ├── populate.cfm
│ ├── routes.cfm
│ └── runner.cfmNote: By default, TestBox runs tests located in the tests/specs/ directory, unless configured otherwise.
Update tests/runner.cfm:
For detailed information on TestBox runners and configuration options, refer to the TestBox Runners documentation.
<!--- TestBox Test Runner for Wheels 3.0 --->
<cfsetting requestTimeOut="1800">
<cfscript>
testBox = new testbox.system.TestBox(directory="tests.specs")
setTestboxEnvironment()
if (!structKeyExists(url, "format") || url.format eq "html") {
result = testBox.run(
reporter = "testbox.system.reports.JSONReporter"
);
}
else if(url.format eq "json"){
result = testBox.run(
reporter = "testbox.system.reports.JSONReporter"
);
cfcontent(type="application/json");
cfheader(name="Access-Control-Allow-Origin", value="*");
DeJsonResult = DeserializeJSON(result);
if (DeJsonResult.totalFail > 0 || DeJsonResult.totalError > 0) {
cfheader(statuscode=417);
} else {
cfheader(statuscode=200);
}
// Check if 'only' parameter is provided in the URL
if (structKeyExists(url, "only") && url.only eq "failure,error") {
allBundles = DeJsonResult.bundleStats;
if(DeJsonResult.totalFail > 0 || DeJsonResult.totalError > 0){
// Filter test results
filteredBundles = [];
for (bundle in DeJsonResult.bundleStats) {
if (bundle.totalError > 0 || bundle.totalFail > 0) {
filteredSuites = [];
for (suite in bundle.suiteStats) {
if (suite.totalError > 0 || suite.totalFail > 0) {
filteredSpecs = [];
for (spec in suite.specStats) {
if (spec.status eq "Error" || spec.status eq "Failed") {
arrayAppend(filteredSpecs, spec);
}
}
if (arrayLen(filteredSpecs) > 0) {
suite.specStats = filteredSpecs;
arrayAppend(filteredSuites, suite);
}
}
}
if (arrayLen(filteredSuites) > 0) {
bundle.suiteStats = filteredSuites;
arrayAppend(filteredBundles, bundle);
}
}
}
DeJsonResult.bundleStats = filteredBundles;
// Update the result with filtered data
count = 1;
for(bundle in allBundles){
writeOutput("Bundle: #bundle.name##Chr(13)##Chr(10)#")
writeOutput("CFML Engine: #DeJsonResult.CFMLEngine# #DeJsonResult.CFMLEngineVersion##Chr(13)##Chr(10)#")
writeOutput("Duration: #bundle.totalDuration#ms#Chr(13)##Chr(10)#")
writeOutput("Labels: #ArrayToList(DeJsonResult.labels, ', ')##Chr(13)##Chr(10)#")
writeOutput("╔═══════════════════════════════════════════════════════════╗#Chr(13)##Chr(10)#║ Suites ║ Specs ║ Passed ║ Failed ║ Errored ║ Skipped ║#Chr(13)##Chr(10)#╠═══════════════════════════════════════════════════════════╣#Chr(13)##Chr(10)#║ #NumberFormat(bundle.totalSuites,'999')# ║ #NumberFormat(bundle.totalSpecs,'999')# ║ #NumberFormat(bundle.totalPass,'999')# ║ #NumberFormat(bundle.totalFail,'999')# ║ #NumberFormat(bundle.totalError,'999')# ║ #NumberFormat(bundle.totalSkipped,'999')# ║#Chr(13)##Chr(10)#╚═══════════════════════════════════════════════════════════╝#Chr(13)##Chr(10)##Chr(13)##Chr(10)#")
if(bundle.totalFail > 0 || bundle.totalError > 0){
for(suite in DeJsonResult.bundleStats[count].suiteStats){
writeOutput("Suite with Error or Failure: #suite.name##Chr(13)##Chr(10)##Chr(13)##Chr(10)#")
for(spec in suite.specStats){
writeOutput(" Spec Name: #spec.name##Chr(13)##Chr(10)#")
writeOutput(" Error Message: #spec.failMessage##Chr(13)##Chr(10)#")
writeOutput(" Error Detail: #spec.failDetail##Chr(13)##Chr(10)##Chr(13)##Chr(10)##Chr(13)##Chr(10)#")
}
}
count += 1;
}
writeOutput("#Chr(13)##Chr(10)##Chr(13)##Chr(10)##Chr(13)##Chr(10)#")
}
}else{
for(bundle in DeJsonResult.bundleStats){
writeOutput("Bundle: #bundle.name##Chr(13)##Chr(10)#")
writeOutput("CFML Engine: #DeJsonResult.CFMLEngine# #DeJsonResult.CFMLEngineVersion##Chr(13)##Chr(10)#")
writeOutput("Duration: #bundle.totalDuration#ms#Chr(13)##Chr(10)#")
writeOutput("Labels: #ArrayToList(DeJsonResult.labels, ', ')##Chr(13)##Chr(10)#")
writeOutput("╔═══════════════════════════════════════════════════════════╗#Chr(13)##Chr(10)#║ Suites ║ Specs ║ Passed ║ Failed ║ Errored ║ Skipped ║#Chr(13)##Chr(10)#╠═══════════════════════════════════════════════════════════╣#Chr(13)##Chr(10)#║ #NumberFormat(bundle.totalSuites,'999')# ║ #NumberFormat(bundle.totalSpecs,'999')# ║ #NumberFormat(bundle.totalPass,'999')# ║ #NumberFormat(bundle.totalFail,'999')# ║ #NumberFormat(bundle.totalError,'999')# ║ #NumberFormat(bundle.totalSkipped,'999')# ║#Chr(13)##Chr(10)#╚═══════════════════════════════════════════════════════════╝#Chr(13)##Chr(10)##Chr(13)##Chr(10)##Chr(13)##Chr(10)#")
}
}
}else{
writeOutput(result)
}
}
else if (url.format eq "txt") {
result = testBox.run(
reporter = "testbox.system.reports.TextReporter"
)
cfcontent(type="text/plain");
writeOutput(result)
}
else if(url.format eq "junit"){
result = testBox.run(
reporter = "testbox.system.reports.ANTJUnitReporter"
)
cfcontent(type="text/xml");
writeOutput(result)
}
// reset the original environment
application.wheels = application.$$$wheels
structDelete(application, "$$$wheels")
if(!structKeyExists(url, "format") || url.format eq "html"){
// Use our html template
type = "App";
include "/wheels/tests_testbox/html.cfm";
}
private function setTestboxEnvironment() {
// creating backup for original environment
application.$$$wheels = Duplicate(application.wheels)
// load testbox routes
application.wo.$include(template = "/tests/routes.cfm")
application.wo.$setNamedRoutePositions()
local.AssetPath = "/tests/_assets/"
application.wo.set(rewriteFile = "index.cfm")
application.wo.set(controllerPath = local.AssetPath & "controllers")
application.wo.set(viewPath = local.AssetPath & "views")
application.wo.set(modelPath = local.AssetPath & "models")
application.wo.set(wheelsComponentPath = "/wheels")
/* turn off default validations for testing */
application.wheels.automaticValidations = false
application.wheels.assetQueryString = false
application.wheels.assetPaths = false
/* redirections should always delay when testing */
application.wheels.functions.redirectTo.delay = true
/* turn off transactions by default */
application.wheels.transactionMode = "none"
/* turn off request query caching */
application.wheels.cacheQueriesDuringRequest = false
// CSRF
application.wheels.csrfCookieName = "_wheels_test_authenticity"
application.wheels.csrfCookieEncryptionAlgorithm = "AES"
application.wheels.csrfCookieEncryptionSecretKey = GenerateSecretKey("AES")
application.wheels.csrfCookieEncryptionEncoding = "Base64"
// Setup CSRF token and cookie. The cookie can always be in place, even when the session-based CSRF storage is being
// tested.
dummyController = application.wo.controller("dummy")
csrfToken = dummyController.$generateCookieAuthenticityToken()
cookie[application.wheels.csrfCookieName] = Encrypt(
SerializeJSON({authenticityToken = csrfToken}),
application.wheels.csrfCookieEncryptionSecretKey,
application.wheels.csrfCookieEncryptionAlgorithm,
application.wheels.csrfCookieEncryptionEncoding
)
if(structKeyExists(url, "db") && listFind("mysql,sqlserver,postgres,h2", url.db)){
application.wheels.dataSourceName = "wheelstestdb_" & url.db;
} else if (application.wheels.coreTestDataSourceName eq "|datasourceName|") {
application.wheels.dataSourceName = "wheelstestdb";
} else {
application.wheels.dataSourceName = application.wheels.coreTestDataSourceName;
}
application.testenv.db = application.wo.$dbinfo(datasource = application.wheels.dataSourceName, type = "version")
local.populate = StructKeyExists(url, "populate") ? url.populate : true
if (local.populate) {
include "populate.cfm"
}
}
</cfscript>Update tests/populate.cfm for test database setup:
<cfscript>
// Populate test database with sample data
try {
// Create test users
testUser = model("User").create({
username: "testuser",
email: "[email protected]",
password: "password123",
firstName: "Test",
lastName: "User"
});
// Create test blog posts
testPost = model("Post").create({
title: "Test Blog Post",
content: "This is a test blog post content.",
userId: testUser.id,
published: true
});
// Create test community content
testCommunityPost = model("CommunityPost").create({
title: "Test Community Post",
content: "Community test content",
userId: testUser.id
});
writeOutput("Test data populated successfully.<br>");
} catch (any e) {
writeOutput("Error populating test data: " & e.message & "<br>");
}
</cfscript>TestBox 6 test bundles should extend wheels.Testbox and use BDD syntax with describe(), it(), and expect().
For comprehensive information on TestBox BDD syntax and expectations, see the TestBox BDD documentation and TestBox Expectations documentation.
Create tests/specs/controllers/ExampleControllerSpec.cfc:
component extends="wheels.Testbox" {
function beforeAll() {
variables.baseUrl = "http://localhost:8080";
variables.home = variables.baseUrl & "/";
}
function run() {
describe("Front Page Functions Tests", () => {
it("should return 200 status code for home page", () => {
cfhttp(url=variables.home, method="GET", result="response");
expect(response.status_code).toBe(200);
expect(response.responseheader.status_code).toBe(200);
});
it("should contain expected home page content", () => {
cfhttp(url=variables.home, method="GET", result="response");
expect(response.filecontent).toInclude("<title>");
expect(response.filecontent).toInclude("html");
});
it("should have proper content type", () => {
cfhttp(url=variables.home, method="GET", result="response");
expect(response.responseheader["Content-Type"]).toInclude("text/html");
});
});
}
}Create tests/specs/controllers/ApiControllerSpec.cfc:
component extends="wheels.Testbox" {
function beforeAll() {
variables.baseUrl = "http://localhost:8080";
variables.apiUrl = variables.baseUrl & "/api";
}
function run() {
describe("GET /api/users", () => {
beforeEach(() => {
// Set up API authentication if needed
variables.headers = {
"Content-Type": "application/json",
"Accept": "application/json"
};
});
it("should return JSON response with 200 status", () => {
cfhttp(
url=variables.apiUrl & "/users",
method="GET",
result="response"
) {
cfhttpparam(type="header", name="Content-Type", value="application/json");
cfhttpparam(type="header", name="Accept", value="application/json");
}
expect(response.status_code).toBe(200);
// Parse JSON response
var jsonResponse = deserializeJSON(response.filecontent);
expect(jsonResponse).toBeStruct();
expect(jsonResponse).toHaveKey("data");
});
});
describe("POST /api/users", () => {
it("should create new user with valid data", () => {
var userData = {
username: "apitest_#createUUID()#",
email: "[email protected]",
password: "password123"
};
cfhttp(
url=variables.apiUrl & "/users",
method="POST",
result="response"
) {
cfhttpparam(type="header", name="Content-Type", value="application/json");
cfhttpparam(type="body", value=serializeJSON(userData));
}
expect(response.status_code).toBe(201);
var jsonResponse = deserializeJSON(response.filecontent);
expect(jsonResponse.data.username).toBe(userData.username);
});
});
}
}Create tests/specs/controllers/AuthenticationControllerSpec.cfc:
component extends="wheels.Testbox" {
function beforeAll() {
variables.baseUrl = "http://localhost:8080";
variables.authUrl = variables.baseUrl & "/auth";
}
function run() {
describe("Login Flow", () => {
beforeEach(() => {
// Create test user for authentication tests
variables.testUser = {
username: "authtest",
email: "[email protected]",
password: "password123"
};
});
it("should display login page", () => {
cfhttp(url=variables.authUrl & "/login", method="GET", result="response");
expect(response.status_code).toBe(200);
expect(response.filecontent).toInclude("login");
});
it("should authenticate valid user", () => {
cfhttp(
url=variables.authUrl & "/login",
method="POST",
result="response"
) {
cfhttpparam(type="formfield", name="username", value=variables.testUser.username);
cfhttpparam(type="formfield", name="password", value=variables.testUser.password);
cfhttpparam(type="formfield", name="csrf_token", value=session.csrf_token);
}
// Should redirect after successful login
expect(response.status_code).toBe(302);
expect(response.responseheader).toHaveKey("Location");
});
it("should reject invalid credentials", () => {
cfhttp(
url=variables.authUrl & "/login",
method="POST",
result="response"
) {
cfhttpparam(type="formfield", name="username", value="invalid");
cfhttpparam(type="formfield", name="password", value="invalid");
cfhttpparam(type="formfield", name="csrf_token", value=session.csrf_token);
}
expect(response.status_code).toBe(200);
expect(response.filecontent).toInclude("error");
});
});
describe("Logout Flow", () => {
it("should logout user successfully", () => {
cfhttp(url=variables.authUrl & "/logout", method="POST", result="response") {
cfhttpparam(type="formfield", name="csrf_token", value=session.csrf_token);
}
expect(response.status_code).toBe(302);
});
});
}
}Create tests/specs/controllers/PostControllerSpec.cfc:
component extends="wheels.Testbox" {
function beforeAll() {
variables.baseUrl = "http://localhost:8080";
variables.blogUrl = variables.baseUrl & "/blog";
}
function run() {
describe("Blog Index", () => {
it("should display blog posts list", () => {
cfhttp(url=variables.blogUrl, method="GET", result="response");
expect(response.status_code).toBe(200);
expect(response.filecontent).toInclude("blog");
});
});
describe("Individual Blog Post", () => {
it("should display specific blog post", () => {
cfhttp(url=variables.blogUrl & "/1", method="GET", result="response");
// Either 200 (post exists) or 404 (post doesn't exist)
expect([200, 404]).toInclude(response.status_code);
});
});
describe("Blog Post Creation", () => {
it("should create new blog post with valid data", () => {
var postData = {
title: "Test Blog Post",
content: "This is test content for the blog post.",
published: true
};
cfhttp(
url=variables.blogUrl & "/create",
method="POST",
result="response"
) {
cfhttpparam(type="formfield", name="title", value=postData.title);
cfhttpparam(type="formfield", name="content", value=postData.content);
cfhttpparam(type="formfield", name="published", value=postData.published);
cfhttpparam(type="formfield", name="csrf_token", value=session.csrf_token);
}
// Should redirect after successful creation
expect([201, 302]).toInclude(response.status_code);
});
});
}
}For detailed information on testing functions and utility methods, refer to the TestBox Unit Testing documentation.
Create tests/specs/functions/ExampleSpec.cfc:
component extends="wheels.Testbox" {
function run() {
describe("String Helper Functions", () => {
it("should strip spaces correctly", () => {
var result = stripSpaces(" hello world ");
expect(result).toBe("helloworld");
});
it("should format currency properly", () => {
var result = formatCurrency(1234.56);
expect(result).toInclude("$");
expect(result).toInclude("1,234.56");
});
});
describe("Date Helper Functions", () => {
it("should format date correctly", () => {
var testDate = createDate(2024, 1, 15);
var result = formatDisplayDate(testDate);
expect(result).toInclude("Jan");
expect(result).toInclude("15");
expect(result).toInclude("2024");
});
});
describe("Validation Functions", () => {
it("should validate email addresses", () => {
expect(isValidEmail("[email protected]")).toBeTrue();
expect(isValidEmail("invalid-email")).toBeFalse();
expect(isValidEmail("")).toBeFalse();
});
it("should validate phone numbers", () => {
expect(isValidPhone("(555) 123-4567")).toBeTrue();
expect(isValidPhone("555-123-4567")).toBeTrue();
expect(isValidPhone("invalid")).toBeFalse();
});
});
}
}Wheels 3.0 provides convenient URL routing for both TestBox and legacy testing:
TestBox Testing URLs:
# Run your application TestBox tests
http://localhost:8080/wheels/app/tests
# Run Wheels core framework TestBox tests
http://localhost:8080/wheels/core/testsLegacy Testing URLs (RocketUnit):
# Run your application legacy tests
http://localhost:8080/wheels/legacy/app/tests
# Run Wheels core framework legacy tests
http://localhost:8080/wheels/legacy/core/testsAccess your TestBox tests through multiple formats:
# HTML Interface (default)
http://localhost:8080/wheels/app/tests
# JSON Output (for CI/CD)
http://localhost:8080/wheels/app/tests?format=json
# Plain Text Output
http://localhost:8080/wheels/app/tests?format=txtFor more information on running tests and available formats, see the TestBox Web Runner documentation.
You can also run tests for the Wheels framework itself:
# Run Wheels core TestBox tests
http://localhost:8080/wheels/core/tests
# Run Wheels core legacy tests
http://localhost:8080/wheels/legacy/core/testsCustomize your test runs using the convenient URLs:
# Run specific test bundles
http://localhost:8080/wheels/app/tests?bundles=HomeControllerSpec,ApiControllerSpec
# Run tests with specific labels
http://localhost:8080/wheels/app/tests?labels=integration,api
# Exclude certain tests
http://localhost:8080/wheels/app/tests?excludes=slow,external
# Combine parameters
http://localhost:8080/wheels/app/tests?format=json&db=mysql&populate=true&bundles=ApiControllerSpecYour test suite should provide comprehensive coverage for:
Status codes (200, 404, 500, 302, etc.)
Response headers (Content-Type, Location, etc.)
Response content validation
Redirect behavior
Page rendering and templates
Form processing and validation
Authentication and authorization
API endpoints and JSON responses
CRUD operations
Error handling
Model creation and updates
Data validation and constraints
Relationships and associations
Transaction handling
Data integrity
CSRF token validation
Authentication flows
Authorization checks
Input sanitization
SQL injection prevention
Utility functions
Helper methods
Date and string formatting
Validation rules
Custom algorithms
For detailed guidance on what to test and testing strategies, see the TestBox Testing Code Coverage documentation.
For comprehensive testing best practices and advanced techniques, refer to the TestBox Testing documentation.
Note: This section documents the legacy RocketUnit testing framework used in earlier Wheels versions. For new Wheels 3.0 applications, use the TestBox approach documented above. This information is maintained for reference and migration purposes.
Prior to Wheels 3.0, the framework used RocketUnit as its testing infrastructure. RocketUnit was a comprehensive testing framework that provided both unit testing and integration testing capabilities specifically tailored for Wheels applications.
At some point, your code is going to break. Upgrades, feature enhancements, and bug fixes are all part of the development lifecycle. Quite often with deadlines, you don't have the time to test the functionality of your entire application with every change you make.
The problem is that today's fix could be tomorrow's bug. What if there were an automated way of checking if that change you're making is going to break something? That's where writing tests for your application can be invaluable.
RocketUnit provided a complete testing solution with the following features:
Unit Testing: Testing individual functions and methods in isolation
Integration Testing: Testing complete request flows and component interactions
Assertion Library: Comprehensive set of assertion methods for validation
Test Lifecycle Management: Setup and teardown methods for test preparation
Package Organization: Hierarchical test organization and execution
Multiple Output Formats: HTML, text, and custom reporting formats
Database Testing Support: Built-in database seeding and cleanup capabilities
The legacy RocketUnit testing framework used a specific directory structure within your Wheels application:
your-app/
├── tests/
│ ├── Test.cfc # Parent test component (extends wheels.Test)
│ ├── functions/ # Function-level unit tests
│ │ └── Example.cfc # Example function tests
│ └── requests/ # Integration tests for request flows
│ └── Example.cfc # Example request tests
└── wheels/
├── Test.cfc # Core testing framework component
└── test/
└── functions.cfm # Testing framework implementationThis was the base component that all test components should extend:
component extends="wheels.Test" hint="I am the parent test." {
function beforeAll() {
// Executes once before the test suite runs
}
function setup() {
// Executes before every test case
}
function teardown() {
// Executes after every test case
}
function afterAll() {
// Executes once after the test suite runs
}
}Key Features:
Lifecycle Methods: Provided complete test lifecycle management
Framework Integration: Extended wheels.Test for full framework integration
Documentation Support: Included section and category metadata
Example test file for testing controller, model, and global functions:
component extends="app.tests.Test" hint="I am an example test for functions." {
function packageSetup() {
// Executes once before this package's first test case
}
function packageTeardown() {
// Executes once after this package's last test case
}
function testExample() {
// Example test with simple assertion
assert('true');
}
}Key Features:
Package-level Lifecycle: Setup and teardown for entire test packages
Unit Testing Focus: Designed for testing individual functions in isolation
Simple Assertions: Used basic assert() statements for validation
Example test file for integration testing where controllers, models, and other components work together:
component extends="app.tests.Test" hint="I am an example test for requests." {
function packageSetup() {
// Setup for integration test package
}
function packageTeardown() {
// Cleanup for integration test package
}
function testRequestFlow() {
// Test complete request processing
assert('true');
}
}Key Features:
Integration Testing: Tested complete request flows and component interactions
Request Context: Simulated full HTTP request/response cycles
Component Interaction: Validated how controllers, models, and views worked together
The core testing framework component provided all testing functionality:
component hint="I am the testing framework for Wheels applications." {
public any function $runTest() {
// Main test execution method
}
public void function assert(required any expression) {
// Basic assertion method
}
public void function fail(string message = "") {
// Explicit test failure
}
public any function debug(required any expression) {
// Debug expression evaluation
}
public string function raised(required any expression) {
// Error catching and type return
}
}The comprehensive testing framework implementation included:
Assertion Functions:
assert(expression) - Evaluated expressions and recorded failures
fail(message) - Explicitly failed tests with custom messages
debug(expression) - Examined expressions during testing
raised(expression) - Caught and returned error types
The RocketUnit framework followed these organizational principles:
Collections of test cases organized in directories, allowing for:
Logical grouping of related tests
Package-level setup and teardown
Hierarchical test execution
Modular test organization
Components containing multiple tests for specific functionality:
Focused on single areas of functionality
Contained multiple related test methods
Provided component-level lifecycle management
Extended the base Test component
Methods starting with "test" that contained assertions:
Named with descriptive test names (e.g., testUserValidation())
Contained one or more assertion statements
Focused on specific behaviors or outcomes
Used simple assertion syntax
Statements that should evaluate to true for tests to pass:
assert(expression) - Basic true/false validation
fail(message) - Explicit test failure with custom message
debug(expression) - Expression evaluation for debugging
raised(expression) - Error handling and type checking
The RocketUnit framework supported multiple execution patterns:
Entire Test Packages:
# Run all tests in functions package
http://localhost:8080/wheels/packages/app?package=functions
# Run all tests in requests package
http://localhost:8080/wheels/packages/app?package=requestsSpecific Test Cases:
# Run specific test component
http://localhost:8080/wheels/packages/app?package=functions&test=Example
# Run individual test method
http://localhost:8080/wheels/packages/app?package=functions&test=Example&method=testExampleWith Filtering Options:
# Run tests with specific labels
http://localhost:8080/wheels/packages/app?labels=unit,fast
# Exclude slow tests
http://localhost:8080/wheels/packages/app?exclude=slow,integrationThe framework supported different categories of tests:
Core Tests: Framework-level tests for Wheels components App Tests: Application-specific tests for your code Plugin Tests: Tests for Wheels plugins and extensions
Database Seeding Capabilities:
Automatic test database population
Data cleanup between tests
Transaction-based test isolation
Custom seeding scripts
Test Environment Initialization:
Isolated test environment setup
Configuration override capabilities
Mock service integration
Resource allocation and cleanup
Debugging and Output Formatting:
Detailed test execution reports
Error stack traces and debugging info
Performance timing information
Custom output formatters
Error Handling and Reporting:
Comprehensive error capture
Test failure analysis
Exception type categorization
Detailed failure reporting
tests/functions/ # Unit testing individual functions
tests/requests/ # Integration testing request flows
tests/models/ # Model-specific testing (optional)
tests/controllers/ # Controller-specific testing (optional)Suite Level: beforeAll() and afterAll() for entire test suite
Package Level: packageSetup() and packageTeardown() for test packages
Test Case Level: setup() and teardown() for individual test cases
component extends="app.tests.Test" {
function packageSetup() {
variables.testData = {
user: { username: "testuser", email: "[email protected]" }
};
}
function setup() {
variables.testUser = model("User").create(variables.testData.user);
}
function testUserCreation() {
assert('StructKeyExists(variables, "testUser")');
assert('variables.testUser.persisted()');
assert('variables.testUser.username eq "testuser"');
}
function testUserValidation() {
invalidUser = model("User").new({username: "", email: "invalid"});
assert('NOT invalidUser.valid()');
assert('invalidUser.hasErrors()');
}
function teardown() {
if (StructKeyExists(variables, "testUser")) {
variables.testUser.delete();
}
}
function packageTeardown() {
// Cleanup package-level resources
}
}The RocketUnit framework was tightly integrated with the Wheels framework, providing:
Model Testing Integration:
Automatic database transaction management
Model validation testing helpers
Relationship testing utilities
Database seeding and cleanup
Controller Testing Integration:
Request simulation capabilities
Response validation helpers
Session and cookie management
Route testing functionality
View Testing Integration:
Template rendering validation
Output content verification
Helper function testing
Layout and partial testing
When migrating from the legacy RocketUnit system to TestBox 6, consider the following mapping:
assert(expression) → expect(result).toBeTrue()
fail(message) → fail(message) (same syntax)
debug(expression) → debug(expression) (same syntax)
Test methods → Wrapped in describe() and it() blocks
tests/functions/ → tests/specs/functions/
tests/requests/ → tests/specs/controllers/
Component extensions change from app.tests.Test to wheels.Testbox
File names changed to include Spec or Test to align with TestBox 6 naming requirements
packageSetup() → beforeAll() in describe block
packageTeardown() → afterAll() in describe block
setup() → beforeEach() in describe block
teardown() → afterEach() in describe block
The RocketUnit framework served the Wheels community well for many years, providing a solid foundation for test-driven development in CFML applications. While TestBox now provides more modern BDD/TDD capabilities, understanding the legacy system helps with:
Migration Planning: Understanding existing test structures for conversion
Historical Context: Appreciating the evolution of CFML testing frameworks
Legacy Maintenance: Supporting older Wheels applications still using RocketUnit
Framework Archaeology: Understanding the testing heritage of the Wheels framework
The comprehensive testing infrastructure provided by RocketUnit established many of the testing patterns and practices that continue in the modern TestBox implementation, ensuring continuity and familiarity for developers transitioning between the frameworks.
This comprehensive testing approach ensures your Wheels 3.0 application is thoroughly validated across all components, provides multi-format output for different environments, and supports various database configurations for complete coverage while maintaining reference information for legacy RocketUnit systems.
If you already have application-level tests written with RocketUnit in a Wheels 2.x app, you don’t need to rewrite them immediately when upgrading to Wheels 3.0. Wheels 3.0 still provides backward compatibility for running RocketUnit tests alongside TestBox. To run your existing RocketUnit tests:
Copy the entire contents of that tests/ folder.
Paste the copied folder into your Wheels 3.0 application under tests/RocketUnit/.
Run the legacy tests by visiting the RocketUnit runner in your browser: http://localhost:8080/wheels/legacy/app/tests
This approach lets you continue running your legacy RocketUnit test suite unchanged inside a Wheels 3.0 project while you gradually migrate to TestBox. It’s particularly useful for teams upgrading large applications where a complete migration cannot be done in one step.